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: | +
+
Dashboard card screenshot
+
    +
  1. The date the application was submitted.
  2. +
  3. The application's status.
  4. +
  5. What you need to do with the application.
  6. +
  7. The journal's title.
  8. +
  9. Which group the application is assigned to.
  10. +
  11. To who the application is assigned.
  12. +
+
+ + - 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 -
  • `; + editorListFrag += `
  • ` + if (data.editors[ed].email) { + editorListFrag += `${ed}${isEd}` + } + else { + editorListFrag += `${ed}${isEd} (no email)` + } + + editorListFrag += `${appCount} applications +
  • `; + } } // ~~-> ApplicationSearch:Page~~ @@ -118,7 +126,7 @@ doaj.dashboard.renderGroupInfo = function(data) { // ]}) editorListFrag += `
  • Unassigned - ${data.unassigned.applications} applications + ${data.unassigned.applications} applications
  • `; let appStatusProgressBar = ""; @@ -137,7 +145,7 @@ doaj.dashboard.renderGroupInfo = function(data) { "sort": [{"admin.date_applied": {"order": "asc"}}] }) appStatusProgressBar += `
  • - + ${data.by_status[status].applications}
  • `; } @@ -160,7 +168,7 @@ doaj.dashboard.renderGroupInfo = function(data) { let frag = `

    ${data.editor_group.name}’s open applications - ${data.total.applications} applications + ${data.total.applications} applications

    diff --git a/portality/static/js/doaj.fieldrender.edges.js b/portality/static/js/doaj.fieldrender.edges.js index 01f5589f2f..169ce93133 100644 --- a/portality/static/js/doaj.fieldrender.edges.js +++ b/portality/static/js/doaj.fieldrender.edges.js @@ -1551,13 +1551,12 @@ $.extend(true, doaj, { var textIdSelector = edges.css_id_selector(this.namespace, "text", this); var text = this.component.jq(textIdSelector).val(); - if (text === "") { - return; - } - // if there is search text, then proceed to run the search var val = this.component.jq(element).val(); this.component.setSearchField(val, false); + if (text === "") { + return; + } this.component.setSearchText(text); }; @@ -2277,7 +2276,7 @@ $.extend(true, doaj, { // the remove block looks different, depending on the kind of filter to remove if (def.filter === "term" || def.filter === "terms") { - filters += ''; + filters += ''; filters += def.display + valDisplay; filters += ' '; filters += ""; @@ -2332,6 +2331,7 @@ $.extend(true, doaj, { this.removeFilter = function (element) { var el = this.component.jq(element); + var sf = this.component; // if this is a compound filter, remove it by id var compound = el.attr("data-compound"); @@ -2347,20 +2347,9 @@ $.extend(true, doaj, { var value = false; if (ft === "terms" || ft === "term") { - val = el.attr("data-value"); - // translate string value to a type required by a model - if (val === "true"){ - value = true; - } - else if (val === "false"){ - value = false; - } - else if (!isNaN(parseInt(val))){ - value = parseInt(val); - } - else { - value = val; - } + let values = sf.mustFilters[field].values; + let idx = el.attr("data-value-idx") + value = values[idx].val } else if (ft === "range") { value = {}; diff --git a/portality/static/js/edges/admin.applications.edge.js b/portality/static/js/edges/admin.applications.edge.js index 4fc1a2aeeb..3fcdbc3ab7 100644 --- a/portality/static/js/edges/admin.applications.edge.js +++ b/portality/static/js/edges/admin.applications.edge.js @@ -206,20 +206,20 @@ $.extend(true, doaj, { } ], fieldDisplays: { - 'admin.application_status.exact': 'Application status', + 'admin.application_status.exact': 'Status', 'index.application_type.exact' : 'Application', - 'index.has_editor_group.exact' : 'Has editor group?', - 'index.has_editor.exact' : 'Has Associate Editor?', + 'index.has_editor_group.exact' : 'Editor group', + 'index.has_editor.exact' : 'Associate Editor', 'admin.editor_group.exact' : 'Editor group', 'admin.editor.exact' : 'Editor', 'index.classification.exact' : 'Classification', - 'index.language.exact' : 'Journal language', - 'index.country.exact' : 'Country of publisher', + 'index.language.exact' : 'Language', + 'index.country.exact' : 'Country', 'index.subject.exact' : 'Subject', 'bibjson.publisher.name.exact' : 'Publisher', 'bibjson.provider.exact' : 'Platform, Host, Aggregator', - "index.has_apc.exact" : "Publication charges?", - 'index.license.exact' : 'Journal license' + "index.has_apc.exact" : "Charges?", + 'index.license.exact' : 'License' }, valueMaps : { "index.application_type.exact" : { diff --git a/portality/static/js/edges/admin.journalarticle.edge.js b/portality/static/js/edges/admin.journalarticle.edge.js index af19af1789..0508530334 100644 --- a/portality/static/js/edges/admin.journalarticle.edge.js +++ b/portality/static/js/edges/admin.journalarticle.edge.js @@ -515,15 +515,15 @@ $.extend(true, doaj, { fieldDisplays: { "es_type.exact": "Showing", "admin.in_doaj" : "In DOAJ?", - "index.language.exact" : "Journal language", + "index.language.exact" : "Language", "bibjson.publisher.name.exact" : "Publisher", "index.classification.exact" : "Classification", "index.subject.exact" : "Subject", - "index.country.exact" : "Country of publisher", - "index.license.exact" : "Journal license", + "index.country.exact" : "Country", + "index.license.exact" : "License", "bibjson.year.exact" : "Year of publication", - "bibjson.journal.title.exact" : "Journal title", - "index.has_apc.exact" : "Publication charges?" + "bibjson.journal.title.exact" : "Title", + "index.has_apc.exact" : "Charges?" }, valueMaps : { "es_type.exact" : { diff --git a/portality/static/js/edges/admin.journals.edge.js b/portality/static/js/edges/admin.journals.edge.js index f08d570ac5..f6d6aef096 100644 --- a/portality/static/js/edges/admin.journals.edge.js +++ b/portality/static/js/edges/admin.journals.edge.js @@ -469,21 +469,21 @@ $.extend(true, doaj, { category: "selected-filters", fieldDisplays: { "admin.in_doaj" : "In DOAJ?", - "index.has_seal.exact" : "DOAJ Seal", + "index.has_seal.exact" : "Seal?", "admin.owner.exact" : "Owner", - "index.has_editor_group.exact" : "Has editor group?", - "index.has_editor.exact" : "Has Associate Editor?", + "index.has_editor_group.exact" : "Editor group?", + "index.has_editor.exact" : "Associate Editor?", "admin.editor_group.exact" : "Editor group", "admin.editor.exact" : "Associate Editor", - "index.license.exact" : "Journal license", + "index.license.exact" : "License", "bibjson.publisher.name.exact" : "Publisher", "index.classification.exact" : "Classification", "index.subject.exact" : "Subject", - "index.language.exact" : "Journal language", - "index.country.exact" : "Country of publisher", + "index.language.exact" : "Language", + "index.country.exact" : "Country", "index.continued.exact" : "Continued", "bibjson.discontinued_date" : "Discontinued Year", - "index.has_apc.exact" : "Publication charges?" + "index.has_apc.exact" : "Charges?" }, valueMaps : { "admin.in_doaj" : { diff --git a/portality/static/js/edges/admin.update_requests.edge.js b/portality/static/js/edges/admin.update_requests.edge.js index 38ea75566f..22cc532911 100644 --- a/portality/static/js/edges/admin.update_requests.edge.js +++ b/portality/static/js/edges/admin.update_requests.edge.js @@ -207,20 +207,20 @@ $.extend(true, doaj, { id: "selected-filters", category: "selected-filters", fieldDisplays: { - 'admin.application_status.exact': 'Application Status', + 'admin.application_status.exact': 'Status', 'index.application_type.exact' : 'Update Request', - 'index.has_editor_group.exact' : 'Has Editor Group?', - 'index.has_editor.exact' : 'Has Associate Editor?', + 'index.has_editor_group.exact' : 'Editor Group?', + 'index.has_editor.exact' : 'Associate Editor?', 'admin.editor_group.exact' : 'Editor Group', 'admin.editor.exact' : 'Editor', 'index.classification.exact' : 'Classification', - 'index.language.exact' : 'Journal language', - 'index.country.exact' : 'Country of publisher', + 'index.language.exact' : 'Language', + 'index.country.exact' : 'Country', 'index.subject.exact' : 'Subject', 'bibjson.publisher.name.exact' : 'Publisher', 'bibjson.provider.exact' : 'Platform, Host, Aggregator', - "index.has_apc.exact" : "Publication charges?", - 'index.license.exact' : 'Journal license' + "index.has_apc.exact" : "Charges?", + 'index.license.exact' : 'License' }, valueMaps : { "index.application_type.exact" : { diff --git a/portality/static/js/edges/associate.applications.edge.js b/portality/static/js/edges/associate.applications.edge.js index 7ff2a1b444..c1b51f00f5 100644 --- a/portality/static/js/edges/associate.applications.edge.js +++ b/portality/static/js/edges/associate.applications.edge.js @@ -27,6 +27,8 @@ $.extend(true, doaj, { doaj.components.searchingNotification(), // facets + doaj.facets.openOrClosed(), + edges.newRefiningANDTermSelector({ id: "application_status", category: "facet", @@ -300,14 +302,15 @@ $.extend(true, doaj, { id: "selected-filters", category: "selected-filters", fieldDisplays: { - 'admin.application_status.exact': 'Application Status', + 'admin.application_status.exact': 'Status', + 'index.application_type.exact': "Record type", 'index.classification.exact' : 'Classification', - 'index.language.exact' : 'Journal language', - 'index.country.exact' : 'Country of publisher', + 'index.language.exact' : 'Language', + 'index.country.exact' : 'Country', 'index.subject.exact' : 'Subject', 'bibjson.publisher.name.exact' : 'Publisher', - 'index.license.exact' : 'Journal license', - "index.has_apc.exact" : "Publication charges?" + 'index.license.exact' : 'License', + "index.has_apc.exact" : "Charges?" } }) ]; diff --git a/portality/static/js/edges/associate.journals.edge.js b/portality/static/js/edges/associate.journals.edge.js index 6077ce0cf6..e10c2efa15 100644 --- a/portality/static/js/edges/associate.journals.edge.js +++ b/portality/static/js/edges/associate.journals.edge.js @@ -310,14 +310,14 @@ $.extend(true, doaj, { fieldDisplays: { "admin.in_doaj" : "In DOAJ?", "admin.owner.exact" : "Owner", - "index.license.exact" : "Journal license", + "index.license.exact" : "License", "bibjson.publisher.name.exact" : "Publisher", "index.classification.exact" : "Classification", "index.subject.exact" : "Subject", - "index.language.exact" : "Journal language", - "index.country.exact" : "Country of publisher", - "index.title.exact" : "Journal title", - "index.has_apc.exact" : "Publication charges?" + "index.language.exact" : "Language", + "index.country.exact" : "Country", + "index.title.exact" : "Title", + "index.has_apc.exact" : "Charges?" }, valueMaps : { "admin.in_doaj" : { diff --git a/portality/static/js/edges/editor.groupapplications.edge.js b/portality/static/js/edges/editor.groupapplications.edge.js index 3faf1f843a..5e40fd9e22 100644 --- a/portality/static/js/edges/editor.groupapplications.edge.js +++ b/portality/static/js/edges/editor.groupapplications.edge.js @@ -27,6 +27,8 @@ $.extend(true, doaj, { doaj.components.searchingNotification(), // facets + doaj.facets.openOrClosed(), + edges.newRefiningANDTermSelector({ id: "application_status", category: "facet", @@ -42,20 +44,20 @@ $.extend(true, doaj, { hideInactive: true }) }), - edges.newRefiningANDTermSelector({ - id: "application_type", - category: "facet", - field: "index.application_type.exact", - display: "Record type", - deactivateThreshold: 1, - renderer: edges.bs3.newRefiningANDTermSelectorRenderer({ - controls: true, - open: false, - togglable: true, - countFormat: countFormat, - hideInactive: true - }) - }), + // edges.newRefiningANDTermSelector({ + // id: "application_type", + // category: "facet", + // field: "index.application_type.exact", + // display: "Record type", + // deactivateThreshold: 1, + // renderer: edges.bs3.newRefiningANDTermSelectorRenderer({ + // controls: true, + // open: false, + // togglable: true, + // countFormat: countFormat, + // hideInactive: true + // }) + // }), edges.newRefiningANDTermSelector({ id: "has_editor", category: "facet", diff --git a/portality/static/js/edges/editor.groupjournals.edge.js b/portality/static/js/edges/editor.groupjournals.edge.js index 632d04ca9c..9c145ba018 100644 --- a/portality/static/js/edges/editor.groupjournals.edge.js +++ b/portality/static/js/edges/editor.groupjournals.edge.js @@ -364,17 +364,17 @@ $.extend(true, doaj, { fieldDisplays: { "admin.in_doaj" : "In DOAJ?", "admin.owner.exact" : "Owner", - "index.has_editor.exact" : "Has Associate Editor?", + "index.has_editor.exact" : "Associate Editor?", "admin.editor_group.exact" : "Editor group", "admin.editor.exact" : "Associate Editor", - "index.license.exact" : "Journal license", + "index.license.exact" : "License", "bibjson.publisher.name.exact" : "Publisher", "index.classification.exact" : "Classification", "index.subject.exact" : "Subject", - "index.language.exact" : "Journal language", - "index.country.exact" : "Country of publisher", - "index.title.exact" : "Journal title", - "index.has_apc.exact" : "Publication charges?" + "index.language.exact" : "Language", + "index.country.exact" : "Country", + "index.title.exact" : "Title", + "index.has_apc.exact" : "Charges?" }, valueMaps : { "admin.in_doaj" : { diff --git a/portality/static/js/edges/notifications.edge.js b/portality/static/js/edges/notifications.edge.js index 65788540ad..4b8c00e083 100644 --- a/portality/static/js/edges/notifications.edge.js +++ b/portality/static/js/edges/notifications.edge.js @@ -10,6 +10,10 @@ $.extend(true, doaj, { seen_url: "/dashboard/notifications/{notification_id}/seen", icons: { + alert: ` + + + `, finished: ` @@ -34,6 +38,7 @@ $.extend(true, doaj, { }, classifications: { + alert: "Requires attention", finished: "Task has completed", status_change: "Application status change", assign: "Assigned to user" diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index 99367a6c91..c970e12c18 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -1195,7 +1195,6 @@ var formulaic = { this.init(); }, - newClickableOwner : function(params) { return edges.instantiate(formulaic.widgets.ClickableOwner, params) }, @@ -1238,7 +1237,27 @@ var formulaic = { this.init(); }, + newClickToCopy : function(params) { + return edges.instantiate(formulaic.widgets.ClickToCopy, params) + }, + ClickToCopy : function(params) { + this.fieldDef = params.fieldDef; + this.init = function() { + var elements = $("#click-to-copy--" + this.fieldDef.name); + edges.on(elements, "click", this, "copy"); + }; + this.copy = function(element) { + let form = new doaj.af.BaseApplicationForm() + let value = form.determineFieldsValue(this.fieldDef.name) + let value_to_copy = form.convertValueToText(value); + navigator.clipboard.writeText(value_to_copy) + var confirmation = $("#copy-confirmation--" + this.fieldDef.name); + confirmation.text("Copied: " + value_to_copy); + confirmation.show().delay(3000).fadeOut(); + }; + this.init(); + }, newTrimWhitespace : function(params) { return edges.instantiate(formulaic.widgets.TrimWhitespace, params) }, diff --git a/portality/static/js/notifications.js b/portality/static/js/notifications.js index 6bf24b3010..c564aff81e 100644 --- a/portality/static/js/notifications.js +++ b/portality/static/js/notifications.js @@ -58,6 +58,14 @@ doaj.notifications.notificationsReceived = function(data) { } $(".notification_action_link").on("click", doaj.notifications.notificationClicked); + $(".dropdown--notifications").hoverIntent(doaj.notifications.showDropdown, doaj.notifications.hideDropdown); +} + +doaj.notifications.showDropdown = function(e) { + $("#top_notifications").show(); +} +doaj.notifications.hideDropdown = function() { + $("#top_notifications").hide(); } doaj.notifications.notificationClicked = function(event) { diff --git a/portality/static/js/tourist.js b/portality/static/js/tourist.js new file mode 100644 index 0000000000..cc0f375f0a --- /dev/null +++ b/portality/static/js/tourist.js @@ -0,0 +1,75 @@ +if (!doaj.hasOwnProperty("tourist")) { doaj.tourist = {}} + +doaj.tourist.cookiePrefix = ""; +doaj.tourist.allTours = []; +doaj.tourist.currentTour = null; +doaj.tourist.contentId = null; + +doaj.tourist.init = function(params) { + doaj.tourist.allTours = params.tours || []; + doaj.tourist.cookiePrefix = params.cookie_prefix; + + $(".trigger_tour").on("click", doaj.tourist.triggerTour); + + let first = doaj.tourist.findNextTour(); + if (first) { + doaj.tourist.start(first); + } +} + +doaj.tourist.findNextTour = function() { + let cookies = document.cookie.split("; "); + + let first = false; + for (let tour of doaj.tourist.allTours) { + let cookieName = doaj.tourist.cookiePrefix + tour.content_id + "=" + tour.content_id; + let cookie = cookies.find(c => c === cookieName); + if (!cookie) { + first = tour; + break; + } + } + return first; +} + +doaj.tourist.start = function (tour) { + doaj.tourist.contentId = tour.content_id; + doaj.tourist.currentTour = new Tourguide({ + src: `/tours/${tour.content_id}`, + onStart: doaj.tourist.startCallback, + onComplete: doaj.tourist.completeCallback + }); + doaj.tourist.currentTour.start() +} + +doaj.tourist.startCallback = function(options) { + doaj.tourist.seen({tour: doaj.tourist.contentId}); +} + +doaj.tourist.completeCallback = function(options) { + doaj.tourist.currentTour = null; + doaj.tourist.contentId = null; + + let next = doaj.tourist.findNextTour(); + if (next) { + doaj.tourist.start(next); + } else { + $([document.documentElement, document.body]).animate({ + scrollTop: 0 + }, 500); + } +} + +doaj.tourist.seen = function(params) { + $.ajax({ + type: "GET", + url: `/tours/${doaj.tourist.contentId}/seen`, + success: function() {} + }) +} + +doaj.tourist.triggerTour = function(event) { + event.preventDefault(); + let tour_id = $(event.currentTarget).attr("data-tour-id"); + doaj.tourist.start({content_id: tour_id}); +} \ No newline at end of file diff --git a/portality/static/js/vendors/jquery.hoverIntent.min.js b/portality/static/js/vendors/jquery.hoverIntent.min.js new file mode 100644 index 0000000000..270354a739 --- /dev/null +++ b/portality/static/js/vendors/jquery.hoverIntent.min.js @@ -0,0 +1,9 @@ +/*! + * hoverIntent v1.10.2 // 2020.04.28 // jQuery v1.7.0+ + * http://briancherne.github.io/jquery-hoverIntent/ + * + * You may use hoverIntent under the terms of the MIT license. Basically that + * means you are free to use hoverIntent as long as this header is left intact. + * Copyright 2007-2019 Brian Cherne + */ +!function(factory){"use strict";"function"==typeof define&&define.amd?define(["jquery"],factory):"object"==typeof module&&module.exports?module.exports=factory(require("jquery")):jQuery&&!jQuery.fn.hoverIntent&&factory(jQuery)}(function($){"use strict";function track(ev){cX=ev.pageX,cY=ev.pageY}function isFunction(value){return"function"==typeof value}var cX,cY,_cfg={interval:100,sensitivity:6,timeout:0},INSTANCE_COUNT=0,compare=function(ev,$el,s,cfg){if(Math.sqrt((s.pX-cX)*(s.pX-cX)+(s.pY-cY)*(s.pY-cY)) arr.length) len = arr.length; + for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; + return arr2; + } + function _nonIterableSpread() { + throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); + } + function _nonIterableRest() { + throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); + } + function _toPrimitive(input, hint) { + if (typeof input !== "object" || input === null) return input; + var prim = input[Symbol.toPrimitive]; + if (prim !== undefined) { + var res = prim.call(input, hint || "default"); + if (typeof res !== "object") return res; + throw new TypeError("@@toPrimitive must return a primitive value."); + } + return (hint === "string" ? String : Number)(input); + } + function _toPropertyKey(arg) { + var key = _toPrimitive(arg, "string"); + return typeof key === "symbol" ? key : String(key); + } + + var umbrella_min = {exports: {}}; + + /* Umbrella JS 3.3.0 umbrellajs.com */ + + (function (module) { + var u=function(t,e){return this instanceof u?t instanceof u?t:((t="string"==typeof t?this.select(t,e):t)&&t.nodeName&&(t=[t]),void(this.nodes=this.slice(t))):new u(t,e)};u.prototype={get length(){return this.nodes.length}},u.prototype.nodes=[],u.prototype.addClass=function(){return this.eacharg(arguments,function(t,e){t.classList.add(e);})},u.prototype.adjacent=function(o,t,i){return "number"==typeof t&&(t=0===t?[]:new Array(t).join().split(",").map(Number.call,Number)),this.each(function(n,r){var e=document.createDocumentFragment();u(t||{}).map(function(t,e){e="function"==typeof o?o.call(this,t,e,n,r):o;return "string"==typeof e?this.generate(e):u(e)}).each(function(t){this.isInPage(t)?e.appendChild(u(t).clone().first()):e.appendChild(t);}),i.call(this,n,e);})},u.prototype.after=function(t,e){return this.adjacent(t,e,function(t,e){t.parentNode.insertBefore(e,t.nextSibling);})},u.prototype.append=function(t,e){return this.adjacent(t,e,function(t,e){t.appendChild(e);})},u.prototype.args=function(t,e,n){return (t="string"!=typeof(t="function"==typeof t?t(e,n):t)?this.slice(t).map(this.str(e,n)):t).toString().split(/[\s,]+/).filter(function(t){return t.length})},u.prototype.array=function(o){var i=this;return this.nodes.reduce(function(t,e,n){var r;return o?(r="string"==typeof(r=(r=o.call(i,e,n))||!1)?u(r):r)instanceof u&&(r=r.nodes):r=e.innerHTML,t.concat(!1!==r?r:[])},[])},u.prototype.attr=function(t,e,r){return r=r?"data-":"",this.pairs(t,e,function(t,e){return t.getAttribute(r+e)},function(t,e,n){n?t.setAttribute(r+e,n):t.removeAttribute(r+e);})},u.prototype.before=function(t,e){return this.adjacent(t,e,function(t,e){t.parentNode.insertBefore(e,t);})},u.prototype.children=function(t){return this.map(function(t){return this.slice(t.children)}).filter(t)},u.prototype.clone=function(){return this.map(function(t,e){var n=t.cloneNode(!0),r=this.getAll(n);return this.getAll(t).each(function(t,e){for(var n in this.mirror)this.mirror[n]&&this.mirror[n](t,r.nodes[e]);}),n})},u.prototype.getAll=function(t){return u([t].concat(u("*",t).nodes))},u.prototype.mirror={},u.prototype.mirror.events=function(t,e){if(t._e)for(var n in t._e)t._e[n].forEach(function(t){u(e).on(n,t.callback);});},u.prototype.mirror.select=function(t,e){u(t).is("select")&&(e.value=t.value);},u.prototype.mirror.textarea=function(t,e){u(t).is("textarea")&&(e.value=t.value);},u.prototype.closest=function(e){return this.map(function(t){do{if(u(t).is(e))return t}while((t=t.parentNode)&&t!==document)})},u.prototype.data=function(t,e){return this.attr(t,e,!0)},u.prototype.each=function(t){return this.nodes.forEach(t.bind(this)),this},u.prototype.eacharg=function(n,r){return this.each(function(e,t){this.args(n,e,t).forEach(function(t){r.call(this,e,t);},this);})},u.prototype.empty=function(){return this.each(function(t){for(;t.firstChild;)t.removeChild(t.firstChild);})},u.prototype.filter=function(e){var t=e instanceof u?function(t){return -1!==e.nodes.indexOf(t)}:"function"==typeof e?e:function(t){return t.matches=t.matches||t.msMatchesSelector||t.webkitMatchesSelector,t.matches(e||"*")};return u(this.nodes.filter(t))},u.prototype.find=function(e){return this.map(function(t){return u(e||"*",t)})},u.prototype.first=function(){return this.nodes[0]||!1},u.prototype.generate=function(t){return /^\s* ]/.test(t)?u(document.createElement("table")).html(t).children().children().nodes:/^\s* ]/.test(t)?u(document.createElement("table")).html(t).children().children().children().nodes:/^\s*\n\n\n\n\n"; + + var COMPLETE = 'complete', + CANCELED = 'canceled'; + + function raf(task){ + if('requestAnimationFrame' in window){ + return window.requestAnimationFrame(task); + } + + setTimeout(task, 16); + } + + function setElementScroll$1(element, x, y){ + + if(element.self === element){ + element.scrollTo(x, y); + }else { + element.scrollLeft = x; + element.scrollTop = y; + } + } + + function getTargetScrollLocation(scrollSettings, parent){ + var align = scrollSettings.align, + target = scrollSettings.target, + targetPosition = target.getBoundingClientRect(), + parentPosition, + x, + y, + differenceX, + differenceY, + targetWidth, + targetHeight, + leftAlign = align && align.left != null ? align.left : 0.5, + topAlign = align && align.top != null ? align.top : 0.5, + leftOffset = align && align.leftOffset != null ? align.leftOffset : 0, + topOffset = align && align.topOffset != null ? align.topOffset : 0, + leftScalar = leftAlign, + topScalar = topAlign; + + if(scrollSettings.isWindow(parent)){ + targetWidth = Math.min(targetPosition.width, parent.innerWidth); + targetHeight = Math.min(targetPosition.height, parent.innerHeight); + x = targetPosition.left + parent.pageXOffset - parent.innerWidth * leftScalar + targetWidth * leftScalar; + y = targetPosition.top + parent.pageYOffset - parent.innerHeight * topScalar + targetHeight * topScalar; + x -= leftOffset; + y -= topOffset; + x = scrollSettings.align.lockX ? parent.pageXOffset : x; + y = scrollSettings.align.lockY ? parent.pageYOffset : y; + differenceX = x - parent.pageXOffset; + differenceY = y - parent.pageYOffset; + }else { + targetWidth = targetPosition.width; + targetHeight = targetPosition.height; + parentPosition = parent.getBoundingClientRect(); + var offsetLeft = targetPosition.left - (parentPosition.left - parent.scrollLeft); + var offsetTop = targetPosition.top - (parentPosition.top - parent.scrollTop); + x = offsetLeft + (targetWidth * leftScalar) - parent.clientWidth * leftScalar; + y = offsetTop + (targetHeight * topScalar) - parent.clientHeight * topScalar; + x -= leftOffset; + y -= topOffset; + x = Math.max(Math.min(x, parent.scrollWidth - parent.clientWidth), 0); + y = Math.max(Math.min(y, parent.scrollHeight - parent.clientHeight), 0); + x = scrollSettings.align.lockX ? parent.scrollLeft : x; + y = scrollSettings.align.lockY ? parent.scrollTop : y; + differenceX = x - parent.scrollLeft; + differenceY = y - parent.scrollTop; + } + + return { + x: x, + y: y, + differenceX: differenceX, + differenceY: differenceY + }; + } + + function animate(parent){ + var scrollSettings = parent._scrollSettings; + + if(!scrollSettings){ + return; + } + + var maxSynchronousAlignments = scrollSettings.maxSynchronousAlignments; + + var location = getTargetScrollLocation(scrollSettings, parent), + time = Date.now() - scrollSettings.startTime, + timeValue = Math.min(1 / scrollSettings.time * time, 1); + + if(scrollSettings.endIterations >= maxSynchronousAlignments){ + setElementScroll$1(parent, location.x, location.y); + parent._scrollSettings = null; + return scrollSettings.end(COMPLETE); + } + + var easeValue = 1 - scrollSettings.ease(timeValue); + + setElementScroll$1(parent, + location.x - location.differenceX * easeValue, + location.y - location.differenceY * easeValue + ); + + if(time >= scrollSettings.time){ + scrollSettings.endIterations++; + // Align ancestor synchronously + scrollSettings.scrollAncestor && animate(scrollSettings.scrollAncestor); + animate(parent); + return; + } + + raf(animate.bind(null, parent)); + } + + function defaultIsWindow(target){ + return target.self === target + } + + function transitionScrollTo(target, parent, settings, scrollAncestor, callback){ + var idle = !parent._scrollSettings, + lastSettings = parent._scrollSettings, + now = Date.now(), + cancelHandler, + passiveOptions = { passive: true }; + + if(lastSettings){ + lastSettings.end(CANCELED); + } + + function end(endType){ + parent._scrollSettings = null; + + if(parent.parentElement && parent.parentElement._scrollSettings){ + parent.parentElement._scrollSettings.end(endType); + } + + if(settings.debug){ + console.log('Scrolling ended with type', endType, 'for', parent); + } + + callback(endType); + if(cancelHandler){ + parent.removeEventListener('touchstart', cancelHandler, passiveOptions); + parent.removeEventListener('wheel', cancelHandler, passiveOptions); + } + } + + var maxSynchronousAlignments = settings.maxSynchronousAlignments; + + if(maxSynchronousAlignments == null){ + maxSynchronousAlignments = 3; + } + + parent._scrollSettings = { + startTime: now, + endIterations: 0, + target: target, + time: settings.time, + ease: settings.ease, + align: settings.align, + isWindow: settings.isWindow || defaultIsWindow, + maxSynchronousAlignments: maxSynchronousAlignments, + end: end, + scrollAncestor + }; + + if(!('cancellable' in settings) || settings.cancellable){ + cancelHandler = end.bind(null, CANCELED); + parent.addEventListener('touchstart', cancelHandler, passiveOptions); + parent.addEventListener('wheel', cancelHandler, passiveOptions); + } + + if(idle){ + animate(parent); + } + + return cancelHandler + } + + function defaultIsScrollable(element){ + return ( + 'pageXOffset' in element || + ( + element.scrollHeight !== element.clientHeight || + element.scrollWidth !== element.clientWidth + ) && + getComputedStyle(element).overflow !== 'hidden' + ); + } + + function defaultValidTarget(){ + return true; + } + + function findParentElement(el){ + if (el.assignedSlot) { + return findParentElement(el.assignedSlot); + } + + if (el.parentElement) { + if(el.parentElement.tagName.toLowerCase() === 'body'){ + return el.parentElement.ownerDocument.defaultView || el.parentElement.ownerDocument.ownerWindow; + } + return el.parentElement; + } + + if (el.getRootNode){ + var parent = el.getRootNode(); + if(parent.nodeType === 11) { + return parent.host; + } + } + } + + var scrollIntoView = function(target, settings, callback){ + if(!target){ + return; + } + + if(typeof settings === 'function'){ + callback = settings; + settings = null; + } + + if(!settings){ + settings = {}; + } + + settings.time = isNaN(settings.time) ? 1000 : settings.time; + settings.ease = settings.ease || function(v){return 1 - Math.pow(1 - v, v / 2);}; + settings.align = settings.align || {}; + + var parent = findParentElement(target), + parents = 1; + + function done(endType){ + parents--; + if(!parents){ + callback && callback(endType); + } + } + + var validTarget = settings.validTarget || defaultValidTarget; + var isScrollable = settings.isScrollable; + + if(settings.debug){ + console.log('About to scroll to', target); + + if(!parent){ + console.error('Target did not have a parent, is it mounted in the DOM?'); + } + } + + var scrollingElements = []; + + while(parent){ + if(settings.debug){ + console.log('Scrolling parent node', parent); + } + + if(validTarget(parent, parents) && (isScrollable ? isScrollable(parent, defaultIsScrollable) : defaultIsScrollable(parent))){ + parents++; + scrollingElements.push(parent); + } + + parent = findParentElement(parent); + + if(!parent){ + done(COMPLETE); + break; + } + } + + return scrollingElements.reduce((cancel, parent, index) => transitionScrollTo(target, parent, settings, scrollingElements[index + 1], done), null); + }; + + function assert(assertion, message) { + if (!assertion) throw "TourguideJS: ".concat(message); + return true; + } + + function clamp(number, min, max) { + min = isNaN(min) ? number : min; + max = isNaN(max) ? number : max; + return Math.max(min, Math.min(number, max)); + } + function getDataContents() { + var data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ""; + var defaults = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var parts = data.split(";"); + var result = _objectSpread2({}, defaults); + parts.forEach(function (part) { + var entries = (part || "").split(":"); + result[(entries[0] || "").trim()] = (entries[1] || "").trim(); + }); + return result; + } + function isTargetValid(target) { + return target && target.offsetParent !== null; + } + + /** + * getting bounding client rect and additional properties + * @param {Element | string} element target element or selector + * @param {Element} root root element + * @returns {{ width: number, height: number, top: number, bottom: number, left: number, right: number, viewTop: number, viewBottom: number, viewLeft: number, viewRight: number }} object + */ + function getBoundingClientRect(element, root) { + var rect = u$2(element).size(); + var view = getViewportRect(root); + return { + width: rect.width, + height: rect.height, + top: rect.top + view.scrollY, + bottom: rect.bottom + view.scrollY, + left: rect.left + view.scrollX, + right: rect.right + view.scrollX, + viewTop: rect.top, + viewBottom: rect.bottom, + viewLeft: rect.left, + viewRight: rect.right + }; + } + + /** + * getting viewport rect and additional properties + * @param {Element | string} element target element or selector + * @returns {{ width: number, height: number, scrollX: number, scrollY: number, rootWidth: number, rootHeight: number, rootTop: number, rootLeft: number }} object + */ + function getViewportRect(element) { + try { + var rect = u$2(element).size(); + return { + width: window.innerWidth, + height: window.innerHeight, + scrollX: window.scrollX, + scrollY: window.scrollY, + rootWidth: rect.width, + rootHeight: rect.height, + rootTop: rect.top, + rootLeft: rect.left + }; + } catch (error) { + console.error(error); + throw Error("element is invalid: ".concat(element)); + } + } + function setStyle(element, styleObj) { + Object.assign(u$2(element).first().style, styleObj); + } + + /** + * convert the color object to the sets of css variables + * @param {Object} colors color object + * @param {string} [prefix] prefix of css variable name. default: "--tourguide" + * @param {string} [selector] target css selector. default: ":root" + * @returns {string} converted string + * @example + * input: { overlay: "gray", background: "white", bulletCurrent: "red" } + * output: ":root { --tourguide-overlay: gray; --tourguide-background: white; --tourguide-bullet-current: red; }" + */ + function colorObjToStyleVarString(colors) { + var prefix = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "--tourguide"; + var selector = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ":host"; + var styleArray = []; + Object.entries(colors).forEach(function (_ref) { + var _ref2 = _slicedToArray(_ref, 2), + key = _ref2[0], + value = _ref2[1]; + var splitNameArray = [prefix]; + var prevIndex = 0; + for (var i = 0; i < key.length; i += 1) { + if ("A" <= key[i] && key[i] <= "Z") { + splitNameArray.push(key.substring(prevIndex, i).toLowerCase()); + prevIndex = i; + } + } + splitNameArray.push(key.substring(prevIndex, key.length).toLowerCase()); + styleArray.push("".concat(splitNameArray.join("-"), ": ").concat(value)); + }); + return "".concat(selector, " {\n").concat(styleArray.join(";\n"), ";\n}"); + } + + /** + * scroll element by coordinates (cross browser support) + * @param {Element} element target element + * @param {number} x scroll offset from left + * @param {number} y scroll offset from top + */ + function setElementScroll(element, x, y) { + if (element.self === element) { + element.scrollTo(x, y); + } else { + element.scrollLeft = x; + element.scrollTop = y; + } + } + + /** + * Smooth scroll element by coordinates (cross browser support) + * @param {{ element: Element, x: number, y: number }[]} scrollItems + * @param {number} time duration time + */ + function animateScroll(scrollItems, time) { + var startTime = Date.now(); + function raf(task) { + if ("requestAnimationFrame" in window) { + return window.requestAnimationFrame(task); + } + setTimeout(task, 16); + } + function ease(v) { + return 1 - Math.pow(1 - v, v / 2); + } + function animate(el, x, y) { + if (!el) { + console.warn("target element ".concat(el, " not found, skip")); + return; + } + var diffTime = Date.now() - startTime; + var timeValue = Math.min(1 / time * diffTime, 1); + var easeValue = 1 - ease(timeValue); + var differenceX = x - el.scrollLeft; + var differenceY = y - el.scrollTop; + setElementScroll(el, x - differenceX * easeValue, y - differenceY * easeValue); + if (diffTime >= time) { + setElementScroll(el, x, y); + return; + } + raf(animate.bind(null, el, x, y)); + } + scrollItems.forEach(function (item) { + animate(item.element, item.x, item.y); + }); + } + + /** + * Getting scroll coordinates (cross browser support) + * @param {Element | string} target target element + * @returns {{ element: Element, x: number, y: number }[]} scrollItems + */ + function getScrollCoordinates(target) { + var scrollItems = []; + var targetUEl = u$2(target); + do { + if (!targetUEl) targetUEl = false; + if (!targetUEl.first()) targetUEl = false; + try { + var element = targetUEl.first(); + if (element.scrollHeight !== element.height || element.scrollWidth !== element.width) { + scrollItems.push({ + element: targetUEl.first(), + x: targetUEl.first().scrollLeft, + y: targetUEl.first().scrollTop + }); + } + targetUEl = targetUEl.parent(); + } catch (error) { + targetUEl = false; + } + } while (targetUEl); + return scrollItems; + } + function getMaxZIndex() { + return Math.max.apply(Math, _toConsumableArray(Array.from(document.querySelectorAll('body *'), function (el) { + return parseFloat(window.getComputedStyle(el).zIndex); + }).filter(function (zIndex) { + return !Number.isNaN(zIndex); + })).concat([0])); + } + + function t$1(t){return t.split("-")[1]}function e$1(t){return "y"===t?"height":"width"}function n$2(t){return t.split("-")[0]}function o$1(t){return ["top","bottom"].includes(n$2(t))?"x":"y"}function i$1(i,r,a){let{reference:l,floating:s}=i;const c=l.x+l.width/2-s.width/2,f=l.y+l.height/2-s.height/2,u=o$1(r),m=e$1(u),g=l[m]/2-s[m]/2,d="x"===u;let p;switch(n$2(r)){case"top":p={x:c,y:l.y-s.height};break;case"bottom":p={x:c,y:l.y+l.height};break;case"right":p={x:l.x+l.width,y:f};break;case"left":p={x:l.x-s.width,y:f};break;default:p={x:l.x,y:l.y};}switch(t$1(r)){case"start":p[u]-=g*(a&&d?-1:1);break;case"end":p[u]+=g*(a&&d?-1:1);}return p}const r$2=async(t,e,n)=>{const{placement:o="bottom",strategy:r="absolute",middleware:a=[],platform:l}=n,s=a.filter(Boolean),c=await(null==l.isRTL?void 0:l.isRTL(e));let f=await l.getElementRects({reference:t,floating:e,strategy:r}),{x:u,y:m}=i$1(f,o,c),g=o,d={},p=0;for(let n=0;n({name:"arrow",options:n,async fn(i){const{element:r,padding:l=0}=n||{},{x:s,y:c,placement:f,rects:m,platform:g}=i;if(null==r)return {};const d=a$1(l),p={x:s,y:c},h=o$1(f),y=e$1(h),x=await g.getDimensions(r),w="y"===h?"top":"left",v="y"===h?"bottom":"right",b=m.reference[y]+m.reference[h]-p[h]-m.floating[y],R=p[h]-m.reference[h],A=await(null==g.getOffsetParent?void 0:g.getOffsetParent(r));let P=A?"y"===h?A.clientHeight||0:A.clientWidth||0:0;0===P&&(P=m.floating[y]);const T=b/2-R/2,O=d[w],D=P-x[y]-d[v],E=P/2-x[y]/2+T,L=u$1(O,E,D),k=null!=t$1(f)&&E!=L&&m.reference[y]/2-(Et.concat(e,e+"-start",e+"-end")),[]),p$1={left:"right",right:"left",bottom:"top",top:"bottom"};function h$1(t){return t.replace(/left|right|bottom|top/g,(t=>p$1[t]))}function y$1(n,i,r){void 0===r&&(r=!1);const a=t$1(n),l=o$1(n),s=e$1(l);let c="x"===l?a===(r?"end":"start")?"right":"left":"start"===a?"bottom":"top";return i.reference[s]>i.floating[s]&&(c=h$1(c)),{main:c,cross:h$1(c)}}const x$1={start:"end",end:"start"};function w$1(t){return t.replace(/start|end/g,(t=>x$1[t]))}const v$1=function(e){return void 0===e&&(e={}),{name:"autoPlacement",options:e,async fn(o){var i,r,a;const{rects:l,middlewareData:c,placement:f,platform:u,elements:m}=o,{alignment:g,allowedPlacements:p=d$1,autoAlignment:h=!0,...x}=e,v=void 0!==g||p===d$1?function(e,o,i){return (e?[...i.filter((n=>t$1(n)===e)),...i.filter((n=>t$1(n)!==e))]:i.filter((t=>n$2(t)===t))).filter((n=>!e||t$1(n)===e||!!o&&w$1(n)!==n))}(g||null,h,p):p,b=await s$1(o,x),R=(null==(i=c.autoPlacement)?void 0:i.index)||0,A=v[R];if(null==A)return {};const{main:P,cross:T}=y$1(A,l,await(null==u.isRTL?void 0:u.isRTL(m.floating)));if(f!==A)return {reset:{placement:v[0]}};const O=[b[n$2(A)],b[P],b[T]],D=[...(null==(r=c.autoPlacement)?void 0:r.overflows)||[],{placement:A,overflows:O}],E=v[R+1];if(E)return {data:{index:R+1,overflows:D},reset:{placement:E}};const L=D.slice().sort(((t,e)=>t.overflows[0]-e.overflows[0])),k=null==(a=L.find((t=>{let{overflows:e}=t;return e.every((t=>t<=0))})))?void 0:a.placement,B=k||L[0].placement;return B!==f?{data:{index:R+1,overflows:D},reset:{placement:B}}:{}}}};const O$1=function(e){return void 0===e&&(e=0),{name:"offset",options:e,async fn(i){const{x:r,y:a}=i,l=await async function(e,i){const{placement:r,platform:a,elements:l}=e,s=await(null==a.isRTL?void 0:a.isRTL(l.floating)),c=n$2(r),f=t$1(r),u="x"===o$1(r),m=["left","top"].includes(c)?-1:1,g=s&&u?-1:1,d="function"==typeof i?i(e):i;let{mainAxis:p,crossAxis:h,alignmentAxis:y}="number"==typeof d?{mainAxis:d,crossAxis:0,alignmentAxis:null}:{mainAxis:0,crossAxis:0,alignmentAxis:null,...d};return f&&"number"==typeof y&&(h="end"===f?-1*y:y),u?{x:h*g,y:p*m}:{x:p*m,y:h*g}}(i,e);return {x:r+l.x,y:a+l.y,data:l}}}}; + + function n$1(t){var e;return (null==(e=t.ownerDocument)?void 0:e.defaultView)||window}function o(t){return n$1(t).getComputedStyle(t)}function i(t){return f(t)?(t.nodeName||"").toLowerCase():""}let r$1;function l(){if(r$1)return r$1;const t=navigator.userAgentData;return t&&Array.isArray(t.brands)?(r$1=t.brands.map((t=>t.brand+"/"+t.version)).join(" "),r$1):navigator.userAgent}function c(t){return t instanceof n$1(t).HTMLElement}function s(t){return t instanceof n$1(t).Element}function f(t){return t instanceof n$1(t).Node}function u(t){if("undefined"==typeof ShadowRoot)return !1;return t instanceof n$1(t).ShadowRoot||t instanceof ShadowRoot}function a(t){const{overflow:e,overflowX:n,overflowY:i,display:r}=o(t);return /auto|scroll|overlay|hidden|clip/.test(e+i+n)&&!["inline","contents"].includes(r)}function d(t){return ["table","td","th"].includes(i(t))}function h(t){const e=/firefox/i.test(l()),n=o(t),i=n.backdropFilter||n.WebkitBackdropFilter;return "none"!==n.transform||"none"!==n.perspective||!!i&&"none"!==i||e&&"filter"===n.willChange||e&&!!n.filter&&"none"!==n.filter||["transform","perspective"].some((t=>n.willChange.includes(t)))||["paint","layout","strict","content"].some((t=>{const e=n.contain;return null!=e&&e.includes(t)}))}function p(){return !/^((?!chrome|android).)*safari/i.test(l())}function g(t){return ["html","body","#document"].includes(i(t))}const m=Math.min,y=Math.max,x=Math.round;function w(t){const e=o(t);let n=parseFloat(e.width),i=parseFloat(e.height);const r=t.offsetWidth,l=t.offsetHeight,c=x(n)!==r||x(i)!==l;return c&&(n=r,i=l),{width:n,height:i,fallback:c}}function v(t){return s(t)?t:t.contextElement}const b={x:1,y:1};function L(t){const e=v(t);if(!c(e))return b;const n=e.getBoundingClientRect(),{width:o,height:i,fallback:r}=w(e);let l=(r?x(n.width):n.width)/o,s=(r?x(n.height):n.height)/i;return l&&Number.isFinite(l)||(l=1),s&&Number.isFinite(s)||(s=1),{x:l,y:s}}function E(t,e,o,i){var r,l;void 0===e&&(e=!1),void 0===o&&(o=!1);const c=t.getBoundingClientRect(),f=v(t);let u=b;e&&(i?s(i)&&(u=L(i)):u=L(t));const a=f?n$1(f):window,d=!p()&&o;let h=(c.left+(d&&(null==(r=a.visualViewport)?void 0:r.offsetLeft)||0))/u.x,g=(c.top+(d&&(null==(l=a.visualViewport)?void 0:l.offsetTop)||0))/u.y,m=c.width/u.x,y=c.height/u.y;if(f){const t=n$1(f),e=i&&s(i)?n$1(i):i;let o=t.frameElement;for(;o&&i&&e!==t;){const t=L(o),e=o.getBoundingClientRect(),i=getComputedStyle(o);e.x+=(o.clientLeft+parseFloat(i.paddingLeft))*t.x,e.y+=(o.clientTop+parseFloat(i.paddingTop))*t.y,h*=t.x,g*=t.y,m*=t.x,y*=t.y,h+=e.x,g+=e.y,o=n$1(o).frameElement;}}return {width:m,height:y,top:g,right:h+m,bottom:g+y,left:h,x:h,y:g}}function R(t){return ((f(t)?t.ownerDocument:t.document)||window.document).documentElement}function T(t){return s(t)?{scrollLeft:t.scrollLeft,scrollTop:t.scrollTop}:{scrollLeft:t.pageXOffset,scrollTop:t.pageYOffset}}function C(t){return E(R(t)).left+T(t).scrollLeft}function F(t,e,n){const o=c(e),r=R(e),l=E(t,!0,"fixed"===n,e);let s={scrollLeft:0,scrollTop:0};const f={x:0,y:0};if(o||!o&&"fixed"!==n)if(("body"!==i(e)||a(r))&&(s=T(e)),c(e)){const t=E(e,!0);f.x=t.x+e.clientLeft,f.y=t.y+e.clientTop;}else r&&(f.x=C(r));return {x:l.left+s.scrollLeft-f.x,y:l.top+s.scrollTop-f.y,width:l.width,height:l.height}}function W(t){if("html"===i(t))return t;const e=t.assignedSlot||t.parentNode||(u(t)?t.host:null)||R(t);return u(e)?e.host:e}function D(t){return c(t)&&"fixed"!==o(t).position?t.offsetParent:null}function S(t){const e=n$1(t);let r=D(t);for(;r&&d(r)&&"static"===o(r).position;)r=D(r);return r&&("html"===i(r)||"body"===i(r)&&"static"===o(r).position&&!h(r))?e:r||function(t){let e=W(t);for(;c(e)&&!g(e);){if(h(e))return e;e=W(e);}return null}(t)||e}function A(t){const e=W(t);return g(e)?t.ownerDocument.body:c(e)&&a(e)?e:A(e)}function H(t,e){var o;void 0===e&&(e=[]);const i=A(t),r=i===(null==(o=t.ownerDocument)?void 0:o.body),l=n$1(i);return r?e.concat(l,l.visualViewport||[],a(i)?i:[]):e.concat(i,H(i))}function O(e,i,r){return "viewport"===i?l$1(function(t,e){const o=n$1(t),i=R(t),r=o.visualViewport;let l=i.clientWidth,c=i.clientHeight,s=0,f=0;if(r){l=r.width,c=r.height;const t=p();(t||!t&&"fixed"===e)&&(s=r.offsetLeft,f=r.offsetTop);}return {width:l,height:c,x:s,y:f}}(e,r)):s(i)?function(t,e){const n=E(t,!0,"fixed"===e),o=n.top+t.clientTop,i=n.left+t.clientLeft,r=c(t)?L(t):{x:1,y:1},l=t.clientWidth*r.x,s=t.clientHeight*r.y,f=i*r.x,u=o*r.y;return {top:u,left:f,right:f+l,bottom:u+s,x:f,y:u,width:l,height:s}}(i,r):l$1(function(t){var e;const n=R(t),i=T(t),r=null==(e=t.ownerDocument)?void 0:e.body,l=y(n.scrollWidth,n.clientWidth,r?r.scrollWidth:0,r?r.clientWidth:0),c=y(n.scrollHeight,n.clientHeight,r?r.scrollHeight:0,r?r.clientHeight:0);let s=-i.scrollLeft+C(t);const f=-i.scrollTop;return "rtl"===o(r||n).direction&&(s+=y(n.clientWidth,r?r.clientWidth:0)-l),{width:l,height:c,x:s,y:f}}(R(e)))}const P={getClippingRect:function(t){let{element:e,boundary:n,rootBoundary:r,strategy:l}=t;const c="clippingAncestors"===n?function(t,e){const n=e.get(t);if(n)return n;let r=H(t).filter((t=>s(t)&&"body"!==i(t))),l=null;const c="fixed"===o(t).position;let f=c?W(t):t;for(;s(f)&&!g(f);){const t=o(f),e=h(f);(c?e||l:e||"static"!==t.position||!l||!["absolute","fixed"].includes(l.position))?l=t:r=r.filter((t=>t!==f)),f=W(f);}return e.set(t,r),r}(e,this._c):[].concat(n),f=[...c,r],u=f[0],a=f.reduce(((t,n)=>{const o=O(e,n,l);return t.top=y(o.top,t.top),t.right=m(o.right,t.right),t.bottom=m(o.bottom,t.bottom),t.left=y(o.left,t.left),t}),O(e,u,l));return {width:a.right-a.left,height:a.bottom-a.top,x:a.left,y:a.top}},convertOffsetParentRelativeRectToViewportRelativeRect:function(t){let{rect:e,offsetParent:n,strategy:o}=t;const r=c(n),l=R(n);if(n===l)return e;let s={scrollLeft:0,scrollTop:0},f={x:1,y:1};const u={x:0,y:0};if((r||!r&&"fixed"!==o)&&(("body"!==i(n)||a(l))&&(s=T(n)),c(n))){const t=E(n);f=L(n),u.x=t.x+n.clientLeft,u.y=t.y+n.clientTop;}return {width:e.width*f.x,height:e.height*f.y,x:e.x*f.x-s.scrollLeft*f.x+u.x,y:e.y*f.y-s.scrollTop*f.y+u.y}},isElement:s,getDimensions:function(t){return w(t)},getOffsetParent:S,getDocumentElement:R,getScale:L,async getElementRects(t){let{reference:e,floating:n,strategy:o}=t;const i=this.getOffsetParent||S,r=this.getDimensions;return {reference:F(e,await i(n),o),floating:{x:0,y:0,...await r(n)}}},getClientRects:t=>Array.from(t.getClientRects()),isRTL:t=>"rtl"===o(t).direction};const V=(t,n,o)=>{const i=new Map,r={platform:P,...o},l={...r.platform,_c:i};return r$2(t,n,{...r,platform:l})}; + + var e={"":["",""],_:["",""],"*":["",""],"~":["",""],"\n":["
    "]," ":["
    "],"-":["
    "]};function n(e){return e.replace(RegExp("^"+(e.match(/^(\t| )+/)||"")[0],"gm"),"")}function r(e){return (e+"").replace(/"/g,""").replace(//g,">")}function t(a,c){var o,l,g,s,p,u=/((?:^|\n+)(?:\n---+|\* \*(?: \*)+)\n)|(?:^``` *(\w*)\n([\s\S]*?)\n```$)|((?:(?:^|\n+)(?:\t| {2,}).+)+\n*)|((?:(?:^|\n)([>*+-]|\d+\.)\s+.*)+)|(?:!\[([^\]]*?)\]\(([^)]+?)\))|(\[)|(\](?:\(([^)]+?)\))?)|(?:(?:^|\n+)([^\s].*)\n(-{3,}|={3,})(?:\n+|$))|(?:(?:^|\n+)(#{1,6})\s*(.+)(?:\n+|$))|(?:`([^`].*?)`)|( \n\n*|\n{2,}|__|\*\*|[_*]|~~)/gm,m=[],h="",i=c||{},d=0;function f(n){var r=e[n[1]||""],t=m[m.length-1]==n;return r?r[1]?(t?m.pop():m.push(n),r[0|t]):r[0]:n}function $(){for(var e="";m.length;)e+=f(m[m.length-1]);return e}for(a=a.replace(/^\[(.+?)\]:\s*(.+)$/gm,function(e,n,r){return i[n.toLowerCase()]=r,""}).replace(/^\n+|\n+$/g,"");g=u.exec(a);)l=a.substring(d,g.index),d=u.lastIndex,o=g[0],l.match(/[^\\](\\\\)*\\$/)||((p=g[3]||g[4])?o='
    "+n(r(p).replace(/^\n+|\n+$/g,""))+"
    ":(p=g[6])?(p.match(/\./)&&(g[5]=g[5].replace(/^\d+/gm,"")),s=t(n(g[5].replace(/^\s*[>*+.-]/gm,""))),">"==p?p="blockquote":(p=p.match(/\./)?"ol":"ul",s=s.replace(/^(.*)(\n|$)/gm,"
  • $1
  • ")),o="<"+p+">"+s+""):g[8]?o=''+r(g[7])+'':g[10]?(h=h.replace("",''),o=$()+""):g[9]?o="":g[12]||g[14]?o="<"+(p="h"+(g[14]?g[14].length:g[13]>"="?1:2))+">"+t(g[12]||g[15],i)+"":g[16]?o=""+r(g[16])+"":(g[17]||g[1])&&(o=f(g[17]||"--"))),h+=l,h+=o;return (h+a.substring(d)+$()).replace(/^\n+|\n+$/g,"")} + + var keepinview = function keepinview(_ref) { + var _ref$padding = _ref.padding, + padding = _ref$padding === void 0 ? 0 : _ref$padding; + return { + name: "keepinview", + fn: function fn(_ref2) { + var x = _ref2.x, + y = _ref2.y, + rects = _ref2.rects, + middlewareData = _ref2.middlewareData, + platform = _ref2.platform; + var documentDimentions = platform.getDimensions(document.body); + var _x = clamp(x, padding, documentDimentions.width - rects.floating.width - padding); + var _y = clamp(y, padding, documentDimentions.height - rects.floating.height - padding); + var dx = x - _x; + var dy = y - _y; + var arrow = middlewareData.arrow; + if (arrow) { + if (arrow.x && dx) arrow.x += dx; + if (arrow.y && dy) arrow.y += dy; + } + return { + x: _x, + y: _y + }; + } + }; + }; + function positionTooltip(target, tooltipEl, arrowEl, context) { + //context._options.root + V(target, tooltipEl, { + // placement: 'bottom-start', + middleware: [ + // flip(), + v$1({ + alignment: 'bottom-start' + }), O$1(function (props) { + var side = props.placement.split("-")[0]; + switch (side) { + case "top": + return 32; + case "left": + case "right": + return 24; + default: + return 6; + } + }), m$1({ + element: arrowEl, + padding: 8 + }), keepinview({ + padding: 24 + })] + }).then(function (_ref3) { + var x = _ref3.x, + y = _ref3.y, + middlewareData = _ref3.middlewareData, + placement = _ref3.placement; + setStyle(tooltipEl, { + left: "".concat(x, "px"), + top: "".concat(y, "px") + }); + if (middlewareData.arrow) { + var side = placement.split("-")[0]; + var staticSide = { + top: "bottom", + right: "left", + bottom: "top", + left: "right" + }[side]; + setStyle(arrowEl, _defineProperty({ + left: middlewareData.arrow.x != null ? "".concat(middlewareData.arrow.x, "px") : '', + top: middlewareData.arrow.y != null ? "".concat(middlewareData.arrow.y, "px") : '', + right: "", + bottom: "" + }, staticSide, "".concat(-arrowEl.offsetWidth / 2, "px"))); + } + }); + } + var Step = /*#__PURE__*/function () { + function Step(step, context) { + var _this = this; + _classCallCheck(this, Step); + this.active = false; + this.first = false; + this.last = false; + this.container = null; + this.highlight = null; + this.tooltip = null; + this.arrow = null; + this.context = context; + this._target = null; + this._timerHandler = null; + this._scrollCancel = null; + var data; + if (!(step instanceof HTMLElement)) { + data = step; + this._selector = step.selector; + } else { + this.target = step; + data = getDataContents(u$2(step).data("tour")); + } + assert(data.hasOwnProperty("title"), "missing required step parameter: title\n" + JSON.stringify(data, null, 2) + "\n" + "see this doc for more detail: https://github.com/LikaloLLC/tourguide.js#json-based-approach"); + assert(data.hasOwnProperty("content"), "missing required step parameter: content\n" + JSON.stringify(data, null, 2) + "\n" + "see this doc for more detail: https://github.com/LikaloLLC/tourguide.js#json-based-approach"); + this.index = parseInt(data.step); + this.title = data.title; + this.content = t(data.content); + this.image = data.image; + this.width = data.width; + this.height = data.height; + this.layout = data.layout || "vertical"; + this.placement = data.placement || "bottom"; + this.overlay = data.overlay !== false; + this.navigation = data.navigation !== false; + if (data.image && context.options.preloadimages && !/^data:/i.test(data.image)) { + var preload = new Image(); + // preload.onload = (e) => { + // }; + preload.onerror = function () { + console.error(new Error("Invalid image URL: ".concat(data.image))); + _this.image = null; + }; + preload.src = this.image; + } + this.actions = []; + if (data.actions) { + if (!Array.isArray(data.actions)) { + console.error(new Error("actions must be array but got ".concat(_typeof(data.actions)))); + } else { + this.actions = data.actions; + } + } + // this.adjust = this.adjust.bind(this); + } + _createClass(Step, [{ + key: "el", + get: function get() { + var _this2 = this; + if (!this.container) { + var image = u$2("
    ".concat(this.image ? "") : "", "
    ")); + var content = u$2("
    \n
    ").concat(this.context._decorateText(this.title, this), "
    \n
    ").concat(this.context._decorateText(this.content, this), "
    \n
    ")); + content.find('a').on('click', function (e) { + _this2.context.action(e, { + action: "link" + }); + }); + if (Array.isArray(this.actions) && this.actions.length > 0) { + var actions = u$2("
    \n ".concat(this.actions.map(function (action, index) { + return "<".concat(action.href ? "a" : "button", " id=\"").concat(action.id, "\" ").concat(action.href ? "href=\"".concat(action.href, "\"") : "", " ").concat(action.target ? "target=\"".concat(action.target, "\"") : "", " class=\"button").concat(action.primary ? " primary" : "", "\" data-index=\"").concat(index, "\">").concat(action.label, ""); + }).join(""), "\n
    ")); + actions.find('a, button').on('click', function (e) { + var action = _this2.actions[parseInt(e.target.dataset.index)]; + if (action.action) e.preventDefault(); + _this2.context.action(e, action); + }); + content.append(actions); + } + var tooltip = this.tooltip = u$2("
    "); + if (this.width) setStyle(tooltip, { + width: this.width + "px", + maxWidth: this.width + "px" + }); + if (this.height) setStyle(tooltip, { + height: this.height + "px", + maxHeight: this.height + "px" + }); + var tooltipinner = u$2("
    ")); + var container = u$2("
    "); + container.append(image).append(content); + var _arrow = this.arrow = u$2("
    "); + if (this.navigation) { + var footer = u$2("
    \n \n ".concat(!this.first ? "" : "", "\n ").concat(this.last ? "" : "", "\n ").concat(this.context._steps.length > 1 ? "
    \n
      ".concat(this.context._steps.map(function (step, i) { + return "
    • "); + }).join(""), "
    \n
    ") : "", "\n
    ")); + footer.find(".guided-tour-step-button-prev").on("click", this.context.previous); + footer.find(".guided-tour-step-button-next").on("click", this.context.next); + footer.find(".guided-tour-step-button-close").on("click", this.context.stop); + footer.find(".guided-tour-step-button-complete").on("click", this.context.complete); + footer.find(".guided-tour-step-bullets button").on("click", function (e) { + return _this2.context.go(parseInt(u$2(e.target).data("index"))); + }); + tooltipinner.append(_arrow).append(container).append(footer); + } else tooltipinner.append(_arrow).append(container); + tooltip.append(tooltipinner); + this.container = u$2("
    ")); + if (this.overlay && isTargetValid(this.target)) { + var highlight = this.highlight = u$2("
    "); + this.container.append(highlight).append(tooltip); + } else this.container.append(tooltip); + } + return this.container; + } + }, { + key: "target", + get: function get() { + return this._target || this._selector && u$2(this._selector).first(); + }, + set: function set(target) { + this._target = target; + } + }, { + key: "attach", + value: function attach(root) { + u$2(root).append(this.el); + } + }, { + key: "remove", + value: function remove() { + this.hide(); + this.el.remove(); + } + }, { + key: "position", + value: function position() { + var view = getViewportRect(this.context._options.root); + var tooltip = this.tooltip; + var highlight = this.highlight; + var highlightStyle = { + top: 0, + left: 0, + width: 0, + height: 0 + }; + if (isTargetValid(this.target)) { + if (this.overlay && this.highlight) { + var targetRect = getBoundingClientRect(this.target, this.context._options.root); + highlightStyle.top = targetRect.top - this.context.options.padding; + highlightStyle.left = targetRect.left - this.context.options.padding; + highlightStyle.width = targetRect.width + this.context.options.padding * 2; + highlightStyle.height = targetRect.height + this.context.options.padding * 2; + setStyle(highlight, highlightStyle); + } + positionTooltip(this.target, tooltip.first(), this.arrow.first(), this.context); + } else { + if (this.overlay && this.highlight) setStyle(highlight, highlightStyle); + var tootipStyle = {}; + var tooltipRect = getBoundingClientRect(tooltip, this.context._options.root); + tootipStyle.top = view.height / 2 + view.scrollY - view.rootTop - tooltipRect.height / 2; + tootipStyle.left = view.width / 2 + view.scrollX - view.rootLeft - tooltipRect.width / 2; + tootipStyle.bottom = "unset"; + tootipStyle.right = "unset"; + tooltip.addClass("guided-tour-arrow-none"); + setStyle(tooltip, tootipStyle); + if (this.overlay) this.context._overlay.show(); + } + } + }, { + key: "cancel", + value: function cancel() { + if (this._timerHandler) clearTimeout(this._timerHandler); + if (this._scrollCancel) this._scrollCancel(); + } + }, { + key: "show", + value: function show() { + var _this3 = this; + this.cancel(); + if (!this.active) { + var show = function show() { + _this3.el.addClass("active"); // Add 'active' first to calculate the tooltip real size on the DOM. + _this3.context._overlay.hide(); + _this3.position(); + _this3.active = true; + _this3.container.find(".guided-tour-step-tooltip, button.primary, .guided-tour-step-button-complete, .guided-tour-step-button-next").last().focus({ + preventScroll: true + }); + }; + var animationspeed = clamp(this.context.options.animationspeed, 120, 1000); + if (isTargetValid(this.target)) { + this._scrollCancel = scrollIntoView(this.target, { + time: animationspeed, + cancellable: false, + align: { + top: 0.5, + left: 0.5 + } + }); + } + this._timerHandler = setTimeout(show, animationspeed * 3); + return true; + } + return false; + } + }, { + key: "hide", + value: function hide() { + this.cancel(); + if (this.active) { + this.el.removeClass("active"); + this.tooltip.removeClass("guided-tour-arrow-top"); + this.tooltip.removeClass("guided-tour-arrow-bottom"); + if (this.overlay) this.context._overlay.show(); + this.active = false; + return true; + } + return false; + } + }, { + key: "toJSON", + value: function toJSON() { + var index = this.index, + title = this.title, + content = this.content, + image = this.image, + active = this.active; + return { + index: index, + title: title, + content: content, + image: image, + active: active + }; + } + }]); + return Step; + }(); + + var Overlay = /*#__PURE__*/function () { + function Overlay(context) { + _classCallCheck(this, Overlay); + this.context = context; + this.container = null; + this.active = false; + } + _createClass(Overlay, [{ + key: "el", + get: function get() { + if (!this.container) { + this.container = u$2("
    "); + } + return this.container; + } + }, { + key: "attach", + value: function attach(root) { + u$2(root).append(this.el); + } + }, { + key: "remove", + value: function remove() { + this.hide(); + this.el.remove(); + } + }, { + key: "show", + value: function show() { + if (!this.active) { + this.el.addClass("active"); + this.active = true; + return true; + } + return false; + } + }, { + key: "hide", + value: function hide() { + if (this.active) { + this.el.removeClass("active"); + this.active = false; + return true; + } + return false; + } + }, { + key: "toJSON", + value: function toJSON() { + var active = this.active; + return { + active: active + }; + } + }]); + return Overlay; + }(); + + function hexToRGB(hex) { + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)] : null; + } + function componentToHex(c) { + var hex = c.toString(16); + return hex.length == 1 ? "0" + hex : hex; + } + function rgbToHex(r, g, b) { + return "#" + componentToHex(Math.floor(r)) + componentToHex(Math.floor(g)) + componentToHex(Math.floor(b)); + } + function RGBToHSL(r, g, b) { + r /= 255; + g /= 255; + b /= 255; + var l = Math.max(r, g, b); + var s = l - Math.min(r, g, b); + var h = s ? l === r ? (g - b) / s : l === g ? 2 + (b - r) / s : 4 + (r - g) / s : 0; + return [60 * h < 0 ? 60 * h + 360 : 60 * h, 100 * (s ? l <= 0.5 ? s / (2 * l - s) : s / (2 - (2 * l - s)) : 0), 100 * (2 * l - s) / 2]; + } + function HSLToRGB(h, s, l) { + s /= 100; + l /= 100; + var k = function k(n) { + return (n + h / 30) % 12; + }; + var a = s * Math.min(l, 1 - l); + var f = function f(n) { + return l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1))); + }; + return [255 * f(0), 255 * f(8), 255 * f(4)]; + } + function hexToHSL(hex) { + return RGBToHSL.apply(null, hexToRGB(hex)); + } + function HSLToHex(h, s, l) { + return rgbToHex.apply(null, HSLToRGB(h, s, l)); + } + function adjust(hex, h, s, l) { + var hsl = hexToHSL(hex); + hsl[0] = clamp(hsl[0] * h, 0, 255); + hsl[1] = clamp(hsl[1] * s, 0, 255); + hsl[2] = clamp(hsl[2] * l, 0, 255); + return HSLToHex.apply(null, hsl); + } + function setAutoColors(defaultStyle, optionsStyle) { + var style = Object.assign(defaultStyle, optionsStyle || {}); + var filter = /Color$/; + var accentColor = style.accentColor; + Object.keys(style).filter(function (key) { + return filter.test(key) && style[key] === "auto"; + }).forEach(function (key) { + switch (key) { + case "focusColor": + case "stepButtonNextColor": + case "stepButtonCompleteColor": + case "bulletCurrentColor": + style[key] = accentColor; + break; + case "bulletColor": + style[key] = adjust(accentColor, 1, 0.8, 1.4); + break; + case "bulletVisitedColor": + style[key] = adjust(accentColor, 1, 0.3, 1.2); + break; + case "stepButtonPrevColor": + case "stepButtonCloseColor": + style[key] = adjust(accentColor, 1, 0.2, 0.8); + break; + } + }); + return style; + } + + var ActionHandler = /*#__PURE__*/_createClass(function ActionHandler(name, handlerFn) { + _classCallCheck(this, ActionHandler); + this.name = name; + this.onAction = handlerFn; + }); + + function parseProperties(props) { + return (props || "").split(",").map(function (p) { + return p.trim(); + }).filter(Boolean); + } + function getMatches(str, regex) { + var matches = [], + m; + regex.lastIndex = 0; + while ((m = regex.exec(str)) !== null) { + // This is necessary to avoid infinite loops with zero-width matches + if (m.index === regex.lastIndex) { + regex.lastIndex++; + } + + // The result can be accessed through the `m`-variable. + // m.forEach((match, groupIndex) => { + matches.push({ + match: m[0], + start: m.index, + length: m[0].length, + properties: parseProperties(m[1]) + }); + // }); + } + + return matches; + } + var ContentDecorator = /*#__PURE__*/function () { + function ContentDecorator(match, decoratorFn) { + _classCallCheck(this, ContentDecorator); + if (typeof match === 'string') this.match = new RegExp("{s*".concat(match.trim(), "s*(,.+?)?s*?}"), 'gmi');else this.match = match; + this.decoratorFn = decoratorFn; + } + _createClass(ContentDecorator, [{ + key: "test", + value: function test(text) { + return this.match.test(text); + } + }, { + key: "render", + value: function render(text, step, context) { + try { + var matches = getMatches(text, this.match).reverse(); + return this.decoratorFn(text, matches, step, context); + } catch (e) { + console.warn(e); + return text; + } + } + }]); + return ContentDecorator; + }(); + + var Style = ":host {\n position: absolute;\n overflow: visible;\n top: 0;\n left: 0;\n width: 0;\n height: 0;\n box-sizing: border-box;\n line-height: 1.4;\n text-align: left;\n text-rendering: optimizespeed;\n font-family: var(--tourguide-font-family);\n font-size: var(--tourguide-font-size);\n color: var(--tourguide-text-color);\n /* 1 */\n -webkit-text-size-adjust: 100%;\n /* 2 */\n -moz-tab-size: 4;\n /* 3 */\n tab-size: 4;\n /* 3 */\n}\n\n* {\n margin: 0;\n padding: 0;\n background: none;\n border: none;\n border-width: 0;\n border-style: none;\n border-color: currentColor;\n box-shadow: none;\n color: inherit;\n appearance: none;\n font-size: inherit;\n font-weight: inherit;\n text-decoration: none;\n}\n\na,\nbutton {\n cursor: pointer;\n}\na:hover, a:focus,\nbutton:hover,\nbutton:focus {\n outline: 5px auto var(--tourguide-focus-color);\n}\n\n.guided-tour-overlay {\n display: none;\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n z-index: 1;\n background-color: var(--tourguide-overlay-color);\n}\n.guided-tour-overlay.active {\n display: block;\n}\n\n.guided-tour-step {\n display: none;\n}\n.guided-tour-step.active {\n display: block;\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: all;\n}\n.guided-tour-step.active .guided-tour-step-highlight {\n position: absolute;\n box-sizing: border-box;\n border-radius: 4px;\n box-shadow: 0 0 0 999em var(--tourguide-overlay-color);\n z-index: 1;\n}\n.guided-tour-step.active .guided-tour-step-tooltip {\n position: absolute;\n margin: 16px 0;\n z-index: 2;\n background-color: var(--tourguide-background-color);\n width: var(--tourguide-tooltip-width);\n max-width: max-content;\n border-radius: 5px;\n box-sizing: border-box;\n box-shadow: 0 0 3em -0.8em #000;\n transition: opacity 150ms;\n}\n@media screen and (max-width: 760px) {\n .guided-tour-step.active .guided-tour-step-tooltip {\n max-width: 85vw;\n width: max-content !important;\n }\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner {\n display: flex;\n flex-direction: column;\n height: 100%;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-arrow {\n position: absolute;\n width: 1em;\n height: 1em;\n background: var(--tourguide-background-color);\n z-index: -1;\n transform: rotate(45deg);\n pointer-events: none;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content-container {\n display: flex;\n flex-direction: column;\n flex-grow: 1;\n height: calc(100% - 2.6em);\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-image {\n flex-grow: 1;\n flex-shrink: 1;\n overflow: hidden;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-image img {\n width: 100%;\n height: 100%;\n border-radius: 4px 4px 0 0;\n object-fit: cover;\n object-position: center;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content-wrapper {\n margin: 1.5em 2.5em;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-title {\n font-size: 1.4em;\n margin-bottom: 0.8em;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content {\n flex-shrink: 0;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content b,\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content strong {\n font-weight: bold;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content i,\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content em {\n font-style: italic;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content a {\n cursor: pointer;\n text-decoration: underline;\n color: var(--tourguide-accent-color);\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content mark {\n background: inherit;\n text-shadow: 0px 2px 4px #ff0;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content code,\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content dfn {\n padding: 1px 6px 1px 4px;\n border-radius: 4px;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content code {\n background-color: #f0f0f0;\n color: #e83e8c;\n font-family: monospace;\n font-size: 87.5%;\n word-break: break-word;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content dfn {\n font-style: italic;\n background-color: #ffc6e5;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content p,\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content ul,\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content ol,\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content blockquote {\n margin: 1em 0;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content p:last-child,\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content ul:last-child,\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content ol:last-child,\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content blockquote:last-child {\n margin-bottom: 0;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content blockquote {\n padding-left: 1em;\n border-left: 4px solid silver;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content ul,\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content ol {\n padding-left: 1.5em;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content ul li,\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-content ol li {\n margin: 0.3em 0;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-actions {\n display: flex;\n column-gap: 0.5em;\n margin-top: 1.5em;\n justify-content: end;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-actions .button {\n color: var(--tourguide-accent-color);\n padding: 0.5em 1em;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-actions .button.primary {\n background: var(--tourguide-accent-color);\n padding: 0.5em 1.5em;\n color: #fff;\n border-radius: 4px;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-actions .button.primary:hover, .guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-actions .button.primary:focus {\n filter: brightness(120%);\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-icon {\n display: inline-block;\n overflow: hidden;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-button {\n flex-direction: column;\n justify-content: center;\n /* <-- actual veertical align */\n display: inline-flex;\n text-align: center;\n cursor: pointer;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-button .guided-tour-icon {\n align-self: center;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-button-close {\n position: absolute;\n top: 0;\n right: 0;\n width: 2em;\n height: 2em;\n color: var(--tourguide-step-button-close-color);\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-button-prev,\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-button-next,\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-button-complete {\n width: 3em;\n height: 3em;\n background: var(--tourguide-background-color);\n border-radius: 50%;\n margin-top: -1.5em;\n position: absolute;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-button-prev {\n color: var(--tourguide-step-button-prev-color);\n left: 0;\n transform: translateX(-50%);\n top: 50%;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-button-next {\n color: var(--tourguide-step-button-next-color);\n right: 0;\n transform: translateX(50%);\n top: 50%;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-button-complete {\n color: var(--tourguide-step-button-complete-color);\n right: 0;\n transform: translateX(50%);\n top: 50%;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-footer {\n flex-shrink: 0;\n flex-grow: 0;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-bullets {\n text-align: center;\n line-height: 16px;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-bullets ul {\n list-style: none;\n margin: 0 1em 1em;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-bullets ul li {\n display: inline-block;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-bullets ul li button {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n display: inline-block;\n background-color: var(--tourguide-bullet-color);\n border: 8px solid var(--tourguide-background-color);\n box-sizing: content-box;\n cursor: pointer;\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-bullets ul li button.complete {\n background-color: var(--tourguide-bullet-visited-color);\n}\n.guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner .guided-tour-step-bullets ul li button.current {\n background-color: var(--tourguide-bullet-current-color);\n}\n@media screen and (min-width: 760px) {\n .guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner.step-layout-horizontal .guided-tour-step-content-container {\n flex-direction: row;\n height: 100%;\n }\n .guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner.step-layout-horizontal .guided-tour-step-content-container .guided-tour-step-content-wrapper {\n flex: 1 1 auto;\n }\n .guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner.step-layout-horizontal .guided-tour-step-content-container .guided-tour-step-image {\n width: 50%;\n margin-bottom: calc((1em + 24px) * -1);\n flex: 0 0 auto;\n }\n .guided-tour-step.active .guided-tour-step-tooltip .guided-tour-step-tooltip-inner.step-layout-horizontal .guided-tour-step-content-container .guided-tour-step-image img {\n border-radius: 4px 0 0 4px;\n height: 100%;\n object-fit: cover;\n object-position: center;\n }\n}\n.guided-tour-step.active .guided-tour-step-tooltip.guided-tour-arrow-none .guided-tour-step-tooltip-inner .guided-tour-arrow {\n display: none;\n}"; + + var StepsSource = { + DOM: 0, + JSON: 1, + REMOTE: 2 + }; + var defaultKeyNavOptions = { + next: "ArrowRight", + prev: "ArrowLeft", + first: "Home", + last: "End", + complete: null, + stop: "Escape" + }; + var defaultStyle = { + fontFamily: 'sans-serif', + fontSize: "14px", + tooltipWidth: "40vw", + overlayColor: "rgba(0, 0, 0, 0.5)", + textColor: "#333", + accentColor: "#0d6efd", + focusColor: "auto", + bulletColor: "auto", + bulletVisitedColor: "auto", + bulletCurrentColor: "auto", + stepButtonCloseColor: "auto", + stepButtonPrevColor: "auto", + stepButtonNextColor: "auto", + stepButtonCompleteColor: "auto", + backgroundColor: "#fff" + }; + function isEventAttrbutesMatched(event, keyOption) { + var type = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "keyup"; + if (_typeof(event) === "object") { + var eventAttrsMap = { + type: type + }; + if (typeof keyOption === "number") { + eventAttrsMap.keyCode = keyOption; + } else if (typeof keyOption === "string") { + eventAttrsMap.key = keyOption; + } else if (_typeof(keyOption) === "object") { + eventAttrsMap = _objectSpread2(_objectSpread2({}, keyOption), {}, { + type: type + }); + } else { + throw new Error("keyboardNavigation option invalid. should be predefined object or false. Check documentation."); + } + var eventAttrs = Object.entries(eventAttrsMap).map(function (_ref) { + var _ref2 = _slicedToArray(_ref, 2), + key = _ref2[0], + value = _ref2[1]; + return { + key: key, + value: value + }; + }); + return !eventAttrs.filter(function (attr) { + return event[attr.key] !== attr.value; + }).length; + } + return false; + } + var Tour = /*#__PURE__*/function () { + function Tour() { + var _this = this; + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + _classCallCheck(this, Tour); + this._options = Object.assign({ + root: "body", + selector: "[data-tour]", + animationspeed: 120, + padding: 5, + steps: null, + src: null, + restoreinitialposition: true, + preloadimages: false, + request: { + options: { + mode: "cors", + cache: "no-cache" + }, + headers: { + "Content-Type": "application/json" + } + }, + keyboardNavigation: defaultKeyNavOptions, + actionHandlers: [], + contentDecorators: [], + onStart: function onStart() {}, + onStop: function onStop() {}, + onComplete: function onComplete() {}, + onStep: function onStep() {}, + onAction: function onAction() {} + }, options, { + style: setAutoColors(defaultStyle, options.colors || options.style) + }); + this._overlay = null; + this._steps = []; + this._current = 0; + this._active = false; + this._stepsSrc = StepsSource.DOM; + this._ready = false; + this._initialposition = null; + if (_typeof(this._options.steps) === "object" && Array.isArray(this._options.steps)) { + this._stepsSrc = StepsSource.JSON; + this._steps = this._options.steps.map(function (o, index) { + return new Step(_objectSpread2(_objectSpread2({}, o), {}, { + step: o.step || index + }), _this); + }); + this._ready = true; + } else if (typeof this._options.src === "string") { + this._stepsSrc = StepsSource.REMOTE; + fetch(new Request(this._options.src, this._options.request)).then(function (response) { + return response.json().then(function (data) { + _this._steps = data.map(function (o, index) { + return new Step(_objectSpread2(_objectSpread2({}, o), {}, { + step: o.step || index + }), _this); + }); + _this._ready = true; + }); + }); + } else if (u$2(this._options.selector).length > 0) { + this._stepsSrc = StepsSource.DOM; + this._ready = true; + } else { + throw new Error("Tour is not configured properly. Check documentation."); + } + this._containerElement = document.createElement("aside"); + this._containerElement.classList.add("__guided-tour-container"); + u$2(this._options.root).append(this._containerElement); + this._shadowRoot = this._containerElement.attachShadow({ + mode: "closed" + }); + this._injectIcons(); + this._injectStyles(); + this.start = this.start.bind(this); + this.next = this.next.bind(this); + this.previous = this.previous.bind(this); + this.go = this.go.bind(this); + this.stop = this.stop.bind(this); + this.complete = this.complete.bind(this); + // this.action = this.action.bind(this); + this._keyboardHandler = this._keyboardHandler.bind(this); + } + _createClass(Tour, [{ + key: "currentstep", + get: function get() { + return this._steps[this._current]; + } + }, { + key: "length", + get: function get() { + return this._steps.length; + } + }, { + key: "steps", + get: function get() { + return this._steps.map(function (step) { + return step.toJSON(); + }); + } + }, { + key: "hasnext", + get: function get() { + return this.nextstep !== this._current; + } + }, { + key: "nextstep", + get: function get() { + return clamp(this._current + 1, 0, this.length - 1); + } + }, { + key: "previousstep", + get: function get() { + return clamp(this._current - 1, 0); + } + }, { + key: "options", + get: function get() { + return this._options; + } + }, { + key: "_injectIcons", + value: function _injectIcons() { + if (u$2("#GuidedTourIconSet", this._shadowRoot).length === 0) { + u$2(this._shadowRoot).append(u$2(Icons)); + } + } + }, { + key: "_injectStyles", + value: function _injectStyles() { + // const global = u(""); + // u(":root > head").append(global); + var style = u$2("")); + u$2(this._shadowRoot).append(style); + var colors = u$2("")); + u$2(this._shadowRoot).append(colors); + } + }, { + key: "_keyboardHandler", + value: function _keyboardHandler(event) { + if (this._options.keyboardNavigation.next && isEventAttrbutesMatched(event, this._options.keyboardNavigation.next)) { + this.next(); + } else if (this._options.keyboardNavigation.prev && isEventAttrbutesMatched(event, this._options.keyboardNavigation.prev)) { + this.previous(); + } else if (this._options.keyboardNavigation.first && isEventAttrbutesMatched(event, this._options.keyboardNavigation.first)) { + this.go(0); + } else if (this._options.keyboardNavigation.last && isEventAttrbutesMatched(event, this._options.keyboardNavigation.last)) { + this.go(this._steps.length - 1); + } else if (this._options.keyboardNavigation.stop && isEventAttrbutesMatched(event, this._options.keyboardNavigation.stop)) { + this.stop(); + } else if (this._options.keyboardNavigation.complete && isEventAttrbutesMatched(event, this._options.keyboardNavigation.complete)) { + this.complete(); + } + } + }, { + key: "_decorateText", + value: function _decorateText(text, step) { + var _this2 = this; + var _text = text; + this._options.contentDecorators.forEach(function (decorator) { + if (decorator.test(_text)) _text = decorator.render(_text, step, _this2); + }); + return _text; + } + }, { + key: "init", + value: function init() { + var _this3 = this; + this.reset(); + // u(this._options.root).addClass("guided-tour"); + this._overlay = new Overlay(this); + if (this._stepsSrc === StepsSource.DOM) { + var steps = u$2(this._options.selector).nodes; + this._steps = steps.map(function (el) { + return new Step(el, _this3); + }); + } + this._steps = this._steps.sort(function (a, b) { + return a.index - b.index; + }); + this._steps[0].first = true; + this._steps[this.length - 1].last = true; + } + }, { + key: "reset", + value: function reset() { + if (this._active) this.stop(); + if (this._stepsSrc === StepsSource.DOM) { + this._steps = []; + } + this._current = 0; + } + }, { + key: "start", + value: function start() { + var _this4 = this; + var step = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + if (this._ready) { + this._containerElement.style.zIndex = getMaxZIndex() + 1; + if (this._options.restoreinitialposition) { + this._initialposition = getScrollCoordinates(this._options.root); + } + if (!this._active) { + u$2(this._options.root).addClass("__guided-tour-active"); + this.init(); + this._overlay.attach(this._shadowRoot); + this._steps.forEach(function (step) { + return step.attach(_this4._shadowRoot); + }); + this._current = step; + this.currentstep.show(); + this._active = true; + this._options.onStart(this._options); + if (this._options.keyboardNavigation) { + if (Object.prototype.toString.call(this._options.keyboardNavigation) !== "[object Object]") throw new Error("keyboardNavigation option invalid. should be predefined object or false. Check documentation."); + u$2(":root").on("keyup", this._keyboardHandler); + } + } else { + this.go(step, "start"); + } + } else { + setTimeout(function () { + _this4.start(step); + }, 50); + } + } + }, { + key: "action", + value: function action(event, _action) { + if (this._active) { + switch (_action.action) { + case "next": + this.next(); + break; + case "previous": + this.previous(); + break; + case "stop": + this.stop(); + break; + case "complete": + this.complete(); + break; + default: + { + var handler = this._options.actionHandlers.find(function (handler) { + return handler.name === _action.action; + }); + if (handler) handler.onAction(event, _action, this); + } + } + if (typeof this._options.onAction === "function") { + this._options.onAction(event, _action, this); + } + } + } + }, { + key: "next", + value: function next() { + if (this._active) { + this.go(this.nextstep, "next"); + } + } + }, { + key: "previous", + value: function previous() { + if (this._active) { + this.go(this.previousstep, "previous"); + } + } + }, { + key: "go", + value: function go(step, type) { + if (this._active && this._current !== step) { + this.currentstep.hide(); + this._current = clamp(step, 0, this.length - 1); + this.currentstep.show(); + this._options.onStep(this.currentstep, type); + } + } + }, { + key: "stop", + value: function stop() { + if (this._active) { + this.currentstep.hide(); + this._active = false; + this._overlay.remove(); + this._steps.forEach(function (step) { + return step.remove(); + }); + u$2(this._options.root).removeClass("__guided-tour-active"); + if (this._options.keyboardNavigation) { + u$2(":root").off("keyup", this._keyboardHandler); + } + if (this._options.restoreinitialposition && this._initialposition) { + animateScroll(this._initialposition, this._options.animationspeed); + } + this._options.onStop(this._options); + } + } + }, { + key: "complete", + value: function complete() { + if (this._active) { + this.stop(); + this._options.onComplete(); + } + } + }, { + key: "deinit", + value: function deinit() { + if (this._ready) { + this._containerElement.remove(); + this._containerElement = null; + this._active = false; + this._ready = false; + } + } + }]); + return Tour; + }(); + Tour.ActionHandler = ActionHandler; + Tour.ContentDecorator = ContentDecorator; + + return Tour; + +})(); diff --git a/portality/static/vendor/tourguide-1.1.2/tourguide.min.js b/portality/static/vendor/tourguide-1.1.2/tourguide.min.js new file mode 100644 index 0000000000..91db4926dd --- /dev/null +++ b/portality/static/vendor/tourguide-1.1.2/tourguide.min.js @@ -0,0 +1 @@ +!function(){function t(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);e&&(o=o.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,o)}return n}function e(e){for(var n=1;nt.length)&&(e=t.length);for(var n=0,o=new Array(e);n ]/.test(t)?e(document.createElement("table")).html(t).children().children().nodes:/^\s* ]/.test(t)?e(document.createElement("table")).html(t).children().children().children().nodes:/^\s*=n)return m(t,o.x,o.y),t._scrollSettings=null,e.end(f);var s=1-e.ease(r);if(m(t,o.x-o.differenceX*s,o.y-o.differenceY*s),i>=e.time)return e.endIterations++,e.scrollAncestor&&v(e.scrollAncestor),void v(t);!function(t){if("requestAnimationFrame"in window)return window.requestAnimationFrame(t);setTimeout(t,16)}(v.bind(null,t))}}function y(t){return t.self===t}function b(t){return"pageXOffset"in t||(t.scrollHeight!==t.clientHeight||t.scrollWidth!==t.clientWidth)&&"hidden"!==getComputedStyle(t).overflow}function x(){return!0}function w(t){if(t.assignedSlot)return w(t.assignedSlot);if(t.parentElement)return"body"===t.parentElement.tagName.toLowerCase()?t.parentElement.ownerDocument.defaultView||t.parentElement.ownerDocument.ownerWindow:t.parentElement;if(t.getRootNode){var e=t.getRootNode();if(11===e.nodeType)return e.host}}var _=function(t,e,n){if(t){"function"==typeof e&&(n=e,e=null),e||(e={}),e.time=isNaN(e.time)?1e3:e.time,e.ease=e.ease||function(t){return 1-Math.pow(1-t,t/2)},e.align=e.align||{};var o=w(t),i=1,r=e.validTarget||x,s=e.isScrollable;e.debug&&(console.log("About to scroll to",t),o||console.error("Target did not have a parent, is it mounted in the DOM?"));for(var u=[];o;)if(e.debug&&console.log("Scrolling parent node",o),r(o,i)&&(s?s(o,b):b(o))&&(i++,u.push(o)),!(o=w(o))){a(f);break}return u.reduce(((n,o,i)=>function(t,e,n,o,i){var r,s=!e._scrollSettings,u=e._scrollSettings,a=Date.now(),l={passive:!0};function c(t){e._scrollSettings=null,e.parentElement&&e.parentElement._scrollSettings&&e.parentElement._scrollSettings.end(t),n.debug&&console.log("Scrolling ended with type",t,"for",e),i(t),r&&(e.removeEventListener("touchstart",r,l),e.removeEventListener("wheel",r,l))}u&&u.end(g);var d=n.maxSynchronousAlignments;return null==d&&(d=3),e._scrollSettings={startTime:a,endIterations:0,target:t,time:n.time,ease:n.ease,align:n.align,isWindow:n.isWindow||y,maxSynchronousAlignments:d,end:c,scrollAncestor:o},"cancellable"in n&&!n.cancellable||(r=c.bind(null,g),e.addEventListener("touchstart",r,l),e.addEventListener("wheel",r,l)),s&&v(e),r}(t,o,e,u[i+1],a)),null)}function a(t){--i||n&&n(t)}};function k(t,e){if(!t)throw"TourguideJS: ".concat(e);return!0}function C(t,e,n){return e=isNaN(e)?t:e,n=isNaN(n)?t:n,Math.max(e,Math.min(t,n))}function S(t){return t&&null!==t.offsetParent}function E(t,e){var n=h(t).size(),o=j(e);return{width:n.width,height:n.height,top:n.top+o.scrollY,bottom:n.bottom+o.scrollY,left:n.left+o.scrollX,right:n.right+o.scrollX,viewTop:n.top,viewBottom:n.bottom,viewLeft:n.left,viewRight:n.right}}function j(t){try{var e=h(t).size();return{width:window.innerWidth,height:window.innerHeight,scrollX:window.scrollX,scrollY:window.scrollY,rootWidth:e.width,rootHeight:e.height,rootTop:e.top,rootLeft:e.left}}catch(e){throw console.error(e),Error("element is invalid: ".concat(t))}}function T(t,e){Object.assign(h(t).first().style,e)}function O(t,e,n){t.self===t?t.scrollTo(e,n):(t.scrollLeft=e,t.scrollTop=n)}function L(t,e){var n=Date.now();function o(t,i,r){if(t){var s=Date.now()-n,u=1-function(t){return 1-Math.pow(1-t,t/2)}(Math.min(1/e*s,1));O(t,i-(i-t.scrollLeft)*u,r-(r-t.scrollTop)*u),s>=e?O(t,i,r):function(t){if("requestAnimationFrame"in window)return window.requestAnimationFrame(t);setTimeout(t,16)}(o.bind(null,t,i,r))}else console.warn("target element ".concat(t," not found, skip"))}t.forEach((function(t){o(t.element,t.x,t.y)}))}function A(t){var e=[],n=h(t);do{n||(n=!1),n.first()||(n=!1);try{var o=n.first();o.scrollHeight===o.height&&o.scrollWidth===o.width||e.push({element:n.first(),x:n.first().scrollLeft,y:n.first().scrollTop}),n=n.parent()}catch(t){n=!1}}while(n);return e}function N(){return Math.max.apply(Math,a(Array.from(document.querySelectorAll("body *"),(function(t){return parseFloat(window.getComputedStyle(t).zIndex)})).filter((function(t){return!Number.isNaN(t)}))).concat([0]))}function R(t){return t.split("-")[1]}function M(t){return"y"===t?"height":"width"}function P(t){return t.split("-")[0]}function H(t){return["top","bottom"].includes(P(t))?"x":"y"}function D(t,e,n){let{reference:o,floating:i}=t;const r=o.x+o.width/2-i.width/2,s=o.y+o.height/2-i.height/2,u=H(e),a=M(u),l=o[a]/2-i[a]/2,c="x"===u;let d;switch(P(e)){case"top":d={x:r,y:o.y-i.height};break;case"bottom":d={x:r,y:o.y+o.height};break;case"right":d={x:o.x+o.width,y:s};break;case"left":d={x:o.x-i.width,y:s};break;default:d={x:o.x,y:o.y}}switch(R(e)){case"start":d[u]-=l*(n&&c?-1:1);break;case"end":d[u]+=l*(n&&c?-1:1)}return d}function B(t){return"number"!=typeof t?function(t){return{top:0,right:0,bottom:0,left:0,...t}}(t):{top:t,right:t,bottom:t,left:t}}function z(t){return{...t,top:t.y,left:t.x,right:t.x+t.width,bottom:t.y+t.height}}const I=Math.min,W=Math.max;const q=t=>({name:"arrow",options:t,async fn(e){const{element:n,padding:o=0}=t||{},{x:i,y:r,placement:s,rects:u,platform:a}=e;if(null==n)return{};const l=B(o),c={x:i,y:r},d=H(s),p=M(d),h=await a.getDimensions(n),f="y"===d?"top":"left",g="y"===d?"bottom":"right",m=u.reference[p]+u.reference[d]-c[d]-u.floating[p],v=c[d]-u.reference[d],y=await(null==a.getOffsetParent?void 0:a.getOffsetParent(n));let b=y?"y"===d?y.clientHeight||0:y.clientWidth||0:0;0===b&&(b=u.floating[p]);const x=m/2-v/2,w=l[f],_=b-h[p]-l[g],k=b/2-h[p]/2+x,C=function(t,e,n){return W(t,I(e,n))}(w,k,_),S=null!=R(s)&&k!=C&&u.reference[p]/2-(kt.concat(e,e+"-start",e+"-end")),[]),X={left:"right",right:"left",bottom:"top",top:"bottom"};function Y(t){return t.replace(/left|right|bottom|top/g,(t=>X[t]))}const $={start:"end",end:"start"};const V=function(t){return void 0===t&&(t={}),{name:"autoPlacement",options:t,async fn(e){var n,o,i;const{rects:r,middlewareData:s,placement:u,platform:a,elements:l}=e,{alignment:c,allowedPlacements:d=F,autoAlignment:p=!0,...h}=t,f=void 0!==c||d===F?function(t,e,n){return(t?[...n.filter((e=>R(e)===t)),...n.filter((e=>R(e)!==t))]:n.filter((t=>P(t)===t))).filter((n=>!t||R(n)===t||!!e&&function(t){return t.replace(/start|end/g,(t=>$[t]))}(n)!==n))}(c||null,p,d):d,g=await async function(t,e){var n;void 0===e&&(e={});const{x:o,y:i,platform:r,rects:s,elements:u,strategy:a}=t,{boundary:l="clippingAncestors",rootBoundary:c="viewport",elementContext:d="floating",altBoundary:p=!1,padding:h=0}=e,f=B(h),g=u[p?"floating"===d?"reference":"floating":d],m=z(await r.getClippingRect({element:null==(n=await(null==r.isElement?void 0:r.isElement(g)))||n?g:g.contextElement||await(null==r.getDocumentElement?void 0:r.getDocumentElement(u.floating)),boundary:l,rootBoundary:c,strategy:a})),v="floating"===d?{...s.floating,x:o,y:i}:s.reference,y=await(null==r.getOffsetParent?void 0:r.getOffsetParent(u.floating)),b=await(null==r.isElement?void 0:r.isElement(y))&&await(null==r.getScale?void 0:r.getScale(y))||{x:1,y:1},x=z(r.convertOffsetParentRelativeRectToViewportRelativeRect?await r.convertOffsetParentRelativeRectToViewportRelativeRect({rect:v,offsetParent:y,strategy:a}):v);return{top:(m.top-x.top+f.top)/b.y,bottom:(x.bottom-m.bottom+f.bottom)/b.y,left:(m.left-x.left+f.left)/b.x,right:(x.right-m.right+f.right)/b.x}}(e,h),m=(null==(n=s.autoPlacement)?void 0:n.index)||0,v=f[m];if(null==v)return{};const{main:y,cross:b}=function(t,e,n){void 0===n&&(n=!1);const o=R(t),i=H(t),r=M(i);let s="x"===i?o===(n?"end":"start")?"right":"left":"start"===o?"bottom":"top";return e.reference[r]>e.floating[r]&&(s=Y(s)),{main:s,cross:Y(s)}}(v,r,await(null==a.isRTL?void 0:a.isRTL(l.floating)));if(u!==v)return{reset:{placement:f[0]}};const x=[g[P(v)],g[y],g[b]],w=[...(null==(o=s.autoPlacement)?void 0:o.overflows)||[],{placement:v,overflows:x}],_=f[m+1];if(_)return{data:{index:m+1,overflows:w},reset:{placement:_}};const k=w.slice().sort(((t,e)=>t.overflows[0]-e.overflows[0])),C=null==(i=k.find((t=>{let{overflows:e}=t;return e.every((t=>t<=0))})))?void 0:i.placement,S=C||k[0].placement;return S!==u?{data:{index:m+1,overflows:w},reset:{placement:S}}:{}}}},J=function(t){return void 0===t&&(t=0),{name:"offset",options:t,async fn(e){const{x:n,y:o}=e,i=await async function(t,e){const{placement:n,platform:o,elements:i}=t,r=await(null==o.isRTL?void 0:o.isRTL(i.floating)),s=P(n),u=R(n),a="x"===H(n),l=["left","top"].includes(s)?-1:1,c=r&&a?-1:1,d="function"==typeof e?e(t):e;let{mainAxis:p,crossAxis:h,alignmentAxis:f}="number"==typeof d?{mainAxis:d,crossAxis:0,alignmentAxis:null}:{mainAxis:0,crossAxis:0,alignmentAxis:null,...d};return u&&"number"==typeof f&&(h="end"===u?-1*f:f),a?{x:h*c,y:p*l}:{x:p*l,y:h*c}}(e,t);return{x:n+i.x,y:o+i.y,data:i}}}};function G(t){var e;return(null==(e=t.ownerDocument)?void 0:e.defaultView)||window}function U(t){return G(t).getComputedStyle(t)}function Z(t){return nt(t)?(t.nodeName||"").toLowerCase():""}let K;function Q(){if(K)return K;const t=navigator.userAgentData;return t&&Array.isArray(t.brands)?(K=t.brands.map((t=>t.brand+"/"+t.version)).join(" "),K):navigator.userAgent}function tt(t){return t instanceof G(t).HTMLElement}function et(t){return t instanceof G(t).Element}function nt(t){return t instanceof G(t).Node}function ot(t){return"undefined"!=typeof ShadowRoot&&(t instanceof G(t).ShadowRoot||t instanceof ShadowRoot)}function it(t){const{overflow:e,overflowX:n,overflowY:o,display:i}=U(t);return/auto|scroll|overlay|hidden|clip/.test(e+o+n)&&!["inline","contents"].includes(i)}function rt(t){return["table","td","th"].includes(Z(t))}function st(t){const e=/firefox/i.test(Q()),n=U(t),o=n.backdropFilter||n.WebkitBackdropFilter;return"none"!==n.transform||"none"!==n.perspective||!!o&&"none"!==o||e&&"filter"===n.willChange||e&&!!n.filter&&"none"!==n.filter||["transform","perspective"].some((t=>n.willChange.includes(t)))||["paint","layout","strict","content"].some((t=>{const e=n.contain;return null!=e&&e.includes(t)}))}function ut(){return!/^((?!chrome|android).)*safari/i.test(Q())}function at(t){return["html","body","#document"].includes(Z(t))}const lt=Math.min,ct=Math.max,dt=Math.round;function pt(t){const e=U(t);let n=parseFloat(e.width),o=parseFloat(e.height);const i=t.offsetWidth,r=t.offsetHeight,s=dt(n)!==i||dt(o)!==r;return s&&(n=i,o=r),{width:n,height:o,fallback:s}}function ht(t){return et(t)?t:t.contextElement}const ft={x:1,y:1};function gt(t){const e=ht(t);if(!tt(e))return ft;const n=e.getBoundingClientRect(),{width:o,height:i,fallback:r}=pt(e);let s=(r?dt(n.width):n.width)/o,u=(r?dt(n.height):n.height)/i;return s&&Number.isFinite(s)||(s=1),u&&Number.isFinite(u)||(u=1),{x:s,y:u}}function mt(t,e,n,o){var i,r;void 0===e&&(e=!1),void 0===n&&(n=!1);const s=t.getBoundingClientRect(),u=ht(t);let a=ft;e&&(o?et(o)&&(a=gt(o)):a=gt(t));const l=u?G(u):window,c=!ut()&&n;let d=(s.left+(c&&(null==(i=l.visualViewport)?void 0:i.offsetLeft)||0))/a.x,p=(s.top+(c&&(null==(r=l.visualViewport)?void 0:r.offsetTop)||0))/a.y,h=s.width/a.x,f=s.height/a.y;if(u){const t=G(u),e=o&&et(o)?G(o):o;let n=t.frameElement;for(;n&&o&&e!==t;){const t=gt(n),e=n.getBoundingClientRect(),o=getComputedStyle(n);e.x+=(n.clientLeft+parseFloat(o.paddingLeft))*t.x,e.y+=(n.clientTop+parseFloat(o.paddingTop))*t.y,d*=t.x,p*=t.y,h*=t.x,f*=t.y,d+=e.x,p+=e.y,n=G(n).frameElement}}return{width:h,height:f,top:p,right:d+h,bottom:p+f,left:d,x:d,y:p}}function vt(t){return((nt(t)?t.ownerDocument:t.document)||window.document).documentElement}function yt(t){return et(t)?{scrollLeft:t.scrollLeft,scrollTop:t.scrollTop}:{scrollLeft:t.pageXOffset,scrollTop:t.pageYOffset}}function bt(t){return mt(vt(t)).left+yt(t).scrollLeft}function xt(t,e,n){const o=tt(e),i=vt(e),r=mt(t,!0,"fixed"===n,e);let s={scrollLeft:0,scrollTop:0};const u={x:0,y:0};if(o||!o&&"fixed"!==n)if(("body"!==Z(e)||it(i))&&(s=yt(e)),tt(e)){const t=mt(e,!0);u.x=t.x+e.clientLeft,u.y=t.y+e.clientTop}else i&&(u.x=bt(i));return{x:r.left+s.scrollLeft-u.x,y:r.top+s.scrollTop-u.y,width:r.width,height:r.height}}function wt(t){if("html"===Z(t))return t;const e=t.assignedSlot||t.parentNode||(ot(t)?t.host:null)||vt(t);return ot(e)?e.host:e}function _t(t){return tt(t)&&"fixed"!==U(t).position?t.offsetParent:null}function kt(t){const e=G(t);let n=_t(t);for(;n&&rt(n)&&"static"===U(n).position;)n=_t(n);return n&&("html"===Z(n)||"body"===Z(n)&&"static"===U(n).position&&!st(n))?e:n||function(t){let e=wt(t);for(;tt(e)&&!at(e);){if(st(e))return e;e=wt(e)}return null}(t)||e}function Ct(t){const e=wt(t);return at(e)?t.ownerDocument.body:tt(e)&&it(e)?e:Ct(e)}function St(t,e){var n;void 0===e&&(e=[]);const o=Ct(t),i=o===(null==(n=t.ownerDocument)?void 0:n.body),r=G(o);return i?e.concat(r,r.visualViewport||[],it(o)?o:[]):e.concat(o,St(o))}function Et(t,e,n){return"viewport"===e?z(function(t,e){const n=G(t),o=vt(t),i=n.visualViewport;let r=o.clientWidth,s=o.clientHeight,u=0,a=0;if(i){r=i.width,s=i.height;const t=ut();(t||!t&&"fixed"===e)&&(u=i.offsetLeft,a=i.offsetTop)}return{width:r,height:s,x:u,y:a}}(t,n)):et(e)?function(t,e){const n=mt(t,!0,"fixed"===e),o=n.top+t.clientTop,i=n.left+t.clientLeft,r=tt(t)?gt(t):{x:1,y:1},s=t.clientWidth*r.x,u=t.clientHeight*r.y,a=i*r.x,l=o*r.y;return{top:l,left:a,right:a+s,bottom:l+u,x:a,y:l,width:s,height:u}}(e,n):z(function(t){var e;const n=vt(t),o=yt(t),i=null==(e=t.ownerDocument)?void 0:e.body,r=ct(n.scrollWidth,n.clientWidth,i?i.scrollWidth:0,i?i.clientWidth:0),s=ct(n.scrollHeight,n.clientHeight,i?i.scrollHeight:0,i?i.clientHeight:0);let u=-o.scrollLeft+bt(t);const a=-o.scrollTop;return"rtl"===U(i||n).direction&&(u+=ct(n.clientWidth,i?i.clientWidth:0)-r),{width:r,height:s,x:u,y:a}}(vt(t)))}const jt={getClippingRect:function(t){let{element:e,boundary:n,rootBoundary:o,strategy:i}=t;const r="clippingAncestors"===n?function(t,e){const n=e.get(t);if(n)return n;let o=St(t).filter((t=>et(t)&&"body"!==Z(t))),i=null;const r="fixed"===U(t).position;let s=r?wt(t):t;for(;et(s)&&!at(s);){const t=U(s),e=st(s);(r?e||i:e||"static"!==t.position||!i||!["absolute","fixed"].includes(i.position))?i=t:o=o.filter((t=>t!==s)),s=wt(s)}return e.set(t,o),o}(e,this._c):[].concat(n),s=[...r,o],u=s[0],a=s.reduce(((t,n)=>{const o=Et(e,n,i);return t.top=ct(o.top,t.top),t.right=lt(o.right,t.right),t.bottom=lt(o.bottom,t.bottom),t.left=ct(o.left,t.left),t}),Et(e,u,i));return{width:a.right-a.left,height:a.bottom-a.top,x:a.left,y:a.top}},convertOffsetParentRelativeRectToViewportRelativeRect:function(t){let{rect:e,offsetParent:n,strategy:o}=t;const i=tt(n),r=vt(n);if(n===r)return e;let s={scrollLeft:0,scrollTop:0},u={x:1,y:1};const a={x:0,y:0};if((i||!i&&"fixed"!==o)&&(("body"!==Z(n)||it(r))&&(s=yt(n)),tt(n))){const t=mt(n);u=gt(n),a.x=t.x+n.clientLeft,a.y=t.y+n.clientTop}return{width:e.width*u.x,height:e.height*u.y,x:e.x*u.x-s.scrollLeft*u.x+a.x,y:e.y*u.y-s.scrollTop*u.y+a.y}},isElement:et,getDimensions:function(t){return pt(t)},getOffsetParent:kt,getDocumentElement:vt,getScale:gt,async getElementRects(t){let{reference:e,floating:n,strategy:o}=t;const i=this.getOffsetParent||kt,r=this.getDimensions;return{reference:xt(e,await i(n),o),floating:{x:0,y:0,...await r(n)}}},getClientRects:t=>Array.from(t.getClientRects()),isRTL:t=>"rtl"===U(t).direction},Tt=(t,e,n)=>{const o=new Map,i={platform:jt,...n},r={...i.platform,_c:o};return(async(t,e,n)=>{const{placement:o="bottom",strategy:i="absolute",middleware:r=[],platform:s}=n,u=r.filter(Boolean),a=await(null==s.isRTL?void 0:s.isRTL(e));let l=await s.getElementRects({reference:t,floating:e,strategy:i}),{x:c,y:d}=D(l,o,a),p=o,h={},f=0;for(let n=0;n",""],_:["",""],"*":["",""],"~":["",""],"\n":["
    "]," ":["
    "],"-":["
    "]};function Lt(t){return t.replace(RegExp("^"+(t.match(/^(\t| )+/)||"")[0],"gm"),"")}function At(t){return(t+"").replace(/"/g,""").replace(//g,">")}function Nt(t,e){var n,o,i,r,s,u=/((?:^|\n+)(?:\n---+|\* \*(?: \*)+)\n)|(?:^``` *(\w*)\n([\s\S]*?)\n```$)|((?:(?:^|\n+)(?:\t| {2,}).+)+\n*)|((?:(?:^|\n)([>*+-]|\d+\.)\s+.*)+)|(?:!\[([^\]]*?)\]\(([^)]+?)\))|(\[)|(\](?:\(([^)]+?)\))?)|(?:(?:^|\n+)([^\s].*)\n(-{3,}|={3,})(?:\n+|$))|(?:(?:^|\n+)(#{1,6})\s*(.+)(?:\n+|$))|(?:`([^`].*?)`)|( \n\n*|\n{2,}|__|\*\*|[_*]|~~)/gm,a=[],l="",c=e||{},d=0;function p(t){var e=Ot[t[1]||""],n=a[a.length-1]==t;return e?e[1]?(n?a.pop():a.push(t),e[0|n]):e[0]:t}function h(){for(var t="";a.length;)t+=p(a[a.length-1]);return t}for(t=t.replace(/^\[(.+?)\]:\s*(.+)$/gm,(function(t,e,n){return c[e.toLowerCase()]=n,""})).replace(/^\n+|\n+$/g,"");i=u.exec(t);)o=t.substring(d,i.index),d=u.lastIndex,n=i[0],o.match(/[^\\](\\\\)*\\$/)||((s=i[3]||i[4])?n='
    "+Lt(At(s).replace(/^\n+|\n+$/g,""))+"
    ":(s=i[6])?(s.match(/\./)&&(i[5]=i[5].replace(/^\d+/gm,"")),r=Nt(Lt(i[5].replace(/^\s*[>*+.-]/gm,""))),">"==s?s="blockquote":(s=s.match(/\./)?"ol":"ul",r=r.replace(/^(.*)(\n|$)/gm,"
  • $1
  • ")),n="<"+s+">"+r+""):i[8]?n=''+At(i[7])+'':i[10]?(l=l.replace("
    ",''),n=h()+""):i[9]?n="":i[12]||i[14]?n="<"+(s="h"+(i[14]?i[14].length:i[13]>"="?1:2))+">"+Nt(i[12]||i[15],c)+"":i[16]?n=""+At(i[16])+"":(i[17]||i[1])&&(n=p(i[17]||"--"))),l+=o,l+=n;return(l+t.substring(d)+h()).replace(/^\n+|\n+$/g,"")}var Rt=function(){function t(i,r){var s,u=this;if(o(this,t),this.active=!1,this.first=!1,this.last=!1,this.container=null,this.highlight=null,this.tooltip=null,this.arrow=null,this.context=r,this._target=null,this._timerHandler=null,this._scrollCancel=null,i instanceof HTMLElement?(this.target=i,s=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},o=t.split(";"),i=e({},n);return o.forEach((function(t){var e=(t||"").split(":");i[(e[0]||"").trim()]=(e[1]||"").trim()})),i}(h(i).data("tour"))):(s=i,this._selector=i.selector),k(s.hasOwnProperty("title"),"missing required step parameter: title\n"+JSON.stringify(s,null,2)+"\nsee this doc for more detail: https://github.com/LikaloLLC/tourguide.js#json-based-approach"),k(s.hasOwnProperty("content"),"missing required step parameter: content\n"+JSON.stringify(s,null,2)+"\nsee this doc for more detail: https://github.com/LikaloLLC/tourguide.js#json-based-approach"),this.index=parseInt(s.step),this.title=s.title,this.content=Nt(s.content),this.image=s.image,this.width=s.width,this.height=s.height,this.layout=s.layout||"vertical",this.placement=s.placement||"bottom",this.overlay=!1!==s.overlay,this.navigation=!1!==s.navigation,s.image&&r.options.preloadimages&&!/^data:/i.test(s.image)){var a=new Image;a.onerror=function(){console.error(new Error("Invalid image URL: ".concat(s.image))),u.image=null},a.src=this.image}this.actions=[],s.actions&&(Array.isArray(s.actions)?this.actions=s.actions:console.error(new Error("actions must be array but got ".concat(n(s.actions)))))}return r(t,[{key:"el",get:function(){var t=this;if(!this.container){var e=h('
    '.concat(this.image?''):"","
    ")),n=h('
    \n
    ').concat(this.context._decorateText(this.title,this),'
    \n
    ').concat(this.context._decorateText(this.content,this),"
    \n
    "));if(n.find("a").on("click",(function(e){t.context.action(e,{action:"link"})})),Array.isArray(this.actions)&&this.actions.length>0){var o=h('
    \n '.concat(this.actions.map((function(t,e){return"<".concat(t.href?"a":"button",' id="').concat(t.id,'" ').concat(t.href?'href="'.concat(t.href,'"'):""," ").concat(t.target?'target="'.concat(t.target,'"'):"",' class="button').concat(t.primary?" primary":"",'" data-index="').concat(e,'">').concat(t.label,"")})).join(""),"\n
    "));o.find("a, button").on("click",(function(e){var n=t.actions[parseInt(e.target.dataset.index)];n.action&&e.preventDefault(),t.context.action(e,n)})),n.append(o)}var i=this.tooltip=h('
    ');this.width&&T(i,{width:this.width+"px",maxWidth:this.width+"px"}),this.height&&T(i,{height:this.height+"px",maxHeight:this.height+"px"});var r=h('
    ')),s=h('
    ');s.append(e).append(n);var u=this.arrow=h('
    ');if(this.navigation){var a=h('"));a.find(".guided-tour-step-button-prev").on("click",this.context.previous),a.find(".guided-tour-step-button-next").on("click",this.context.next),a.find(".guided-tour-step-button-close").on("click",this.context.stop),a.find(".guided-tour-step-button-complete").on("click",this.context.complete),a.find(".guided-tour-step-bullets button").on("click",(function(e){return t.context.go(parseInt(h(e.target).data("index")))})),r.append(u).append(s).append(a)}else r.append(u).append(s);if(i.append(r),this.container=h('')),this.overlay&&S(this.target)){var l=this.highlight=h('
    ');this.container.append(l).append(i)}else this.container.append(i)}return this.container}},{key:"target",get:function(){return this._target||this._selector&&h(this._selector).first()},set:function(t){this._target=t}},{key:"attach",value:function(t){h(t).append(this.el)}},{key:"remove",value:function(){this.hide(),this.el.remove()}},{key:"position",value:function(){var t,e,n,o,i,r,u=j(this.context._options.root),a=this.tooltip,l=this.highlight,c={top:0,left:0,width:0,height:0};if(S(this.target)){if(this.overlay&&this.highlight){var d=E(this.target,this.context._options.root);c.top=d.top-this.context.options.padding,c.left=d.left-this.context.options.padding,c.width=d.width+2*this.context.options.padding,c.height=d.height+2*this.context.options.padding,T(l,c)}t=this.target,e=a.first(),n=this.arrow.first(),this.context,Tt(t,e,{middleware:[V({alignment:"bottom-start"}),J((function(t){switch(t.placement.split("-")[0]){case"top":return 32;case"left":case"right":return 24;default:return 6}})),q({element:n,padding:8}),(o={padding:24},i=o.padding,r=void 0===i?0:i,{name:"keepinview",fn:function(t){var e=t.x,n=t.y,o=t.rects,i=t.middlewareData,s=t.platform.getDimensions(document.body),u=C(e,r,s.width-o.floating.width-r),a=C(n,r,s.height-o.floating.height-r),l=e-u,c=n-a,d=i.arrow;return d&&(d.x&&l&&(d.x+=l),d.y&&c&&(d.y+=c)),{x:u,y:a}}})]}).then((function(t){var o=t.x,i=t.y,r=t.middlewareData,u=t.placement;if(T(e,{left:"".concat(o,"px"),top:"".concat(i,"px")}),r.arrow){var a={top:"bottom",right:"left",bottom:"top",left:"right"}[u.split("-")[0]];T(n,s({left:null!=r.arrow.x?"".concat(r.arrow.x,"px"):"",top:null!=r.arrow.y?"".concat(r.arrow.y,"px"):"",right:"",bottom:""},a,"".concat(-n.offsetWidth/2,"px")))}}))}else{this.overlay&&this.highlight&&T(l,c);var p={},h=E(a,this.context._options.root);p.top=u.height/2+u.scrollY-u.rootTop-h.height/2,p.left=u.width/2+u.scrollX-u.rootLeft-h.width/2,p.bottom="unset",p.right="unset",a.addClass("guided-tour-arrow-none"),T(a,p),this.overlay&&this.context._overlay.show()}}},{key:"cancel",value:function(){this._timerHandler&&clearTimeout(this._timerHandler),this._scrollCancel&&this._scrollCancel()}},{key:"show",value:function(){var t=this;if(this.cancel(),!this.active){var e=function(){t.el.addClass("active"),t.context._overlay.hide(),t.position(),t.active=!0,t.container.find(".guided-tour-step-tooltip, button.primary, .guided-tour-step-button-complete, .guided-tour-step-button-next").last().focus({preventScroll:!0})},n=C(this.context.options.animationspeed,120,1e3);return S(this.target)&&(this._scrollCancel=_(this.target,{time:n,cancellable:!1,align:{top:.5,left:.5}})),this._timerHandler=setTimeout(e,3*n),!0}return!1}},{key:"hide",value:function(){return this.cancel(),!!this.active&&(this.el.removeClass("active"),this.tooltip.removeClass("guided-tour-arrow-top"),this.tooltip.removeClass("guided-tour-arrow-bottom"),this.overlay&&this.context._overlay.show(),this.active=!1,!0)}},{key:"toJSON",value:function(){return{index:this.index,title:this.title,content:this.content,image:this.image,active:this.active}}}]),t}(),Mt=function(){function t(e){o(this,t),this.context=e,this.container=null,this.active=!1}return r(t,[{key:"el",get:function(){return this.container||(this.container=h('')),this.container}},{key:"attach",value:function(t){h(t).append(this.el)}},{key:"remove",value:function(){this.hide(),this.el.remove()}},{key:"show",value:function(){return!this.active&&(this.el.addClass("active"),this.active=!0,!0)}},{key:"hide",value:function(){return!!this.active&&(this.el.removeClass("active"),this.active=!1,!0)}},{key:"toJSON",value:function(){return{active:this.active}}}]),t}();function Pt(t){var e=t.toString(16);return 1==e.length?"0"+e:e}function Ht(t,e,n){return"#"+Pt(Math.floor(t))+Pt(Math.floor(e))+Pt(Math.floor(n))}function Dt(t,e,n){t/=255,e/=255,n/=255;var o=Math.max(t,e,n),i=o-Math.min(t,e,n),r=i?o===t?(e-n)/i:o===e?2+(n-t)/i:4+(t-e)/i:0;return[60*r<0?60*r+360:60*r,100*(i?o<=.5?i/(2*o-i):i/(2-(2*o-i)):0),100*(2*o-i)/2]}function Bt(t){return Dt.apply(null,function(t){var e=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(t);return e?[parseInt(e[1],16),parseInt(e[2],16),parseInt(e[3],16)]:null}(t))}function zt(t,e,n){return Ht.apply(null,function(t,e,n){n/=100;var o=function(e){return(e+t/30)%12},i=(e/=100)*Math.min(n,1-n),r=function(t){return n-i*Math.max(-1,Math.min(o(t)-3,Math.min(9-o(t),1)))};return[255*r(0),255*r(8),255*r(4)]}(t,e,n))}function It(t,e,n,o){var i=Bt(t);return i[0]=C(i[0]*e,0,255),i[1]=C(i[1]*n,0,255),i[2]=C(i[2]*o,0,255),zt.apply(null,i)}function Wt(t,e){var n=Object.assign(t,e||{}),o=/Color$/,i=n.accentColor;return Object.keys(n).filter((function(t){return o.test(t)&&"auto"===n[t]})).forEach((function(t){switch(t){case"focusColor":case"stepButtonNextColor":case"stepButtonCompleteColor":case"bulletCurrentColor":n[t]=i;break;case"bulletColor":n[t]=It(i,1,.8,1.4);break;case"bulletVisitedColor":n[t]=It(i,1,.3,1.2);break;case"stepButtonPrevColor":case"stepButtonCloseColor":n[t]=It(i,1,.2,.8)}})),n}var qt=r((function t(e,n){o(this,t),this.name=e,this.onAction=n}));var Ft=function(){function t(e,n){o(this,t),this.match="string"==typeof e?new RegExp("{s*".concat(e.trim(),"s*(,.+?)?s*?}"),"gmi"):e,this.decoratorFn=n}return r(t,[{key:"test",value:function(t){return this.match.test(t)}},{key:"render",value:function(t,e,n){try{var o=function(t,e){var n,o,i=[];for(e.lastIndex=0;null!==(n=e.exec(t));)n.index===e.lastIndex&&e.lastIndex++,i.push({match:n[0],start:n.index,length:n[0].length,properties:(o=n[1],(o||"").split(",").map((function(t){return t.trim()})).filter(Boolean))});return i}(t,this.match).reverse();return this.decoratorFn(t,o,e,n)}catch(e){return console.warn(e),t}}}]),t}(),Xt=0,Yt=1,$t=2,Vt={next:"ArrowRight",prev:"ArrowLeft",first:"Home",last:"End",complete:null,stop:"Escape"},Jt={fontFamily:"sans-serif",fontSize:"14px",tooltipWidth:"40vw",overlayColor:"rgba(0, 0, 0, 0.5)",textColor:"#333",accentColor:"#0d6efd",focusColor:"auto",bulletColor:"auto",bulletVisitedColor:"auto",bulletCurrentColor:"auto",stepButtonCloseColor:"auto",stepButtonPrevColor:"auto",stepButtonNextColor:"auto",stepButtonCompleteColor:"auto",backgroundColor:"#fff"};function Gt(t,o){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"keyup";if("object"===n(t)){var r={type:i};if("number"==typeof o)r.keyCode=o;else if("string"==typeof o)r.key=o;else{if("object"!==n(o))throw new Error("keyboardNavigation option invalid. should be predefined object or false. Check documentation.");r=e(e({},o),{},{type:i})}var s=Object.entries(r).map((function(t){var e=u(t,2);return{key:e[0],value:e[1]}}));return!s.filter((function(e){return t[e.key]!==e.value})).length}return!1}var Ut=function(){function t(){var i=this,r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(o(this,t),this._options=Object.assign({root:"body",selector:"[data-tour]",animationspeed:120,padding:5,steps:null,src:null,restoreinitialposition:!0,preloadimages:!1,request:{options:{mode:"cors",cache:"no-cache"},headers:{"Content-Type":"application/json"}},keyboardNavigation:Vt,actionHandlers:[],contentDecorators:[],onStart:function(){},onStop:function(){},onComplete:function(){},onStep:function(){},onAction:function(){}},r,{style:Wt(Jt,r.colors||r.style)}),this._overlay=null,this._steps=[],this._current=0,this._active=!1,this._stepsSrc=Xt,this._ready=!1,this._initialposition=null,"object"===n(this._options.steps)&&Array.isArray(this._options.steps))this._stepsSrc=Yt,this._steps=this._options.steps.map((function(t,n){return new Rt(e(e({},t),{},{step:t.step||n}),i)})),this._ready=!0;else if("string"==typeof this._options.src)this._stepsSrc=$t,fetch(new Request(this._options.src,this._options.request)).then((function(t){return t.json().then((function(t){i._steps=t.map((function(t,n){return new Rt(e(e({},t),{},{step:t.step||n}),i)})),i._ready=!0}))}));else{if(!(h(this._options.selector).length>0))throw new Error("Tour is not configured properly. Check documentation.");this._stepsSrc=Xt,this._ready=!0}this._containerElement=document.createElement("aside"),this._containerElement.classList.add("__guided-tour-container"),h(this._options.root).append(this._containerElement),this._shadowRoot=this._containerElement.attachShadow({mode:"closed"}),this._injectIcons(),this._injectStyles(),this.start=this.start.bind(this),this.next=this.next.bind(this),this.previous=this.previous.bind(this),this.go=this.go.bind(this),this.stop=this.stop.bind(this),this.complete=this.complete.bind(this),this._keyboardHandler=this._keyboardHandler.bind(this)}return r(t,[{key:"currentstep",get:function(){return this._steps[this._current]}},{key:"length",get:function(){return this._steps.length}},{key:"steps",get:function(){return this._steps.map((function(t){return t.toJSON()}))}},{key:"hasnext",get:function(){return this.nextstep!==this._current}},{key:"nextstep",get:function(){return C(this._current+1,0,this.length-1)}},{key:"previousstep",get:function(){return C(this._current-1,0)}},{key:"options",get:function(){return this._options}},{key:"_injectIcons",value:function(){0===h("#GuidedTourIconSet",this._shadowRoot).length&&h(this._shadowRoot).append(h(''))}},{key:"_injectStyles",value:function(){var t=h(""));h(this._shadowRoot).append(t);var e=h(""));h(this._shadowRoot).append(e)}},{key:"_keyboardHandler",value:function(t){this._options.keyboardNavigation.next&&Gt(t,this._options.keyboardNavigation.next)?this.next():this._options.keyboardNavigation.prev&&Gt(t,this._options.keyboardNavigation.prev)?this.previous():this._options.keyboardNavigation.first&&Gt(t,this._options.keyboardNavigation.first)?this.go(0):this._options.keyboardNavigation.last&&Gt(t,this._options.keyboardNavigation.last)?this.go(this._steps.length-1):this._options.keyboardNavigation.stop&&Gt(t,this._options.keyboardNavigation.stop)?this.stop():this._options.keyboardNavigation.complete&&Gt(t,this._options.keyboardNavigation.complete)&&this.complete()}},{key:"_decorateText",value:function(t,e){var n=this,o=t;return this._options.contentDecorators.forEach((function(t){t.test(o)&&(o=t.render(o,e,n))})),o}},{key:"init",value:function(){var t=this;if(this.reset(),this._overlay=new Mt(this),this._stepsSrc===Xt){var e=h(this._options.selector).nodes;this._steps=e.map((function(e){return new Rt(e,t)}))}this._steps=this._steps.sort((function(t,e){return t.index-e.index})),this._steps[0].first=!0,this._steps[this.length-1].last=!0}},{key:"reset",value:function(){this._active&&this.stop(),this._stepsSrc===Xt&&(this._steps=[]),this._current=0}},{key:"start",value:function(){var t=this,e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0;if(this._ready){if(this._containerElement.style.zIndex=N()+1,this._options.restoreinitialposition&&(this._initialposition=A(this._options.root)),this._active)this.go(e,"start");else if(h(this._options.root).addClass("__guided-tour-active"),this.init(),this._overlay.attach(this._shadowRoot),this._steps.forEach((function(e){return e.attach(t._shadowRoot)})),this._current=e,this.currentstep.show(),this._active=!0,this._options.onStart(this._options),this._options.keyboardNavigation){if("[object Object]"!==Object.prototype.toString.call(this._options.keyboardNavigation))throw new Error("keyboardNavigation option invalid. should be predefined object or false. Check documentation.");h(":root").on("keyup",this._keyboardHandler)}}else setTimeout((function(){t.start(e)}),50)}},{key:"action",value:function(t,e){if(this._active){switch(e.action){case"next":this.next();break;case"previous":this.previous();break;case"stop":this.stop();break;case"complete":this.complete();break;default:var n=this._options.actionHandlers.find((function(t){return t.name===e.action}));n&&n.onAction(t,e,this)}"function"==typeof this._options.onAction&&this._options.onAction(t,e,this)}}},{key:"next",value:function(){this._active&&this.go(this.nextstep,"next")}},{key:"previous",value:function(){this._active&&this.go(this.previousstep,"previous")}},{key:"go",value:function(t,e){this._active&&this._current!==t&&(this.currentstep.hide(),this._current=C(t,0,this.length-1),this.currentstep.show(),this._options.onStep(this.currentstep,e))}},{key:"stop",value:function(){this._active&&(this.currentstep.hide(),this._active=!1,this._overlay.remove(),this._steps.forEach((function(t){return t.remove()})),h(this._options.root).removeClass("__guided-tour-active"),this._options.keyboardNavigation&&h(":root").off("keyup",this._keyboardHandler),this._options.restoreinitialposition&&this._initialposition&&L(this._initialposition,this._options.animationspeed),this._options.onStop(this._options))}},{key:"complete",value:function(){this._active&&(this.stop(),this._options.onComplete())}},{key:"deinit",value:function(){this._ready&&(this._containerElement.remove(),this._containerElement=null,this._active=!1,this._ready=!1)}}]),t}();Ut.ActionHandler=qt,Ut.ContentDecorator=Ft}(); diff --git a/portality/tasks/article_duplicate_report.py b/portality/tasks/article_duplicate_report.py index b92616d837..04419674e6 100644 --- a/portality/tasks/article_duplicate_report.py +++ b/portality/tasks/article_duplicate_report.py @@ -72,7 +72,7 @@ def run(self): n = dates.now() diff = (n - start).total_seconds() expected_total = ((diff / a_count) * total) - estimated_finish = dates.format(dates.after(start, expected_total)) + estimated_finish = dates.format(dates.seconds_after(start, expected_total)) a_count += 1 article = models.Article(_source={'id': a[0], 'created_date': a[1], 'bibjson': {'identifier': json.loads(a[2]), 'link': json.loads(a[3]), 'title': a[4]}, 'admin': {'in_doaj': json.loads(a[5])}}) diff --git a/portality/tasks/async_workflow_notifications.py b/portality/tasks/async_workflow_notifications.py index 4bea0b1104..b0236af7ec 100644 --- a/portality/tasks/async_workflow_notifications.py +++ b/portality/tasks/async_workflow_notifications.py @@ -333,7 +333,7 @@ def associate_editor_notifications(emails_dict, limit=None): assoc_email = assoc.email except AttributeError: # There isn't an account for that id - app.logger.warn("No account found for ID {0}".format(assoc_id)) + app.logger.warning("No account found for ID {0}".format(assoc_id)) continue text = render_template('email/workflow_reminder_fragments/assoc_ed_age_frag', num_idle=idle, x_days=X_DAYS, num_very_idle=very_idle, y_weeks=Y_WEEKS, url=url) diff --git a/portality/tasks/consumer_long_running.py b/portality/tasks/consumer_long_running.py index bf0aa61b61..ff8763e148 100644 --- a/portality/tasks/consumer_long_running.py +++ b/portality/tasks/consumer_long_running.py @@ -16,4 +16,4 @@ from portality.tasks.harvester import scheduled_harvest # noqa from portality.tasks.anon_export import scheduled_anon_export, anon_export # noqa from portality.tasks.old_data_cleanup import scheduled_old_data_cleanup, execute_old_data_cleanup # noqa -from portality.tasks.monitor_bgjobs import scheduled_monitor_bgjobs, execute_monitor_bgjobs +from portality.tasks.monitor_bgjobs import scheduled_monitor_bgjobs, execute_monitor_bgjobs # noqa diff --git a/portality/tasks/consumer_main_queue.py b/portality/tasks/consumer_main_queue.py index 2eda75d8da..ea3774d560 100644 --- a/portality/tasks/consumer_main_queue.py +++ b/portality/tasks/consumer_main_queue.py @@ -26,3 +26,4 @@ from portality.tasks.async_workflow_notifications import async_workflow_notifications # noqa from portality.tasks.check_latest_es_backup import scheduled_check_latest_es_backup, check_latest_es_backup # noqa from portality.tasks.request_es_backup import scheduled_request_es_backup, request_es_backup # noqa +from portality.tasks.find_discontinued_soon import scheduled_find_discontinued_soon, find_discontinued_soon # noqa diff --git a/portality/tasks/find_discontinued_soon.py b/portality/tasks/find_discontinued_soon.py new file mode 100644 index 0000000000..379618dc0a --- /dev/null +++ b/portality/tasks/find_discontinued_soon.py @@ -0,0 +1,119 @@ +from portality.core import app +from portality.bll import DOAJ +from portality.lib import dates +from portality import models + +from portality.tasks.redis_huey import main_queue + +from portality.background import BackgroundTask, BackgroundApi +from portality.tasks.helpers import background_helper +from portality.ui.messages import Messages +from portality import constants + + +class DiscontinuedSoonQuery: + def __init__(self): + self._delta = app.config.get('DISCONTINUED_DATE_DELTA', 0) + self._date = dates.days_after_now(days=self._delta) + + def query(self): + return { + "query": { + "bool": { + "filter": { + "bool": { + "must": [ + {"term": {"bibjson.discontinued_date": dates.format(self._date, format="%Y-%m-%d")}}, + {"term": {"admin.in_doaj": True}} + ] + } + } + } + } + } + + +# ~~FindDiscontinuedSoonBackgroundTask:Task~~ +class FindDiscontinuedSoonBackgroundTask(BackgroundTask): + __action__ = "find_discontinued_soon" + + def __init__(self, job): + super(FindDiscontinuedSoonBackgroundTask, self).__init__(job) + self._delta = app.config.get('DISCONTINUED_DATE_DELTA', 0) + self._date = dates.days_after_now(days=self._delta) + + def find_journals_discontinuing_soon(self): + jdata = [] + + for journal in models.Journal.iterate(q=DiscontinuedSoonQuery().query(), keepalive='5m', wrap=True): + # ~~->Journal:Model~~ + jdata.append(journal.id) + self.background_job.add_audit_message(Messages.DISCONTINUED_JOURNAL_FOUND_LOG.format(id=journal.id)) + + return jdata + + def run(self): + journals = self.find_journals_discontinuing_soon() + if len(journals): + for j in journals: + DOAJ.eventsService().trigger(models.Event( + constants.EVENT_JOURNAL_DISCONTINUING_SOON, + self.background_job.user, + { + "journal": j, + "discontinue_date": self._date + })) + self.background_job.add_audit_message(Messages.DISCONTINUED_JOURNALS_FOUND_NOTIFICATION_SENT_LOG) + else: + self.background_job.add_audit_message(Messages.NO_DISCONTINUED_JOURNALS_FOUND_LOG) + + def cleanup(self): + """ + Cleanup after a successful OR failed run of the task + :return: + """ + pass + + @classmethod + def prepare(cls, username, **kwargs): + """ + Take an arbitrary set of keyword arguments and return an instance of a BackgroundJob, + or fail with a suitable exception + + :param username: User account for this task to complete as + :param kwargs: arbitrary keyword arguments pertaining to this task type + :return: a BackgroundJob instance representing this task + """ + + # first prepare a job record + job = background_helper.create_job(username, cls.__action__, + queue_id=huey_helper.queue_id, ) + return job + + @classmethod + def submit(cls, background_job): + """ + Submit the specified BackgroundJob to the background queue + + :param background_job: the BackgroundJob instance + :return: + """ + background_job.save() + find_discontinued_soon.schedule(args=(background_job.id,), delay=10) + + +huey_helper = FindDiscontinuedSoonBackgroundTask.create_huey_helper(main_queue) + + +@huey_helper.register_schedule +def scheduled_find_discontinued_soon(): + user = app.config.get("SYSTEM_USERNAME") + job = FindDiscontinuedSoonBackgroundTask.prepare(user) + FindDiscontinuedSoonBackgroundTask.submit(job) + + +@huey_helper.register_execute(is_load_config=False) +def find_discontinued_soon(job_id): + job = models.BackgroundJob.pull(job_id) + task = FindDiscontinuedSoonBackgroundTask(job) + BackgroundApi.execute(task) diff --git a/portality/tasks/harvester_helpers/epmc/client.py b/portality/tasks/harvester_helpers/epmc/client.py index fb742b0714..2957e9fe62 100644 --- a/portality/tasks/harvester_helpers/epmc/client.py +++ b/portality/tasks/harvester_helpers/epmc/client.py @@ -37,9 +37,9 @@ def check_epmc_version(resp_json): received_ver = resp_json['version'] configured_ver = app.config.get("EPMC_TARGET_VERSION") if received_ver != configured_ver: - app.logger.warn("Mismatching EPMC API version; recommend checking for changes. Expected '{0}' Found '{1}'".format(configured_ver, received_ver)) + app.logger.warning("Mismatching EPMC API version; recommend checking for changes. Expected '{0}' Found '{1}'".format(configured_ver, received_ver)) except KeyError: - app.logger.warn("Couldn't check EPMC API version; did not find 'version' key in response. Proceed with caution as the EPMC API may have changed.") + app.logger.warning("Couldn't check EPMC API version; did not find 'version' key in response. Proceed with caution as the EPMC API may have changed.") def to_keywords(s): diff --git a/portality/tasks/helpers/background_helper.py b/portality/tasks/helpers/background_helper.py index 2790475729..66a15343e8 100644 --- a/portality/tasks/helpers/background_helper.py +++ b/portality/tasks/helpers/background_helper.py @@ -26,7 +26,7 @@ def get_queue_id_by_task_queue(task_queue: RedisHuey): elif task_queue.name == main_queue.name: return constants.BGJOB_QUEUE_ID_MAIN else: - app.logger.warn(f'unknown task_queue[{task_queue}]') + app.logger.warning(f'unknown task_queue[{task_queue}]') return constants.BGJOB_QUEUE_ID_UNKNOWN @@ -141,7 +141,7 @@ def _load_bgtask_safe(_mi): return _mi.module_finder.find_spec(_mi.name).loader.load_module(_mi.name) except RuntimeError as e: if 'No configuration for scheduled action' in str(e): - app.logger.warn(f'config for {_mi.name} not found') + app.logger.warning(f'config for {_mi.name} not found') return None raise e diff --git a/portality/templates/_js_includes.html b/portality/templates/_js_includes.html index 6fa762915e..50f3abdeff 100644 --- a/portality/templates/_js_includes.html +++ b/portality/templates/_js_includes.html @@ -3,6 +3,7 @@ {# get jquery js #} + {# get bootstrap js #} {# TODO: this is probably not the right place to keep this file #} {# I’m using a much smaller & reduced version of Boostrap’s JS, with some modified classes and only the components we need #} diff --git a/portality/templates/admin/admin_base.html b/portality/templates/admin/admin_base.html index 44b29ad4bc..39d0dc8483 100644 --- a/portality/templates/admin/admin_base.html +++ b/portality/templates/admin/admin_base.html @@ -4,6 +4,10 @@ {% endblock %} +{% block nav %} +{% include 'dashboard/nav.html' %} +{% endblock %} + {% block content %}
    {% block admin_content %} diff --git a/portality/templates/application_form/_contact.html b/portality/templates/application_form/_contact.html index 0108a011b8..b312055c22 100644 --- a/portality/templates/application_form/_contact.html +++ b/portality/templates/application_form/_contact.html @@ -1,7 +1,13 @@ {% if obj.owner %} {% set account = obj.owner_account %} - + {% if account.id == current_user.id %} +
      +
    • Assigned to you
    • +
    + {% else %} + + {% endif %} {% endif %} \ No newline at end of file diff --git a/portality/templates/application_form/_field.html b/portality/templates/application_form/_field.html index 8ebbaa9fe9..2e6ce601a5 100644 --- a/portality/templates/application_form/_field.html +++ b/portality/templates/application_form/_field.html @@ -8,6 +8,10 @@ {% if f.help("long_help") %} More help {% endif %} + {% if f.has_widget("click_to_copy") %} + Copy value + + {% endif %} {% if f.optional %}(Optional){% endif %} {% endset %} diff --git a/portality/templates/application_form/_list.html b/portality/templates/application_form/_list.html index bd75ea5795..02567676cc 100644 --- a/portality/templates/application_form/_list.html +++ b/portality/templates/application_form/_list.html @@ -7,6 +7,10 @@ {% if f.help("long_help") %} More help {% endif %} + {% if f.has_widget("click_to_copy") %} + Copy value + + {% endif %} {% if f.optional %}(Optional){% endif %} {% if f.get("hint") %}

    {{ f.hint | safe }}

    {% endif %} diff --git a/portality/templates/application_form/editorial_form_body.html b/portality/templates/application_form/editorial_form_body.html index 7912037689..53be1f5437 100644 --- a/portality/templates/application_form/editorial_form_body.html +++ b/portality/templates/application_form/editorial_form_body.html @@ -1,6 +1,5 @@ {% include "application_form/_edit_status.html" %} {% include "application_form/_backend_validation.html" %} -{% include "application_form/_contact.html" %} {% import "application_form/_application_warning_msg.html" as _msg %} {{ _msg.build_journal_withdrawn_deleted_msg(obj) }} diff --git a/portality/templates/application_form/editorial_side_panel.html b/portality/templates/application_form/editorial_side_panel.html index cde46632fc..34db9ba93e 100644 --- a/portality/templates/application_form/editorial_side_panel.html +++ b/portality/templates/application_form/editorial_side_panel.html @@ -1,3 +1,4 @@ +{% include "application_form/_contact.html" %} {% if obj %}

    LOCKED FOR EDITING UNTIL {{lock.expire_formatted()}}

    {% if obj.application_status != constants.APPLICATION_STATUS_ACCEPTED %} @@ -17,7 +18,6 @@

    LOCKED FOR EDITING UNTIL -
    {% set fs = formulaic_context.fieldset("notes") %} {% if fs %} diff --git a/portality/templates/dashboard/_todo.html b/portality/templates/dashboard/_todo.html new file mode 100644 index 0000000000..35bccf0c93 --- /dev/null +++ b/portality/templates/dashboard/_todo.html @@ -0,0 +1,189 @@ +{% set TODOS = { + constants.TODO_MANED_STALLED: { + "text" : "Stalled Chase Editor", + "show_status": true, + "colour" : "var(--salmon)", + "feather": "coffee" + }, + constants.TODO_MANED_FOLLOW_UP_OLD: { + "text": "Old Chase Editor", + "show_status": true, + "colour" : "var(--sanguine)", + "feather": "clock" + }, + constants.TODO_MANED_READY: { + "text" : "Ready Make decision", + "colour" : "var(--dark-green)", + "feather": "check-circle", + "link" : url_for('admin.suggestions') + "?source=" + search_query_source(term=[{"admin.application_status.exact":"ready"}]) + }, + constants.TODO_MANED_COMPLETED: { + "text" : "Completed Chase Editor", + "colour" : "var(--mid-green)", + "feather": "user-check", + "link" : url_for('admin.suggestions') + "?source=" + search_query_source(term=[{"admin.application_status.exact":"completed"}]) + }, + constants.TODO_MANED_ASSIGN_PENDING: { + "text" : "Pending Chase Editor", + "colour" : "var(--yellow)", + "feather": "inbox", + "link" : url_for('admin.suggestions') + "?source=" + search_query_source(term=[{"admin.application_status.exact":"pending"}]) + }, + constants.TODO_EDITOR_STALLED: { + "text" : "Stalled Chase Associate Editor", + "show_status": true, + "colour" : "var(--salmon)", + "feather": "inbox", + }, + constants.TODO_EDITOR_FOLLOW_UP_OLD: { + "text" : "Old Chase Associate Editor", + "show_status": true, + "colour" : "var(--sanguine)", + "feather": "clock" + }, + constants.TODO_EDITOR_COMPLETED: { + "text" : "Completed Review and set to Ready", + "colour" : "var(--mid-green)", + "feather": "inbox", + "link" : url_for('editor.group_suggestions') + "?source=" + search_query_source(term=[{"admin.application_status.exact":"completed"}]) + }, + constants.TODO_EDITOR_ASSIGN_PENDING: { + "text" : "Pending Assign to Associate Editor", + "colour" : "var(--yellow)", + "feather": "inbox", + "link" : url_for('editor.group_suggestions') + "?source=" + search_query_source(term=[{"admin.application_status.exact":"pending"}]) + }, + constants.TODO_EDITOR_ASSIGN_PENDING_LOW_PRIORITY: { + "text" : "Pending Assign to Associate Editor", + "colour" : "var(--yellow)", + "feather": "inbox", + "link" : url_for('editor.group_suggestions') + "?source=" + search_query_source(term=[{"admin.application_status.exact":"pending"}]) + }, + constants.TODO_ASSOCIATE_PROGRESS_STALLED: { + "text" : "Stalled Complete as soon as possible", + "show_status": true, + "colour" : "var(--salmon)", + "feather": "inbox", + }, + constants.TODO_ASSOCIATE_FOLLOW_UP_OLD: { + "text": "Old Complete as soon as possible", + "show_status": true, + "colour" : "var(--sanguine)", + "feather": "clock" + }, + constants.TODO_ASSOCIATE_START_PENDING: { + "text" : "Pending Start your review", + "colour" : "var(--yellow)", + "feather": "inbox", + "link" : url_for('editor.associate_suggestions') + "?source=" + search_query_source(term=[{"admin.application_status.exact":"pending"}]) + }, + constants.TODO_ASSOCIATE_ALL_APPLICATIONS: { + "text" : "Application Continue review", + "show_status": true, + "colour" : "var(--yellow)", + "feather": "inbox", + "link" : url_for('editor.associate_suggestions') + } + } +%} + +
    + {% if todos|length == 0 %} +
    +

    + + + {% if current_user.has_role("admin") %} + {% set source = search_query_source( + term=[ + {"admin.editor.exact" : current_user.id}, + {"index.application_type.exact" : "new application"} + ], + sort=[{"admin.date_applied": {"order": "asc"}}] + ) + %} + All priority tasks have been done. Check your queue for more open applications + {% elif current_user.has_role("editor") %} + {% set source = search_query_source( + term=[ + {"index.application_type.exact" : "new application"} + ], + sort=[{"admin.date_applied": {"order": "asc"}}] + ) + %} + All priority tasks have been done. Check your queue for more open applications. If you need more applications to be assigned to your group, contact your Managing Editor. + {% elif current_user.has_role("associate_editor") %} + {% set source = search_query_source( + term=[ + {"index.application_type.exact" : "new application"} + ], + sort=[{"admin.date_applied": {"order": "asc"}}] + ) + %} + All priority tasks have been done. Check your queue for more open applications. If you need more to be assigned to you, contact your Editor or Managing Editor. + {% endif %} + +

    +
    + {% endif %} +
      + {% for todo in todos %} + {# TODO only show tasks for this user’s groups #} + {# TODO integrated priority in list display + {{ todo.boost }} + #} + {% set action = TODOS[todo.action_id[0]] %} + {% set app_route = "admin.application" if current_user.has_role("admin") else "editor.application" %} + {% set app_url = url_for(app_route, application_id=todo.object_id) %} + {% set app_date = todo.object.created_timestamp %} +
    1. +
      + + +
      +
    2. + {% endfor %} +
    + {# TODO to be displayed once we implement page listing out all tasks + + #} +
    \ No newline at end of file diff --git a/portality/templates/dashboard/index.html b/portality/templates/dashboard/index.html index 346ff943e2..79bf1c26e9 100644 --- a/portality/templates/dashboard/index.html +++ b/portality/templates/dashboard/index.html @@ -1,84 +1,9 @@ -{% extends "layouts/dashboard_base.html" %} +{% extends "admin/admin_base.html" %} {# ~~Dashboard:Page~~ #} -{% set TODOS = { - constants.TODO_MANED_STALLED: { - "text" : "Stalled +8 wks inactive", - "colour" : "var(--salmon)", - "feather": "coffee" - }, - constants.TODO_MANED_FOLLOW_UP_OLD: { - "text": "Old +10 wks old", - "colour" : "var(--sanguine)", - "feather": "clock" - }, - constants.TODO_MANED_READY: { - "text" : "Ready", - "colour" : "var(--dark-green)", - "feather": "check-circle", - "link" : url_for('admin.suggestions') + "?source=" + search_query_source(term=[{"admin.application_status.exact":"ready"}]) - }, - constants.TODO_MANED_COMPLETED: { - "text" : "Completed +2 wks updated", - "colour" : "var(--mid-green)", - "feather": "user-check", - "link" : url_for('admin.suggestions') + "?source=" + search_query_source(term=[{"admin.application_status.exact":"completed"}]) - }, - constants.TODO_MANED_ASSIGN_PENDING: { - "text" : "Pending +2 wks submitted", - "colour" : "var(--yellow)", - "feather": "inbox", - "link" : url_for('admin.suggestions') + "?source=" + search_query_source(term=[{"admin.application_status.exact":"pending"}]) - } - } -%} -{% block content %} -
    - {% if todos|length == 0 %} -
    -

    - 🎉 You have no priority tasks to complete!

    - Keep an eye on the ongoing applications to make sure there are no processing delays.
    -

    -
    - {% else %} -

    - You have {{ todos|length }}{% if todos|length >= config.get("TODO_LIST_SIZE")%}+{% endif %} applications in your priority list: -

    - {% endif %} -
      - {% for todo in todos %} - {# TODO only show tasks for this user’s groups #} - {# TODO integrated priority in list display - {{ todo.boost }} - #} - {% set action = TODOS[todo.action_id[0]] %} - {% set app_url = url_for('admin.application', application_id=todo.object_id) %} - {% if loop.index <= 12 %} - {% include "includes/_todo_item.html" %} - {% endif %} - {% endfor %} -
    - {% if todos|length > 12 %} - - - {% endif %} -
    +{% block content %} + {% include "dashboard/_todo.html" %}
    {# ~~->$GroupStatus:Feature~~ #}

    Activity

    @@ -89,11 +14,17 @@

    Activity

      {# managed_groups is inherited from the dashboard_base template #} {# ~~^-> EditorGroup:Model ~~ #} + {% if managed_groups|length == 0 %} +

      + You do not manage any groups now. +

      + {% else %} {% for eg in managed_groups %} {% endfor %} + {% endif %}
    @@ -111,7 +42,7 @@

    Activity

    {% include "includes/_hotjar.html" %} diff --git a/portality/templates/doaj/index.html b/portality/templates/doaj/index.html index c1599f238a..02bb78b878 100644 --- a/portality/templates/doaj/index.html +++ b/portality/templates/doaj/index.html @@ -81,13 +81,17 @@

    DOAJ in numbers

    About the directory

    -

    DOAJ is a unique and extensive index of diverse open access journals from around the world, driven by a growing community, committed to ensuring quality content is freely available online for everyone.

    -

    All DOAJ services are free of charge including being indexed. All data is freely available.

    +

    DOAJ is a unique and extensive index of diverse open access journals from around the world, driven by a growing community, and is committed to ensuring quality content is freely available online for everyone.

    +

    DOAJ is committed to keeping its services free of charge, including being indexed, and its data freely available.

    About DOAJ

    How to apply

    Apply now

    +

    DOAJ is twenty years old in 2023.

    +

    + Fund our 20th anniversary campiagn +

    diff --git a/portality/templates/doaj/toc.html b/portality/templates/doaj/toc.html index 14872aa014..ccf3ac1a88 100644 --- a/portality/templates/doaj/toc.html +++ b/portality/templates/doaj/toc.html @@ -17,6 +17,9 @@ "Dulcinea" : "https://www.accesoabierto.net/dulcinea/lista/REVISTA/", } %} + {% if bibjson.discontinued_date is not none and bibjson.discontinued_date | is_in_the_past %} +

    Ceased publication on {{ bibjson.discontinued_datestamp.strftime("%d %B %Y") }}

    + {% endif %}
    diff --git a/portality/templates/editor/associate_applications.html b/portality/templates/editor/associate_applications.html index 4762e08fbb..4b12ca16a1 100644 --- a/portality/templates/editor/associate_applications.html +++ b/portality/templates/editor/associate_applications.html @@ -5,8 +5,6 @@ {% block page_title %}Applications assigned to you{% endblock %} {% block editor_content %} -

    Applications assigned to you

    -
    {% endblock %} diff --git a/portality/templates/editor/associate_journals.html b/portality/templates/editor/associate_journals.html index 93167d5b0e..6d3a3b75d6 100644 --- a/portality/templates/editor/associate_journals.html +++ b/portality/templates/editor/associate_journals.html @@ -5,8 +5,6 @@ {% block page_title %}Journals assigned to you{% endblock %} {% block editor_content %} -

    Journals assigned to you

    -
    {% endblock %} diff --git a/portality/templates/editor/dashboard.html b/portality/templates/editor/dashboard.html new file mode 100644 index 0000000000..f22196c2b2 --- /dev/null +++ b/portality/templates/editor/dashboard.html @@ -0,0 +1,48 @@ +{% extends "editor/editor_base.html" %} + +{% block editor_content %} + {% include "dashboard/_todo.html" %} +
    + {# ~~->$GroupStatus:Feature~~ #} + {% if editor_of_groups | length != 0 %} +

    Activity

    +
    + + {% endif %} + + {# TODO: there’s a bit of a11y work to be done here; we need to indicate which tabs are hidden and which + aren’t using ARIA attributes. #} + {# TODO: the first tab content needs to be shown by default, without a "click to see" message. #} +
    +
    +
    +
    +
    +{% endblock %} + +{% block extra_js_bottom %} + + +{% endblock %} \ No newline at end of file diff --git a/portality/templates/editor/editor_base.html b/portality/templates/editor/editor_base.html index 3d488e6e72..1fc7bfbc81 100644 --- a/portality/templates/editor/editor_base.html +++ b/portality/templates/editor/editor_base.html @@ -1,21 +1,18 @@ -{% extends "layouts/public_base.html" %} +{% extends "layouts/dashboard_base.html" %} {% block extra_stylesheets %} {% endblock %} -{% block page_title %}Editor dashboard{% endblock %} +{% block nav %} +{% include 'editor/nav.html' %} +{% endblock %} {% block content %} -
    -

    Editor dashboard

    - {% include 'editor/nav.html' %} - -
    - {% block editor_content %} - {% endblock %} -
    -
    +
    + {% block editor_content %} + {% endblock %} +
    {% include "includes/_hotjar.html" %} {% endblock %} diff --git a/portality/templates/editor/group_applications.html b/portality/templates/editor/group_applications.html index 6f89e52845..bb0bd2053e 100644 --- a/portality/templates/editor/group_applications.html +++ b/portality/templates/editor/group_applications.html @@ -5,8 +5,6 @@ {% block page_title %}Your group’s applications{% endblock %} {% block editor_content %} -

    Your group’s applications

    -
    {% endblock %} diff --git a/portality/templates/editor/group_journals.html b/portality/templates/editor/group_journals.html index 0589346db9..822152ceff 100644 --- a/portality/templates/editor/group_journals.html +++ b/portality/templates/editor/group_journals.html @@ -5,8 +5,6 @@ {% block page_title %}Your group’s journals{% endblock %} {% block editor_content %} -

    Your group’s journals

    -
    {% endblock %} diff --git a/portality/templates/editor/index.html b/portality/templates/editor/index.html deleted file mode 100644 index db9228af47..0000000000 --- a/portality/templates/editor/index.html +++ /dev/null @@ -1,52 +0,0 @@ -{% extends "editor/editor_base.html" %} -{# ~~EditorIndex:Page->EditorGroup:Model~~ #} - -{% block editor_content %} -

    - Hi {{ current_user.id }}!
    Your group(s): -

    - {% if current_user.has_role("editor") %} - {% for group in editor_of %} -

    — Editor of {{group.name}}

    - {% set maned = group.get_maned_account() %} - {% if maned %} -

    Managing Editor:

    -

    {{maned.id}}

    - {% endif %} - {% set associates = group.get_associate_accounts() %} - {% if associates and associates|length > 0 %} -

    Associate editors:

    -
      - {% for ass in group.get_associate_accounts() %} -
    • {{ass.id}}
    • - {% endfor %} -
    - {% else %} -

    There are no Associate Editors in this group.

    - {% endif %} - {% endfor %} - {% endif %} - {% if current_user.has_role("associate_editor") %} - {% for group in associate_of %} -

    Associate Editor of {{group.name}}

    - {% set editor = group.get_editor_account() %} -

    Group editor:

    -

    {{editor.id}}

    - {% set maned = group.get_maned_account() %} - {% if maned %} -

    Managing Editor:

    -

    {{maned.id}}

    - {% endif %} - {% set associates = group.get_associate_accounts() %} - {% if associates and associates|length > 0 %} -

    Other associate editors:

    -
      - {% for ass in group.get_associate_accounts() %} -
    • {{ass.id}}
    • - {% endfor %} -
    - {% endif %} - {% endfor %} - {% endif %} -

    Contact the Managing Editors

    -{% endblock %} diff --git a/portality/templates/editor/nav.html b/portality/templates/editor/nav.html index 156732b623..2ab38010b1 100644 --- a/portality/templates/editor/nav.html +++ b/portality/templates/editor/nav.html @@ -1,29 +1,35 @@ {% set index = url_for("editor.index") %} -{% set group_journals = url_for('editor.group_journals') %} {% set group_apps = url_for('editor.group_suggestions') %} -{% set ass_journals = url_for('editor.associate_journals') %} +{% set group_journals = url_for('editor.group_journals') %} {% set ass_apps = url_for('editor.associate_suggestions') %} +{% set ass_journals = url_for('editor.associate_journals') %} + + +{# +Tabs removed for https://github.com/DOAJ/doajPM/issues/3422 +(ass_journals, "Journals assigned to you", None, "book-open") +(group_journals, "Your group’s journals", "list_group_journals", "book") +#} {% set tabs = [ - (index, "Group Info", None), - (group_journals, "Your group’s journals", "list_group_journals"), - (group_apps, "Your group’s applications", "list_group_suggestions"), - (ass_journals, "Journals assigned to you", None), - (ass_apps, "Applications assigned to you", None) + (index, "Dashboard", None, "list"), + (group_apps, "Your group’s applications", "list_group_suggestions", "users"), + (ass_apps, "Applications assigned to you", None, "file-text") ] %} -
    - -
    + {% endif %} + {% endfor %} + + diff --git a/portality/templates/email/discontinue_soon.jinja2 b/portality/templates/email/discontinue_soon.jinja2 new file mode 100644 index 0000000000..25debb60a8 --- /dev/null +++ b/portality/templates/email/discontinue_soon.jinja2 @@ -0,0 +1,7 @@ +{# +~~FindDiscontinuedSoonBackgroundTask:Email~~ +#} + +Following journals will discontinue in {{ days }} days. + +{{ data }} \ No newline at end of file diff --git a/portality/templates/includes/_header-secondary-navigation-account.html b/portality/templates/includes/_header-secondary-navigation-account.html index dc5ecb7620..35b3b6aa2c 100644 --- a/portality/templates/includes/_header-secondary-navigation-account.html +++ b/portality/templates/includes/_header-secondary-navigation-account.html @@ -17,7 +17,7 @@ {% endif %} - {% if current_user.has_role("editor") or current_user.has_role("associate_editor") %} + {% if (current_user.has_role("editor") or current_user.has_role("associate_editor")) and not current_user.has_role("admin") %}
  • Editor diff --git a/portality/templates/includes/_tourist.html b/portality/templates/includes/_tourist.html new file mode 100644 index 0000000000..3a4598c548 --- /dev/null +++ b/portality/templates/includes/_tourist.html @@ -0,0 +1,13 @@ +{% set tourservice = services.tourService() %} +{% set tours = tourservice.activeTours(request.path, current_user) %} +{% if tours|length > 0 %} + + + + + +{% endif %} \ No newline at end of file diff --git a/portality/templates/includes/_tourist_nav.html b/portality/templates/includes/_tourist_nav.html new file mode 100644 index 0000000000..f2fa255992 --- /dev/null +++ b/portality/templates/includes/_tourist_nav.html @@ -0,0 +1,25 @@ +{% set tourservice = services.tourService() %} +{% set tours = tourservice.activeTours(request.path, current_user) %} + +{% if tours|length > 0 %} +
  • +{% endif %} \ No newline at end of file diff --git a/portality/templates/layouts/dashboard_base.html b/portality/templates/layouts/dashboard_base.html index d6366735b4..355463e4e2 100644 --- a/portality/templates/layouts/dashboard_base.html +++ b/portality/templates/layouts/dashboard_base.html @@ -4,6 +4,8 @@ {# we're potentially going need this in a few places in inherited files, so lets just put it here #} {# ~~-> EditorGroup:Model ~~ #} {% set managed_groups, maned_assignments = maned_of() %} +{% set editor_of_groups, editor_of_assignments = editor_of() %} +{% set associate_of_groups, associate_of_assignments = associate_of() %} {# ~~Dashboard:Template~~ #} {% block base_content %} @@ -15,94 +17,163 @@ {% endif %}
    -

    DOAJ Dashboard

    +

    DOAJ Dashboard

    - {% include 'dashboard/nav.html' %} + {% block nav %} + {% include 'dashboard/nav.html' %} + {% endblock %} {% block extra_header %}{% endblock %}
    {% block main_panel %}
    - {% if request.path == "/dashboard/" %}
    -

    {{ current_user.id }} (Managing Editor)

    {# TODO: insert role of user here #} - {% endif %} +

    + + + + {{ current_user.id }} + {% if current_user.has_role("admin") %} + (Managing Editor) + {% elif current_user.has_role("editor") %} + (Editor) + {% elif current_user.has_role("associate_editor") %} + (Associate Editor) + {% endif %} + + +

    {% block page_title %} - Hi{% if current_user.name %}, {{ current_user.name }}{% endif %}! + Hi, {% if current_user.name %}{{ current_user.name }}{% else %} + {{ current_user.id }}{% endif %}! {% endblock %}

    - {% if request.path == "/dashboard/" %}
    - {% endif %}
    {% include "includes/_flash_notification.html" %} {% block content %}{% endblock %} -

    - - - - Log out - - - - Settings - - -

    -
    - {% include "includes/_back-to-top.html" %} +

    + + + + Log out + + + + Settings + + +

    + + {% include "includes/_back-to-top.html" %} {% endblock %} {% include '_js_includes.html' %} @@ -118,5 +189,7 @@

    {% endif %} + {% include "includes/_tourist.html" %} + {% endblock %} diff --git a/portality/templates/publisher/help.html b/portality/templates/publisher/help.html index ebd3d91571..9f4daddeb4 100644 --- a/portality/templates/publisher/help.html +++ b/portality/templates/publisher/help.html @@ -204,10 +204,17 @@

    Failed XML uploads explained

    A journal may have two ISSNs: an ISSN for the print version and an ISSN for the electronic version. Sometimes the ISSNs of the journal have changed.
    The print and online ISSNs you have supplied are identical. If you supply 2 ISSNs they must be different: an ISSN for the print version and an ISSN for the electronic version. +
    + ISSNs provided don't match any journal. We do not have a record of one or both of those ISSNs in DOAJ.
    +
    Check that all the Article ISSNs in the file are correct

    + Check that the journal to which you are trying to upload article metadata is indexed in DOAJ.
    +
    + Check that the ISSNs in the metadata are both seen on the DOAJ journal record.
    +
    If you need to have the ISSNs of your DOAJ record updated, please contact us and we will check that the ISSNs are registered at the ISSN Portal and will then update the record accordingly.

    If you believe all the ISSNs for the articles are correct, please contact us with the relevant details. diff --git a/portality/ui/messages.py b/portality/ui/messages.py index 094751c97c..ac7f9163bc 100644 --- a/portality/ui/messages.py +++ b/portality/ui/messages.py @@ -110,6 +110,12 @@ class Messages(object): NOTIFY__DEFAULT_SHORT_NOTIFICATION = "You have a new notification" + DISCONTINUED_JOURNAL_FOUND_LOG = "Journal discontinuing soon found: {id}" + DISCONTINUED_JOURNALS_FOUND_NOTIFICATION_SENT_LOG = "Notification with journals discontinuing soon sent." + DISCONTINUED_JOURNALS_FOUND_NOTIFICATION_ERROR_LOG = "Error sending notification with journals discontinuing soon." + NO_DISCONTINUED_JOURNALS_FOUND_LOG = "No journals discontinuing soon found" + + @classmethod def flash(cls, tup): if isinstance(tup, tuple): diff --git a/portality/upgrade.py b/portality/upgrade.py index fb600977cc..1f33ba6140 100644 --- a/portality/upgrade.py +++ b/portality/upgrade.py @@ -6,6 +6,8 @@ from datetime import datetime, timedelta from copy import deepcopy from collections import OrderedDict +from typing import TypedDict, List, Dict + from portality import models from portality.dao import ScrollTimeoutException from portality.lib import plugin, dates @@ -14,12 +16,12 @@ from portality.dao import ScrollTimeoutException MODELS = { - "journal": models.Journal, #~~->Journal:Model~~ - "article": models.Article, #~~->Article:Model~~ - "suggestion": models.Suggestion, #~~->Application:Model~~ + "journal": models.Journal, # ~~->Journal:Model~~ + "article": models.Article, # ~~->Article:Model~~ + "suggestion": models.Suggestion, # ~~->Application:Model~~ "application": models.Application, - "account": models.Account, #~~->Account:Model~~ - "background_job": models.BackgroundJob #~~->BackgroundJob:Model~~ + "account": models.Account, # ~~->Account:Model~~ + "background_job": models.BackgroundJob # ~~->BackgroundJob:Model~~ } @@ -29,7 +31,42 @@ def upgrade_article(self, article): pass -def do_upgrade(definition, verbose, save_batches=None): +class UpgradeType(TypedDict): + type: str # name / key of the MODELS class + action: str # default is update + query: dict # ES query to use to find the records to upgrade + keepalive: str # ES keepalive time for the scroll, default 1m + scroll_size: int # ES scroll size, default 1000 + + """ + python path of functions to run on the record + interface of the function should be: + my_function(instance: DomainObject | dict) -> DomainObject | dict + """ + functions: List[str] + + """ + instance would be a DomainObject if True, otherwise a dict + default is True + """ + init_with_model: bool # + + """ + tasks to run on the record + that will only work if init_with_model is True + + format of each task: + { function name of model : kwargs } + """ + tasks: List[Dict[str, dict]] + + +class Definition(TypedDict): + batch: int + types: List[UpgradeType] + + +def do_upgrade(definition: Definition, verbose, save_batches=None): # get the source and target es definitions # ~~->Elasticsearch:Technology~~ @@ -54,7 +91,8 @@ def do_upgrade(definition, verbose, save_batches=None): # Iterate through all of the records in the model class try: - for result in model_class.iterate(q=tdef.get("query", default_query), keepalive=tdef.get("keepalive", "1m"), page_size=tdef.get("scroll_size", 1000), wrap=False): + for result in model_class.iterate(q=tdef.get("query", default_query), keepalive=tdef.get("keepalive", "1m"), + page_size=tdef.get("scroll_size", 1000), wrap=False): original = deepcopy(result) if tdef.get("init_with_model", True): @@ -83,7 +121,8 @@ def do_upgrade(definition, verbose, save_batches=None): result.prep() except AttributeError: if verbose: - print(tdef.get("type"), result.id, "has no prep method - no, pre-save preparation being done") + print(tdef.get("type"), result.id, + "has no prep method - no, pre-save preparation being done") pass data = result.data @@ -134,7 +173,8 @@ def do_upgrade(definition, verbose, save_batches=None): f.write(json.dumps(batch, indent=2)) print(dates.now(), "wrote batch to file {x}".format(x=fn)) - print(dates.now(), "scroll timed out / writing ", len(batch), "to", tdef.get("type"), ";", total, "of", max) + print(dates.now(), "scroll timed out / writing ", len(batch), "to", + tdef.get("type"), ";", total, "of", max) model_class.bulk(batch, action=action, req_timeout=120) batch = [] @@ -180,6 +220,7 @@ def recurse(context, c, o): if __name__ == "__main__": # ~~->Migrate:Script~~ import argparse + parser = argparse.ArgumentParser() parser.add_argument("-u", "--upgrade", help="path to upgrade definition") parser.add_argument("-v", "--verbose", action="store_true", help="verbose output to stdout during processing") diff --git a/portality/view/admin.py b/portality/view/admin.py index 9e39473b03..010907c3b1 100644 --- a/portality/view/admin.py +++ b/portality/view/admin.py @@ -68,7 +68,7 @@ def journals_list(): try: query = json.loads(request.values.get("q")) except: - app.logger.warn("Bad Request at admin/journals: " + str(request.values.get("q"))) + app.logger.warning("Bad Request at admin/journals: " + str(request.values.get("q"))) abort(400) # get the total number of journals to be affected @@ -89,7 +89,7 @@ def journals_list(): try: query = json.loads(request.data) except: - app.logger.warn("Bad Request at admin/journals: " + str(request.data)) + app.logger.warning("Bad Request at admin/journals: " + str(request.data)) abort(400) # get only the query part @@ -123,7 +123,7 @@ def articles_list(): try: query = json.loads(request.data) except: - app.logger.warn("Bad Request at admin/journals: " + str(request.data)) + app.logger.warning("Bad Request at admin/journals: " + str(request.data)) abort(400) # get only the query part diff --git a/portality/view/doajservices.py b/portality/view/doajservices.py index 3e0076ba7b..23466732d7 100644 --- a/portality/view/doajservices.py +++ b/portality/view/doajservices.py @@ -6,7 +6,7 @@ from portality.core import app from portality.decorators import ssl_required, write_required, restrict_to_role from portality.util import jsonp -from portality import lock +from portality import lock, models from portality.bll import DOAJ blueprint = Blueprint('doajservices', __name__) @@ -107,7 +107,7 @@ def group_status(group_id): :param group_id: :return: """ - if not current_user.has_role("admin"): + if (not (current_user.has_role("editor") and models.EditorGroup.pull(group_id).editor == current_user.id)) and (not current_user.has_role("admin")): abort(404) svc = DOAJ.todoService() stats = svc.group_stats(group_id) diff --git a/portality/view/editor.py b/portality/view/editor.py index 1608f15e08..24e28d8a26 100644 --- a/portality/view/editor.py +++ b/portality/view/editor.py @@ -23,14 +23,16 @@ def restrict(): return restrict_to_role('editor_area') -# build an editor's page where things can be done -@blueprint.route('/') +@blueprint.route("/") @login_required @ssl_required def index(): - editor_of = models.EditorGroup.groups_by_editor(current_user.id) - associate_of = models.EditorGroup.groups_by_associate(current_user.id) - return render_template('editor/index.html', editor_of=editor_of, associate_of=associate_of, managing_editor=app.config.get("MANAGING_EDITOR_EMAIL")) + # ~~-> Todo:Service~~ + svc = DOAJ.todoService() + todos = svc.top_todo(current_user._get_current_object(), size=app.config.get("TODO_LIST_SIZE")) + # ~~-> Dashboard:Page~~ + return render_template('editor/dashboard.html', todos=todos) + @blueprint.route('/group_journals') @login_required diff --git a/portality/view/oaipmh.py b/portality/view/oaipmh.py index 5006c13f02..73a5158dfe 100644 --- a/portality/view/oaipmh.py +++ b/portality/view/oaipmh.py @@ -305,7 +305,7 @@ def get_record(dao, base_url, specified_oai_endpoint, identifier=None, metadata_ def identify(dao, base_url): repo_name = app.config.get("SERVICE_NAME") - admin_email = app.config.get("ADMIN_EMAIL") + admin_email = app.config.get("OAI_ADMIN_EMAIL", app.config.get("ADMIN_EMAIL")) idobj = Identify(base_url, repo_name, admin_email) idobj.earliest_datestamp = dao.earliest_datestamp() return idobj diff --git a/portality/view/tours.py b/portality/view/tours.py new file mode 100644 index 0000000000..c0d9d6dffa --- /dev/null +++ b/portality/view/tours.py @@ -0,0 +1,38 @@ +import json +import yaml +import os + +from flask import Blueprint, make_response, request, abort +from portality.core import app +from portality.bll import DOAJ + +blueprint = Blueprint('tours', __name__) + +@blueprint.route('/', methods=['GET']) +def tour(content_id=None): + tourdir = os.path.join(app.config["BASE_FILE_PATH"], "..", "cms", "tours") + tourpath = os.path.join(tourdir, content_id + ".yml") + with open(tourpath) as f: + data = yaml.load(f, Loader=yaml.FullLoader) + + idx = 0 + for d in data.get("steps"): + idx += 1 + d["step"] = idx + + resp = make_response(json.dumps(data.get("steps", []))) + resp.mimetype = "application/json" + return resp + +@blueprint.route("//seen", methods=["GET"]) +def tour_seen(content_id=None): + tourSvc = DOAJ.tourService() + if not tourSvc.validateContentId(content_id): + abort(404) + + resp = make_response() + cookie_key = app.config.get("TOUR_COOKIE_PREFIX") + content_id + cookie_value = content_id + max_age = app.config.get("TOUR_COOKIE_MAX_AGE") + resp.set_cookie(cookie_key, cookie_value, max_age=max_age, samesite=None, secure=True) + return resp \ No newline at end of file diff --git a/production.cfg b/production.cfg index 1e6e01f2e9..d9d501f253 100644 --- a/production.cfg +++ b/production.cfg @@ -5,11 +5,9 @@ ELASTIC_SEARCH_HOST = "http://10.131.191.132:9200" # doaj-ind ELASTICSEARCH_HOSTS = [{'host': '10.131.191.132', 'port': 9200}, {'host': '10.131.191.133', 'port': 9200}] INDEX_PER_TYPE_SUBSTITUTE = '_doc' - # doaj-public-app-1 doaj-background-app-1 -APP_MACHINES_INTERNAL_IPS = ['10.131.191.139:5050', '10.131.12.33:5050'] + # doaj-public-app-1 doaj-background-app-1 doaj-editor-app-1 +APP_MACHINES_INTERNAL_IPS = ['10.131.191.139:5050', '10.131.12.33:5050', '10.131.56.133:5050'] - # doaj-public-app-1 doaj-bg-app-1 doaj-background-app-1 -#APP_MACHINES_INTERNAL_IPS = ['10.131.191.139:5050', '10.131.56.133:5050', '10.131.12.33:5050'] # The app is served via nginx / cloudlflare - they handle SSL SSL = False @@ -58,7 +56,12 @@ PLAUSIBLE_URL = "https://plausible.io" # Run notifications through Kafka in production. #EVENT_SEND_FUNCTION = "portality.events.kafka_producer.send_event" -EVENT_SEND_FUNCTION = "portality.events.shortcircuit.send_event" +#EVENT_SEND_FUNCTION = "portality.events.shortcircuit.send_event" + +# 2023-08-03 Use the combined event sender for max traffic - doaj-kafka machine +EVENT_SEND_FUNCTION = "portality.events.combined.send_event" +KAFKA_BROKER = "kafka://10.131.35.14:9092" +KAFKA_BOOTSTRAP_SERVER = "10.131.35.14:9092" # https://github.com/DOAJ/doajPM/issues/3565 2023-03-07 PRESERVATION_PAGE_UNDER_MAINTENANCE = False diff --git a/setup.py b/setup.py index 571b37f957..a83bf6daf2 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='doaj', - version='6.3.10', + version='6.3.16', packages=find_packages(), install_requires=[ "awscli==1.20.50", diff --git a/test.cfg b/test.cfg index b6b18f3b15..ff4bd661e8 100644 --- a/test.cfg +++ b/test.cfg @@ -55,7 +55,8 @@ HUEY_SCHEDULE = { "harvest": {"month": "*", "day": "*", "day_of_week": "*", "hour": "5", "minute": "30"}, "anon_export": CRON_NEVER, "old_data_cleanup": {"month": "*", "day": "*", "day_of_week": "3", "hour": "12", "minute": "0"}, - "monitor_bgjobs": {"month": "*", "day": "*/6", "day_of_week": "*", "hour": "10", "minute": "0"} + "monitor_bgjobs": {"month": "*", "day": "*/6", "day_of_week": "*", "hour": "10", "minute": "0"}, + "find_discontinued_soon": {"month": "*", "day": "*", "day_of_week": "*", "hour": "0", "minute": "3"} } # ======================= @@ -64,7 +65,12 @@ PUBLIC_REGISTER = True LOGIN_VIA_ACCOUNT_ID = True # 2022-12-09 enable the shorcircuit handler until we can fix kafka -EVENT_SEND_FUNCTION = "portality.events.shortcircuit.send_event" +#EVENT_SEND_FUNCTION = "portality.events.shortcircuit.send_event" + +# 2023-08-02 Use the combined event sender for max traffic - doaj-kafka machine +EVENT_SEND_FUNCTION = "portality.events.combined.send_event" +KAFKA_BROKER = "kafka://10.131.35.14:9092" +KAFKA_BOOTSTRAP_SERVER = "10.131.35.14:9092" # No plausible on test PLAUSIBLE_URL = None