From 5e82740aab9112a6e0beb70c9724d9c1d4e7c8f7 Mon Sep 17 00:00:00 2001 From: Cleber Santos Date: Tue, 13 Feb 2024 17:36:25 -0300 Subject: [PATCH 01/16] Update invitation email copy Update footer design and content Update header logo --- .../static/templates/blocks/team_info_en.html | 3 + .../static/templates/blocks/team_info_fr.html | 3 + .../static/templates/blocks/write_us_en.html | 5 - .../footers/footer_contact_us_en.html | 30 ----- .../templates/footers/footer_info_en.html | 57 ++++++--- .../templates/footers/footer_info_fr.html | 57 ++++++--- src/apps/mailing/static/templates/header.html | 20 +--- .../templates/invitation_new_user_en.html | 103 ++++------------ .../templates/invitation_new_user_fr.html | 110 ++++-------------- .../invitation_registered_user_en.html | 98 ++++------------ .../invitation_registered_user_fr.html | 106 ++++------------- .../static/templates/reset_password_en.html | 7 +- .../static/templates/response_alert_en.html | 9 +- ...transfer_ownership_registered_user_en.html | 13 +-- ...ansfer_ownership_unregistered_user_en.html | 15 +-- 15 files changed, 199 insertions(+), 437 deletions(-) create mode 100644 src/apps/mailing/static/templates/blocks/team_info_en.html create mode 100644 src/apps/mailing/static/templates/blocks/team_info_fr.html delete mode 100644 src/apps/mailing/static/templates/blocks/write_us_en.html delete mode 100644 src/apps/mailing/static/templates/footers/footer_contact_us_en.html diff --git a/src/apps/mailing/static/templates/blocks/team_info_en.html b/src/apps/mailing/static/templates/blocks/team_info_en.html new file mode 100644 index 00000000000..dc1ccd5f00a --- /dev/null +++ b/src/apps/mailing/static/templates/blocks/team_info_en.html @@ -0,0 +1,3 @@ + + – The MindLogger Team + diff --git a/src/apps/mailing/static/templates/blocks/team_info_fr.html b/src/apps/mailing/static/templates/blocks/team_info_fr.html new file mode 100644 index 00000000000..a605af3c467 --- /dev/null +++ b/src/apps/mailing/static/templates/blocks/team_info_fr.html @@ -0,0 +1,3 @@ + + – L'équipe MindLogger + diff --git a/src/apps/mailing/static/templates/blocks/write_us_en.html b/src/apps/mailing/static/templates/blocks/write_us_en.html deleted file mode 100644 index 9a24f38e103..00000000000 --- a/src/apps/mailing/static/templates/blocks/write_us_en.html +++ /dev/null @@ -1,5 +0,0 @@ - - - Need help? Visit our Help Center. - - diff --git a/src/apps/mailing/static/templates/footers/footer_contact_us_en.html b/src/apps/mailing/static/templates/footers/footer_contact_us_en.html deleted file mode 100644 index 3f7da96beac..00000000000 --- a/src/apps/mailing/static/templates/footers/footer_contact_us_en.html +++ /dev/null @@ -1,30 +0,0 @@ -
- - - - - - - -
- Child Mind Institute -
- - - - - - - -
- © Child Mind Institute 2024 -
- Contact - us -
-
-
\ No newline at end of file diff --git a/src/apps/mailing/static/templates/footers/footer_info_en.html b/src/apps/mailing/static/templates/footers/footer_info_en.html index 9423065fc75..25970f1e7f1 100644 --- a/src/apps/mailing/static/templates/footers/footer_info_en.html +++ b/src/apps/mailing/static/templates/footers/footer_info_en.html @@ -1,17 +1,40 @@ -
- - - - - - - -
- Child Mind Institute -
- MindLogger by Child Mind Institute, is a no-code platform for mental health delivery. MindLogger is not responsible for content created by outside parties. -
-
\ No newline at end of file + + + + + + + + + + +
+ + + + + + + + + + + +
+ Get the MindLogger mobile app: +
+ + Get iOS app + + + Get Android app +
+ Need help? Visit our Help Center. +
+
+ Child Mind Institute +
+ The Child Mind Institute is the creator of MindLogger but is not responsible for content created by outside parties. +
diff --git a/src/apps/mailing/static/templates/footers/footer_info_fr.html b/src/apps/mailing/static/templates/footers/footer_info_fr.html index 17ee5f1ae3c..7d712355389 100644 --- a/src/apps/mailing/static/templates/footers/footer_info_fr.html +++ b/src/apps/mailing/static/templates/footers/footer_info_fr.html @@ -1,17 +1,40 @@ -
- - - - - - - -
- Child Mind Institute -
- MindLogger de Child Mind Institute n'est pas responsable du contenu créé par des tiers. -
-
\ No newline at end of file + + + + + + + + + + +
+ + + + + + + + + + + +
+ Obtenez l'application mobile MindLogger: +
+ + Get iOS app + + + Get Android app +
+ Besoin d'aide? Visitez notre centre d'aide. +
+
+ Child Mind Institute +
+ Le Child Mind Institute est le créateur de MindLogger mais n'est pas responsable du contenu créé par des tiers. +
diff --git a/src/apps/mailing/static/templates/header.html b/src/apps/mailing/static/templates/header.html index f994fe29518..bc829a9173f 100644 --- a/src/apps/mailing/static/templates/header.html +++ b/src/apps/mailing/static/templates/header.html @@ -1,16 +1,4 @@ -
- - - - - - - - - -
\ No newline at end of file +
+ +
diff --git a/src/apps/mailing/static/templates/invitation_new_user_en.html b/src/apps/mailing/static/templates/invitation_new_user_en.html index cfc8e3ffdc7..cf894a3894f 100644 --- a/src/apps/mailing/static/templates/invitation_new_user_en.html +++ b/src/apps/mailing/static/templates/invitation_new_user_en.html @@ -1,83 +1,28 @@ + -{% include 'header.html' %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- {{ first_name }} {{ last_name }}! -
- Welcome to "{{ applet_name }}"! -
- You have been invited to become a {{ role }} of "{{ applet_name - }}", which runs in the MindLogger app (see below). -
- To accept this invitation, click below and your internet browser - will open to the "{{ applet_name }}" invitation page: -
- - Go to "{{ applet_name }}" invitation page - -
- After you have accepted the invitation, you will be able to access - "{{ applet_name }}" in the free MindLogger app on your mobile - device, if you follow three simple steps (see the - - user guide - for greater detail): -
-
    -
  1. - Install the MindLogger app on your mobile device, if it - isn’t already installed. -
  2. -
  3. - Open the MindLogger app on your mobile device, and log in -
  4. -
  5. - Tap "{{ applet_name }}" on the MindLogger home screen and - you are ready to go! If "{{ applet_name }}" does not - appear, refresh the screen by sliding your finger downwards - from the top, and a spinning wheel should appear while - loading "{{ applet_name }}". -
  6. -
-
- Thank you for accepting the invitation to use "{{ applet_name }}"! -
- -The MindLogger Team -
-{% include 'footers/footer_info_en.html' %} + {% include 'header.html' %} + + + + + + + + + + + {% include 'blocks/team_info_en.html' %} +
+ Hi {{first_name}}, +
+ You have been invited to become a {{role}} of "{{applet_name}}" in MindLogger, a data collection platform for mental health and learning challenges. +
+ Create + your account and get started +
+ {% include 'footers/footer_info_en.html' %} - \ No newline at end of file + diff --git a/src/apps/mailing/static/templates/invitation_new_user_fr.html b/src/apps/mailing/static/templates/invitation_new_user_fr.html index bdc6bdefa22..95b219775a1 100644 --- a/src/apps/mailing/static/templates/invitation_new_user_fr.html +++ b/src/apps/mailing/static/templates/invitation_new_user_fr.html @@ -1,90 +1,28 @@ + -{% include 'header.html' %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- {{ first_name }} {{ last_name }}! -
- Bienvenue dans "{{ applet_name }}"! -
- Vous avez été invité à devenir {{ role }} de "{{ applet_name - }}", dans l'application MindLogger (voir ci-dessous). -
- Pour accepter cette invitation, cliquez ci-dessous et votre - navigateur Internet s'ouvrira sur la page d'invitation - de "{{ applet_name }}": -
- - Aller à la page d'invitation "{{ applet_name }}". - -
- Après avoir accepté l'invitation, vous pourrez accéder à - "{{ applet_name }}" dans l'application gratuite MindLogger sur votre - appareil mobile, en suivant trois étapes simples (voir - - le guide de l'utilisateur - pour plus de détails): -
-
    -
  1. - Installez l'application MindLogger sur votre appareil - mobile, si elle n'est pas déjà installée. -
  2. -
  3. - Ouvrez l'application MindLogger sur votre appareil mobile et - connectez-vous (si vous avez un compte MindLogger) ou - inscrivez-vous (si vous êtes nouveau sur MindLogger). - Pour vous inscrire, appuyez sur "Nouvel utilisateur" sur - l'écran de connexion et entrez l'adresse email à laquelle - vous avez reçu l'invitation. -
  4. -
  5. - Appuyez sur "{{ applet_name }}" sur l'écran d'accueil de - MindLogger et vous êtes prêt ! Si "{{ applet_name }}" - n'apparaît pas, rafraîchissez l'écran en faisant glisser - votre doigt vers le bas à partir du haut, et une roue - devrait apparaître pendant le chargement - de "{{ applet_name }}". -
  6. -
-
- Merci d'avoir accepté l'invitation à utiliser "{{ applet_name }}"! -
- -L'équipe MindLogger -
-{% include 'footers/footer_info_fr.html' %} + {% include 'header.html' %} + + + + + + + + + + + {% include 'blocks/team_info_fr.html' %} +
+ Salut {{first_name}}, +
+ Vous avez été invité à devenir {{role}} de "{{applet_name}}", dans l'application MindLogger (voir ci-dessous). +
+ Créez + votre compte et commencez +
+ {% include 'footers/footer_info_fr.html' %} - \ No newline at end of file + diff --git a/src/apps/mailing/static/templates/invitation_registered_user_en.html b/src/apps/mailing/static/templates/invitation_registered_user_en.html index 1cb028de8a3..752a5f4cf17 100644 --- a/src/apps/mailing/static/templates/invitation_registered_user_en.html +++ b/src/apps/mailing/static/templates/invitation_registered_user_en.html @@ -1,78 +1,28 @@ + -{% include 'header.html' %} - - - - - - - - - - - - - - - - - - - - - - - - - -
- {{ first_name }} {{ last_name }}! -
- Welcome to "{{ applet_name }}"! -
- You have been invited to become a {{ role }} of "{{ applet_name - }}", which runs in the MindLogger app (see below). -
- To accept this invitation, click below and your internet browser - will open to the "{{ applet_name }}" invitation page: -
- - Go to "{{ applet_name }}" invitation page - -
- After you have accepted the invitation, you will be able to access - "{{ applet_name }}" in the free MindLogger app on your mobile - device, if you follow three simple steps (see the - - user guide - for greater detail): -
-
    -
  1. - Install the MindLogger app on your mobile device, if it - isn’t already installed. -
  2. -
  3. - Open the MindLogger app on your mobile device, and log in -
  4. -
  5. - Tap "{{ applet_name }}" on the MindLogger home screen and - you are ready to go! If "{{ applet_name }}" does not - appear, refresh the screen by sliding your finger downwards - from the top, and a spinning wheel should appear while - loading "{{ applet_name }}". -
  6. -
-
- Thank you for accepting the invitation to use "{{ applet_name }}"! -
-{% include 'footers/footer_info_en.html' %} + {% include 'header.html' %} + + + + + + + + + + + {% include 'blocks/team_info_en.html' %} +
+ Hi {{first_name}}, +
+ You have been invited to become a {{role}} of "{{applet_name}}" in MindLogger. +
+ Create + your account and get started +
+ {% include 'footers/footer_info_en.html' %} - \ No newline at end of file + diff --git a/src/apps/mailing/static/templates/invitation_registered_user_fr.html b/src/apps/mailing/static/templates/invitation_registered_user_fr.html index cf1edcc1797..95b219775a1 100644 --- a/src/apps/mailing/static/templates/invitation_registered_user_fr.html +++ b/src/apps/mailing/static/templates/invitation_registered_user_fr.html @@ -1,86 +1,28 @@ + -{% include 'header.html' %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- {{ first_name }} {{ last_name }}! -
- Bienvenue dans "{{ applet_name }}"! -
- Vous avez été invité à devenir {{ role }} de "{{ applet_name - }}", dans l'application MindLogger (voir ci-dessous). -
- Pour accepter cette invitation, cliquez ci-dessous et votre - navigateur Internet s'ouvrira sur la page d'invitation - de "{{ applet_name }}": -
- - Aller à la page d'invitation "{{ applet_name }}". - -
- Après avoir accepté l'invitation, vous pourrez accéder à - "{{ applet_name }}" dans l'application gratuite MindLogger sur votre - appareil mobile, en suivant trois étapes simples (voir - - le guide de l'utilisateur - pour plus de détails): -
-
    -
  1. - Installez l'application MindLogger sur votre appareil - mobile, si elle n'est pas déjà installée. -
  2. -
  3. - Ouvrez l'application MindLogger sur votre appareil mobile et - connectez-vous. -
  4. -
  5. - Appuyez sur "{{ applet_name }}" sur l'écran d'accueil de - MindLogger et c'est tout ! Si "{{ applet_name }}" - n'apparaît pas, rafraîchissez l'écran en faisant glisser - votre doigt vers le bas à partir du haut et une roue - devrait apparaître pendant le chargement - de "{{ applet_name }}". -
  6. -
-
- Merci d'avoir accepté l'invitation à utiliser "{{ applet_name }}"! -
- -L'équipe MindLogger -
-{% include 'footers/footer_info_fr.html' %} + {% include 'header.html' %} + + + + + + + + + + + {% include 'blocks/team_info_fr.html' %} +
+ Salut {{first_name}}, +
+ Vous avez été invité à devenir {{role}} de "{{applet_name}}", dans l'application MindLogger (voir ci-dessous). +
+ Créez + votre compte et commencez +
+ {% include 'footers/footer_info_fr.html' %} - \ No newline at end of file + diff --git a/src/apps/mailing/static/templates/reset_password_en.html b/src/apps/mailing/static/templates/reset_password_en.html index 4471c1fbe97..00d1b31ceb7 100644 --- a/src/apps/mailing/static/templates/reset_password_en.html +++ b/src/apps/mailing/static/templates/reset_password_en.html @@ -23,12 +23,9 @@ After updating your password, you won’t see your past data in the app. But don’t worry, we still have it. - {% include 'blocks/write_us_en.html' %} - - – The MindLogger Team - + {% include 'blocks/team_info_en.html' %} - {% include 'footers/footer_contact_us_en.html' %} + {% include 'footers/footer_info_en.html' %} diff --git a/src/apps/mailing/static/templates/response_alert_en.html b/src/apps/mailing/static/templates/response_alert_en.html index 51e824a2470..454ffe82c8f 100644 --- a/src/apps/mailing/static/templates/response_alert_en.html +++ b/src/apps/mailing/static/templates/response_alert_en.html @@ -13,14 +13,9 @@ Please log in to see them. - {% include 'blocks/write_us_en.html' %} - - - – The MindLogger Team - - + {% include 'blocks/team_info_en.html' %} - {% include 'footers/footer_contact_us_en.html' %} + {% include 'footers/footer_info_en.html' %} diff --git a/src/apps/mailing/static/templates/transfer_ownership_registered_user_en.html b/src/apps/mailing/static/templates/transfer_ownership_registered_user_en.html index e31a4062ea1..e19dda46adc 100644 --- a/src/apps/mailing/static/templates/transfer_ownership_registered_user_en.html +++ b/src/apps/mailing/static/templates/transfer_ownership_registered_user_en.html @@ -10,24 +10,19 @@ - {{applet_owner}} is inviting you to take over ownership of "{{applet_name}}" on MindLogger. + {{applet_owner}} is inviting you to take over ownership of "{{applet_name}}" on MindLogger. - + Create account and accept invitation - {% include 'blocks/write_us_en.html' %} - - - – The MindLogger Team - - + {% include 'blocks/team_info_en.html' %} - {% include 'footers/footer_contact_us_en.html' %} + {% include 'footers/footer_info_en.html' %} diff --git a/src/apps/mailing/static/templates/transfer_ownership_unregistered_user_en.html b/src/apps/mailing/static/templates/transfer_ownership_unregistered_user_en.html index f769b34a815..cfb0dcb1daa 100644 --- a/src/apps/mailing/static/templates/transfer_ownership_unregistered_user_en.html +++ b/src/apps/mailing/static/templates/transfer_ownership_unregistered_user_en.html @@ -10,25 +10,20 @@ - {{applet_owner}} is inviting you to take over ownership of "{{applet_name}}" on MindLogger, a next-generation data collection and information delivery platform that streamlines the management of children’s mental health and - learning challenges. + {{applet_owner}} is inviting you to take over ownership of "{{applet_name}}" on MindLogger, a next-generation data collection and information delivery platform that streamlines the management of children’s mental + health and learning challenges. - + Create account and accept invitation - {% include 'blocks/write_us_en.html' %} - - - – The MindLogger Team - - + {% include 'blocks/team_info_en.html' %} - {% include 'footers/footer_contact_us_en.html' %} + {% include 'footers/footer_info_en.html' %} From a3487b974766278a4096093c5d5130db2c9756c4 Mon Sep 17 00:00:00 2001 From: Cleber Santos Date: Thu, 15 Feb 2024 13:55:04 -0300 Subject: [PATCH 02/16] Update email assets URL to use new bucket --- .../static/templates/footers/footer_info_en.html | 10 +++------- .../static/templates/footers/footer_info_fr.html | 10 +++------- src/apps/mailing/static/templates/header.html | 3 +-- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/apps/mailing/static/templates/footers/footer_info_en.html b/src/apps/mailing/static/templates/footers/footer_info_en.html index 25970f1e7f1..f8679307ac4 100644 --- a/src/apps/mailing/static/templates/footers/footer_info_en.html +++ b/src/apps/mailing/static/templates/footers/footer_info_en.html @@ -9,14 +9,10 @@ - - Get iOS app + Get iOS app - - Get Android app + Get Android app @@ -29,7 +25,7 @@ - Child Mind Institute + Child Mind Institute diff --git a/src/apps/mailing/static/templates/footers/footer_info_fr.html b/src/apps/mailing/static/templates/footers/footer_info_fr.html index 7d712355389..9ab4ecf2548 100644 --- a/src/apps/mailing/static/templates/footers/footer_info_fr.html +++ b/src/apps/mailing/static/templates/footers/footer_info_fr.html @@ -9,14 +9,10 @@ - - Get iOS app + Get iOS app - - Get Android app + Get Android app @@ -29,7 +25,7 @@ - Child Mind Institute + Child Mind Institute diff --git a/src/apps/mailing/static/templates/header.html b/src/apps/mailing/static/templates/header.html index bc829a9173f..b4a884709d2 100644 --- a/src/apps/mailing/static/templates/header.html +++ b/src/apps/mailing/static/templates/header.html @@ -1,4 +1,3 @@
- +
From ef6ca9051a785e30044ceea381b391bade3289b6 Mon Sep 17 00:00:00 2001 From: Marty Date: Fri, 23 Feb 2024 12:07:28 -0800 Subject: [PATCH 03/16] Add alternate text for mindlogger logo Co-authored-by: Farmer Paul --- src/apps/mailing/static/templates/header.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/mailing/static/templates/header.html b/src/apps/mailing/static/templates/header.html index b4a884709d2..6c6a822950b 100644 --- a/src/apps/mailing/static/templates/header.html +++ b/src/apps/mailing/static/templates/header.html @@ -1,3 +1,3 @@
- + MindLogger
From 6413f0952889316a353c224d895a979901058eba Mon Sep 17 00:00:00 2001 From: Marty Date: Fri, 23 Feb 2024 12:08:27 -0800 Subject: [PATCH 04/16] Add french alternate text to Apple Store logo Co-authored-by: Farmer Paul --- src/apps/mailing/static/templates/footers/footer_info_fr.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/mailing/static/templates/footers/footer_info_fr.html b/src/apps/mailing/static/templates/footers/footer_info_fr.html index 9ab4ecf2548..170c43de147 100644 --- a/src/apps/mailing/static/templates/footers/footer_info_fr.html +++ b/src/apps/mailing/static/templates/footers/footer_info_fr.html @@ -9,7 +9,7 @@ - Get iOS app + Obtenez l'application iOS Get Android app From b9df2ee2f4462e09522780f74cd554342bb9b51f Mon Sep 17 00:00:00 2001 From: Marty Date: Fri, 23 Feb 2024 12:09:03 -0800 Subject: [PATCH 05/16] Add french alternate text to Google Play store logo Co-authored-by: Farmer Paul --- src/apps/mailing/static/templates/footers/footer_info_fr.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/mailing/static/templates/footers/footer_info_fr.html b/src/apps/mailing/static/templates/footers/footer_info_fr.html index 170c43de147..099ebb214e5 100644 --- a/src/apps/mailing/static/templates/footers/footer_info_fr.html +++ b/src/apps/mailing/static/templates/footers/footer_info_fr.html @@ -12,7 +12,7 @@ Obtenez l'application iOS - Get Android app + Obtenez l'application Android From f58e31f84ee871ae9c0d693d0d2d4a3617b8f534 Mon Sep 17 00:00:00 2001 From: Rodrigo Merlo Date: Fri, 23 Feb 2024 16:29:36 -0300 Subject: [PATCH 06/16] Update README file organizing sections and contents --- .env.default | 6 +- .github/workflows/tests.yaml | 1 + README.md | 339 +++++++++++++++-------------------- 3 files changed, 147 insertions(+), 199 deletions(-) diff --git a/.env.default b/.env.default index 1305aaa5384..74800fc2ca1 100644 --- a/.env.default +++ b/.env.default @@ -19,11 +19,13 @@ REDIS__HOST=redis # CORS CORS__ALLOW_ORIGINS=* +#CORS__ALLOW_ORIGINS_REGEX= CORS__ALLOW_CREDENTIALS=true CORS__ALLOW_METHODS=* CORS__ALLOW_HEADERS=* + # Authentication AUTHENTICATION__ACCESS_TOKEN__SECRET_KEY="secret1" AUTHENTICATION__REFRESH_TOKEN__SECRET_KEY="secret2" @@ -80,7 +82,7 @@ MONGO__DB=mindlogger MONGO__AES_KEY= # RabbitMq -RABBITMQ__USE_SSL=False +# Uncommnent for local development +# RABBITMQ__USE_SSL=False -GIRDER_MONGO_URI="mongodb://localhost:27017/mindlogger" diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index e9269015478..1725751b6fb 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -11,5 +11,6 @@ jobs: - name: "Running tests" run: | cp .env.default .env + echo -e "\nRABBITMQ__USE_SSL=False" >> .env docker compose up -d make dtest diff --git a/README.md b/README.md index a80b8522139..a4c96d897f0 100644 --- a/README.md +++ b/README.md @@ -1,111 +1,38 @@ -# ChildMindInstitute +# ChildMindInstitute - Mindlogger Backend API -# Mindlogger +This repository is used as a backend for the service MindLogger -## About +## 1. Getting Started / Installation -👉 This repository is used as a backend for the service MindLogger [HERE](https://github.com/ChildMindInstitute/mindlogger-backend-refactor). +### 1.1 Create `.env` file for future needs -🔌 **Web application is powered by:** - -- ✅ [Python3.10+](https://www.python.org/downloads/release/python-3108/) -- ✅ [Pipenv](https://pipenv.pypa.io/en/latest/) -- ✅ [FastAPI](https://fastapi.tiangolo.com) -- ✅ [Postgesql](https://www.postgresql.org/docs/14/index.html) -- ✅ [Redis](https://redis.io) -- ✅ [Docker](https://docs.docker.com/get-docker/) -- ✅ [Pydantic](https://pydantic-docs.helpmanual.io) -- ✅ [SQLAlchemy](https://www.sqlalchemy.org/) - -And - -- ✅ [The 12-Factor App](https://12factor.net) - -
- -🔌 **Code quality tools:** - -- ✅ [ruff](https://github.com/astral-sh/ruff) -- ✅ [isort](https://github.com/PyCQA/isort) -- ✅ [mypy](https://github.com/python/mypy) -- ✅ [pytest](https://github.com/pytest-dev/pytest) - -
- -## ✋ Mandatory steps - -### 1. Clone the project 🌐 - -```bash -git clone git@github.com:ChildMindInstitute/mindlogger-backend-refactor.git -``` - -### 2. Setup environment variables ⚙️ - -👉 Project is configured via environment variables. You have to export them into your session from which you are running the application locally of via Docker. - -👉 All default variables configured for making easy to run application via Docker in a few clicks - -> 💡 All of them you can find in `.env.default` - -#### 2.1 Description 📜 - -| Key | Default value | Description | -| ----------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | -| DATABASE\_\_HOST | postgres | Database Host | -| DATABASE\_\_USER | postgres | User name for Postgresql Database user | -| DATABASE\_\_PASSWORD | postgres | Password for Postgresql Database user | -| DATABASE\_\_DB | mindlogger_backend | Database name | -| CORS\_\_ALLOW_ORIGINS | `*` | Represents the list of allowed origins. Set the `Access-Control-Allow-Origin` header. Example: `https://dev.com,http://localohst:8000` | -| CORS\_\_ALLOW_CREDENTIALS | true | Set the `Access-Control-Allow-Credentials` header | -| CORS\_\_ALLOW_METHODS | `*` | Set the `Access-Control-Allow-Methods` header | -| CORS\_\_ALLOW_HEADERS | `*` | Set the `Access-Control-Allow-Headers` header | -| AUTHENTICATION**ACCESS_TOKEN**SECRET_KEY | secret1 | Access token's salt | -| AUTHENTICATION**REFRESH_TOKEN**SECRET_KEY | secret2 | Refresh token salt | -| AUTHENTICATION\_\_ALGORITHM | HS256 | The JWT's algorithm | -| AUTHENTICATION**ACCESS_TOKEN**EXPIRATION | 30 | Time in minutes after which the access token will stop working | -| AUTHENTICATION**REFRESH_TOKEN**EXPIRATION | 30 | Time in minutes after which the refresh token will stop working | -| ADMIN_DOMAIN | - | Admin panel domain | - -##### ✋ Mandatory: - -> You can see that some environment variables have double underscore (`__`) instead of `_`. -> -> As far as `pydantic` supports [nested settings models](https://pydantic-docs.helpmanual.io/usage/settings/) it uses to have cleaner code - -#### 2.2 Create `.env` file for future needs - -It is hightly recommended to create `.env` file as far as it is needed for setting up the project with Local and Docker approaches. +It is highly recommended to create an `.env` file as far as it is needed for setting up the project with Local and Docker approaches. ```bash cp .env.default .env ``` -#### 2.3 Generate secret keys, update .env with values +### 1.2 Generate secret keys, update .env with values ```bash openssl rand -hex 32 ``` -
- -## 👨‍🦯 Local development +### 1.3 Setup Redis -### 1. Decide how would you run storages 🤔 - -#### 1.1 Setup locally +#### 1.3.1 Locally ✅ [🐧 Linux](https://redis.io/docs/getting-started/installation/install-redis-on-linux/) ✅ [ MacOs](https://redis.io/docs/getting-started/installation/install-redis-on-mac-os/) -#### 1.2 Install via Docker 🐳 +#### 1.3.2 Install via Docker ```bash docker-compose up -d redis ``` -### 2. Install all project dependencies 🧱 +### 1.4. Install all project dependencies Pipenv used as a default dependencies manager @@ -143,45 +70,133 @@ set -o allexport; source .env; set +o allexport
-### 3. Provide code quality ✨ +## 2. Usage -#### 3.1 Using pre-commit hooks 🔬 +### 2.1 Running locally -It is a good practice to use Git hooks to provide better commits. +> 🛑 **NOTE:** Don't forget to set the `PYTHONPATH` environment variable, e.g: export PYTHONPATH=src/ -For increased security during development, install `git-secrets` to scan code for aws keys. +In project we use simplified version of imports: `from apps.application_name import class_name, function_name, module_nanme`. -Please use this link for that: https://github.com/awslabs/git-secrets#installing-git-secrets +To do this we must have `src/` folder specified in a **PATH**. -`.pre-commit-config.yaml` is placed in the root of the repository. +P.S. You don't need to do this additional step if you run application via Docker container 🤫 -👉 Once you have installed `git-secrets` and `pre-commit` simply run the following command. +```bash +uvicorn src.main:app --proxy-headers --port {PORT} --reload +``` + +### 2.2 Running via docker ```bash -make aws-scan +docker-compose up ``` -👉 Then all your staged cahnges will be checked via git hooks on every `git commit` +Additional `docker-compose up` flags that might be useful for development -#### 3.2 Using Makefile 🤖 +```bash +-d # Run docker containers as deamons (in background) +--no-recreate # If containers already exist, don't recreate them +``` -### 4. Running the application ▶️ +#### 2.2.1 Stop the application 🛑 -> 🛑 **NOTE:** Don't forget to set the `PYTHONPATH` environment variable, e.g: export PYTHONPATH=src/ +```bash +docker-compose down +``` -In project we use simplified version of imports: `from apps.application_name import class_name, function_name, module_nanme`. +Additional `docker-compose down` flags that might be useful for development + +```bash +-v # Remove with all volumes +``` +### 2.3 Running using Makefile -For doing this we must have `src/` folder specified in a **PATH**. +You can use the `Makefile` to work with project (run the application / code quality tools / tests ...) -P.S. You don't need to do this additional step if you run application via Docker container 🤫 +For local usage: ```bash -uvicorn src.main:app --proxy-headers --port {PORT} --reload +# Run the application +make run + +# Check the code quality +make cq + +# Check tests passing +make test + +# Check everything in one hop +make check ``` +### 2.4 Docker development + +#### 2.4.1. Build application images + +```bash +docker-compose build +``` + +✅ Make sure that you completed `.env` file. It is using by default in `docker-compose.yaml` file for buildnig. + +✅ Check building with `docker images` command. You should see the record with `fastapi_service`. + +💡 If you would like to debug the application insode Docker comtainer make sure that you use `COMPOSE_FILE=docker-compose.dev.yaml` in `.env`. It has opened stdin and tty. + + +## 3 Build With + +- ✅ [Python3.10+](https://www.python.org/downloads/release/python-3108/) +- ✅ [Pipenv](https://pipenv.pypa.io/en/latest/) +- ✅ [FastAPI](https://fastapi.tiangolo.com) +- ✅ [Postgresql](https://www.postgresql.org/docs/14/index.html) +- ✅ [Redis](https://redis.io) +- ✅ [Docker](https://docs.docker.com/get-docker/) +- ✅ [Pydantic](https://pydantic-docs.helpmanual.io) +- ✅ [SQLAlchemy](https://www.sqlalchemy.org/) + +And + +- ✅ [The 12-Factor App](https://12factor.net) + +
+ +**Code quality tools:** + +- ✅ [ruff](https://github.com/astral-sh/ruff) +- ✅ [isort](https://github.com/PyCQA/isort) +- ✅ [mypy](https://github.com/python/mypy) +- ✅ [pytest](https://github.com/pytest-dev/pytest)
-### 5. Running Tests ▶️ +## 4. Environment Variables + +| Key | Default value | Description | +| ----------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | +| DATABASE\_\_HOST | postgres | Database Host | +| DATABASE\_\_USER | postgres | User name for Postgresql Database user | +| DATABASE\_\_PASSWORD | postgres | Password for Postgresql Database user | +| DATABASE\_\_DB | mindlogger_backend | Database name | +| CORS\_\_ALLOW_ORIGINS | `*` | Represents the list of allowed origins. Set the `Access-Control-Allow-Origin` header. Example: `https://dev.com,http://localohst:8000` | +| CORS__ALLOW_ORIGINS_REGEX | - | Regex pattern of allowed origins. | +| CORS\_\_ALLOW_CREDENTIALS | true | Set the `Access-Control-Allow-Credentials` header | +| CORS\_\_ALLOW_METHODS | `*` | Set the `Access-Control-Allow-Methods` header | +| CORS\_\_ALLOW_HEADERS | `*` | Set the `Access-Control-Allow-Headers` header | +| AUTHENTICATION**ACCESS_TOKEN**SECRET_KEY | secret1 | Access token's salt | +| AUTHENTICATION**REFRESH_TOKEN**SECRET_KEY | secret2 | Refresh token salt | +| AUTHENTICATION\_\_ALGORITHM | HS256 | The JWT's algorithm | +| AUTHENTICATION**ACCESS_TOKEN**EXPIRATION | 30 | Time in minutes after which the access token will stop working | +| AUTHENTICATION**REFRESH_TOKEN**EXPIRATION | 30 | Time in minutes after which the refresh token will stop working | +| ADMIN_DOMAIN | - | Admin panel domain | +| RABBITMQ__USE_SSL | True | Rabbitmq ssl setting, turn false to local development +##### ✋ Mandatory: + +> You can see that some environment variables have double underscore (`__`) instead of `_`. +> +> As far as `pydantic` supports [nested settings models](https://pydantic-docs.helpmanual.io/usage/settings/) it uses to have cleaner code + +## 5. Testing The `pytest` framework is using in order to write unit tests. Currently postgresql is used as a database for tests with running configurations that are defined in `pyproject.toml` @@ -200,7 +215,7 @@ DATABASE__DB=test 127.0.0.1 postgres ``` -#### Adjust your database for using with tests +### 5.1. Adjust your database for using with tests ⚠️️ Remember that you have to do this only once before the first test. @@ -225,7 +240,7 @@ psql# create user test; psql# alter user test with password 'test'; ``` -#### Test coverage +### 5.2. Test coverage To correctly calculate test coverage, you need to run the coverage with the `--concurrency=thread,gevent` parameter: @@ -234,67 +249,10 @@ coverage run --concurrency=thread,gevent -m pytest coverage report -m ``` -
-
- -## 🐳 Docker development - -### 1. Build application images 🔨 - -```bash -docker-compose build -``` - -✅ Make sure that you completed `.env` file. It is using by default in `docker-compose.yaml` file for buildnig. - -✅ Check building with `docker images` command. You should see the record with `fastapi_service`. - -💡 If you would like to debug the application insode Docker comtainer make sure that you use `COMPOSE_FILE=docker-compose.dev.yaml` in `.env`. It has opened stdin and tty. - -### 2. Running the application ▶️ - -```bash -docker-compose up -``` - -Additional `docker-compose up` flags that might be useful for development - -```bash --d # Run docker containers as deamons (in background) ---no-recreate # If containers already exist, don't recreate them -``` - -#### Stop the application 🛑 - -```bash -docker-compose down -``` - -Additional `docker-compose down` flags that might be useful for development - -```bash --v # Remove with all volumes -``` - -#### Run only tests 🛑 +### 5.3. Running test via docker (This is how tests are running on CI) -```bash -make dtest -``` - -### 3. Provide code quality ✨ - -✋ Only in case you want to setup the Git hooks inside your Docker container and burn down in hell you may skip this step. 👹 🔥 - -👉 For the rest of audience it is recommended: - -1. Don't install pre-commit hooks -2. Use Makefile to run all commands in Docker container - -Usage: - ```bash # Check the code quality make dcq @@ -306,57 +264,41 @@ make dtest make dcheck ``` -## 💼 Additional - -### Makefile - -You can use the `Makefile` to work with project (run the application / code quality tools / tests ...) +## 6. Scripts -For local usage: +### 6.1 Using pre-commit hooks -```bash -# Run the application -make run - -# Check the code quality -# make cq - -# Check tests passing -make test +It is a good practice to use Git hooks to provide better commits. -# Check everything in one hop -make check +For increased security during development, install `git-secrets` to scan code for aws keys. -... -``` +Please use this link for that: https://github.com/awslabs/git-secrets#installing-git-secrets -💡 If you want run web-app locally you can use the next command +`.pre-commit-config.yaml` is placed in the root of the repository. -Run web-app locally (don't forget to activate the environment) +👉 Once you have installed `git-secrets` and `pre-commit` simply run the following command. ```bash -make run +make aws-scan ``` -### CORS policy - -By default CORS policy accepts all connections +👉 Then all your staged cahnges will be checked via git hooks on every `git commit` -## Alembic (migration) +### 6.2 Alembic (migration) -### 1. Add a new migrations file 🔨 +#### 6.2.1 Add a new migrations file 🔨 ```bash alembic revision --autogenerate -m "Add a new field" ``` -### 2. Upgrade to the latest migration 🔨 +#### 6.2.2. Upgrade to the latest migration 🔨 ```bash alembic upgrade head ``` -### 3. Downgrade to the specific one 🔨 +#### 6.2.3. Downgrade to the specific one 🔨 ```bash alembic downgrade 0e43c346b90d @@ -364,13 +306,13 @@ alembic downgrade 0e43c346b90d ✅ This hash is taken from the generated file in the migrations folder -### 3. Downgrade to the specific one 🔨 +#### 6.2.3. Downgrade to the specific one 🔨 ```bash alembic downgrade 0e43c346b90d ``` -### 4. Removing the migration 🔨 +#### 6.2.4. Removing the migration 🔨 💡 Do not forget that alembic saves the migration version into the database. @@ -378,13 +320,13 @@ alembic downgrade 0e43c346b90d delete from alembic_version; ``` -### 5. Upgrade arbitrary servers +#### 6.2.5. Upgrade arbitrary servers ```bash alembic -c alembic_arbitrary.ini upgrade head ``` -### 6. Database relation structure +#### 6.2.6. Database relation structure ```mermaid @@ -637,11 +579,11 @@ Flow_histories }o--|| Applet_histories: "" ``` -## Arbitrary setup +## 7. Arbitrary setup You can connect arbitrary file storage and database by filling special fields in table `user_workspaces`. -### 1. PostgreSQL +### 7.1. PostgreSQL Add your database connection string into `database_uri` In next format: @@ -650,11 +592,14 @@ In next format: postgresql+asyncpg://:@:port/database ``` -### 2. AWS S3 and GCP S3 +### 7.2. AWS S3 and GCP S3 For AWS S3 bucket next fields are required: `storage_region`,`storage_bucket`, `storage_access_key`,`storage_secret_key`. -### 3. Azure Blob +### 7.3. Azure Blob In case of Azure blob, specify your connection string into field `storage_secret_key` + + +## 8. License \ No newline at end of file From f27b0a90623a654d5c19c790ce5a0d8725fa1689 Mon Sep 17 00:00:00 2001 From: Carlos Chacon Date: Mon, 26 Feb 2024 12:03:08 -0600 Subject: [PATCH 07/16] Add font to header template --- src/apps/mailing/static/templates/header.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/apps/mailing/static/templates/header.html b/src/apps/mailing/static/templates/header.html index 6c6a822950b..31062dd9b3d 100644 --- a/src/apps/mailing/static/templates/header.html +++ b/src/apps/mailing/static/templates/header.html @@ -1,3 +1,10 @@ + +
MindLogger
From 11d6bc25ef3656602faa49412421127c119b0870 Mon Sep 17 00:00:00 2001 From: Carlos Chacon Date: Mon, 26 Feb 2024 13:04:02 -0600 Subject: [PATCH 08/16] M2-5342: Update button copy on transfer template --- .../templates/transfer_ownership_registered_user_en.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apps/mailing/static/templates/transfer_ownership_registered_user_en.html b/src/apps/mailing/static/templates/transfer_ownership_registered_user_en.html index e19dda46adc..5f0a7705b97 100644 --- a/src/apps/mailing/static/templates/transfer_ownership_registered_user_en.html +++ b/src/apps/mailing/static/templates/transfer_ownership_registered_user_en.html @@ -16,8 +16,8 @@ Create - account and accept invitation + style="padding: 0.4rem 1rem; font-size: 0.8rem; line-height: 1.5; border-radius: 100px; display: inline-block; font-weight: 400; text-align: center; vertical-align: middle; text-decoration: none; color: #ffffff; background-color: #00639A; border-color: #00639A;"> + Click here to accept this invitation {% include 'blocks/team_info_en.html' %} From cea1e1dca28cd6d1d44619c58468e92e5e124c9a Mon Sep 17 00:00:00 2001 From: Cleber Santos Date: Wed, 28 Feb 2024 11:41:30 -0300 Subject: [PATCH 09/16] Update email welcome message for transfer ownership unregisted user --- .../templates/transfer_ownership_unregistered_user_en.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/mailing/static/templates/transfer_ownership_unregistered_user_en.html b/src/apps/mailing/static/templates/transfer_ownership_unregistered_user_en.html index cfb0dcb1daa..555a5ec8b3c 100644 --- a/src/apps/mailing/static/templates/transfer_ownership_unregistered_user_en.html +++ b/src/apps/mailing/static/templates/transfer_ownership_unregistered_user_en.html @@ -5,7 +5,7 @@ From 3b150b579aeb7e22708ad5b56dfb39055565a04a Mon Sep 17 00:00:00 2001 From: Cleber Santos Date: Wed, 28 Feb 2024 16:53:36 -0300 Subject: [PATCH 10/16] Fix copy for button in invite registered user --- .../static/templates/invitation_registered_user_en.html | 4 ++-- .../static/templates/invitation_registered_user_fr.html | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/apps/mailing/static/templates/invitation_registered_user_en.html b/src/apps/mailing/static/templates/invitation_registered_user_en.html index 752a5f4cf17..cd90cfa248a 100644 --- a/src/apps/mailing/static/templates/invitation_registered_user_en.html +++ b/src/apps/mailing/static/templates/invitation_registered_user_en.html @@ -16,8 +16,8 @@ {% include 'blocks/team_info_en.html' %} diff --git a/src/apps/mailing/static/templates/invitation_registered_user_fr.html b/src/apps/mailing/static/templates/invitation_registered_user_fr.html index 95b219775a1..24b6a1da303 100644 --- a/src/apps/mailing/static/templates/invitation_registered_user_fr.html +++ b/src/apps/mailing/static/templates/invitation_registered_user_fr.html @@ -16,8 +16,7 @@ {% include 'blocks/team_info_fr.html' %} From c81cb15046762bd5f14a92314c903857ca4b9dab Mon Sep 17 00:00:00 2001 From: Ilya Bogretsov <35212515+ibogretsov@users.noreply.github.com> Date: Thu, 29 Feb 2024 10:20:05 +0300 Subject: [PATCH 11/16] feature/M2 5207 tests for applets api (#1130) * M2-5207: Add missing tests for applets API * M2-5207: Split big tests on several small tests to test thing --- conftest.py | 17 +- .../activities/domain/activity_item_base.py | 4 +- .../activities/domain/response_type_config.py | 30 +- .../activities/tests/fixtures/activities.py | 576 +++- src/apps/activities/tests/fixtures/configs.py | 140 +- src/apps/activities/tests/fixtures/items.py | 134 +- .../tests/fixtures/response_values.py | 17 +- .../tests/fixtures/scores_reports.py | 48 +- .../unit/domain/test_activity_item_create.py | 227 +- .../unit/domain/test_custom_validation.py | 2 +- src/apps/activity_flows/domain/flow_update.py | 8 +- src/apps/answers/tests/conftest.py | 11 - src/apps/applets/api/applets.py | 5 +- .../applets/domain/applet_create_update.py | 5 + src/apps/applets/tests/conftest.py | 64 +- src/apps/applets/tests/fixtures/applets.py | 196 +- src/apps/applets/tests/test_applet.py | 1831 +++-------- .../tests/test_applet_activity_items.py | 2681 +++-------------- src/apps/applets/tests/test_applet_folder.py | 25 +- src/apps/applets/tests/test_applet_link.py | 18 +- .../applets/tests/test_applet_settings.py | 8 +- src/apps/shared/test/client.py | 20 +- 22 files changed, 2122 insertions(+), 3945 deletions(-) diff --git a/conftest.py b/conftest.py index 309d17a7058..5c9c0833fd9 100644 --- a/conftest.py +++ b/conftest.py @@ -22,6 +22,7 @@ from infrastructure.app import create_app from infrastructure.database.core import build_engine from infrastructure.database.deps import get_session +from infrastructure.utility import FCMNotificationTest pytest_plugins = [ "apps.activities.tests.fixtures.configs", @@ -197,9 +198,14 @@ def pytest_collection_modifyitems(items) -> None: @pytest.fixture -def remote_image() -> str: +def local_image_name() -> str: + return "test.jpg" + + +@pytest.fixture +def remote_image(local_image_name: str) -> str: # TODO: add support for localimages for tests - return "https://www.w3schools.com/css/img_5terre_wide.jpg" + return f"http://localhost/{local_image_name}" @pytest.fixture @@ -268,3 +274,10 @@ def f() -> AsyncSession: new=get_session, ) return mock + + +@pytest.fixture +def fcm_client() -> FCMNotificationTest: + client = FCMNotificationTest() + client.notifications.clear() + return client diff --git a/src/apps/activities/domain/activity_item_base.py b/src/apps/activities/domain/activity_item_base.py index 8fc9d142908..38d9e72cd36 100644 --- a/src/apps/activities/domain/activity_item_base.py +++ b/src/apps/activities/domain/activity_item_base.py @@ -1,7 +1,7 @@ from pydantic import BaseModel, Field, root_validator, validator from apps.activities.domain.conditional_logic import ConditionalLogic -from apps.activities.domain.response_type_config import NoneResponseType, ResponseType, ResponseTypeValueConfig +from apps.activities.domain.response_type_config import ResponseType, ResponseTypeValueConfig from apps.activities.errors import ( AlertFlagMissingSingleMultiRowItemError, AlertFlagMissingSliderItemError, @@ -88,7 +88,7 @@ def validate_response_values(cls, value, values): response_type = values.get("response_type") if not response_type: return value - if response_type not in list(NoneResponseType): + if response_type not in ResponseType.get_non_response_types(): if type(value) is not ResponseTypeValueConfig[response_type]["value"]: try: value = ResponseTypeValueConfig[response_type]["value"](**value) diff --git a/src/apps/activities/domain/response_type_config.py b/src/apps/activities/domain/response_type_config.py index 6739d4f27a8..e21d337af55 100644 --- a/src/apps/activities/domain/response_type_config.py +++ b/src/apps/activities/domain/response_type_config.py @@ -324,20 +324,6 @@ def validate_nodes(cls, value, values) -> ABTrailsNodes | None: return value -class NoneResponseType(str, Enum): - TEXT = "text" - MESSAGE = "message" - TIMERANGE = "timeRange" - GEOLOCATION = "geolocation" - PHOTO = "photo" - VIDEO = "video" - DATE = "date" - TIME = "time" - FLANKER = "flanker" - STABILITYTRACKER = "stabilityTracker" - ABTRAILS = "ABTrails" - - class ResponseType(str, Enum): TEXT = "text" SINGLESELECT = "singleSelect" @@ -361,6 +347,22 @@ class ResponseType(str, Enum): STABILITYTRACKER = "stabilityTracker" ABTRAILS = "ABTrails" + @classmethod + def get_non_response_types(cls): + return ( + cls.TEXT, + cls.MESSAGE, + cls.TIMERANGE, + cls.GEOLOCATION, + cls.PHOTO, + cls.VIDEO, + cls.DATE, + cls.TIME, + cls.FLANKER, + cls.STABILITYTRACKER, + cls.ABTRAILS, + ) + class PerformanceTaskType(str, Enum): FLANKER = "flanker" diff --git a/src/apps/activities/tests/fixtures/activities.py b/src/apps/activities/tests/fixtures/activities.py index 20f4dd5f562..601e43f640b 100644 --- a/src/apps/activities/tests/fixtures/activities.py +++ b/src/apps/activities/tests/fixtures/activities.py @@ -3,11 +3,583 @@ import pytest from apps.activities.domain.activity_create import ActivityCreate, ActivityItemCreate +from apps.activities.domain.response_type_config import ( + ABTrailsConfig, + ABTrailsDeviceType, + ABTrailsOrder, + BlockConfiguration, + BlockType, + ButtonConfiguration, + FlankerConfig, + InputType, + MessageConfig, + Phase, + ResponseType, + SamplingMethod, + StabilityTrackerConfig, + StimulusConfigId, + StimulusConfiguration, +) from apps.shared.enums import Language @pytest.fixture -def activity_create(single_select_item_create: ActivityItemCreate) -> ActivityCreate: +def activity_create() -> ActivityCreate: + return ActivityCreate(name="test", description={Language.ENGLISH: "test"}, items=[], key=uuid.uuid4()) + + +@pytest.fixture +def activity_ab_trails_ipad_create() -> ActivityCreate: + # All values are hardcoded on UI side. The 'key' field is random uuid + return ActivityCreate( + name="A/B Trails iPad", + description={Language.ENGLISH: "A/B Trails"}, + is_hidden=False, + report_included_item_name="", + key=uuid.uuid4(), + items=[ + ActivityItemCreate( + question={"en": "Sample A"}, + response_type=ResponseType.ABTRAILS, + response_values=None, + config=ABTrailsConfig(device_type=ABTrailsDeviceType.TABLET, order_name=ABTrailsOrder.FIRST), + name="ABTrails_tablet_1", + ), + ActivityItemCreate( + question={"en": "Test A"}, + response_type=ResponseType.ABTRAILS, + response_values=None, + config=ABTrailsConfig(device_type=ABTrailsDeviceType.TABLET, order_name=ABTrailsOrder.SECOND), + name="ABTrails_tablet_2", + ), + ActivityItemCreate( + question={"en": "Sample B"}, + response_type=ResponseType.ABTRAILS, + response_values=None, + config=ABTrailsConfig(device_type=ABTrailsDeviceType.TABLET, order_name=ABTrailsOrder.THIRD), + name="ABTrails_tablet_3", + ), + ActivityItemCreate( + question={"en": "Test B"}, + response_type=ResponseType.ABTRAILS, + response_values=None, + config=ABTrailsConfig(device_type=ABTrailsDeviceType.TABLET, order_name=ABTrailsOrder.FOURTH), + name="ABTrails_tablet_4", + ), + ], + ) + + +@pytest.fixture +def activity_ab_trails_mobile_create() -> ActivityCreate: + # All values are hardcoded on UI side. The 'key' field is random uuid + return ActivityCreate( + name="A/B Trails Mobile", + description={Language.ENGLISH: "A/B Trails"}, + is_hidden=False, + report_included_item_name="", + key=uuid.uuid4(), + items=[ + ActivityItemCreate( + question={"en": "Sample A"}, + response_type=ResponseType.ABTRAILS, + response_values=None, + config=ABTrailsConfig(device_type=ABTrailsDeviceType.MOBILE, order_name=ABTrailsOrder.FIRST), + name="ABTrails_mobile_1", + is_hidden=False, + ), + ActivityItemCreate( + question={"en": "Test A"}, + response_type=ResponseType.ABTRAILS, + response_values=None, + config=ABTrailsConfig(device_type=ABTrailsDeviceType.MOBILE, order_name=ABTrailsOrder.SECOND), + name="ABTrails_mobile_2", + is_hidden=False, + ), + ActivityItemCreate( + question={"en": "Sample B"}, + response_type=ResponseType.ABTRAILS, + response_values=None, + config=ABTrailsConfig(device_type=ABTrailsDeviceType.MOBILE, order_name=ABTrailsOrder.THIRD), + name="ABTrails_mobile_3", + is_hidden=False, + ), + ActivityItemCreate( + question={"en": "Test B"}, + response_type=ResponseType.ABTRAILS, + response_values=None, + config=ABTrailsConfig(device_type=ABTrailsDeviceType.MOBILE, order_name=ABTrailsOrder.FOURTH), + name="ABTrails_mobile_4", + is_hidden=False, + ), + ], + ) + + +@pytest.fixture +def activity_flanker_create(remote_image: str, local_image_name: str) -> ActivityCreate: + stimulus_id = uuid.uuid4() + return ActivityCreate( + name="Simple & Choice Reaction Time Task Builder", + description={Language.ENGLISH: "description"}, + is_hidden=False, + report_included_item_name="", + is_performance_task=False, + key=uuid.uuid4(), + items=[ + ActivityItemCreate( + question={"en": "description"}, + response_type=ResponseType.MESSAGE, + response_values=None, + config=MessageConfig(remove_back_button=True, timer=None), + name="Flanker_VSR_instructions", + is_hidden=False, + ), + ActivityItemCreate( + question={"en": "description"}, + response_type=ResponseType.MESSAGE, + response_values=None, + config=MessageConfig(remove_back_button=True, timer=None), + name="Flanker_Practice_instructions_1", + is_hidden=False, + ), + ActivityItemCreate( + question={"en": ""}, + response_type=ResponseType.FLANKER, + response_values=None, + config=FlankerConfig( + stimulus_trials=[ + StimulusConfiguration( + id=StimulusConfigId(str(stimulus_id)), + image=remote_image, + text=local_image_name, + value=0, + ) + ], + blocks=[ + BlockConfiguration(name="Block 1", order=[StimulusConfigId(str(stimulus_id))]), + BlockConfiguration(name="Block 2", order=[StimulusConfigId(str(stimulus_id))]), + BlockConfiguration(name="Block 3", order=[StimulusConfigId(str(stimulus_id))]), + BlockConfiguration(name="Block 4", order=[StimulusConfigId(str(stimulus_id))]), + ], + buttons=[ + ButtonConfiguration(text="", image=remote_image, value=0), + ButtonConfiguration(text="", image=remote_image, value=1), + ], + next_button="OK", + fixation_duration=None, + fixation_screen=None, + minimum_accuracy=75, + sample_size=1, + sampling_method=SamplingMethod.RANDOMIZE_ORDER, + show_feedback=True, + show_fixation=False, + show_results=True, + trial_duration=3000, + is_last_practice=False, + is_first_practice=True, + is_last_test=False, + block_type=BlockType.PRACTICE, + ), + name="Flanker_Practice_1", + ), + ActivityItemCreate( + question={"en": "description"}, + response_type=ResponseType.MESSAGE, + response_values=None, + config=MessageConfig(remove_back_button=True, timer=None), + name="Flanker_Practice_instructions_2", + is_hidden=False, + ), + ActivityItemCreate( + question={"en": ""}, + response_type=ResponseType.FLANKER, + response_values=None, + config=FlankerConfig( + stimulus_trials=[ + StimulusConfiguration( + id=StimulusConfigId(str(stimulus_id)), + image=remote_image, + text=local_image_name, + value=0, + ) + ], + blocks=[ + BlockConfiguration(name="Block 1", order=[StimulusConfigId(str(stimulus_id))]), + BlockConfiguration(name="Block 2", order=[StimulusConfigId(str(stimulus_id))]), + BlockConfiguration(name="Block 3", order=[StimulusConfigId(str(stimulus_id))]), + BlockConfiguration(name="Block 4", order=[StimulusConfigId(str(stimulus_id))]), + ], + buttons=[ + ButtonConfiguration( + text="", + image=remote_image, + value=0, + ), + ButtonConfiguration( + text="", + image=remote_image, + value=1, + ), + ], + next_button="OK", + fixation_duration=None, + fixation_screen=None, + minimum_accuracy=75, + sample_size=1, + sampling_method=SamplingMethod.RANDOMIZE_ORDER, + show_feedback=True, + show_fixation=False, + show_results=True, + trial_duration=3000, + is_last_practice=False, + is_first_practice=False, + is_last_test=False, + block_type=BlockType.PRACTICE, + ), + name="Flanker_Practice_2", + ), + ActivityItemCreate( + question={"en": "description"}, + response_type=ResponseType.MESSAGE, + response_values=None, + config=MessageConfig(remove_back_button=True, timer=None), + name="Flanker_Practice_instructions_3", + ), + ActivityItemCreate( + question={"en": ""}, + response_type=ResponseType.FLANKER, + response_values=None, + config=FlankerConfig( + stimulus_trials=[ + StimulusConfiguration( + id=StimulusConfigId(str(stimulus_id)), + image=remote_image, + text=local_image_name, + value=0, + ) + ], + blocks=[ + BlockConfiguration(name="Block 1", order=[StimulusConfigId(str(stimulus_id))]), + BlockConfiguration(name="Block 2", order=[StimulusConfigId(str(stimulus_id))]), + BlockConfiguration(name="Block 3", order=[StimulusConfigId(str(stimulus_id))]), + BlockConfiguration(name="Block 4", order=[StimulusConfigId(str(stimulus_id))]), + ], + buttons=[ + ButtonConfiguration( + text="", + image=remote_image, + value=0, + ), + ButtonConfiguration( + text="", + image=remote_image, + value=1, + ), + ], + next_button="OK", + fixation_duration=None, + fixation_screen=None, + minimum_accuracy=75, + sample_size=1, + sampling_method=SamplingMethod.RANDOMIZE_ORDER, + show_feedback=True, + show_fixation=False, + show_results=True, + trial_duration=3000, + is_last_practice=True, + is_first_practice=False, + is_last_test=False, + block_type=BlockType.PRACTICE, + ), + name="Flanker_Practice_3", + ), + ActivityItemCreate( + question={"en": "description"}, + response_type=ResponseType.MESSAGE, + response_values=None, + config=MessageConfig(remove_back_button=True, timer=None), + name="Flanker_test_instructions_1", + ), + ActivityItemCreate( + question={"en": ""}, + response_type=ResponseType.FLANKER, + response_values=None, + config=FlankerConfig( + stimulus_trials=[ + StimulusConfiguration( + id=StimulusConfigId(str(stimulus_id)), + image=remote_image, + text=local_image_name, + value=0, + ) + ], + blocks=[ + BlockConfiguration(name="Block 1", order=[StimulusConfigId(str(stimulus_id))]), + BlockConfiguration(name="Block 2", order=[StimulusConfigId(str(stimulus_id))]), + BlockConfiguration(name="Block 3", order=[StimulusConfigId(str(stimulus_id))]), + BlockConfiguration(name="Block 4", order=[StimulusConfigId(str(stimulus_id))]), + ], + buttons=[ + ButtonConfiguration( + text="", + image=remote_image, + value=0, + ), + ButtonConfiguration( + text="", + image=remote_image, + value=1, + ), + ], + next_button="Continue", + fixation_duration=None, + fixation_screen=None, + minimum_accuracy=None, + sample_size=1, + sampling_method=SamplingMethod.RANDOMIZE_ORDER, + show_feedback=False, + show_fixation=False, + show_results=True, + trial_duration=3000, + is_last_practice=False, + is_first_practice=False, + is_last_test=False, + block_type=BlockType.TEST, + ), + name="Flanker_test_1", + ), + ActivityItemCreate( + question={"en": "description"}, + response_type=ResponseType.MESSAGE, + response_values=None, + config=MessageConfig(remove_back_button=True, timer=None), + name="Flanker_test_instructions_2", + ), + ActivityItemCreate( + question={"en": ""}, + response_type=ResponseType.FLANKER, + response_values=None, + config=FlankerConfig( + stimulus_trials=[ + StimulusConfiguration( + id=StimulusConfigId(str(stimulus_id)), + image=remote_image, + text=local_image_name, + value=0, + weight=None, + ) + ], + blocks=[ + BlockConfiguration(name="Block 1", order=[StimulusConfigId(str(stimulus_id))]), + BlockConfiguration(name="Block 2", order=[StimulusConfigId(str(stimulus_id))]), + BlockConfiguration(name="Block 3", order=[StimulusConfigId(str(stimulus_id))]), + BlockConfiguration(name="Block 4", order=[StimulusConfigId(str(stimulus_id))]), + ], + buttons=[ + ButtonConfiguration( + text="", + image=remote_image, + value=0, + ), + ButtonConfiguration( + text="", + image=remote_image, + value=1, + ), + ], + next_button="Continue", + fixation_duration=None, + fixation_screen=None, + minimum_accuracy=None, + sample_size=1, + sampling_method=SamplingMethod.RANDOMIZE_ORDER, + show_feedback=False, + show_fixation=False, + show_results=True, + trial_duration=3000, + is_last_practice=False, + is_first_practice=False, + is_last_test=False, + block_type=BlockType.TEST, + ), + name="Flanker_test_2", + ), + ActivityItemCreate( + question={"en": "description"}, + response_type=ResponseType.MESSAGE, + response_values=None, + config=MessageConfig(remove_back_button=True, timer=None), + name="Flanker_test_instructions_3", + ), + ActivityItemCreate( + question={"en": ""}, + response_type=ResponseType.FLANKER, + response_values=None, + config=FlankerConfig( + stimulus_trials=[ + StimulusConfiguration( + id=StimulusConfigId(str(stimulus_id)), + image=remote_image, + text=local_image_name, + value=0, + weight=None, + ) + ], + blocks=[ + BlockConfiguration(name="Block 1", order=[StimulusConfigId(str(stimulus_id))]), + BlockConfiguration(name="Block 2", order=[StimulusConfigId(str(stimulus_id))]), + BlockConfiguration(name="Block 3", order=[StimulusConfigId(str(stimulus_id))]), + BlockConfiguration(name="Block 4", order=[StimulusConfigId(str(stimulus_id))]), + ], + buttons=[ + ButtonConfiguration( + text="", + image=remote_image, + value=0, + ), + ButtonConfiguration( + text="", + image=remote_image, + value=1, + ), + ], + next_button="Finish", + fixation_duration=None, + fixation_screen=None, + minimum_accuracy=None, + sample_size=1, + sampling_method=SamplingMethod.RANDOMIZE_ORDER, + show_feedback=False, + show_fixation=False, + show_results=True, + trial_duration=3000, + is_last_practice=False, + is_first_practice=False, + is_last_test=True, + block_type=BlockType.TEST, + ), + name="Flanker_test_3", + ), + ], + ) + + +@pytest.fixture +def actvitiy_cst_gyroscope_create() -> ActivityCreate: + return ActivityCreate( + name="CST Gyroscope", + description={Language.ENGLISH: "description"}, + is_hidden=False, + report_included_item_name="", + key=uuid.uuid4(), + items=[ + ActivityItemCreate( + question={"en": "description"}, + response_type=ResponseType.MESSAGE, + response_values=None, + config=MessageConfig(remove_back_button=True, timer=None), + name="Gyroscope_General_instruction", + ), + ActivityItemCreate( + question={"en": "description"}, + response_type=ResponseType.MESSAGE, + response_values=None, + config=MessageConfig(remove_back_button=True, timer=None), + name="Gyroscope_Calibration_Practice_instruction", + ), + ActivityItemCreate( + question={"en": ""}, + response_type=ResponseType.STABILITYTRACKER, + response_values=None, + config=StabilityTrackerConfig( + user_input_type=InputType.GYROSCOPE, + phase=Phase.PRACTICE, + trials_number=3, + duration_minutes=5.0, + lambda_slope=20.0, + ), + name="Gyroscope_Calibration_Practice", + ), + ActivityItemCreate( + question={"en": "description"}, + response_type=ResponseType.MESSAGE, + response_values=None, + config=MessageConfig(remove_back_button=True, timer=None), + name="Gyroscope_Test_instruction", + ), + ActivityItemCreate( + question={"en": ""}, + response_type=ResponseType.STABILITYTRACKER, + response_values=None, + config=StabilityTrackerConfig( + user_input_type=InputType.GYROSCOPE, + phase=Phase.TEST, + trials_number=3, + duration_minutes=5.0, + lambda_slope=20.0, + ), + name="Gyroscope_Test", + ), + ], + ) + + +@pytest.fixture +def actvitiy_cst_touch_create() -> ActivityCreate: return ActivityCreate( - name="test", description={Language.ENGLISH: "test"}, items=[single_select_item_create], key=uuid.uuid4() + name="CST Touch", + description={Language.ENGLISH: "description"}, + is_hidden=False, + report_included_item_name="", + key=uuid.uuid4(), + items=[ + ActivityItemCreate( + question={"en": "description"}, + response_type=ResponseType.MESSAGE, + response_values=None, + config=MessageConfig(remove_back_button=True, timer=None), + name="Touch_General_instruction", + ), + ActivityItemCreate( + question={"en": "Description"}, + response_type=ResponseType.MESSAGE, + response_values=None, + config=MessageConfig(remove_back_button=True, timer=None), + name="Touch_Calibration_Practice_instruction", + ), + ActivityItemCreate( + question={"en": ""}, + response_type=ResponseType.STABILITYTRACKER, + response_values=None, + config=StabilityTrackerConfig( + user_input_type=InputType.TOUCH, + phase=Phase.PRACTICE, + trials_number=3, + duration_minutes=5.0, + lambda_slope=20.0, + ), + name="Touch_Calibration_Practice", + ), + ActivityItemCreate( + question={"en": "Description"}, + response_type=ResponseType.MESSAGE, + response_values=None, + config=MessageConfig(remove_back_button=True, timer=None), + name="Touch_Test_instruction", + ), + ActivityItemCreate( + question={"en": ""}, + response_type=ResponseType.STABILITYTRACKER, + response_values=None, + config=StabilityTrackerConfig( + user_input_type=InputType.TOUCH, + phase=Phase.TEST, + trials_number=3, + duration_minutes=5.0, + lambda_slope=20.0, + ), + name="Touch_Test", + ), + ], ) diff --git a/src/apps/activities/tests/fixtures/configs.py b/src/apps/activities/tests/fixtures/configs.py index 86708c98991..cba39b2479d 100644 --- a/src/apps/activities/tests/fixtures/configs.py +++ b/src/apps/activities/tests/fixtures/configs.py @@ -2,15 +2,25 @@ from apps.activities.domain.response_type_config import ( AdditionalResponseOption, + AudioConfig, + AudioPlayerConfig, DateConfig, + DefaultConfig, DrawingConfig, + GeolocationConfig, + MessageConfig, MultiSelectionConfig, MultiSelectionRowsConfig, + NumberSelectionConfig, + PhotoConfig, SingleSelectionConfig, SingleSelectionRowsConfig, SliderConfig, SliderRowsConfig, TextConfig, + TimeConfig, + TimeRangeConfig, + VideoConfig, ) @@ -20,63 +30,86 @@ def additional_response_option() -> AdditionalResponseOption: @pytest.fixture -def single_select_config( - additional_response_option: AdditionalResponseOption, -) -> SingleSelectionConfig: +def default_config(additional_response_option: AdditionalResponseOption) -> DefaultConfig: + return DefaultConfig( + remove_back_button=False, skippable_item=False, additional_response_option=additional_response_option, timer=0 + ) + + +@pytest.fixture +def single_select_config(default_config: DefaultConfig) -> SingleSelectionConfig: return SingleSelectionConfig( randomize_options=False, - timer=0, add_scores=False, add_tokens=False, set_alerts=False, add_tooltip=False, set_palette=False, - remove_back_button=False, - skippable_item=False, - additional_response_option=additional_response_option, + **default_config.dict(), ) @pytest.fixture -def multi_select_config(single_select_config) -> MultiSelectionConfig: +def multi_select_config(single_select_config: SingleSelectionConfig) -> MultiSelectionConfig: data = single_select_config.dict() return MultiSelectionConfig(**data) @pytest.fixture -def slider_config( - additional_response_option: AdditionalResponseOption, -) -> SliderConfig: +def slider_config(default_config: DefaultConfig) -> SliderConfig: return SliderConfig( - timer=0, add_scores=False, set_alerts=False, - remove_back_button=False, - skippable_item=False, show_tick_marks=False, show_tick_labels=False, continuous_slider=False, - additional_response_option=additional_response_option, + **default_config.dict(), ) @pytest.fixture -def date_config( - additional_response_option: AdditionalResponseOption, -) -> DateConfig: - return DateConfig( - remove_back_button=False, - skippable_item=False, - timer=0, - additional_response_option=additional_response_option, +def date_config(default_config: DefaultConfig) -> DateConfig: + return DateConfig(**default_config.dict()) + + +@pytest.fixture +def number_selection_config(default_config: DefaultConfig) -> NumberSelectionConfig: + return NumberSelectionConfig(**default_config.dict()) + + +@pytest.fixture +def time_config(default_config: DefaultConfig) -> TimeConfig: + return TimeConfig(**default_config.dict()) + + +@pytest.fixture +def time_range_config(default_config: DefaultConfig) -> TimeRangeConfig: + return TimeRangeConfig(**default_config.dict()) + + +@pytest.fixture +def single_select_row_config(default_config: DefaultConfig) -> SingleSelectionRowsConfig: + return SingleSelectionRowsConfig( + add_scores=False, set_alerts=False, add_tooltip=False, add_tokens=None, **default_config.dict() ) @pytest.fixture -def text_config() -> TextConfig: +def multi_select_row_config( + single_select_row_config: SingleSelectionRowsConfig, +) -> MultiSelectionRowsConfig: + return MultiSelectionRowsConfig(**single_select_row_config.dict()) + + +@pytest.fixture +def slider_rows_config(default_config: DefaultConfig) -> SliderRowsConfig: + return SliderRowsConfig(add_scores=False, set_alerts=False, **default_config.dict()) + + +@pytest.fixture +def text_config(default_config: DefaultConfig) -> TextConfig: return TextConfig( - remove_back_button=False, - skippable_item=False, + **default_config.dict(), correct_answer_required=False, numerical_response_required=False, response_data_identifier=False, @@ -85,44 +118,35 @@ def text_config() -> TextConfig: @pytest.fixture -def drawing_config( - additional_response_option: AdditionalResponseOption, -) -> DrawingConfig: - return DrawingConfig( - remove_back_button=False, - remove_undo_button=False, - additional_response_option=additional_response_option, - skippable_item=False, - timer=0, - ) +def drawing_config(default_config: DefaultConfig) -> DrawingConfig: + return DrawingConfig(remove_undo_button=False, **default_config.dict()) @pytest.fixture -def slider_rows_config() -> SliderRowsConfig: - return SliderRowsConfig( - remove_back_button=False, - skippable_item=False, - add_scores=False, - set_alerts=False, - timer=0, - ) +def photo_config(default_config: DefaultConfig) -> PhotoConfig: + return PhotoConfig(**default_config.dict()) @pytest.fixture -def single_select_row_config() -> SingleSelectionRowsConfig: - return SingleSelectionRowsConfig( - timer=0, - add_scores=False, - set_alerts=False, - add_tooltip=False, - add_tokens=None, - remove_back_button=False, - skippable_item=False, - ) +def video_config(default_config: DefaultConfig) -> VideoConfig: + return VideoConfig(**default_config.dict()) @pytest.fixture -def multi_select_row_config( - single_select_row_config: SingleSelectionRowsConfig, -) -> MultiSelectionRowsConfig: - return MultiSelectionRowsConfig(**single_select_row_config.dict()) +def geolocation_config(default_config: DefaultConfig) -> GeolocationConfig: + return GeolocationConfig(**default_config.dict()) + + +@pytest.fixture +def audio_config(default_config: DefaultConfig) -> AudioConfig: + return AudioConfig(**default_config.dict()) + + +@pytest.fixture +def message_config(default_config: DefaultConfig) -> MessageConfig: + return MessageConfig(**default_config.dict()) + + +@pytest.fixture +def audio_player_config(default_config: DefaultConfig) -> AudioPlayerConfig: + return AudioPlayerConfig(**default_config.dict(), play_once=False) diff --git a/src/apps/activities/tests/fixtures/items.py b/src/apps/activities/tests/fixtures/items.py index 4c90bcb97bd..332e12fd1ca 100644 --- a/src/apps/activities/tests/fixtures/items.py +++ b/src/apps/activities/tests/fixtures/items.py @@ -2,18 +2,33 @@ from apps.activities.domain.activity_create import ActivityItemCreate from apps.activities.domain.response_type_config import ( + AudioConfig, + AudioPlayerConfig, + DateConfig, + DrawingConfig, + GeolocationConfig, + MessageConfig, MultiSelectionConfig, MultiSelectionRowsConfig, + NumberSelectionConfig, + PhotoConfig, ResponseType, SingleSelectionConfig, SingleSelectionRowsConfig, SliderConfig, SliderRowsConfig, TextConfig, + TimeConfig, + TimeRangeConfig, + VideoConfig, ) from apps.activities.domain.response_values import ( + AudioPlayerValues, + AudioValues, + DrawingValues, MultiSelectionRowsValues, MultiSelectionValues, + NumberSelectionValues, SingleSelectionRowsValues, SingleSelectionValues, SliderRowsValues, @@ -45,11 +60,11 @@ def single_select_item_create( def multi_select_item_create( base_item_data: BaseItemData, multi_select_config: MultiSelectionConfig, - multi_select_reponse_values: MultiSelectionValues, + multi_select_response_values: MultiSelectionValues, ) -> ActivityItemCreate: return ActivityItemCreate( response_type=ResponseType.MULTISELECT, - response_values=multi_select_reponse_values, + response_values=multi_select_response_values, config=multi_select_config, **base_item_data.dict(), ) @@ -69,6 +84,47 @@ def slider_item_create( ) +@pytest.fixture +def date_item_create(date_config: DateConfig, base_item_data: BaseItemData) -> ActivityItemCreate: + return ActivityItemCreate( + **base_item_data.dict(), + response_type=ResponseType.DATE, + config=date_config, + ) + + +@pytest.fixture +def number_selection_item_create( + number_selection_config: NumberSelectionConfig, + number_selection_response_values: NumberSelectionValues, + base_item_data: BaseItemData, +) -> ActivityItemCreate: + return ActivityItemCreate( + **base_item_data.dict(), + response_type=ResponseType.NUMBERSELECT, + config=number_selection_config, + response_values=number_selection_response_values, + ) + + +@pytest.fixture +def time_item_create(time_config: TimeConfig, base_item_data: BaseItemData) -> ActivityItemCreate: + return ActivityItemCreate( + **base_item_data.dict(), + response_type=ResponseType.TIME, + config=time_config, + ) + + +@pytest.fixture +def time_range_item_create(time_range_config: TimeRangeConfig, base_item_data: BaseItemData) -> ActivityItemCreate: + return ActivityItemCreate( + **base_item_data.dict(), + response_type=ResponseType.TIMERANGE, + config=time_range_config, + ) + + @pytest.fixture def single_select_row_item_create( base_item_data: BaseItemData, @@ -118,3 +174,77 @@ def text_item_create(text_config: TextConfig, base_item_data: BaseItemData) -> A response_type=ResponseType.TEXT, config=text_config, ) + + +@pytest.fixture +def drawing_item_create( + drawing_config: DrawingConfig, drawing_response_values: DrawingValues, base_item_data: BaseItemData +) -> ActivityItemCreate: + return ActivityItemCreate( + **base_item_data.dict(), + response_type=ResponseType.DRAWING, + config=drawing_config, + response_values=drawing_response_values, + ) + + +@pytest.fixture +def photo_item_create(photo_config: PhotoConfig, base_item_data: BaseItemData) -> ActivityItemCreate: + return ActivityItemCreate( + **base_item_data.dict(), + response_type=ResponseType.PHOTO, + config=photo_config, + ) + + +@pytest.fixture +def video_item_create(video_config: VideoConfig, base_item_data: BaseItemData) -> ActivityItemCreate: + return ActivityItemCreate( + **base_item_data.dict(), + response_type=ResponseType.VIDEO, + config=video_config, + ) + + +@pytest.fixture +def geolocation_item_create(geolocation_config: GeolocationConfig, base_item_data: BaseItemData) -> ActivityItemCreate: + return ActivityItemCreate( + **base_item_data.dict(), + response_type=ResponseType.GEOLOCATION, + config=geolocation_config, + ) + + +@pytest.fixture +def audio_item_create( + audio_config: AudioConfig, audio_response_values: AudioValues, base_item_data: BaseItemData +) -> ActivityItemCreate: + return ActivityItemCreate( + **base_item_data.dict(), + response_type=ResponseType.AUDIO, + config=audio_config, + response_values=audio_response_values, + ) + + +@pytest.fixture +def message_item_create(message_config: MessageConfig, base_item_data: BaseItemData) -> ActivityItemCreate: + return ActivityItemCreate( + **base_item_data.dict(), + response_type=ResponseType.MESSAGE, + config=message_config, + ) + + +@pytest.fixture +def audio_player_item_create( + audio_player_config: AudioPlayerConfig, + audio_player_response_values: AudioPlayerValues, + base_item_data: BaseItemData, +) -> ActivityItemCreate: + return ActivityItemCreate( + **base_item_data.dict(), + response_type=ResponseType.AUDIOPLAYER, + config=audio_player_config, + response_values=audio_player_response_values, + ) diff --git a/src/apps/activities/tests/fixtures/response_values.py b/src/apps/activities/tests/fixtures/response_values.py index d4d0638c2c6..c266b030d6a 100644 --- a/src/apps/activities/tests/fixtures/response_values.py +++ b/src/apps/activities/tests/fixtures/response_values.py @@ -4,6 +4,8 @@ import pytest from apps.activities.domain.response_values import ( + AudioPlayerValues, + AudioValues, DrawingValues, MultiSelectionRowsValues, MultiSelectionValues, @@ -42,7 +44,7 @@ def single_select_response_values() -> SingleSelectionValues: @pytest.fixture -def multi_select_reponse_values( +def multi_select_response_values( single_select_response_values: SingleSelectionValues, ) -> MultiSelectionValues: data = single_select_response_values.dict() @@ -65,7 +67,7 @@ def slider_response_values() -> SliderValues: @pytest.fixture -def number_select_response_values() -> NumberSelectionValues: +def number_selection_response_values() -> NumberSelectionValues: return NumberSelectionValues() @@ -133,3 +135,14 @@ def multi_select_row_response_values( single_select_row_response_values: SingleSelectionRowsValues, ) -> MultiSelectionRowsValues: return MultiSelectionRowsValues(**single_select_row_response_values.dict()) + + +@pytest.fixture +def audio_response_values() -> AudioValues: + return AudioValues(max_duration=1) + + +@pytest.fixture +def audio_player_response_values() -> AudioPlayerValues: + # TODO: Add some audio file + return AudioPlayerValues(file=None) diff --git a/src/apps/activities/tests/fixtures/scores_reports.py b/src/apps/activities/tests/fixtures/scores_reports.py index a49a0794fbe..8b9a1d0c921 100644 --- a/src/apps/activities/tests/fixtures/scores_reports.py +++ b/src/apps/activities/tests/fixtures/scores_reports.py @@ -9,11 +9,14 @@ ScoreConditionalLogic, ScoresAndReports, Section, + SectionConditionalLogic, Subscale, SubscaleCalculationType, SubscaleItem, SubscaleItemType, + SubScaleLookupTable, SubscaleSetting, + TotalScoreTable, ) SCORE_ID = "not_uuid_testscore" @@ -45,6 +48,20 @@ def score() -> Score: ) +@pytest.fixture +def section_conditional_logic() -> SectionConditionalLogic: + return SectionConditionalLogic( + match=Match.ALL, + conditions=[ + EqualCondition( + item_name=SCORE_ID, + payload=ValuePayload(value=1), + type=ConditionType.EQUAL, + ) + ], + ) + + @pytest.fixture def section() -> Section: return Section(type=ReportType.section, name="testsection") @@ -67,15 +84,44 @@ def subscale_item() -> SubscaleItem: @pytest.fixture def subscale(subscale_item: SubscaleItem) -> Subscale: return Subscale( - name="test subscale name", + name="subscale type item", scoring=SubscaleCalculationType.AVERAGE, items=[subscale_item], ) +@pytest.fixture +def subscale_item_type_subscale(subscale: Subscale) -> SubscaleItem: + # Depends on subscalke because name should contain subscale item + return SubscaleItem(name=subscale.name, type=SubscaleItemType.SUBSCALE) + + +@pytest.fixture +def subscale_with_item_type_subscale(subscale_item_type_subscale: SubscaleItem) -> Subscale: + return Subscale( + name="subscale type subscale", items=[subscale_item_type_subscale], scoring=SubscaleCalculationType.AVERAGE + ) + + @pytest.fixture def subscale_setting(subscale: Subscale) -> SubscaleSetting: return SubscaleSetting( calculate_total_score=SubscaleCalculationType.AVERAGE, subscales=[subscale], ) + + +@pytest.fixture +def subscale_total_score_table() -> list[TotalScoreTable]: + return [ + TotalScoreTable(raw_score="0 ~ 2", optional_text="some url"), + TotalScoreTable(raw_score="4 ~ 20", optional_text="some url"), + ] + + +@pytest.fixture +def subscale_lookup_table() -> list[SubScaleLookupTable]: + return [ + SubScaleLookupTable(score="10", age=10, sex="M", raw_score="1", optional_text="some url"), + SubScaleLookupTable(score="20", age=10, sex="F", raw_score="2", optional_text="some url"), + ] diff --git a/src/apps/activities/tests/unit/domain/test_activity_item_create.py b/src/apps/activities/tests/unit/domain/test_activity_item_create.py index 3d92ef347ea..cce69adeac9 100644 --- a/src/apps/activities/tests/unit/domain/test_activity_item_create.py +++ b/src/apps/activities/tests/unit/domain/test_activity_item_create.py @@ -5,13 +5,23 @@ from apps.activities import errors from apps.activities.domain.activity_create import ActivityItemCreate -from apps.activities.domain.response_type_config import DrawingConfig, ResponseType, TextConfig +from apps.activities.domain.response_type_config import ( + DateConfig, + DrawingConfig, + ResponseType, + SingleSelectionConfig, + SliderConfig, + SliderRowsConfig, + TextConfig, +) from apps.activities.domain.response_values import ( DrawingValues, MultiSelectionValues, NumberSelectionValues, SingleSelectionRowsValues, SingleSelectionValues, + SliderRowsValues, + SliderValues, _MultiSelectionValue, ) from apps.activities.tests.utils import BaseItemData @@ -171,18 +181,18 @@ def test_single_select_row_response_values_not_valid_data_matrix_len_options_doe def test_number_selection_response_values_min_value_greater_than_max_value( - number_select_response_values, + number_selection_response_values, ): - data = number_select_response_values.dict() + data = number_selection_response_values.dict() data["min_value"], data["max_value"] = data["max_value"], data["min_value"] with pytest.raises(errors.MinValueError): NumberSelectionValues(**data) def test_number_selection_response_values_min_value_is_equal_max_value( - number_select_response_values, + number_selection_response_values, ): - data = number_select_response_values.dict() + data = number_selection_response_values.dict() data["min_value"], data["max_value"] = 0, 0 with pytest.raises(errors.MinValueError): NumberSelectionValues(**data) @@ -287,9 +297,9 @@ def test_activity_item_create_response_values_not_none_for_non_response_response def test_multi_select_response_values_multiple_none_options( # noqa: E501 - multi_select_reponse_values: MultiSelectionValues, + multi_select_response_values: MultiSelectionValues, ): - data = multi_select_reponse_values.dict() + data = multi_select_response_values.dict() data["options"].append( _MultiSelectionValue( id=str(uuid.uuid4()), @@ -319,3 +329,206 @@ def test_multi_select_response_values_multiple_none_options( # noqa: E501 with pytest.raises(errors.MultiSelectNoneOptionError): MultiSelectionValues(**data) + + +def test_create_item__item_name_with_not_valid_character(base_item_data: BaseItemData, date_config: DateConfig): + name = "%_percent_not_allowed" + with pytest.raises(errors.IncorrectNameCharactersError): + ActivityItemCreate( + question=base_item_data.question, + name=name, + config=date_config, + response_type=ResponseType.DATE, + response_values=None, + ) + + +@pytest.mark.parametrize("field_to_delete", ("add_scores", "set_alerts")) +def test_create_item__not_valid_config_missing_add_scores_and_set_alerts( + single_select_item_create, field_to_delete +) -> None: + data = single_select_item_create.dict() + del data["config"][field_to_delete] + with pytest.raises(errors.IncorrectConfigError) as exc: + ActivityItemCreate(**data) + assert exc.value.message.format(type=SingleSelectionConfig) + + +@pytest.mark.parametrize("response_type", (None, "NotValid")) +def test_create_item__not_valid_response_type(single_select_item_create, response_type) -> None: + data = single_select_item_create.dict() + data["response_type"] = response_type + with pytest.raises(errors.IncorrectResponseValueError) as exc: + ActivityItemCreate(**data) + assert exc.value.message.format(type=ResponseType) + + +@pytest.mark.parametrize("value", (None, {})) +def test_create_single_select_item__not_valid_response_values(single_select_item_create, value): + data = single_select_item_create.dict() + data["response_values"] = value + with pytest.raises(errors.IncorrectResponseValueError) as exc: + ActivityItemCreate(**data) + assert exc.value.message.format(type=SingleSelectionValues) + + +def test_create_item__reponse_type_absent(single_select_item_create) -> None: + data = single_select_item_create.dict() + del data["response_type"] + with pytest.raises(ValueError): + ActivityItemCreate(**data) + + +@pytest.mark.parametrize("fixture_name", ("single_select_item_create", "multi_select_item_create")) +def test_create_single_multi_select_item__add_scores_is_true_without_scores(request, fixture_name): + fixture = request.getfixturevalue(fixture_name) + data = fixture.dict() + data["config"]["add_scores"] = True + with pytest.raises(errors.ScoreRequiredForResponseValueError): + ActivityItemCreate(**data) + + +def test_create_slider_item__add_scores_is_true_without_scores(slider_item_create): + data = slider_item_create.dict() + data["config"]["add_scores"] = True + with pytest.raises(errors.NullScoreError): + ActivityItemCreate(**data) + + +def test_create_slider_item__add_scores__scores_not_for_all_values(slider_item_create): + data = slider_item_create.dict() + min_val = slider_item_create.response_values.min_value + max_val = slider_item_create.response_values.max_value + scores = [i for i in range(max_val - min_val)] + data["config"]["add_scores"] = True + data["response_values"]["scores"] = scores + with pytest.raises(errors.InvalidScoreLengthError): + ActivityItemCreate(**data) + + +def test_create_slider_rows_item__add_scores_is_true__no_scores(slider_rows_item_create): + data = slider_rows_item_create.dict() + data["config"]["add_scores"] = True + data["response_values"]["rows"][0]["scores"] = None + with pytest.raises(errors.NullScoreError): + ActivityItemCreate(**data) + + +def test_create_slider_rows_item__add_scores__scores_not_for_all_values(slider_rows_item_create): + data = slider_rows_item_create.dict() + min_val = slider_rows_item_create.response_values.rows[0].min_value + max_val = slider_rows_item_create.response_values.rows[0].max_value + scores = [i for i in range(max_val - min_val)] + data["config"]["add_scores"] = True + data["response_values"]["rows"][0]["scores"] = scores + with pytest.raises(errors.InvalidScoreLengthError): + ActivityItemCreate(**data) + + +@pytest.mark.parametrize( + "fixture_name,field", + ( + ("single_select_row_item_create", "set_alerts"), + ("single_select_row_item_create", "add_scores"), + ("multi_select_row_item_create", "set_alerts"), + ("multi_select_row_item_create", "add_scores"), + ), +) +def test_create_single_multi_select_row_item_no_datamatrix(request, fixture_name, field): + fixture = request.getfixturevalue(fixture_name) + data = fixture.dict() + data["config"][field] = True + data["response_values"]["data_matrix"] = None + with pytest.raises(errors.DataMatrixRequiredError): + ActivityItemCreate(**data) + + +def test_create_slider_rows_item_with_scores(slider_rows_item_create: ActivityItemCreate): + slider_rows_item_create.response_values = cast(SliderRowsValues, slider_rows_item_create.response_values) + slider_rows_item_create.config = cast(SliderRowsConfig, slider_rows_item_create.config) + min_val = slider_rows_item_create.response_values.rows[0].min_value + max_val = slider_rows_item_create.response_values.rows[0].max_value + slider_rows_item_create.response_values.rows[0].scores = [i for i in range(max_val - min_val + 1)] + slider_rows_item_create.config.add_scores = True + item = ActivityItemCreate(**slider_rows_item_create.dict()) + item.config = cast(SliderRowsConfig, item.config) + item.response_values = cast(SliderRowsValues, item.response_values) + assert item.config.add_scores + assert item.response_values.rows[0].scores == slider_rows_item_create.response_values.rows[0].scores + + +@pytest.mark.parametrize("fixture_name", ("single_select_row_item_create", "multi_select_row_item_create")) +def test_create_single_multi_select_row_item_add_alerts(request, fixture_name): + fixture = request.getfixturevalue(fixture_name) + data = fixture.dict() + data["config"]["set_alerts"] = True + item = ActivityItemCreate(**data) + assert item.config.set_alerts + + +def test_slider_item_with_alert( + base_item_data: BaseItemData, slider_value_alert, slider_response_values, slider_config +): + slider_config.set_alerts = True + slider_response_values.alerts = [slider_value_alert] + item = ActivityItemCreate( + **base_item_data.dict(), + config=slider_config, + response_values=slider_response_values, + response_type=ResponseType.SLIDER, + ) + item.config = cast(SliderConfig, item.config) + item.response_values = cast(SliderValues, item.response_values) + assert item.config.set_alerts + assert isinstance(item.response_values.alerts, list) + assert len(item.response_values.alerts) == 1 + assert item.response_values.alerts[0] == slider_value_alert + + +def test_slider_item__continuous_slider_with_alert( + base_item_data: BaseItemData, slider_value_alert, slider_response_values, slider_config +): + slider_config.set_alerts = True + slider_config.continuous_slider = True + slider_value_alert.min_value = slider_response_values.min_value + slider_value_alert.max_value = slider_response_values.max_value + slider_response_values.alerts = [slider_value_alert] + item = ActivityItemCreate( + **base_item_data.dict(), + config=slider_config, + response_values=slider_response_values, + response_type=ResponseType.SLIDER, + ) + item.config = cast(SliderConfig, item.config) + item.response_values = cast(SliderValues, item.response_values) + assert item.config.set_alerts + assert isinstance(item.response_values.alerts, list) + assert len(item.response_values.alerts) == 1 + assert item.response_values.alerts[0] == slider_value_alert + + +def test_slider_rows_item_with_alert( + base_item_data: BaseItemData, slider_rows_response_values, slider_rows_config, slider_value_alert +): + slider_rows_config.set_alerts = True + slider_rows_response_values.rows[0].alerts = [slider_value_alert] + item = ActivityItemCreate( + **base_item_data.dict(), + config=slider_rows_config, + response_values=slider_rows_response_values, + response_type=ResponseType.SLIDERROWS, + ) + item.config = cast(SliderRowsConfig, item.config) + item.response_values = cast(SliderRowsValues, item.response_values) + assert item.config.set_alerts + assert isinstance(item.response_values.rows[0].alerts, list) + assert item.response_values.rows[0].alerts[0] == slider_value_alert + + +@pytest.mark.parametrize("fixture_name", ("single_select_row_item_create", "multi_select_row_item_create")) +def test_single_multi_select_item_withou_datamatrix(request, fixture_name: str): + fixture = request.getfixturevalue(fixture_name) + data = fixture.dict() + data["response_values"].pop("data_matrix", None) + item = ActivityItemCreate(**data) + assert item.response_values.data_matrix is None # type: ignore[union-attr] diff --git a/src/apps/activities/tests/unit/domain/test_custom_validation.py b/src/apps/activities/tests/unit/domain/test_custom_validation.py index 69cd516679a..208e4edfb9b 100644 --- a/src/apps/activities/tests/unit/domain/test_custom_validation.py +++ b/src/apps/activities/tests/unit/domain/test_custom_validation.py @@ -412,7 +412,7 @@ def test_subscale_inside_subscale_error( ): items0_config: SingleSelectionConfig = items[0].config # type: ignore[assignment] items0_config.add_scores = True - subscale_item.name = "test subscale name" + subscale_item.name = subscale.name subscale_item.type = SubscaleItemType.SUBSCALE subscale.items = [subscale_item] subscale_setting.subscales = [subscale] diff --git a/src/apps/activity_flows/domain/flow_update.py b/src/apps/activity_flows/domain/flow_update.py index 26dda844979..0cb219c9c7d 100644 --- a/src/apps/activity_flows/domain/flow_update.py +++ b/src/apps/activity_flows/domain/flow_update.py @@ -4,21 +4,21 @@ from pydantic import root_validator from apps.activity_flows.domain.base import FlowBase -from apps.shared.domain import InternalModel, PublicModel +from apps.shared.domain import PublicModel -class ActivityFlowItemUpdate(InternalModel): +class ActivityFlowItemUpdate(PublicModel): id: uuid.UUID | None activity_key: uuid.UUID -class PreparedFlowItemUpdate(InternalModel): +class PreparedFlowItemUpdate(PublicModel): id: uuid.UUID | None activity_flow_id: uuid.UUID activity_id: uuid.UUID -class FlowUpdate(FlowBase, InternalModel): +class FlowUpdate(FlowBase, PublicModel): id: uuid.UUID | None items: list[ActivityFlowItemUpdate] diff --git a/src/apps/answers/tests/conftest.py b/src/apps/answers/tests/conftest.py index 547e92eba47..6698266e71a 100644 --- a/src/apps/answers/tests/conftest.py +++ b/src/apps/answers/tests/conftest.py @@ -45,17 +45,6 @@ def scores_and_reports(section: Section) -> ScoresAndReports: ) -@pytest.fixture -def applet_report_configuration_data( - user: User, tom: User, report_server_public_key: str -) -> AppletReportConfigurationBase: - return AppletReportConfigurationBase( - report_server_ip="localhost", - report_public_key=report_server_public_key, - report_recipients=[tom.email_encrypted, user.email_encrypted], - ) - - @pytest.fixture def applet_data( applet_minimal_data: AppletCreate, diff --git a/src/apps/applets/api/applets.py b/src/apps/applets/api/applets.py index dd7514a45db..cee9e722d05 100644 --- a/src/apps/applets/api/applets.py +++ b/src/apps/applets/api/applets.py @@ -45,6 +45,8 @@ from infrastructure.database import atomic from infrastructure.database.deps import get_session from infrastructure.http import get_language +from infrastructure.logger import logger +from infrastructure.utility import FirebaseNotificationType __all__ = [ "applet_create", @@ -65,9 +67,6 @@ "applet_retrieve_by_key", ] -from infrastructure.logger import logger -from infrastructure.utility import FirebaseNotificationType - async def applet_list( user: User = Depends(get_current_user), diff --git a/src/apps/applets/domain/applet_create_update.py b/src/apps/applets/domain/applet_create_update.py index 4e03f7620de..0387e1fe488 100644 --- a/src/apps/applets/domain/applet_create_update.py +++ b/src/apps/applets/domain/applet_create_update.py @@ -30,11 +30,16 @@ def validate_existing_ids_for_duplicate(cls, values): activity_names = set() activity_keys = set() flow_names = set() + assessments_count = 0 for activity in activities: if activity.name in activity_names: raise DuplicateActivityNameError() activity_names.add(activity.name) activity_keys.add(activity.key) + assessments_count += int(activity.is_reviewable) + + if assessments_count > 1: + raise AssessmentLimitExceed() for flow in flows: if flow.name in flow_names: diff --git a/src/apps/applets/tests/conftest.py b/src/apps/applets/tests/conftest.py index aacfeff4502..1d2d98b8f2c 100644 --- a/src/apps/applets/tests/conftest.py +++ b/src/apps/applets/tests/conftest.py @@ -1,37 +1,47 @@ import pytest from sqlalchemy.ext.asyncio import AsyncSession +from apps.activity_flows.domain.flow_create import FlowCreate, FlowItemCreate +from apps.activity_flows.domain.flow_update import ActivityFlowItemUpdate, FlowUpdate from apps.applets.crud.applets import AppletsCRUD from apps.applets.db.schemas import AppletSchema +from apps.applets.domain.applet_create_update import AppletCreate, AppletUpdate from apps.applets.domain.applet_full import AppletFull from apps.applets.domain.applet_link import CreateAccessLink from apps.applets.service.applet import AppletService +from apps.shared.enums import Language +from apps.users.domain import User from apps.workspaces.domain.constants import Role from apps.workspaces.service.user_applet_access import UserAppletAccessService @pytest.fixture -async def applet_one_no_encryption(session: AsyncSession, applet_one: AppletFull): +async def applet_one_no_encryption(session: AsyncSession, tom: User, applet_one: AppletFull) -> AppletFull: crud = AppletsCRUD(session) instance = await crud.update_by_id(applet_one.id, AppletSchema(encryption=None)) assert instance.encryption is None - return applet_one + srv = AppletService(session, tom.id) + applet = await srv.get_full_applet(applet_one.id) + assert applet.encryption is None + return applet @pytest.fixture -async def applet_one_lucy_manager(session: AsyncSession, applet_one: AppletFull, tom, lucy): +async def applet_one_lucy_manager(session: AsyncSession, applet_one: AppletFull, tom: User, lucy: User) -> AppletFull: await UserAppletAccessService(session, tom.id, applet_one.id).add_role(lucy.id, Role.MANAGER) return applet_one @pytest.fixture -async def applet_one_lucy_coordinator(session: AsyncSession, applet_one: AppletFull, tom, lucy): +async def applet_one_lucy_coordinator( + session: AsyncSession, applet_one: AppletFull, tom: User, lucy: User +) -> AppletFull: await UserAppletAccessService(session, tom.id, applet_one.id).add_role(lucy.id, Role.COORDINATOR) return applet_one @pytest.fixture -async def applet_one_with_public_link(session: AsyncSession, applet_one: AppletFull, tom): +async def applet_one_with_public_link(session: AsyncSession, applet_one: AppletFull, tom: User) -> AppletFull: srv = AppletService(session, tom.id) await srv.create_access_link(applet_one.id, CreateAccessLink(require_login=False)) applet = await srv.get_full_applet(applet_one.id) @@ -40,9 +50,51 @@ async def applet_one_with_public_link(session: AsyncSession, applet_one: AppletF @pytest.fixture -async def applet_one_with_link(session: AsyncSession, applet_one: AppletFull, tom): +async def applet_one_with_link(session: AsyncSession, applet_one: AppletFull, tom: User) -> AppletFull: srv = AppletService(session, tom.id) await srv.create_access_link(applet_one.id, CreateAccessLink(require_login=True)) applet = await srv.get_full_applet(applet_one.id) assert applet.link is not None return applet + + +@pytest.fixture +async def applet_one_with_flow( + session: AsyncSession, applet_one: AppletFull, applet_minimal_data: AppletFull, tom: User +): + data = AppletUpdate(**applet_minimal_data.dict()) + flow = FlowUpdate( + name="flow", + items=[ActivityFlowItemUpdate(id=None, activity_key=data.activities[0].key)], + description={Language.ENGLISH: "description"}, + id=None, + ) + data.activity_flows = [flow] + srv = AppletService(session, tom.id) + await srv.update(applet_one.id, data) + applet = await srv.get_full_applet(applet_one.id) + return applet + + +@pytest.fixture +def applet_one_update_data(applet_one: AppletFull) -> AppletUpdate: + return AppletUpdate(**applet_one.dict()) + + +@pytest.fixture +def applet_create_with_flow(applet_minimal_data: AppletCreate) -> AppletCreate: + data = applet_minimal_data.copy(deep=True) + flow = FlowCreate( + name="flow", + items=[FlowItemCreate(activity_key=data.activities[0].key)], + description={Language.ENGLISH: "description"}, + ) + data.activity_flows = [flow] + return data + + +@pytest.fixture +def applet_one_with_flow_update_data(applet_one_with_flow: AppletFull) -> AppletUpdate: + dct = applet_one_with_flow.dict() + dct["activity_flows"][0]["items"][0]["activity_key"] = applet_one_with_flow.activities[0].key + return AppletUpdate(**dct) diff --git a/src/apps/applets/tests/fixtures/applets.py b/src/apps/applets/tests/fixtures/applets.py index d445f120ede..6e21766aa4c 100644 --- a/src/apps/applets/tests/fixtures/applets.py +++ b/src/apps/applets/tests/fixtures/applets.py @@ -11,7 +11,7 @@ from apps.applets.crud.applets import AppletsCRUD from apps.applets.domain.applet_create_update import AppletCreate from apps.applets.domain.applet_full import AppletFull -from apps.applets.domain.base import Encryption +from apps.applets.domain.base import AppletBase, AppletReportConfigurationBase, Encryption from apps.applets.service.applet import AppletService from apps.applets.tests import constants from apps.applets.tests.utils import teardown_applet @@ -56,176 +56,14 @@ def report_server_public_key() -> str: return constants.REPORT_SERVER_PUBLIC_KEY -@pytest.fixture -def activity_flanker_data(): - return dict( - name="Activity_flanker", - key="577dbbda-3afc-4962-842b-8d8d11588bfe", - description=dict( - en="Description Activity flanker.", - fr="Description Activity flanker.", - ), - items=[ - dict( - name="Flanker_VSR_instructionsn", - # Nobody knows for what we need so big description - question=dict( - en="## General Instructions\n\n\n You will " - "see arrows presented at the center of the " - "screen that point either to the left ‘<’ " - "or right ‘>’.\n Press the left button " - "if the arrow is pointing to the left ‘<’ " - "or press the right button if the arrow is " - "pointing to the right ‘>’.\n These arrows " - "will appear in the center of a line of " - "will be arrows pointing in the same " - "direction, e.g.. ‘> > > > >’, or in the " - "opposite direction, e.g. ‘< < > < <’.\n " - "Your job is to respond to the central " - "arrow, no matter what direction the other " - "arrows are pointing.\n For example, you " - "would press the left button for both " - "‘< < < < <’, and ‘> > < > >’ because the " - "middle arrow points to the left.\n " - "Finally, in some trials dashes ‘ - ’ " - "will appear beside the central arrow.\n " - "Again, respond only to the direction " - "of the central arrow. Please respond " - "as quickly and accurately as possible.", - fr="Flanker General instruction text.", - ), - response_type="message", - response_values=None, - config=dict( - remove_back_button=False, - timer=None, - ), - ), - dict( - name="Flanker_Practice_instructions_1", - question=dict( - en="## Instructions\n\nNow you will have a " - "chance to practice the task before moving " - "on to the test phase.\nRemember to " - "respond only to the central arrow\n", - fr="Flanker Сalibration/Practice " "instruction 1 text.", - ), - response_type="message", - response_values=None, - config=dict( - remove_back_button=False, - timer=None, - ), - ), - dict( - name="Flanker_Practise_1", - question=dict( - en="Flanker_Practise_1", - fr="Flanker_Practise_1", - ), - response_type=ResponseType.FLANKER, - response_values=None, - config=dict( - stimulusTrials=[ - { - "id": "1", - "image": "https://600.jpg", - "text": "left-con", - "value": 0, - "weight": 10, - }, - { - "id": "2", - "image": "https://600.jpg", - "text": "right-inc", - "value": 1, - "weight": 10, - }, - { - "id": "3", - "image": "https://600.jpg", - "text": "left-inc", - "value": 0, - "weight": 10, - }, - { - "id": "4", - "image": "https://600.jpg", - "text": "right-con", - "value": 1, - "weight": 10, - }, - { - "id": "5", - "image": "https://600.jpg", - "text": "left-neut", - "value": 0, - "weight": 10, - }, - { - "id": "6", - "image": "https://600.jpg", - "text": "right-neut", - "value": 1, - "weight": 10, - }, - ], - blocks=[ - { - "name": "Block 1", - "order": [ - "left-con", - "right-con", - "left-inc", - "right-inc", - "left-neut", - "right-neut", - ], - }, - { - "name": "Block 2", - "order": [ - "left-con", - "right-con", - "left-inc", - "right-inc", - "left-neut", - "right-neut", - ], - }, - ], - buttons=[ - { - "text": "Button_1_name_<", - "image": "https://1.jpg", - "value": 0, - }, - { - "text": "Button_2_name_>", - "image": "https://2.jpg", - "value": 1, - }, - ], - nextButton="OK", - fixationDuration=500, - fixationScreen={ - "value": "FixationScreen_value", - "image": "https://fixation-screen.jpg", - }, - minimumAccuracy=75, - sampleSize=1, - samplingMethod="randomize-order", - showFeedback=True, - showFixation=True, - showResults=False, - trialDuration=3000, - isLastPractice=False, - isFirstPractice=True, - isLastTest=False, - blockType="practice", - ), - ), - ], +@pytest.fixture(scope="session") +def applet_report_configuration_data( + user: User, tom: User, report_server_public_key: str +) -> AppletReportConfigurationBase: + return AppletReportConfigurationBase( + report_server_ip="localhost", + report_public_key=report_server_public_key, + report_recipients=[tom.email_encrypted, user.email_encrypted], ) @@ -284,12 +122,9 @@ def activity_create_session(item_create: ActivityItemCreate) -> ActivityCreate: @pytest.fixture(scope="session") -def applet_minimal_data(encryption: Encryption, activity_create_session: ActivityCreate) -> AppletCreate: - return AppletCreate( - display_name="Minimal Data", - encryption=encryption, - activities=[activity_create_session], - activity_flows=[], +def applet_base_data(encryption: Encryption) -> AppletBase: + return AppletBase( + display_name="Base Data", link=None, require_login=False, pinned_at=None, @@ -298,9 +133,16 @@ def applet_minimal_data(encryption: Encryption, activity_create_session: Activit stream_enabled=False, stream_ip_address=None, stream_port=None, + encryption=encryption, ) +@pytest.fixture(scope="session") +def applet_minimal_data(applet_base_data: AppletBase, activity_create_session: ActivityCreate) -> AppletCreate: + # TODO: possible need to add activity_flows + return AppletCreate(**applet_base_data.dict(), activities=[activity_create_session], activity_flows=[]) + + @pytest.fixture(scope="session") async def default_theme(global_session: AsyncSession): theme = await ThemeService(global_session, uuid.uuid4()).get_default() diff --git a/src/apps/applets/tests/test_applet.py b/src/apps/applets/tests/test_applet.py index c8b3ecc807b..904212d343b 100644 --- a/src/apps/applets/tests/test_applet.py +++ b/src/apps/applets/tests/test_applet.py @@ -1,15 +1,31 @@ -import asyncio -import collections import copy import http import json import uuid -from copy import deepcopy import pytest - -from apps.activities import errors as activity_errors -from apps.mailing.services import TestMail +from firebase_admin.exceptions import NotFoundError as FireBaseNotFoundError +from pytest_mock import MockerFixture + +from apps.activities.domain.activity_create import ActivityItemCreate +from apps.activities.domain.activity_update import ActivityItemUpdate +from apps.activities.domain.response_type_config import ResponseType +from apps.activities.errors import ( + AssessmentLimitExceed, + DuplicateActivityFlowNameError, + DuplicateActivityItemNameNameError, + DuplicateActivityNameError, + DuplicatedActivitiesError, + DuplicatedActivityFlowsError, + FlowItemActivityKeyNotFoundError, +) +from apps.applets.domain.applet_create_update import AppletCreate, AppletUpdate +from apps.applets.domain.applet_full import AppletFull +from apps.applets.domain.base import AppletReportConfigurationBase, Encryption +from apps.applets.errors import AppletAlreadyExist +from apps.shared.exception import NotFoundError +from apps.shared.test.client import TestClient +from apps.users.domain import User from apps.workspaces.errors import AppletCreationAccessDenied, AppletEncryptionUpdateDenied from config import settings from infrastructure.utility import FCMNotificationTest @@ -37,925 +53,134 @@ class TestApplet: public_applet_detail_url = "/public/applets/{key}" public_applet_base_info_url = f"{public_applet_detail_url}/base_info" - async def test_creating_applet(self, client, tom): - TestMail.mails = [] + async def test_create_applet_with_minimal_data( + self, client: TestClient, tom: User, applet_minimal_data: AppletCreate + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") - create_data = { - "displayName": "7ee3617f-fe7f-49bc-8e0c-da6730a2d1cd", - "encryption": { - "publicKey": "publicKey", - "prime": "privateKey", - "base": "[2]", - "accountId": str(tom.id), - }, - "description": {"en": "Central granddaughter unfortunate"}, - "about": {"en": "channels indexing noisily"}, - "activities": [ - { - "items": [ - { - "name": "AT_single_select", - "question": {"en": "How do you feel today?"}, - "config": { - "removeBackButton": True, - "skippableItem": True, - "randomizeOptions": True, - "addScores": True, - "setAlerts": True, - "addTooltip": True, - "setPalette": True, - "additionalResponseOption": { - "textInputOption": True, - "textInputRequired": True, - }, - }, - "responseValues": { - "options": [ - { - "text": "very bad", - "isHidden": False, - "score": 1, - "image": "image.jpg", - "tooltip": "backing", - "color": "#123", - "value": 0, - }, - { - "text": "bad", - "isHidden": False, - "score": 20, - "image": "image.jpg", - "tooltip": "Generic", - "color": "#456", - "value": 1, - }, - { - "text": "normally", - "isHidden": False, - "score": 30, - "image": "image.jpg", - "tooltip": "Gasoline", - "color": "#789", - "value": 2, - }, - { - "text": "perfect", - "isHidden": False, - "score": 100, - "image": "image.jpg", - "tooltip": "payment", - "color": "#234", - "value": 3, - }, - ] - }, - "responseType": "singleSelect", - }, - { - "name": "AT_multi_select", - "question": {"en": "How do you feel today?"}, - "config": { - "removeBackButton": True, - "skippableItem": True, - "randomizeOptions": True, - "addScores": True, - "setAlerts": True, - "addTooltip": True, - "setPalette": True, - "additionalResponseOption": { - "textInputOption": True, - "textInputRequired": True, - }, - }, - "responseValues": { - "options": [ - { - "text": "very bad", - "isHidden": False, - "score": 30, - "image": "image.jpg", - "tooltip": "Music", - "color": "#567", - "value": 0, - }, - { - "text": "bad", - "isHidden": False, - "score": 1, - "image": "image.jpg", - "tooltip": "East", - "color": "#876", - "value": 1, - }, - { - "text": "normally", - "isHidden": False, - "score": 20, - "image": "image.jpg", - "tooltip": "Sodium", - "color": "#923", - "value": 2, - }, - { - "text": "perfect", - "isHidden": False, - "score": 100, - "image": "image.jpg", - "tooltip": "Electronics", - "color": "#567", - "value": 3, - }, - ] - }, - "responseType": "multiSelect", - }, - { - "name": "AT_slider", - "question": {"en": "How do you feel today?"}, - "responseType": "slider", - "config": { - "removeBackButton": True, - "skippableItem": True, - "addScores": True, - "setAlerts": True, - "showTickMarks": True, - "showTickLabels": True, - "continuousSlider": True, - "additionalResponseOption": { - "textInputOption": True, - "textInputRequired": True, - }, - }, - "responseValues": { - "minLabel": "very bad", - "maxLabel": "extremely good", - "minValue": 0, - "maxValue": 10, - "minImage": "image.jpg", - "maxImage": "image.jpg", - "scores": [ - 0, - 10, - 20, - 30, - 40, - 50, - 60, - 70, - 80, - 90, - 100, - ], - }, - }, - { - "name": "AT_text", - "question": {"en": "How do you feel today?"}, - "config": { - "removeBackButton": True, - "skippableItem": True, - "maxResponseLength": 50, - "correctAnswerRequired": True, - "correctAnswer": "perfect", - "numericalResponseRequired": True, - "responseDataIdentifier": True, - "responseRequired": True, - "isIdentifier": True, - }, - "responseValues": None, - "responseType": "text", - }, - ], - "name": "white", - "key": "19a78ace-5fe5-4a98-8c66-454f973f7f9a", - "isHidden": False, - "description": {"en": "Recumbent hacking Steel"}, - "showAllAtOnce": True, - "isSkippable": True, - "responseIsEditable": True, - "isReviewable": True, - "image": "image.jpg", - "splashScreen": "image.jpg", - "reportIncludedItemName": "AT_single_select", - } - ], - "activityFlows": [ - { - "name": "Metal", - "description": {"en": "East Coupe Northeast"}, - "items": [{"activityKey": "19a78ace-5fe5-4a98-8c66-454f973f7f9a"}], - "reportIncludedActivityName": "white", - "reportIncludedItemName": "AT_single_select", - "isHidden": False, - } - ], - } response = await client.post( self.applet_create_url.format(owner_id=tom.id), - data=create_data, + data=applet_minimal_data, ) assert response.status_code == http.HTTPStatus.CREATED, response.json() + result = response.json()["result"] + # TODO: check response? + assert result response = await client.get(self.applet_detail_url.format(pk=response.json()["result"]["id"])) assert response.status_code == http.HTTPStatus.OK - async def test_creating_applet_failed_by_duplicate_activity_name(self, client, tom): + async def test_creating_applet_failed_by_duplicate_activity_name( + self, client: TestClient, tom: User, applet_minimal_data: AppletCreate + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") - create_data = dict( - display_name="User daily behave", - description=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - about=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - activities=[ - dict( - name="AAA", - key="577dbbda-3afc-4962-842b-8d8d11588bfe", - description=dict( - en="Understand morning feelings.", - fr="Understand morning feelings.", - ), - items=[ - dict( - name="morning_activity_item", - question=dict( - en="How had you slept?", - fr="How had you slept?", - ), - response_type="text", - response_values=None, - is_hidden=True, - config=dict( - max_response_length=200, - correct_answer_required=False, - correct_answer=None, - numerical_response_required=False, - response_data_identifier=False, - response_required=False, - remove_back_button=False, - skippable_item=True, - ), - ), - dict( - name="morning_activity_item_2", - question=dict( - en="How had you woke?", - fr="How had you woke?", - ), - response_type="slider", - response_values=dict( - min_label="Not at all", - max_label="Very much", - min_value=1, - max_value=5, - min_image=None, - max_image=None, - scores=None, - ), - config=dict( - add_scores=False, - set_alerts=False, - show_tick_marks=False, - show_tick_labels=False, - continuous_slider=False, - timer=None, - remove_back_button=False, - skippable_item=True, - additional_response_option=dict( - text_input_option=False, - text_input_required=False, - ), - ), - ), - ], - ), - dict( - name="AAA", - key="577dbbda-3afc-4962-842b-8d8d11588bff", - description=dict( - en="Understand evening feelings.", - fr="Understand evening feelings.", - ), - items=[ - dict( - name="evening_activity_item", - question=dict( - en="How had you slept?", - fr="How had you slept?", - ), - response_type="singleSelect", - response_values=dict( - paletteName="default", - options=[ - dict( - id="41dfea7e-4496-42b3-ab24-3dd7cce71312", - text="Very well", - image=None, - score=None, - tooltip=None, - is_hidden=False, - color=None, - ), - dict( - id="41dfea7e-4496-42b3-ab24-3dd7cce71313", - text="Well", - image=None, - score=None, - tooltip=None, - is_hidden=False, - color=None, - ), - ], - ), - config=dict( - remove_back_button=False, - skippable_item=True, - randomize_options=False, - timer=None, - add_scores=False, - set_alerts=False, - add_tooltip=False, - set_palette=False, - additional_response_option=dict( - text_input_option=False, - text_input_required=False, - ), - ), - ), - dict( - name="evening_activity_item", - question=dict( - en="How had you slept?", - fr="How had you slept?", - ), - response_type="multiSelect", - response_values=dict( - paletteName=None, - options=[ - dict( - id="41dfea7e-4496-42b3-ab24-3dd7cce71312", - text="Very well", - image=None, - score=None, - tooltip=None, - is_hidden=False, - color=None, - ), - dict( - id="41dfea7e-4496-42b3-ab24-3dd7cce71313", - text="Well", - image=None, - score=None, - tooltip=None, - is_hidden=False, - color=None, - ), - ], - ), - config=dict( - remove_back_button=False, - skippable_item=True, - randomize_options=False, - timer=None, - add_scores=False, - set_alerts=False, - add_tooltip=False, - set_palette=False, - additional_response_option=dict( - text_input_option=False, - text_input_required=False, - ), - ), - ), - dict( - name="evening_activity_item33", - question=dict( - en="How had you slept?", - fr="How had you slept?", - ), - response_type="photo", - response_values=None, - config=dict( - remove_back_button=False, - skippable_item=True, - timer=None, - additional_response_option=dict( - text_input_option=False, - text_input_required=False, - ), - ), - ), - ], - ), - ], - ) - response = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=create_data, - ) + data = applet_minimal_data.copy(deep=True) + data.activities.append(data.activities[0]) + response = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) assert response.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY + result = response.json()["result"] + assert len(result) == 1 + assert result[0]["message"] == DuplicateActivityNameError.message - async def test_creating_applet_failed_by_duplicate_activity_item_name(self, client, tom): + async def test_creating_applet_failed_by_duplicate_activity_item_name( + self, client: TestClient, tom: User, applet_minimal_data: AppletCreate + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") - create_data = dict( - display_name="User daily behave", - description=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - about=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - activities=[ - dict( - name="AAA", - key="577dbbda-3afc-4962-842b-8d8d11588bfe", - description=dict( - en="Understand morning feelings.", - fr="Understand morning feelings.", - ), - items=[ - dict( - name="aaa", - question=dict( - en="How had you slept?", - fr="How had you slept?", - ), - response_type="text", - response_values=None, - is_hidden=True, - config=dict( - max_response_length=200, - correct_answer_required=False, - correct_answer=None, - numerical_response_required=False, - response_data_identifier=False, - response_required=False, - remove_back_button=False, - skippable_item=True, - ), - ), - dict( - name="aaa", - question=dict( - en="How had you woke?", - fr="How had you woke?", - ), - response_type="slider", - response_values=dict( - min_label="Not at all", - max_label="Very much", - min_value=1, - max_value=5, - min_image=None, - max_image=None, - scores=None, - ), - config=dict( - add_scores=False, - set_alerts=False, - show_tick_marks=False, - show_tick_labels=False, - continuous_slider=False, - timer=None, - remove_back_button=False, - skippable_item=True, - additional_response_option=dict( - text_input_option=False, - text_input_required=False, - ), - ), - ), - ], - ) - ], - ) - response = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=create_data, - ) + data = applet_minimal_data.copy(deep=True) + data.activities[0].items.append(data.activities[0].items[0]) + response = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) assert response.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY + result = response.json()["result"] + assert len(result) == 1 + assert result[0]["message"] == DuplicateActivityItemNameNameError.message - async def test_create_duplicate_name_applet(self, client, tom): - await client.login(self.login_url, tom.email_encrypted, "Test1234!") - create_data = dict( - display_name="Applet 1", - encryption=dict( - public_key=uuid.uuid4().hex, - account_id=str(uuid.uuid4()), - prime=uuid.uuid4().hex, - base=uuid.uuid4().hex, - ), - description=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - about=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - activities=[ - dict( - name="Morning activity", - key="577dbbda-3afc-4962-842b-8d8d11588bfe", - description=dict( - en="Understand morning feelings.", - fr="Understand morning feelings.", - ), - items=[ - dict( - name="evening_activity_item", - question=dict( - en="How had you slept?", - fr="How had you slept?", - ), - response_type="text", - response_values=None, - config=dict( - max_response_length=200, - correct_answer_required=False, - correct_answer=None, - numerical_response_required=False, - response_data_identifier=False, - response_required=False, - remove_back_button=False, - skippable_item=True, - ), - ), - ], - ), - dict( - name="Evening activity", - key="577dbbda-3afc-4962-842b-8d8d11588bff", - description=dict( - en="Understand evening feelings.", - fr="Understand evening feelings.", - ), - items=[ - dict( - name="evening_activity_item", - question=dict( - en="How had you slept?", - fr="How had you slept?", - ), - response_type="text", - response_values=None, - config=dict( - max_response_length=200, - correct_answer_required=False, - correct_answer=None, - numerical_response_required=False, - response_data_identifier=False, - response_required=False, - remove_back_button=False, - skippable_item=True, - ), - ), - ], - ), - ], - activity_flows=[ - dict( - name="Morning questionnaire", - description=dict( - en="Understand how was the morning", - fr="Understand how was the morning", - ), - items=[dict(activity_key="577dbbda-3afc-" "4962-842b-8d8d11588bfe")], - ) - ], - ) - response = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=create_data, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - assert response.json()["result"][0]["message"] == "Applet already exists." - - async def test_create_duplicate_case_sensitive_name_applet(self, client, tom): + @pytest.mark.parametrize("applet_name", ("duplicate name", "DUPLICATE NAME", "duPlicate Name")) + async def test_create_duplicate_name_applet( + self, client: TestClient, tom: User, applet_minimal_data: AppletCreate, applet_name: str + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") - create_data = dict( - display_name="AppleT 1", - encryption=dict( - public_key=uuid.uuid4().hex, - account_id=str(uuid.uuid4()), - prime=uuid.uuid4().hex, - base=uuid.uuid4().hex, - ), - description=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - about=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - activities=[ - dict( - name="Morning activity", - key="577dbbda-3afc-4962-842b-8d8d11588bfe", - description=dict( - en="Understand morning feelings.", - fr="Understand morning feelings.", - ), - items=[ - dict( - name="evening_activity_item", - question=dict( - en="How had you slept?", - fr="How had you slept?", - ), - response_type="text", - response_values=None, - config=dict( - max_response_length=200, - correct_answer_required=False, - correct_answer=None, - numerical_response_required=False, - response_data_identifier=False, - response_required=False, - remove_back_button=False, - skippable_item=True, - ), - ), - ], - ), - dict( - name="Evening activity", - key="577dbbda-3afc-4962-842b-8d8d11588bff", - description=dict( - en="Understand evening feelings.", - fr="Understand evening feelings.", - ), - items=[ - dict( - name="evening_activity_item", - question=dict( - en="How had you slept?", - fr="How had you slept?", - ), - response_type="text", - response_values=None, - config=dict( - max_response_length=200, - correct_answer_required=False, - correct_answer=None, - numerical_response_required=False, - response_data_identifier=False, - response_required=False, - remove_back_button=False, - skippable_item=True, - ), - ), - ], - ), - ], - activity_flows=[ - dict( - name="Morning questionnaire", - description=dict( - en="Understand how was the morning", - fr="Understand how was the morning", - ), - items=[dict(activity_key="577dbbda-3afc-" "4962-842b-8d8d11588bfe")], - ) - ], - ) - response = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=create_data, - ) + data = applet_minimal_data.copy(deep=True) + data.display_name = applet_name + response = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) + assert response.status_code == http.HTTPStatus.CREATED + response = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) assert response.status_code == http.HTTPStatus.BAD_REQUEST - assert response.json()["result"][0]["message"] == "Applet already exists." + result = response.json()["result"] + assert len(result) == 1 + assert response.json()["result"][0]["message"] == AppletAlreadyExist.message - async def test_update_applet(self, client, tom, device_tom, applet_one): + async def test_update_applet( + self, client: TestClient, tom: User, device_tom: str, applet_one: AppletFull, fcm_client: FCMNotificationTest + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") - update_data = dict( - stream_enabled=True, - stream_ip_address="127.0.0.1", - stream_port=8881, - display_name="Applet 1", - encryption=dict( - public_key=uuid.uuid4().hex, - account_id=str(uuid.uuid4()), - prime=uuid.uuid4().hex, - base=uuid.uuid4().hex, - ), - description=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - about=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - activities=[ - dict( - id="09e3dbf0-aefb-4d0e-9177-bdb321bf3611", - name="Morning activity", - key="577dbbda-3afc-4962-842b-8d8d11588bfe", - description=dict( - en="Understand morning feelings.", - fr="Understand morning feelings.", - ), - items=[ - dict( - id="a18d3409-2c96-4a5e-a1f3-1c1c14be0011", - name="evening_activity_item", - question=dict( - en="How had you slept?", - fr="How had you slept?", - ), - response_type="text", - response_values=None, - config=dict( - max_response_length=200, - correct_answer_required=False, - correct_answer=None, - numerical_response_required=False, - response_data_identifier=False, - response_required=False, - remove_back_button=False, - skippable_item=True, - ), - ), - dict( - name="evening_activity_item2", - question=dict( - en="How was your breakfast?", - fr="How was your breakfast?", - ), - response_type="text", - response_values=None, - config=dict( - max_response_length=200, - correct_answer_required=False, - correct_answer=None, - numerical_response_required=False, - response_data_identifier=False, - response_required=False, - remove_back_button=False, - skippable_item=True, - ), - ), - dict( - id="a18d3409-2c96-4a5e-a1f3-1c1c14be0012", - name="activity_item_time_range", - question={"en": "What is your name?"}, - response_type="timeRange", - response_values=None, - config=dict( - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - remove_back_button=False, - skippable_item=False, - timer=1, - ), - ), - ], - ), - dict( - name="Evening activity", - key="577dbbda-3afc-4962-842b-8d8d11588bff", - description=dict( - en="Understand evening feelings.", - fr="Understand evening feelings.", - ), - items=[ - dict( - question=dict( - en="How had you spent your time?", - fr="How had you spent your time?", - ), - name="evening_activity_item3", - response_type="text", - response_values=None, - config=dict( - max_response_length=200, - correct_answer_required=False, - correct_answer=None, - numerical_response_required=False, - response_data_identifier=False, - response_required=False, - remove_back_button=False, - skippable_item=True, - ), - ), - ], - ), - ], - activity_flows=[ - dict( - name="Morning questionnaire", - description=dict( - en="Understand how was the morning", - fr="Understand how was the morning", - ), - items=[ - dict( - id="7941b770-b649-42fc-832a-870e11bdd402", - activity_key="577dbbda-" "3afc-4962-842b-8d8d11588bfe", - ) - ], - ) - ], - ) - activity_key = update_data["activity_flows"][0]["items"][0]["activity_key"] - wrong_activity_key = uuid.uuid4() - update_data["activity_flows"][0]["items"][0]["activity_key"] = wrong_activity_key - - response = await client.put( - self.applet_detail_url.format(pk=applet_one.id), - data=update_data, - ) - assert response.status_code == activity_errors.FlowItemActivityKeyNotFoundError.status_code - assert response.json()["result"][0]["message"] == activity_errors.FlowItemActivityKeyNotFoundError.message + update_data = AppletUpdate(**applet_one.dict()) - update_data["activity_flows"][0]["items"][0]["activity_key"] = activity_key response = await client.put( self.applet_detail_url.format(pk=applet_one.id), data=update_data, ) assert response.status_code == http.HTTPStatus.OK, response.json() - # TODO: move to fixtures - assert len(FCMNotificationTest.notifications) == 1 - assert device_tom in FCMNotificationTest.notifications - notification = json.loads(FCMNotificationTest.notifications[device_tom][0]) + assert device_tom in fcm_client.notifications + assert len(fcm_client.notifications[device_tom]) == 1 + notification = json.loads(fcm_client.notifications[device_tom][0]) assert notification["title"] == "Applet is updated." - FCMNotificationTest.notifications = collections.defaultdict(list) - data = response.json() - response = await client.put( - self.activity_report_config_url.format( - pk=applet_one.id, - activity_id="09e3dbf0-aefb-4d0e-9177-bdb321bf3611", - ), - data=dict(report_included_item_name="evening_activity_item3"), - ) + async def test_update_applet__add_stream_settings(self, client: TestClient, applet_one: AppletFull, tom: User): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + update_data = AppletUpdate(**applet_one.dict()) + update_data.stream_enabled = True + update_data.stream_ip_address = "127.0.0.1" # type: ignore[assignment] + update_data.stream_port = 8001 + response = await client.put(self.applet_detail_url.format(pk=applet_one.id), data=update_data) assert response.status_code == http.HTTPStatus.OK + response = await client.get(self.applet_detail_url.format(pk=applet_one.id)) + assert response.status_code == http.HTTPStatus.OK + assert response.json()["result"]["streamEnabled"] + assert response.json()["result"]["streamIpAddress"] == str(update_data.stream_ip_address) + assert response.json()["result"]["streamPort"] == update_data.stream_port + + async def test_update_applet_duplicate_name_activity( + self, client: TestClient, tom: User, device_tom: str, applet_one: AppletFull + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + update_data = AppletUpdate(**applet_one.dict()) + update_data.activities.append(update_data.activities[0]) - flow_id = data["result"]["activityFlows"][0]["id"] response = await client.put( - self.flow_report_config_url.format(pk=applet_one.id, flow_id=flow_id), - data=dict( - report_included_activity_name="Morning activity", - report_included_item_name="evening_activity_item3", - ), + self.applet_detail_url.format(pk=applet_one.id), + data=update_data, ) - assert response.status_code == http.HTTPStatus.OK - - # get applet and check stream settings - response = await client.get(self.applet_detail_url.format(pk=applet_one.id)) - assert response.status_code == 200 - assert response.json()["result"]["streamEnabled"] is True - assert response.json()["result"]["streamIpAddress"] == update_data["stream_ip_address"] - assert response.json()["result"]["streamPort"] == update_data["stream_port"] + assert response.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY + result = response.json()["result"] + assert len(result) == 1 + assert result[0]["message"] == DuplicateActivityNameError.message - async def test_duplicate_applet(self, client, tom, applet_one): - TestMail.mails = [] + async def test_duplicate_applet( + self, client: TestClient, tom: User, applet_one: AppletFull, encryption: Encryption + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") - + new_name = "New Name" response = await client.post( self.applet_duplicate_url.format(pk=applet_one.id), - data=dict( - display_name="New name", - encryption=dict( - public_key=uuid.uuid4().hex, - account_id=str(uuid.uuid4()), - prime=uuid.uuid4().hex, - base=uuid.uuid4().hex, - ), - ), + data=dict(display_name=new_name, encryption=encryption.dict()), ) - assert response.status_code == http.HTTPStatus.CREATED, response.json() - - response = await client.get(self.applet_list_url) - assert len(response.json()["result"]) == 3 - assert response.json()["result"][0]["displayName"] == "New name" + assert response.status_code == http.HTTPStatus.CREATED + assert response.json()["result"]["displayName"] == new_name + async def test_duplicate_applet_name_already_exists( + self, client: TestClient, tom: User, applet_one: AppletFull, encryption: Encryption + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") response = await client.post( self.applet_duplicate_url.format(pk=applet_one.id), - data=dict( - display_name="New name", - encryption=dict( - public_key=uuid.uuid4().hex, - account_id=str(uuid.uuid4()), - prime=uuid.uuid4().hex, - base=uuid.uuid4().hex, - ), - ), + data=dict(display_name=applet_one.display_name, encryption=encryption.dict()), ) assert response.status_code == http.HTTPStatus.BAD_REQUEST + result = response.json()["result"] + assert len(result) == 1 + assert result[0]["message"] == AppletAlreadyExist.message - async def test_set_applet_report_configuration(self, client, tom, applet_one): + async def test_set_applet_report_configuration(self, client: TestClient, tom: User, applet_one: AppletFull): await client.login(self.login_url, tom.email_encrypted, "Test1234!") report_configuration = dict( @@ -984,7 +209,7 @@ async def test_set_applet_report_configuration(self, client, tom, applet_one): assert response.json()["result"]["reportIncludeCaseId"] == report_configuration["report_include_case_id"] assert response.json()["result"]["reportEmailBody"] == report_configuration["report_email_body"] - async def test_publish_conceal_applet(self, client, tom, applet_one): + async def test_publish_conceal_applet(self, client: TestClient, tom: User, applet_one: AppletFull): # NOTE: only superadmin can publish an applet await client.login(self.login_url, settings.super_admin.email, settings.super_admin.password) response = await client.post(self.applet_publish_url.format(pk=applet_one.id)) @@ -1005,9 +230,12 @@ async def test_publish_conceal_applet(self, client, tom, applet_one): assert response.status_code == http.HTTPStatus.OK assert response.json()["result"]["isPublished"] is False - async def test_set_encryption(self, client, tom, applet_one_no_encryption, encryption): + async def test_set_encryption( + self, client: TestClient, tom: User, applet_one_no_encryption: AppletFull, encryption: Encryption + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") + assert applet_one_no_encryption.encryption is None response = await client.post( self.applet_set_encryption_url.format(pk=applet_one_no_encryption.id), data=encryption, @@ -1019,23 +247,17 @@ async def test_set_encryption(self, client, tom, applet_one_no_encryption, encry assert result["base"] == encryption.base assert result["accountId"] == encryption.account_id - async def test_set_encryption__encryption_already_set(self, client, tom, applet_one): + async def test_set_encryption__encryption_already_set( + self, client: TestClient, tom: User, applet_one: AppletFull, encryption: Encryption + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") - response = await client.post( - self.applet_set_encryption_url.format(pk=applet_one.id), - data=dict( - public_key=uuid.uuid4().hex, - prime=uuid.uuid4().hex, - base=uuid.uuid4().hex, - account_id=str(uuid.uuid4()), - ), - ) - assert response.status_code == 403 + response = await client.post(self.applet_set_encryption_url.format(pk=applet_one.id), data=encryption) + assert response.status_code == http.HTTPStatus.FORBIDDEN result = response.json()["result"] assert len(result) == 1 assert result[0]["message"] == AppletEncryptionUpdateDenied.message - async def test_applet_list(self, client, tom, applet_one, applet_two): + async def test_applet_list(self, client: TestClient, tom: User, applet_one: AppletFull, applet_two: AppletFull): await client.login(self.login_url, tom.email_encrypted, "Test1234!") response = await client.get(self.applet_list_url) @@ -1045,201 +267,97 @@ async def test_applet_list(self, client, tom, applet_one, applet_two): act_ids = set(i["id"] for i in response.json()["result"]) assert exp_ids == act_ids - async def test_applet_delete(self, client, tom, device_tom, applet_one): + async def test_applet_delete( + self, client: TestClient, tom: User, applet_one: AppletFull, device_tom: str, fcm_client: FCMNotificationTest + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") response = await client.delete( self.applet_detail_url.format(pk=applet_one.id), ) - assert response.status_code == 204, response.json() + assert response.status_code == http.HTTPStatus.NO_CONTENT + + # TODO: move to the fixtures + assert device_tom in fcm_client.notifications + assert len(fcm_client.notifications[device_tom]) == 1 + notification = json.loads(fcm_client.notifications[device_tom][0]) + assert notification["title"] == "Applet is deleted." + async def test_applet_delete__applet_does_not_exists(self, client: TestClient, tom: User, uuid_zero: uuid.UUID): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") response = await client.delete( - self.applet_detail_url.format(pk="00000000-0000-0000-0000-000000000000"), + self.applet_detail_url.format(pk=uuid_zero), ) - assert response.status_code == 404, response.json() + assert response.status_code == http.HTTPStatus.NOT_FOUND - # TODO: move to the fixtures - assert len(FCMNotificationTest.notifications) == 1 - assert device_tom in FCMNotificationTest.notifications - notification = json.loads(FCMNotificationTest.notifications[device_tom][0]) - assert notification["title"] == "Applet is deleted." - FCMNotificationTest.notifications = collections.defaultdict(list) - - async def test_applet_delete_by_manager(self, client, applet_one_lucy_manager): + async def test_applet_delete_by_manager(self, client: TestClient, applet_one_lucy_manager: AppletFull): await client.login(self.login_url, "lucy@gmail.com", "Test123") response = await client.delete( self.applet_detail_url.format(pk=applet_one_lucy_manager.id), ) - assert response.status_code == 204 + assert response.status_code == http.HTTPStatus.NO_CONTENT - async def test_applet_delete_by_coordinator(self, client, applet_one_lucy_coordinator): + async def test_applet_delete_by_coordinator(self, client: TestClient, applet_one_lucy_coordinator: AppletFull): await client.login(self.login_url, "lucy@gmail.com", "Test123") response = await client.delete( self.applet_detail_url.format(pk=applet_one_lucy_coordinator.id), ) - assert response.status_code == 403 - - async def test_applet_list_with_invalid_token(self, client, tom): - from config import settings - - current_access_token_expiration = settings.authentication.access_token.expiration - - settings.authentication.access_token.expiration = 0.05 - await client.login(self.login_url, tom.email_encrypted, "Test1234!") - await asyncio.sleep(4) - response = await client.get(self.applet_list_url) - - settings.authentication.access_token.expiration = current_access_token_expiration - assert response.status_code == 401, response.json() - - async def test_applet_list_with_expired_token(self, client): - response = await client.get( - self.applet_list_url, - headers=dict(Authorization="Bearer invalid_token"), - ) - - assert response.status_code == 401, response.json() + assert response.status_code == http.HTTPStatus.FORBIDDEN - async def test_applet_list_by_filters(self, client, tom, applet_one): + async def test_applet_list_with_limit(self, client: TestClient, tom: User, applet_one: AppletFull): await client.login(self.login_url, tom.email_encrypted, "Test1234!") - response = await client.get(self.applet_list_url, dict(ordering="id", owner_id=1, limit=1)) + response = await client.get(self.applet_list_url, dict(ordering="id", limit=1)) assert response.status_code == http.HTTPStatus.OK assert len(response.json()["result"]) == 1 assert response.json()["result"][0]["id"] == str(applet_one.id) - async def test_applet_detail(self, client, tom, applet_one): + async def test_applet_detail(self, client: TestClient, tom: User, applet_one_with_flow: AppletFull): await client.login(self.login_url, tom.email_encrypted, "Test1234!") - response = await client.get(self.applet_detail_url.format(pk=applet_one.id)) + response = await client.get(self.applet_detail_url.format(pk=applet_one_with_flow.id)) assert response.status_code == http.HTTPStatus.OK result = response.json()["result"] - assert result["displayName"] == applet_one.display_name + assert result["displayName"] == applet_one_with_flow.display_name assert len(result["activities"]) == 1 - # TODO: Add activity_flows and check + assert len(result["activityFlows"]) == 1 - async def test_public_applet_detail(self, client, applet_one_with_public_link): + async def test_public_applet_detail(self, client: TestClient, applet_one_with_public_link: AppletFull): response = await client.get(self.public_applet_detail_url.format(key=applet_one_with_public_link.link)) assert response.status_code == http.HTTPStatus.OK result = response.json()["result"] assert result["displayName"] == applet_one_with_public_link.display_name assert len(result["activities"]) == 1 - # TODO: Add activity_flows and check - async def test_creating_applet_history(self, client, tom): + async def test_create_applet__initial_version_is_created_in_applet_history( + self, client: TestClient, tom: User, applet_minimal_data: AppletCreate + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") - create_data = dict( - display_name="User daily behave", - encryption=dict( - public_key=uuid.uuid4().hex, - account_id=str(uuid.uuid4()), - prime=uuid.uuid4().hex, - base=uuid.uuid4().hex, - ), - description=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - about=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - activities=[ - dict( - name="Morning activity", - key="577dbbda-3afc-4962-842b-8d8d11588bfe", - description=dict( - en="Understand morning feelings.", - fr="Understand morning feelings.", - ), - items=[ - dict( - name="morning_activity_item1", - question=dict( - en="How had you slept?", - fr="How had you slept?", - ), - response_type="text", - response_values=None, - config=dict( - max_response_length=200, - correct_answer_required=False, - correct_answer=None, - numerical_response_required=False, - response_data_identifier=False, - response_required=False, - remove_back_button=False, - skippable_item=True, - ), - ), - ], - ), - dict( - name="Evening activity", - key="577dbbda-3afc-4962-842b-8d8d11588bff", - description=dict( - en="Understand evening feelings.", - fr="Understand evening feelings.", - ), - items=[ - dict( - name="evening_activity_item1", - question=dict( - en="How had you spent your time?", - fr="How had you spent your time?", - ), - response_type="text", - response_values=None, - config=dict( - max_response_length=200, - correct_answer_required=False, - correct_answer=None, - numerical_response_required=False, - response_data_identifier=False, - response_required=False, - remove_back_button=False, - skippable_item=True, - ), - ), - ], - ), - ], - activity_flows=[ - dict( - name="Morning questionnaire", - description=dict( - en="Understand how was the morning", - fr="Understand how was the morning", - ), - items=[dict(activity_key="577dbbda-3afc-" "4962-842b-8d8d11588bfe")], - ) - ], - ) response = await client.post( self.applet_create_url.format(owner_id=tom.id), - data=create_data, + data=applet_minimal_data, ) - assert response.status_code == http.HTTPStatus.CREATED, response.json() - + assert response.status_code == http.HTTPStatus.CREATED version = response.json()["result"]["version"] applet_id = response.json()["result"]["id"] response = await client.get(self.histories_url.format(pk=applet_id)) - assert response.status_code == http.HTTPStatus.OK, response.json() + assert response.status_code == http.HTTPStatus.OK versions = response.json()["result"] assert len(versions) == 1 assert versions[0]["version"] == version - async def test_versions_for_not_existed_applet(self, client, tom): + async def test_get_versions_for_not_existed_applet(self, client: TestClient, tom: User, uuid_zero: uuid.UUID): await client.login(self.login_url, tom.email_encrypted, "Test1234!") - response = await client.get(self.histories_url.format(pk=uuid.uuid4())) - - assert response.status_code == 404, response.json() + response = await client.get(self.histories_url.format(pk=uuid_zero)) + assert response.status_code == http.HTTPStatus.NOT_FOUND - async def test_updating_applet_history(self, client, tom, applet_one, applet_minimal_data): + async def test_update_applet__applet_history_is_updated( + self, client: TestClient, tom: User, applet_one: AppletFull, applet_minimal_data: AppletCreate + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") # first change patch version update_data_patch = applet_one.dict() @@ -1291,367 +409,99 @@ async def test_updating_applet_history(self, client, tom, applet_one, applet_min applet = response.json()["result"] assert applet["version"] == "2.0.0" - # Not valid version + async def test_get_history_version__applet_version_does_not_exist( + self, client: TestClient, tom: User, applet_one: AppletFull + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") response = await client.get(self.history_url.format(pk=applet_one.id, version="0.0.0")) - assert response.status_code == 404, response.json() + assert response.status_code == http.HTTPStatus.NOT_FOUND - async def test_history_changes(self, client, tom): + async def test_get_history_changes__applet_display_name_is_updated( + self, client: TestClient, tom: User, applet_one: AppletFull + ): + # NOTE: Only simple test is tested here. All other history changes are tested in unit tests await client.login(self.login_url, tom.email_encrypted, "Test1234!") - create_data = dict( - display_name="User daily behave", - encryption=dict( - public_key=uuid.uuid4().hex, - account_id=str(uuid.uuid4()), - prime=uuid.uuid4().hex, - base=uuid.uuid4().hex, - ), - description=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - about=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - activities=[ - dict( - name="Morning activity", - key="577dbbda-3afc-4962-842b-8d8d11588bfe", - description=dict( - en="Understand morning feelings.", - fr="Understand morning feelings.", - ), - items=[ - dict( - question=dict( - en="How had you slept?", - fr="How had you slept?", - ), - name="morning_activity_item132", - response_type="text", - response_values=None, - config=dict( - max_response_length=200, - correct_answer_required=False, - correct_answer=None, - numerical_response_required=False, - response_data_identifier=False, - response_required=False, - remove_back_button=False, - skippable_item=True, - ), - ), - ], - ), - dict( - name="Evening activity", - key="577dbbda-3afc-4962-842b-8d8d11588bff", - description=dict( - en="Understand evening feelings.", - fr="Understand evening feelings.", - ), - items=[ - dict( - question=dict( - en="How had you spent your time?", - fr="How had you spent your time?", - ), - name="evening_activity_item132", - response_type="text", - response_values=None, - config=dict( - max_response_length=200, - correct_answer_required=False, - correct_answer=None, - numerical_response_required=False, - response_data_identifier=False, - response_required=False, - remove_back_button=False, - skippable_item=True, - ), - ), - ], - ), - ], - activity_flows=[ - dict( - name="Morning questionnaire", - description=dict( - en="Understand how was the morning", - fr="Understand how was the morning", - ), - items=[dict(activity_key="577dbbda-3afc-" "4962-842b-8d8d11588bfe")], - ) - ], - ) - response = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=create_data, - ) - - update_data = dict( - display_name="User daily behave updated", - encryption=dict( - public_key=uuid.uuid4().hex, - account_id=str(uuid.uuid4()), - prime=uuid.uuid4().hex, - base=uuid.uuid4().hex, - ), - description=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - about=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - activities=[ - dict( - id="09e3dbf0-aefb-4d0e-9177-bdb321bf3615", - name="Morning activity new", - key="577dbbda-3afc-4962-842b-8d8d11588bfe", - description=dict( - en="Understand morning feelings.", - fr="Understand morning feelings.", - ), - items=[ - dict( - question=dict( - en="How had you slept?", - fr="How had you slept?", - ), - name="morning_activity_item132", - response_type="text", - response_values=None, - config=dict( - max_response_length=200, - correct_answer_required=False, - correct_answer=None, - numerical_response_required=False, - response_data_identifier=False, - response_required=False, - remove_back_button=False, - skippable_item=True, - ), - ), - dict( - question=dict( - en="How was your breakfast?", - fr="How was your breakfast?", - ), - name="morning_activity_item133", - response_type="text", - response_values=None, - config=dict( - max_response_length=200, - correct_answer_required=False, - correct_answer=None, - numerical_response_required=False, - response_data_identifier=False, - response_required=False, - remove_back_button=False, - skippable_item=True, - ), - ), - dict( - name="morning_activity_item_2", - question=dict( - en="How had you woke?", - fr="How had you woke?", - ), - response_type="slider", - response_values=dict( - min_label="Not at all", - max_label="Very much", - min_value=1, - max_value=5, - min_image=None, - max_image=None, - scores=[0, 1, 2, 3, 4], - ), - config=dict( - add_scores=True, - set_alerts=False, - show_tick_marks=False, - show_tick_labels=False, - continuous_slider=False, - timer=None, - remove_back_button=False, - skippable_item=True, - additional_response_option=dict( - text_input_option=False, - text_input_required=False, - ), - ), - ), - ], - scores_and_reports=dict( - generateReport=True, - showScoreSummary=True, - reports=[ - dict( - name="morning_activity_item_2", - type="score", - id="morning_activity_item_2", - calculationType="sum", - minScore=0, - maxScore=3, - itemsScore=["morning_activity_item_2"], - message="Hello", - itemsPrint=["morning_activity_item_2"], - conditionalLogic=[ - dict( - name="morning_activity_item_2", - id="morning_activity_item_2", - flagScore=True, - message="Hello2", - match="any", - conditions=[ - dict( - item_name=("morning_activity_item_2"), - type="GREATER_THAN", - payload=dict( - value=1, - ), - ), - ], - ), - ], - ), - dict( - name="morning_activity_item_2", - type="section", - messages="Hello from the other side", - itemsPrint=[ - "morning_activity_item_2", - ], - conditionalLogic=dict( - match="all", - conditions=[ - dict( - item_name=("morning_activity_item_2"), - type="GREATER_THAN", - payload=dict( - value=1, - ), - ), - dict( - item_name=("morning_activity_item_2"), - type="EQUAL_TO_OPTION", - payload=dict( - option_value="1", # noqa E501 - ), - ), - ], - ), - ), - ], - ), - ), - dict( - name="Evening activity", - key="577dbbda-3afc-4962-842b-8d8d11588bff", - description=dict( - en="Understand evening feelings.", - fr="Understand evening feelings.", - ), - items=[ - dict( - question=dict( - en="How had you spent your time?", - fr="How had you spent your time?", - ), - name="evening_activity_item132", - response_type="text", - response_values=None, - config=dict( - max_response_length=200, - correct_answer_required=False, - correct_answer=None, - numerical_response_required=False, - response_data_identifier=False, - response_required=False, - remove_back_button=False, - skippable_item=True, - ), - ), - ], - ), - ], - activity_flows=[ - dict( - name="Morning questionnaire", - description=dict( - en="Understand how was the morning", - fr="Understand how was the morning", - ), - items=[ - dict( - activity_key="577dbbda-" "3afc-4962-842b-8d8d11588bfe", - ) - ], - ) - ], - ) - response = await client.put( - self.applet_detail_url.format(pk=response.json()["result"]["id"]), - data=update_data, - ) - - assert response.status_code == http.HTTPStatus.OK, response.json() + update_data = AppletUpdate(**applet_one.dict()) + new_display_name = "new display name" + update_data.display_name = new_display_name + response = await client.put(self.applet_detail_url.format(pk=applet_one.id), data=update_data) + assert response.status_code == http.HTTPStatus.OK + assert response.json()["result"]["displayName"] == new_display_name version = response.json()["result"]["version"] applet_id = response.json()["result"]["id"] response = await client.get(self.history_changes_url.format(pk=applet_id, version=version)) assert response.status_code == http.HTTPStatus.OK - assert response.json()["result"]["displayName"] == "Applet User daily behave updated updated" - assert len(response.json()["result"]["activities"]) == 4 + assert response.json()["result"]["displayName"] == f"Applet {new_display_name} updated" - async def test_get_applet_unique_name(self, client, tom): + async def test_get_applet_unique_name__name_already_used( + self, client: TestClient, tom: User, applet_one: AppletFull + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") - response = await client.post(self.applet_unique_name_url, data=dict(name="Applet 1")) + response = await client.post(self.applet_unique_name_url, data=dict(name=applet_one.display_name)) assert response.status_code == http.HTTPStatus.OK - assert response.json()["result"]["name"] == "Applet 1 (1)" + assert response.json()["result"]["name"] == f"{applet_one.display_name} (1)" - async def test_get_applet_unique_name_case_insensitive(self, client, tom): + async def test_get_applet_unique_name__name_already_used_case_insensitive( + self, client: TestClient, tom: User, applet_one: AppletFull + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") - response = await client.post(self.applet_unique_name_url, data=dict(name="AppleT 1")) + name = applet_one.display_name.upper() + response = await client.post(self.applet_unique_name_url, data=dict(name=name)) assert response.status_code == http.HTTPStatus.OK - assert response.json()["result"]["name"] == "AppleT 1 (1)" - - async def test_get_applet_activities_info(self, client, tom, applet_minimal_data): + assert response.json()["result"]["name"] == f"{name} (1)" + + async def test_get_applet_activities_info( + self, + client: TestClient, + tom: User, + applet_minimal_data: AppletCreate, + multi_select_item_create: ActivityItemCreate, + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") # create applet with minimal data - multi_select = deepcopy(applet_minimal_data.activities[0].items[0]) - multi_select.name = "test_multiSelect" - multi_select.response_type = "multiSelect" - multi_select.is_hidden = True - applet_minimal_data.activities[0].items.append(multi_select) - response = await client.post(self.applet_create_url.format(owner_id=tom.id), data=applet_minimal_data) + data = applet_minimal_data.copy(deep=True) + multi_select_item_create.is_hidden = True + data.activities[0].items.append(multi_select_item_create) + response = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) + assert response.status_code == http.HTTPStatus.CREATED + assert len(response.json()["result"]["activities"][0]["items"]) == 2 new_applet_id = response.json()["result"]["id"] response = await client.get(self.applet_base_info_url.format(pk=new_applet_id)) - assert response.status_code == 200 - assert response.json()["result"]["displayName"] == applet_minimal_data.display_name + assert response.status_code == http.HTTPStatus.OK + assert response.json()["result"]["displayName"] == data.display_name # check if hidden item is not shown - assert "singleSelect" in response.json()["result"]["activities"][0]["containsResponseTypes"] - assert "multiSelect" not in response.json()["result"]["activities"][0]["containsResponseTypes"] - assert isinstance(response.json()["result"]["activities"][0]["itemCount"], int) + assert ResponseType.SINGLESELECT in response.json()["result"]["activities"][0]["containsResponseTypes"] + assert ResponseType.MULTISELECT not in response.json()["result"]["activities"][0]["containsResponseTypes"] assert response.json()["result"]["activities"][0]["itemCount"] == 1 - async def test_get_public_applet_activities_info(self, client, applet_one_with_public_link): - response = await client.get(self.public_applet_base_info_url.format(key=applet_one_with_public_link.link)) + async def test_get_public_applet_activities_info( + self, + client: TestClient, + tom: User, + applet_one_with_public_link: AppletFull, + multi_select_item_create: ActivityItemCreate, + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + update_data = AppletUpdate(**applet_one_with_public_link.dict()) + multi_select_item_create.is_hidden = True + update_data.activities[0].items.append(ActivityItemUpdate(**multi_select_item_create.dict())) + resp = await client.put(self.applet_detail_url.format(pk=applet_one_with_public_link.id), data=update_data) + assert resp.status_code == http.HTTPStatus.OK + assert resp.json()["result"]["activities"][0]["items"][1]["isHidden"] + assert resp.json()["result"]["activities"][0]["items"][1]["responseType"] == ResponseType.MULTISELECT - assert response.status_code == 200 + response = await client.get(self.public_applet_base_info_url.format(key=applet_one_with_public_link.link)) + assert response.status_code == http.HTTPStatus.OK assert response.json()["result"]["displayName"] == applet_one_with_public_link.display_name - assert "singleSelect" in response.json()["result"]["activities"][0]["containsResponseTypes"] - assert "multiSelect" not in response.json()["result"]["activities"][0]["containsResponseTypes"] - assert isinstance(response.json()["result"]["activities"][0]["itemCount"], int) + assert ResponseType.SINGLESELECT in response.json()["result"]["activities"][0]["containsResponseTypes"] + assert ResponseType.MULTISELECT not in response.json()["result"]["activities"][0]["containsResponseTypes"] assert response.json()["result"]["activities"][0]["itemCount"] == 1 @pytest.mark.usefixtures("applet_one_lucy_manager") @@ -1688,3 +538,246 @@ async def test_create_applet_in_another_workspace_not_owner_user_does_not_have_r result = response.json()["result"] assert len(result) == 1 assert result[0]["message"] == AppletCreationAccessDenied.message + + async def test_update_applet__firebase_error_muted(self, client, tom, applet_minimal_data, mocker, applet_one): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + mocker.patch( + "infrastructure.utility.notification_client.FCMNotificationTest.notify", + side_effect=FireBaseNotFoundError(message="device id not found"), + ) + resp = await client.put(self.applet_detail_url.format(pk=applet_one.id), data=applet_minimal_data) + assert resp.status_code == http.HTTPStatus.OK + + async def test_update_report_config_for_activity__activity_from_another_applet( + self, client: TestClient, tom: User, applet_one: AppletFull, applet_two, applet_report_configuration_data + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + resp = await client.put( + self.activity_report_config_url.format(pk=applet_one.id, activity_id=applet_two.activities[0].id), + data=applet_report_configuration_data, + ) + assert resp.status_code == http.HTTPStatus.NOT_FOUND + result = resp.json()["result"] + assert len(result) == 1 + assert result[0]["message"] == NotFoundError.message + + async def test_update_report_config_for_activity__activity_does_not_exists( + self, + client: TestClient, + tom: User, + applet_one: AppletFull, + applet_report_configuration_data: AppletReportConfigurationBase, + uuid_zero: uuid.UUID, + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + resp = await client.put( + self.activity_report_config_url.format(pk=applet_one.id, activity_id=uuid_zero), + data=applet_report_configuration_data, + ) + assert resp.status_code == http.HTTPStatus.NOT_FOUND + result = resp.json()["result"] + assert len(result) == 1 + assert result[0]["message"] == NotFoundError.message + + async def test_update_report_config_for_activity_flow__activity_from_another_applet( + self, + client: TestClient, + tom: User, + applet_one_with_flow: AppletFull, + applet_two: AppletFull, + applet_report_configuration_data: AppletReportConfigurationBase, + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + resp = await client.put( + self.flow_report_config_url.format(pk=applet_two.id, flow_id=applet_one_with_flow.activity_flows[0].id), + data=applet_report_configuration_data, + ) + assert resp.status_code == http.HTTPStatus.NOT_FOUND + result = resp.json()["result"] + assert len(result) == 1 + assert result[0]["message"] == NotFoundError.message + + async def test_update_report_config_for_activity_flow__activity_does_not_exists( + self, + client: TestClient, + tom: User, + applet_one: AppletFull, + applet_report_configuration_data: AppletReportConfigurationBase, + uuid_zero: uuid.UUID, + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + resp = await client.put( + self.flow_report_config_url.format(pk=applet_one.id, flow_id=uuid_zero), + data=applet_report_configuration_data, + ) + assert resp.status_code == http.HTTPStatus.NOT_FOUND + result = resp.json()["result"] + assert len(result) == 1 + assert result[0]["message"] == NotFoundError.message + + async def test_delete_applet__firebase_error_muted( + self, client: TestClient, tom: User, mocker: MockerFixture, applet_one: AppletFull + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + mocker.patch( + "infrastructure.utility.notification_client.FCMNotificationTest.notify", + side_effect=FireBaseNotFoundError(message="device id not found"), + ) + resp = await client.delete(self.applet_detail_url.format(pk=applet_one.id)) + assert resp.status_code == http.HTTPStatus.NO_CONTENT + + async def test_create_applet__duplicate_flow_name( + self, client: TestClient, tom: User, applet_create_with_flow: AppletCreate + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + data = applet_create_with_flow.copy(deep=True) + data.activity_flows.append(data.activity_flows[0]) + resp = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) + assert resp.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY + result = resp.json()["result"] + assert len(result) == 1 + assert result[0]["message"] == DuplicateActivityFlowNameError.message + + async def test_create_applet__only_one_reviewable_activity_allowed( + self, client: TestClient, tom: User, applet_minimal_data: AppletCreate + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + data = applet_minimal_data.copy(deep=True) + data.activities[0].is_reviewable = True + second_activity = data.activities[0].copy(deep=True) + second_activity.name = data.activities[0].name + "second" + data.activities.append(second_activity) + resp = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) + assert resp.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY + result = resp.json()["result"] + assert len(result) == 1 + assert result[0]["message"] == AssessmentLimitExceed.message + + async def test_update_applet__only_one_reviewable_activity_allowed( + self, client: TestClient, tom: User, applet_one: AppletFull, applet_one_update_data: AppletUpdate + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + data = applet_one_update_data.copy(deep=True) + data.activities[0].is_reviewable = True + second_activity = data.activities[0].copy(deep=True) + second_activity.name += "second" + second_activity.id = None + second_activity.items[0].id = None + data.activities.append(second_activity) + resp = await client.put(self.applet_detail_url.format(pk=applet_one.id), data=data) + assert resp.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY + result = resp.json()["result"] + assert len(result) == 1 + assert result[0]["message"] == AssessmentLimitExceed.message + + async def test_update_applet__duplicated_activity( + self, client: TestClient, tom: User, applet_one_update_data: AppletUpdate, applet_one: AppletFull + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + data = applet_one_update_data.copy(deep=True) + second_activity = data.activities[0].copy(deep=True) + second_activity.name += "second" + request_data = data.dict() + request_data["activities"].append(second_activity.dict()) + resp = await client.put(self.applet_detail_url.format(pk=applet_one.id), data=request_data) + assert resp.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY + result = resp.json()["result"] + assert len(result) == 1 + assert result[0]["message"] == DuplicatedActivitiesError.message + + async def test_update_applet__duplicate_flow_name( + self, client: TestClient, tom: User, applet_one_with_flow_update_data: AppletUpdate, applet_one: AppletFull + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + data = applet_one_with_flow_update_data.copy(deep=True) + request_data = data.dict() + request_data["activity_flows"].append(data.activity_flows[0].dict()) + resp = await client.put(self.applet_detail_url.format(pk=applet_one.id), data=request_data) + assert resp.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY + result = resp.json()["result"] + assert len(result) == 1 + assert result[0]["message"] == DuplicateActivityFlowNameError.message + + async def test_update_applet__duplicate_flow_id( + self, client: TestClient, tom: User, applet_one_with_flow_update_data: AppletUpdate, applet_one: AppletFull + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + data = applet_one_with_flow_update_data.copy(deep=True) + flow = data.activity_flows[0].copy(deep=True) + flow.name += "second" + request_data = data.dict() + request_data["activity_flows"].append(flow.dict()) + resp = await client.put(self.applet_detail_url.format(pk=applet_one.id), data=request_data) + assert resp.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY + result = resp.json()["result"] + assert len(result) == 1 + assert result[0]["message"] == DuplicatedActivityFlowsError.message + + async def test_update_applet__flow_key_is_not_valid( + self, + client: TestClient, + tom: User, + applet_one_with_flow_update_data: AppletUpdate, + applet_one: AppletFull, + uuid_zero: uuid.UUID, + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + data = applet_one_with_flow_update_data.copy(deep=True) + request_data = data.dict() + request_data["activity_flows"][0]["items"][0]["activity_key"] = uuid_zero + resp = await client.put(self.applet_detail_url.format(pk=applet_one.id), data=request_data) + assert resp.status_code == http.HTTPStatus.BAD_REQUEST + result = resp.json()["result"] + assert len(result) == 1 + assert result[0]["message"] == FlowItemActivityKeyNotFoundError.message + + async def test_create_applet_with_flow(self, client: TestClient, tom: User, applet_create_with_flow: AppletCreate): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + resp = await client.post(self.applet_create_url.format(owner_id=tom.id), data=applet_create_with_flow) + assert resp.status_code == http.HTTPStatus.CREATED + result = resp.json()["result"] + assert len(result["activityFlows"]) == 1 + exp_flow = applet_create_with_flow.activity_flows[0] + assert result["activityFlows"][0]["name"] == exp_flow.name + assert result["activityFlows"][0]["description"] == exp_flow.description + assert len(result["activityFlows"][0]["items"]) == 1 + assert result["activityFlows"][0]["items"][0]["activityId"] == result["activities"][0]["id"] + + async def test_update_applet_activity_report_config(self, client: TestClient, tom: User, applet_one: AppletFull): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + assert applet_one.activities[0].report_included_item_name is None + item_name = applet_one.activities[0].items[0].name + resp = await client.put( + self.activity_report_config_url.format( + pk=applet_one.id, + activity_id=applet_one.activities[0].id, + ), + data=dict(report_included_item_name=item_name), + ) + assert resp.status_code == http.HTTPStatus.OK + resp = await client.get(self.applet_detail_url.format(pk=applet_one.id)) + assert resp.status_code == http.HTTPStatus.OK + assert resp.json()["result"]["activities"][0]["reportIncludedItemName"] == item_name + + async def test_update_applet_activity_flow_report_config( + self, client: TestClient, tom: User, applet_one_with_flow: AppletFull + ): + assert applet_one_with_flow.activity_flows[0].report_included_activity_name is None + assert applet_one_with_flow.activity_flows[0].report_included_item_name is None + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + activity_name = applet_one_with_flow.activities[0].name + item_name = applet_one_with_flow.activities[0].items[0].name + resp = await client.put( + self.flow_report_config_url.format( + pk=applet_one_with_flow.id, flow_id=applet_one_with_flow.activity_flows[0].id + ), + data=dict( + report_included_activity_name=activity_name, + report_included_item_name=item_name, + ), + ) + assert resp.status_code == http.HTTPStatus.OK + resp = await client.get(self.applet_detail_url.format(pk=applet_one_with_flow.id)) + assert resp.status_code == http.HTTPStatus.OK + assert resp.json()["result"]["activityFlows"][0]["reportIncludedActivityName"] == activity_name + assert resp.json()["result"]["activityFlows"][0]["reportIncludedItemName"] == item_name diff --git a/src/apps/applets/tests/test_applet_activity_items.py b/src/apps/applets/tests/test_applet_activity_items.py index 8224b0618ba..6ab163cf3dc 100644 --- a/src/apps/applets/tests/test_applet_activity_items.py +++ b/src/apps/applets/tests/test_applet_activity_items.py @@ -1,12 +1,64 @@ import copy +import http import uuid +from typing import cast import pytest +from pydantic.color import Color +from pytest import FixtureRequest from apps.activities import errors as activity_errors -from apps.activities.domain.response_type_config import ResponseType, SingleSelectionConfig -from apps.activities.domain.response_values import SingleSelectionValues -from apps.applets.domain.applet_create_update import AppletUpdate +from apps.activities.domain.activity_create import ActivityCreate, ActivityItemCreate +from apps.activities.domain.conditional_logic import ConditionalLogic, Match +from apps.activities.domain.conditions import EqualToOptionCondition, OptionPayload, SingleSelectConditionType +from apps.activities.domain.response_type_config import PerformanceTaskType, ResponseType, SingleSelectionConfig +from apps.activities.domain.response_values import SingleSelectionValues, SliderValues +from apps.activities.domain.scores_reports import ( + ScoreConditionalLogic, + ScoresAndReports, + SectionConditionalLogic, + Subscale, + SubScaleLookupTable, + SubscaleSetting, + TotalScoreTable, +) +from apps.applets.domain.applet_create_update import AppletCreate, AppletUpdate +from apps.applets.domain.applet_full import AppletFull +from apps.shared.test.client import TestClient +from apps.users.domain import User + + +@pytest.fixture +def activity_create_with_conditional_logic( + activity_create_session: ActivityCreate, + single_select_item_create: ActivityItemCreate, +) -> ActivityCreate: + activity = activity_create_session.copy(deep=True) + single_select_item_create.response_values = cast(SingleSelectionValues, single_select_item_create.response_values) + single_select_with_cond = single_select_item_create.copy(deep=True) + single_select_with_cond.name = single_select_item_create.name + "_with_conditional_logic" + single_select_with_cond.conditional_logic = ConditionalLogic( + match=Match.ALL, + conditions=[ + EqualToOptionCondition( + item_name=single_select_item_create.name, + type=SingleSelectConditionType.EQUAL_TO_OPTION, + payload=OptionPayload(option_value=str(single_select_item_create.response_values.options[0].value)), + ), + ], + ) + activity.items = [single_select_item_create, single_select_with_cond] + return activity + + +@pytest.fixture +def single_select_item_create_with_score(single_select_item_create: ActivityItemCreate) -> ActivityItemCreate: + item_create = single_select_item_create.copy(deep=True) + item_create.config = cast(SingleSelectionConfig, item_create.config) + item_create.response_values = cast(SingleSelectionValues, item_create.response_values) + item_create.response_values.options[0].score = 1 + item_create.config.add_scores = True + return item_create class TestActivityItems: @@ -17,2258 +69,196 @@ class TestActivityItems: activity_detail_url = "activities/{activity_id}" applet_workspace_detail_url = "workspaces/{owner_id}/applets/{pk}" - async def test_creating_applet_with_activity_items(self, client, tom): - await client.login(self.login_url, tom.email_encrypted, "Test1234!") - create_data = dict( - display_name="User daily behave", - encryption=dict( - public_key=uuid.uuid4().hex, - prime=uuid.uuid4().hex, - base=uuid.uuid4().hex, - account_id=str(uuid.uuid4()), - ), - description=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - about=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - activities=[ - dict( - name="Morning activity", - key="577dbbda-3afc-4962-842b-8d8d11588bfe", - description=dict( - en="Understand morning feelings.", - fr="Understand morning feelings.", - ), - items=[ - dict( - name="activity_item_text", - question=dict( - en="How had you slept?", - fr="How had you slept?", - ), - response_type="text", - response_values=None, - config=dict( - max_response_length=200, - correct_answer_required=False, - correct_answer=None, - numerical_response_required=False, - response_data_identifier=False, - response_required=False, - remove_back_button=False, - skippable_item=True, - ), - ), - dict( - name="activity_item_message", - question={"en": "What is your name?"}, - response_type="message", - response_values=None, - config=dict( - remove_back_button=False, - timer=1, - ), - ), - dict( - name="activity_item_number_selection", - question={"en": "What is your name?"}, - response_type="numberSelect", - response_values=dict( - min_value=0, - max_value=10, - ), - config=dict( - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - remove_back_button=False, - skippable_item=False, - ), - ), - dict( - name="activity_item_time_range", - question={"en": "What is your name?"}, - response_type="timeRange", - response_values=None, - config=dict( - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - remove_back_button=False, - skippable_item=False, - timer=1, - ), - ), - dict( - name="activity_item_time_range_2", - question={"en": "What is your name?"}, - response_type="time", - response_values=None, - config=dict( - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - remove_back_button=False, - skippable_item=False, - timer=1, - ), - ), - dict( - name="activity_item_geolocation", - question={"en": "What is your name?"}, - response_type="geolocation", - response_values=None, - config=dict( - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - remove_back_button=False, - skippable_item=False, - ), - ), - dict( - name="activity_item_photo", - question={"en": "What is your name?"}, - response_type="photo", - response_values=None, - config=dict( - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - remove_back_button=False, - skippable_item=False, - ), - ), - dict( - name="activity_item_video", - question={"en": "What is your name?"}, - response_type="video", - response_values=None, - config=dict( - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - remove_back_button=False, - skippable_item=False, - ), - ), - dict( - name="activity_item_date", - question={"en": "What is your name?"}, - response_type="date", - response_values=None, - config=dict( - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - remove_back_button=False, - skippable_item=False, - ), - ), - dict( - name="activity_item_drawing", - question={"en": "What is your name?"}, - response_type="drawing", - response_values=dict( - drawing_background="https://www.w3schools.com/css/img_5terre_wide.jpg", # noqa E501 - drawing_example="https://www.w3schools.com/css/img_5terre_wide.jpg", # noqa E501 - ), - config=dict( - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - remove_back_button=False, - skippable_item=False, - timer=1, - remove_undo_button=False, - navigation_to_top=False, - ), - ), - dict( - name="activity_item_audio", - question={"en": "What is your name?"}, - response_type="audio", - response_values=dict(max_duration=200), - config=dict( - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - remove_back_button=False, - skippable_item=False, - timer=1, - ), - ), - dict( - name="activity_item_audioplayer", - question={"en": "What is your name?"}, - response_type="audioPlayer", - response_values=dict( - file="https://www.w3schools.com/html/horse.mp3", # noqa E501 - ), - config=dict( - remove_back_button=False, - skippable_item=False, - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - play_once=False, - ), - ), - dict( - name="activity_item_sliderrows", - question={"en": "What is your name?"}, - response_type="sliderRows", - response_values=dict( - rows=[ - { - "label": "label1", - "min_label": "min_label1", - "max_label": "max_label1", - "min_value": 0, - "max_value": 10, - "min_image": None, - "max_image": None, - "score": None, - "alerts": [ - dict( - min_value=1, - max_value=4, - alert="alert1", - ), - ], - } - ] - ), - config=dict( - remove_back_button=False, - skippable_item=False, - add_scores=False, - set_alerts=True, - timer=1, - ), - ), - dict( - name="activity_item_multiselectionrows", - question={"en": "What is your name?"}, - response_type="multiSelectRows", - response_values=dict( - rows=[ - { - "id": "17e69155-22cd-4484-8a49-364779ea9df1", # noqa E501 - "row_name": "row1", - "row_image": None, - "tooltip": None, - }, - { - "id": "17e69155-22cd-4484-8a49-364779ea9df2", # noqa E501 - "row_name": "row2", - "row_image": None, - "tooltip": None, - }, - ], - options=[ - { - "id": "17e69155-22cd-4484-8a49-364779ea9de1", # noqa E501 - "text": "option1", - "image": None, - "tooltip": None, - }, - { - "id": "17e69155-22cd-4484-8a49-364779ea9de2", # noqa E501 - "text": "option2", - "image": None, - "tooltip": None, - }, - ], - data_matrix=[ - { - "row_id": "17e69155-22cd-4484-8a49-364779ea9df1", # noqa E501 - "options": [ - { - "option_id": "17e69155-22cd-4484-8a49-364779ea9de1", # noqa E501 - "score": 1, - "alert": None, - }, - { - "option_id": "17e69155-22cd-4484-8a49-364779ea9de2", # noqa E501 - "score": 2, - "alert": None, - }, - ], - }, - { - "row_id": "17e69155-22cd-4484-8a49-364779ea9df2", # noqa E501 - "options": [ - { - "option_id": "17e69155-22cd-4484-8a49-364779ea9de1", # noqa E501 - "score": 3, - "alert": None, - }, - { - "option_id": "17e69155-22cd-4484-8a49-364779ea9de2", # noqa E501 - "score": 4, - "alert": None, - }, - ], - }, - ], - ), - config=dict( - remove_back_button=False, - skippable_item=False, - add_scores=False, - set_alerts=False, - timer=1, - add_tooltip=False, - ), - ), - dict( - name="activity_item_singleselectionrows", - question={"en": "What is your name?"}, - response_type="singleSelectRows", - response_values=dict( - rows=[ - { - "id": "17e69155-22cd-4484-8a49-364779ea9df1", # noqa E501 - "row_name": "row1", - "row_image": None, - "tooltip": None, - }, - { - "id": "17e69155-22cd-4484-8a49-364779ea9df2", # noqa E501 - "row_name": "row2", - "row_image": None, - "tooltip": None, - }, - ], - options=[ - { - "id": "17e69155-22cd-4484-8a49-364779ea9de1", # noqa E501 - "text": "option1", - "image": None, - "tooltip": None, - }, - { - "id": "17e69155-22cd-4484-8a49-364779ea9de2", # noqa E501 - "text": "option2", - "image": None, - "tooltip": None, - }, - ], - data_matrix=[ - { - "row_id": "17e69155-22cd-4484-8a49-364779ea9df1", # noqa E501 - "options": [ - { - "option_id": "17e69155-22cd-4484-8a49-364779ea9de1", # noqa E501 - "score": 1, - "alert": "alert1", - }, - { - "option_id": "17e69155-22cd-4484-8a49-364779ea9de2", # noqa E501 - "score": 2, - "alert": None, - }, - ], - }, - { - "row_id": "17e69155-22cd-4484-8a49-364779ea9df2", # noqa E501 - "options": [ - { - "option_id": "17e69155-22cd-4484-8a49-364779ea9de1", # noqa E501 - "score": 3, - "alert": None, - }, - { - "option_id": "17e69155-22cd-4484-8a49-364779ea9de2", # noqa E501 - "score": 4, - "alert": None, - }, - ], - }, - ], - ), - config=dict( - remove_back_button=False, - skippable_item=False, - add_scores=False, - set_alerts=True, - timer=1, - add_tooltip=False, - ), - ), - dict( - name="activity_item_singleselect", - question={"en": "What is your name?"}, - response_type="singleSelect", - response_values=dict( - palette_name="palette1", - options=[ - { - "text": "option1", - "value": 1, - "alert": "alert1", - }, - { - "text": "option2", - "value": 2, - "alert": "alert2", - }, - ], - ), - config=dict( - remove_back_button=False, - skippable_item=False, - add_scores=False, - set_alerts=True, - timer=1, - add_tooltip=False, - set_palette=False, - randomize_options=False, - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - ), - ), - dict( - name="activity_item_multiselect", - question={"en": "What is your name?"}, - response_type="multiSelect", - response_values=dict( - palette_name="palette1", - options=[ - {"text": "option1", "value": 0}, - {"text": "option2", "value": 1}, - ], - ), - config=dict( - remove_back_button=False, - skippable_item=False, - add_scores=False, - set_alerts=False, - timer=1, - add_tooltip=False, - set_palette=False, - randomize_options=False, - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - ), - ), - dict( - name="activity_item_slideritem", - question={"en": "What is your name?"}, - response_type="slider", - response_values=dict( - min_value=0, - max_value=10, - min_label="min_label", - max_label="max_label", - min_image=None, - max_image=None, - scores=None, - alerts=[ - dict( - min_value=1, - max_value=4, - alert="alert1", - ), - ], - ), - config=dict( - remove_back_button=False, - skippable_item=False, - add_scores=False, - set_alerts=True, - timer=1, - show_tick_labels=False, - show_tick_marks=False, - continuous_slider=True, - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - ), - ), - dict( - name="activity_item_slideritem_another", - question={"en": "What is your name?"}, - response_type="slider", - response_values=dict( - min_value=0, - max_value=10, - min_label="min_label", - max_label="max_label", - min_image=None, - max_image=None, - scores=None, - alerts=[ - dict( - value="1", - alert="alert1", - ), - ], - ), - config=dict( - remove_back_button=False, - skippable_item=False, - add_scores=False, - set_alerts=True, - timer=1, - show_tick_labels=False, - show_tick_marks=False, - continuous_slider=False, - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - ), - ), - ], - ), - ], - activity_flows=[ - dict( - name="Morning questionnaire", - description=dict( - en="Understand how was the morning", - fr="Understand how was the morning", - ), - items=[dict(activity_key="577dbbda-3afc-" "4962-842b-8d8d11588bfe")], - ) - ], - ) - response = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=create_data, - ) - assert response.status_code == 201, response.json() - - response = await client.get(self.applet_detail_url.format(pk=response.json()["result"]["id"])) - assert response.status_code == 200 - - async def test_creating_applet_with_ab_trails_mobile_activity_items(self, client, tom): - await client.login(self.login_url, tom.email_encrypted, "Test1234!") - create_data = dict( - display_name="mobile_activity_applet", - encryption=dict( - public_key=uuid.uuid4().hex, - prime=uuid.uuid4().hex, - base=uuid.uuid4().hex, - account_id=str(uuid.uuid4()), - ), - description=dict( - en="Performance Tasks AB Trails Mobile Applet", - fr="Performance Tasks AB Trails Mobile Applet", - ), - about=dict( - en="Applet AB Trails Mobile Task Builder Activity", - fr="Applet AB Trails Mobile Task Builder Activity", - ), - activities=[ - dict( - name="Activity_ABTrailsMobile", - key="577dbbda-3afc-4962-842b-8d8d11588bfe", - description=dict( - en="Description Activity ABTrailsMobile.", - fr="Description Activity ABTrailsMobile.", - ), - items=[ - dict( - name="AB_Trails_Mobile_1", - question=dict( - en="ab_trails_mobile 1 question", - fr="ab_trails_mobile 1 question", - ), - response_type="ABTrails", - response_values=None, - config=dict( - device_type="mobile", - order_name="first", - ), - ), - dict( - name="AB_Trails_Mobile_2", - question=dict( - en="ab_trails_mobile 2 question", - fr="ab_trails_mobile 2 question", - ), - response_type="ABTrails", - response_values=None, - config=dict( - device_type="mobile", - order_name="second", - ), - ), - dict( - name="AB_Trails_Mobile_3", - question=dict( - en="ab_trails_mobile 3 question", - fr="ab_trails_mobile 3 question", - ), - response_type="ABTrails", - response_values=None, - config=dict( - device_type="mobile", - order_name="third", - ), - ), - dict( - name="AB_Trails_Mobile_4", - question=dict( - en="ab_trails_mobile 4 question", - fr="ab_trails_mobile 4 question", - ), - response_type="ABTrails", - response_values=None, - config=dict( - device_type="mobile", - order_name="fourth", - ), - ), - ], - ), - ], - activity_flows=[ - dict( - name="name_activityFlow", - description=dict( - en="description activityFlow", - fr="description activityFlow", - ), - items=[dict(activity_key="577dbbda-3afc-" "4962-842b-8d8d11588bfe")], - ) - ], - ) - response = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=create_data, - ) - assert response.status_code == 201, response.json() - - response = await client.get(self.applet_detail_url.format(pk=response.json()["result"]["id"])) - assert response.status_code == 200 - - async def test_creating_applet_with_ab_trails_tablet_activity_items(self, client, tom): - await client.login(self.login_url, tom.email_encrypted, "Test1234!") - create_data = dict( - display_name="tablet_activity_applet", - encryption=dict( - public_key=uuid.uuid4().hex, - prime=uuid.uuid4().hex, - base=uuid.uuid4().hex, - account_id=str(uuid.uuid4()), - ), - description=dict( - en="Performance Tasks AB Trails Tablet Applet", - fr="Performance Tasks AB Trails Tablet Applet", - ), - about=dict( - en="Applet AB Trails Tablet Task Builder Activity", - fr="Applet AB Trails Tablet Task Builder Activity", - ), - activities=[ - dict( - name="Activity_ABTrailsTablet", - key="577dbbda-3afc-4962-842b-8d8d11588bfe", - description=dict( - en="Description Activity ABTrailsTablet.", - fr="Description Activity ABTrailsTablet.", - ), - items=[ - dict( - name="AB_Trails_Tablet_1", - question=dict( - en="ab_trails_tablet 1 question", - fr="ab_trails_tablet 1 question", - ), - response_type="ABTrails", - response_values=None, - config=dict( - device_type="tablet", - order_name="first", - ), - ), - dict( - name="AB_Trails_Tablet_2", - question=dict( - en="ab_trails_tablet 2 question", - fr="ab_trails_tablet 2 question", - ), - response_type="ABTrails", - response_values=None, - config=dict( - device_type="tablet", - order_name="second", - ), - ), - dict( - name="AB_Trails_Tablet_3", - question=dict( - en="ab_trails_tablet 3 question", - fr="ab_trails_tablet 3 question", - ), - response_type="ABTrails", - response_values=None, - config=dict( - device_type="tablet", - order_name="third", - ), - ), - dict( - name="AB_Trails_Tablet_4", - question=dict( - en="ab_trails_tablet 4 question", - fr="ab_trails_tablet 4 question", - ), - response_type="ABTrails", - response_values=None, - config=dict( - device_type="tablet", - order_name="fourth", - ), - ), - ], - ), - ], - activity_flows=[ - dict( - name="name_activityFlow", - description=dict( - en="description activityFlow", - fr="description activityFlow", - ), - items=[dict(activity_key="577dbbda-3afc-" "4962-842b-8d8d11588bfe")], - ) - ], - ) - response = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=create_data, - ) - assert response.status_code == 201, response.json() - - response = await client.get(self.applet_detail_url.format(pk=response.json()["result"]["id"])) - assert response.status_code == 200 - - async def test_creating_applet_with_gyroscope_activity_items(self, client, tom): - await client.login(self.login_url, tom.email_encrypted, "Test1234!") - create_data = dict( - display_name="gyroscope_activity_applet", - encryption=dict( - public_key=uuid.uuid4().hex, - prime=uuid.uuid4().hex, - base=uuid.uuid4().hex, - account_id=str(uuid.uuid4()), - ), - description=dict( - en="Performance Tasks CST Gyroscope Applet", - fr="Performance Tasks CST Gyroscope Applet", - ), - about=dict( - en="Applet CST Gyroscope Task Builder Activity", - fr="Applet CST Gyroscope Task Builder Activity", - ), - activities=[ - dict( - name="Activity_gyroscope", - key="577dbbda-3afc-4962-842b-8d8d11588bfe", - description=dict( - en="Description Activity gyroscope.", - fr="Description Activity gyroscope.", - ), - items=[ - dict( - name="Gyroscope_General_instruction", - question=dict( - en="Gyroscope General instruction text.", - fr="Gyroscope General instruction text.", - ), - response_type="message", - response_values=None, - config=dict( - remove_back_button=False, - timer=None, - ), - ), - dict( - name="Gyroscope_Сalibration_Practice_instruction", - question=dict( - en="Gyroscope Сalibration/Practice " "instruction text.", - fr="Gyroscope Сalibration/Practice " "instruction text.", - ), - response_type="message", - response_values=None, - config=dict( - remove_back_button=False, - timer=None, - ), - ), - dict( - name="Gyroscope_Сalibration_Practice", - question=dict( - en="Gyroscope Сalibration/Practice.", - fr="Gyroscope Сalibration/Practice.", - ), - response_type="stabilityTracker", - response_values=None, - config=dict( - user_input_type="gyroscope", - phase="practice", - trials_number=3, - duration_minutes=5, - lambda_slope=0.2, - max_off_target_time=10, - num_test_trials=10, - task_mode="pseudo_stair", - tracking_dims=2, - show_score=True, - basis_func="zeros_1d", - noise_level=0, - task_loop_rate=0.0167, - cycles_per_min=2, - oob_duration=0.2, - initial_lambda=0.075, - show_preview=True, - num_preview_stim=0, - preview_step_gap=100, - dimension_count=1, - max_rad=0.26167, - ), - ), - dict( - name="Gyroscope_Test_instruction", - question=dict( - en="Gyroscope Test instruction text.", - fr="Gyroscope Test instruction text.", - ), - response_type="message", - response_values=None, - config=dict( - remove_back_button=False, - timer=None, - ), - ), - dict( - name="Gyroscope_Test", - question=dict( - en="Gyroscope Test.", - fr="Gyroscope Test.", - ), - response_type="stabilityTracker", - response_values=None, - config=dict( - user_input_type="gyroscope", - phase="test", - trials_number=5, - duration_minutes=7, - lambda_slope=0.2, - max_off_target_time=10, - num_test_trials=10, - task_mode="pseudo_stair", - tracking_dims=2, - show_score=True, - basis_func="zeros_1d", - noise_level=0, - task_loop_rate=0.0167, - cycles_per_min=2, - oob_duration=0.2, - initial_lambda=0.075, - show_preview=True, - num_preview_stim=0, - preview_step_gap=100, - dimension_count=1, - max_rad=0.26167, - ), - ), - ], - ), - ], - activity_flows=[ - dict( - name="name_activityFlow", - description=dict( - en="description activityFlow", - fr="description activityFlow", - ), - items=[dict(activity_key="577dbbda-3afc-" "4962-842b-8d8d11588bfe")], - ) - ], - ) - response = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=create_data, - ) - assert response.status_code == 201, response.json() - - response = await client.get(self.applet_detail_url.format(pk=response.json()["result"]["id"])) - assert response.status_code == 200 - - async def test_creating_applet_with_touch_activity_items(self, client, tom): - await client.login(self.login_url, tom.email_encrypted, "Test1234!") - create_data = dict( - display_name="touch_activity_applet", - encryption=dict( - public_key=uuid.uuid4().hex, - prime=uuid.uuid4().hex, - base=uuid.uuid4().hex, - account_id=str(uuid.uuid4()), - ), - description=dict( - en="Performance Tasks CST Touch Applet", - fr="Performance Tasks CST Touch Applet", - ), - about=dict( - en="Applet CST Touch Task Builder Activity", - fr="Applet CST Touch Task Builder Activity", - ), - activities=[ - dict( - name="Activity_touch", - key="577dbbda-3afc-4962-842b-8d8d11588bfe", - description=dict( - en="Description Activity touch.", - fr="Description Activity touch.", - ), - items=[ - dict( - name="Touch_General_instruction", - question=dict( - en="Touch General instruction text.", - fr="Touch General instruction text.", - ), - response_type="message", - response_values=None, - config=dict( - remove_back_button=False, - timer=None, - ), - ), - dict( - name="Touch_Сalibration_Practice_instruction", - question=dict( - en="Touch Сalibration/Practice " "instruction text.", - fr="Touch Сalibration/Practice " "instruction text.", - ), - response_type="message", - response_values=None, - config=dict( - remove_back_button=False, - timer=None, - ), - ), - dict( - name="Touch_Сalibration_Practice", - question=dict( - en="Touch Сalibration/Practise.", - fr="Touch Сalibration/Practise.", - ), - response_type="stabilityTracker", - response_values=None, - config=dict( - user_input_type="touch", - phase="practice", - trials_number=3, - duration_minutes=5, - lambda_slope=0.2, - max_off_target_time=10, - num_test_trials=10, - task_mode="pseudo_stair", - tracking_dims=2, - show_score=True, - basis_func="zeros_1d", - noise_level=0, - task_loop_rate=0.0167, - cycles_per_min=2, - oob_duration=0.2, - initial_lambda=0.075, - show_preview=True, - num_preview_stim=0, - preview_step_gap=100, - dimension_count=1, - max_rad=0.26167, - ), - ), - dict( - name="Touch_Test_instruction", - question=dict( - en="Touch Test instruction text.", - fr="Touch Test instruction text.", - ), - response_type="message", - response_values=None, - config=dict( - remove_back_button=False, - timer=None, - ), - ), - dict( - name="Touch_Test", - question=dict( - en="Touch Test.", - fr="Touch Test.", - ), - response_type="stabilityTracker", - response_values=None, - config=dict( - user_input_type="touch", - phase="test", - trials_number=5, - duration_minutes=7, - lambda_slope=0.2, - max_off_target_time=10, - num_test_trials=10, - task_mode="pseudo_stair", - tracking_dims=2, - show_score=True, - basis_func="zeros_1d", - noise_level=0, - task_loop_rate=0.0167, - cycles_per_min=2, - oob_duration=0.2, - initial_lambda=0.075, - show_preview=True, - num_preview_stim=0, - preview_step_gap=100, - dimension_count=1, - max_rad=0.26167, - ), - ), - ], - ), - ], - activity_flows=[ - dict( - name="name_activityFlow", - description=dict( - en="description activityFlow", - fr="description activityFlow", - ), - items=[dict(activity_key="577dbbda-3afc-" "4962-842b-8d8d11588bfe")], - ) - ], - ) - response = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=create_data, - ) - assert response.status_code == 201, response.json() - - response = await client.get(self.applet_detail_url.format(pk=response.json()["result"]["id"])) - assert response.status_code == 200 - - async def test_creating_applet_with_activity_items_condition(self, client, tom): - await client.login(self.login_url, tom.email_encrypted, "Test1234!") - create_data = dict( - display_name="User daily behave", - encryption=dict( - public_key=uuid.uuid4().hex, - prime=uuid.uuid4().hex, - base=uuid.uuid4().hex, - account_id=str(uuid.uuid4()), - ), - description=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - about=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - activities=[ - # Activity with conditional logic - dict( - name="Morning activity with conditional logic", - key="577dbbdd-3afc-4962-842b-8d8d11588bfe", - description=dict( - en="Understand morning feelings.", - fr="Understand morning feelings.", - ), - subscale_setting=dict( - calculate_total_score="sum", - total_scores_table_data=[ - dict( - raw_score="1", - optional_text="optional_text", - ), - dict( - raw_score="2", - optional_text="optional_text2", - ), - ], - subscales=[ - dict( - name="subscale1", - scoring="sum", - items=[ - dict( - name="activity_item_singleselect", - type="item", - ), - ], - subscale_table_data=[ - dict( - score="1.2342~1231", - raw_score="1", - age=15, - sex="F", - optional_text="optional_text", - ), - dict( - score="1.2342~1231.12333", - raw_score="1~6", - age=10, - sex="M", - optional_text="optional_text12", - ), - dict( - score=1, - raw_score=1, - age=15, - sex="M", - optional_text="optional_text13", - ), - ], - ), - dict( - name="subscale12", - scoring="sum", - items=[ - dict( - name="activity_item_singleselect", - type="item", - ), - dict( - name="subscale1", - type="subscale", - ), - ], - subscale_table_data=[ - dict( - score="1.2342~1231", - raw_score="1", - age=15, - sex="F", - optional_text="optional_text", - ), - dict( - score="1.2342~1231.12333", - raw_score="1~6", - age=10, - sex="M", - optional_text="optional_text12", - ), - dict( - score=1, - raw_score=1, - age=15, - sex="M", - optional_text="optional_text13", - ), - ], - ), - ], - ), - scores_and_reports=dict( - generateReport=True, - showScoreSummary=True, - reports=[ - dict( - name="activity_item_singleselect_score", - type="score", - id="activity_item_singleselect_score", - calculationType="sum", - minScore=0, - maxScore=3, - itemsScore=["activity_item_singleselect"], - message="Hello", - itemsPrint=[ - "activity_item_singleselect", - "activity_item_multiselect", - "activity_item_slideritem", - "activity_item_text", - ], - conditionalLogic=[ - dict( - name="score1_condition1", - id="activity_item_singleselect_score", - flagScore=True, - message="Hello2", - match="any", - conditions=[ - dict( - item_name=("activity_item_" "singleselect_score"), - type="GREATER_THAN", - payload=dict( - value=1, - ), - ), - dict( - item_name=("activity_item_" "singleselect_score"), - type="GREATER_THAN", - payload=dict( - value=1, - ), - ), - ], - ), - ], - ), - dict( - name="section1", - type="section", - messages="Hello from the other side", - itemsPrint=[ - "activity_item_singleselect", - "activity_item_multiselect", - "activity_item_slideritem", - "activity_item_text", - ], - conditionalLogic=dict( - match="all", - conditions=[ - dict( - item_name=( - "activity_item_singleselect_score" # noqa E501 - ), - type="GREATER_THAN", - payload=dict( - value=1, - ), - ), - dict( - item_name=( - "activity_item_singleselect_score" # noqa E501 - ), - type="EQUAL_TO_OPTION", - payload=dict( - option_value="1", # noqa E501 - ), - ), - dict( - item_name=( - "activity_item_singleselect_score" # noqa E501 - ), - type="NOT_EQUAL_TO_OPTION", - payload=dict( - option_value="2", # noqa E501 - ), - ), - dict( - item_name=( - "activity_item_multiselect" # noqa E501 - ), - type="NOT_INCLUDES_OPTION", - payload=dict( - option_value="1", # noqa E501 - ), - ), - ], - ), - ), - ], - ), - items=[ - dict( - name="activity_item_singleselect", - question={"en": "What is your name?"}, - response_type="singleSelect", - response_values=dict( - palette_name="palette1", - options=[ - { - "text": "option1", - "score": 1, - "id": "25e69155-22cd-4484-8a49-364779ea9de1", # noqa E501 - "value": "1", - }, - { - "text": "option2", - "score": 2, - "id": "26e69155-22cd-4484-8a49-364779ea9de1", # noqa E501 - "value": "2", - }, - ], - ), - config=dict( - remove_back_button=False, - skippable_item=False, - add_scores=True, - set_alerts=False, - timer=1, - add_tooltip=False, - set_palette=False, - randomize_options=False, - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - ), - ), - dict( - name="activity_item_text", - question=dict( - en="How had you slept?", - fr="How had you slept?", - ), - response_type="text", - response_values=None, - config=dict( - max_response_length=200, - correct_answer_required=False, - correct_answer=None, - numerical_response_required=False, - response_data_identifier=False, - response_required=False, - remove_back_button=False, - skippable_item=True, - ), - conditional_logic=dict( - match="any", - conditions=[ - dict( - item_name="activity_item_singleselect", - type="EQUAL_TO_OPTION", - payload=dict( - option_value="1" # noqa E501 - ), - ), - dict( - item_name="activity_item_singleselect_2", - type="NOT_EQUAL_TO_OPTION", - payload=dict( - option_value="2" # noqa E501 - ), - ), - dict( - item_name="activity_item_multiselect", - type="INCLUDES_OPTION", - payload=dict( - option_value="1" # noqa E501 - ), - ), - dict( - item_name="activity_item_multiselect_2", - type="NOT_INCLUDES_OPTION", - payload=dict( - option_value="2" # noqa E501 - ), - ), - dict( - item_name="activity_item_slideritem", - type="GREATER_THAN", - payload=dict( - value=5, - ), - ), - dict( - item_name="activity_item_slideritem_2", - type="OUTSIDE_OF", - payload=dict( - min_value=5, - max_value=10, - ), - ), - ], - ), - ), - dict( - name="activity_item_singleselect_2", - question={"en": "What is your name?"}, - response_type="singleSelect", - response_values=dict( - palette_name="palette1", - options=[ - { - "text": "option1", - "score": 1, - "id": "25e69155-22cd-4484-8a49-364779fa9de1", # noqa E501 - "value": "1", - }, - { - "text": "option2", - "score": 2, - "id": "26e69155-22cd-4484-8a49-364779fa9de1", # noqa E501 - "value": "2", - }, - ], - ), - config=dict( - remove_back_button=False, - skippable_item=False, - add_scores=True, - set_alerts=False, - timer=1, - add_tooltip=False, - set_palette=False, - randomize_options=False, - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - ), - ), - dict( - name="activity_item_multiselect", - question={"en": "What is your name?"}, - response_type="multiSelect", - response_values=dict( - palette_name="palette1", - options=[ - { - "text": "option1", - "id": "27e69155-22cd-4484-8a49-364779ea9de1", # noqa E501 - "value": "1", - }, - { - "text": "option2", - "id": "28e69155-22cd-4484-8a49-364779ea9de1", # noqa E501 - "value": "2", - }, - ], - ), - config=dict( - remove_back_button=False, - skippable_item=False, - add_scores=False, - set_alerts=False, - timer=1, - add_tooltip=False, - set_palette=False, - randomize_options=False, - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - ), - ), - dict( - name="activity_item_multiselect_2", - question={"en": "Option 2?"}, - response_type="multiSelect", - response_values=dict( - palette_name="palette1", - options=[ - { - "text": "option1", - "id": "27e69155-22cd-4484-8a49-364779eb9de1", # noqa E501 - "value": "1", - }, - { - "text": "option2", - "id": "28e69155-22cd-4484-8a49-364779eb9de1", # noqa E501 - "value": "2", - }, - ], - ), - config=dict( - remove_back_button=False, - skippable_item=False, - add_scores=False, - set_alerts=False, - timer=1, - add_tooltip=False, - set_palette=False, - randomize_options=False, - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - ), - ), - dict( - name="activity_item_slideritem", - question={"en": "What is your name?"}, - response_type="slider", - response_values=dict( - min_value=0, - max_value=10, - min_label="min_label", - max_label="max_label", - min_image=None, - max_image=None, - scores=None, - ), - config=dict( - remove_back_button=False, - skippable_item=False, - add_scores=False, - set_alerts=False, - timer=1, - show_tick_labels=False, - show_tick_marks=False, - continuous_slider=False, - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - ), - ), - dict( - name="activity_item_slideritem_2", - question={"en": "What is your name?"}, - response_type="slider", - response_values=dict( - min_value=0, - max_value=10, - min_label="min_label", - max_label="max_label", - min_image=None, - max_image=None, - scores=None, - ), - config=dict( - remove_back_button=False, - skippable_item=False, - add_scores=False, - set_alerts=False, - timer=1, - show_tick_labels=False, - show_tick_marks=False, - continuous_slider=False, - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - ), - ), - dict( - name="activity_item_time_range", - question={"en": "What is your name?"}, - response_type="timeRange", - response_values=None, - config=dict( - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - remove_back_button=False, - skippable_item=False, - timer=1, - ), - conditional_logic=dict( - match="all", - conditions=[ - dict( - item_name="activity_item_singleselect", - type="EQUAL_TO_OPTION", - payload=dict( - option_value="1" # noqa E501 - ), - ), - ], - ), - ), - dict( - name="activity_item_time_range_2", - question={"en": "What is your name?"}, - response_type="time", - response_values=None, - config=dict( - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - remove_back_button=False, - skippable_item=False, - timer=1, - ), - conditional_logic=dict( - match="all", - conditions=[ - dict( - item_name="activity_item_singleselect", - type="EQUAL_TO_OPTION", - payload=dict( - option_value="1" # noqa E501 - ), - ), - dict( - item_name="activity_item_multiselect", - type="INCLUDES_OPTION", - payload=dict( - option_value="1" # noqa E501 - ), - ), - ], - ), - ), - dict( - name="activity_item_audio", - question={"en": "What is your name?"}, - response_type="audio", - response_values=dict(max_duration=200), - config=dict( - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - remove_back_button=False, - skippable_item=False, - timer=1, - ), - ), - ], - ), - ], - activity_flows=[ - dict( - name="Morning questionnaire", - description=dict( - en="Understand how was the morning", - fr="Understand how was the morning", - ), - items=[ - dict( - activity_key="577dbbdd-3afc-4962-842b-8d8d11588bfe" # noqa E501 - ) - ], - ) - ], - ) - response = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=create_data, - ) - assert response.status_code == 400 - assert response.json()["result"][0]["message"] == activity_errors.IncorrectConditionItemIndexError.message - - text_item = create_data["activities"][0]["items"][1] - slider_item_2 = create_data["activities"][0]["items"][6] - create_data["activities"][0]["items"][1] = slider_item_2 - create_data["activities"][0]["items"][6] = text_item - - response = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=create_data, - ) - assert response.status_code == 201, response.json() - assert isinstance( - response.json()["result"]["activities"][0]["items"][6]["conditionalLogic"], - dict, - ) - assert isinstance( - response.json()["result"]["activities"][0]["scoresAndReports"], - dict, - ) - assert isinstance(response.json()["result"]["activities"][0]["subscaleSetting"], dict) - - response = await client.get(self.applet_detail_url.format(pk=response.json()["result"]["id"])) - assert response.status_code == 200 - - activity_id = response.json()["result"]["activities"][0]["id"] - response = await client.get(self.activity_detail_url.format(activity_id=activity_id)) - assert response.status_code == 200 - assert isinstance(response.json()["result"]["items"][6]["conditionalLogic"], dict) - - async def test_creating_activity_items_without_option_value(self, client, tom): - await client.login(self.login_url, tom.email_encrypted, "Test1234!") - create_data = dict( - display_name="User daily behave", - encryption=dict( - public_key=uuid.uuid4().hex, - prime=uuid.uuid4().hex, - base=uuid.uuid4().hex, - account_id=str(uuid.uuid4()), - ), - description=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - about=dict( - en="Understand users behave", - fr="Comprendre le comportement des utilisateurs", - ), - activities=[ - dict( - name="Morning activity", - key="577dbbda-3afc-4962-842b-8d8d11588bfe", - description=dict( - en="Understand morning feelings.", - fr="Understand morning feelings.", - ), - items=[ - dict( - name="activity_item_sliderrows", - question={"en": "What is your name?"}, - response_type="sliderRows", - response_values=dict( - rows=[ - { - "label": "label1", - "min_label": "min_label1", - "max_label": "max_label1", - "min_value": 0, - "max_value": 10, - "min_image": None, - "max_image": None, - "score": None, - "alerts": [ - dict( - min_value=1, - max_value=4, - alert="alert1", - ), - ], - } - ] - ), - config=dict( - remove_back_button=False, - skippable_item=False, - add_scores=False, - set_alerts=True, - timer=1, - ), - ), - dict( - name="activity_item_singleselectionrows", - question={"en": "What is your name?"}, - response_type="singleSelectRows", - response_values=dict( - rows=[ - { - "id": "17e69155-22cd-4484-8a49-364779ea9df1", # noqa E501 - "row_name": "row1", - "row_image": None, - "tooltip": None, - }, - { - "id": "17e69155-22cd-4484-8a49-364779ea9df2", # noqa E501 - "row_name": "row2", - "row_image": None, - "tooltip": None, - }, - ], - options=[ - { - "id": "17e69155-22cd-4484-8a49-364779ea9de1", # noqa E501 - "text": "option1", - "image": None, - "tooltip": None, - }, - { - "id": "17e69155-22cd-4484-8a49-364779ea9de2", # noqa E501 - "text": "option2", - "image": None, - "tooltip": None, - }, - ], - data_matrix=[ - { - "row_id": "17e69155-22cd-4484-8a49-364779ea9df1", # noqa E501 - "options": [ - { - "option_id": "17e69155-22cd-4484-8a49-364779ea9de1", # noqa E501 - "score": 1, - "alert": "alert1", - }, - { - "option_id": "17e69155-22cd-4484-8a49-364779ea9de2", # noqa E501 - "score": 2, - "alert": None, - }, - ], - }, - { - "row_id": "17e69155-22cd-4484-8a49-364779ea9df2", # noqa E501 - "options": [ - { - "option_id": "17e69155-22cd-4484-8a49-364779ea9de1", # noqa E501 - "score": 3, - "alert": None, - }, - { - "option_id": "17e69155-22cd-4484-8a49-364779ea9de2", # noqa E501 - "score": 4, - "alert": None, - }, - ], - }, - ], - ), - config=dict( - remove_back_button=False, - skippable_item=False, - add_scores=False, - set_alerts=True, - timer=1, - add_tooltip=False, - ), - ), - dict( - name="activity_item_slideritem", - question={"en": "What is your name?"}, - response_type="slider", - response_values=dict( - min_value=0, - max_value=10, - min_label="min_label", - max_label="max_label", - min_image=None, - max_image=None, - scores=None, - alerts=[ - dict( - min_value=1, - max_value=4, - alert="alert1", - ), - ], - ), - config=dict( - remove_back_button=False, - skippable_item=False, - add_scores=False, - set_alerts=True, - timer=1, - show_tick_labels=False, - show_tick_marks=False, - continuous_slider=True, - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - ), - ), - dict( - name="activity_item_singleselect", - question={"en": "What is your name?"}, - response_type="singleSelect", - response_values=dict( - palette_name="palette1", - options=[ - { - "text": "option1", - "alert": "alert1", - "value": 0, - }, - { - "text": "option2", - "alert": "alert2", - "value": 1, - }, - ], - ), - config=dict( - remove_back_button=False, - skippable_item=False, - add_scores=False, - set_alerts=True, - timer=1, - add_tooltip=False, - set_palette=False, - randomize_options=False, - additional_response_option={ - "text_input_option": False, - "text_input_required": False, - }, - ), - ), - ], - ), - ], - activity_flows=[ - dict( - name="Morning questionnaire", - description=dict( - en="Understand how was the morning", - fr="Understand how was the morning", - ), - items=[dict(activity_key="577dbbda-3afc-" "4962-842b-8d8d11588bfe")], - ) - ], - ) - response = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=create_data, - ) - assert response.status_code == 201, response.json() - - response = await client.get(self.applet_detail_url.format(pk=response.json()["result"]["id"])) - assert response.status_code == 200 - applet_id = response.json()["result"]["id"] - - response = await client.get( - self.activity_detail_url.format(activity_id=response.json()["result"]["activities"][0]["id"]) - ) - slider_rows_id = response.json()["result"]["items"][0]["id"] - - assert response.status_code == 200 - assert response.json()["result"]["items"][3]["responseValues"]["options"][0]["value"] == 0 - - create_data["activities"][0]["items"][0] = dict( - id=slider_rows_id, - name="activity_item_sliderrows", - question={"en": "What is your name?"}, - response_type="sliderRows", - response_values=dict( - rows=[ - { - "label": "label1", - "min_label": "min_label1", - "max_label": "max_label1", - "min_value": 1, - "max_value": 5, - "min_image": None, - "max_image": None, - "scores": [1, 2, 3, 4, 5], - } - ] - ), - config=dict( - remove_back_button=False, - skippable_item=False, - add_scores=True, - set_alerts=True, - timer=1, - ), - ) - - response = await client.put( - self.applet_detail_url.format(pk=applet_id), - data=create_data, - ) - assert response.status_code == 200 - - async def test_create_applet_with_flanker_preformance_task(self, client, activity_flanker_data, tom): + @pytest.mark.parametrize( + "item_fixture", + ( + "single_select_item_create", + "multi_select_item_create", + "slider_item_create", + "date_item_create", + "number_selection_item_create", + "time_item_create", + "time_range_item_create", + "single_select_row_item_create", + "multi_select_row_item_create", + "slider_rows_item_create", + "text_item_create", + "drawing_item_create", + "photo_item_create", + "video_item_create", + "geolocation_item_create", + "audio_item_create", + "message_item_create", + "audio_player_item_create", + ), + ) + async def test_create_applet_with_each_activity_item( + self, + client: TestClient, + tom: User, + applet_minimal_data: AppletCreate, + item_fixture: str, + request: FixtureRequest, + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") + item_create = request.getfixturevalue(item_fixture) + data = applet_minimal_data.copy(deep=True) + data.activities[0].items = [item_create] + resp = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) + assert resp.status_code == http.HTTPStatus.CREATED + resp = await client.get( + self.applet_workspace_detail_url.format(owner_id=tom.id, pk=resp.json()["result"]["id"]) + ) + assert resp.status_code == http.HTTPStatus.OK + result = resp.json()["result"] + items = result["activities"][0]["items"] + assert len(items) == 1 + item = items[0] + assert item["responseType"] == item_create.response_type + assert item["name"] == item_create.name + assert item["question"] == item_create.question + assert item["isHidden"] == item_create.is_hidden + assert not item["allowEdit"] + if item_create.response_type in ResponseType.get_non_response_types(): + assert item["responseValues"] is None + else: + assert item["responseValues"] == item_create.response_values.dict(by_alias=True) - create_data = dict( - display_name="Flanker", - encryption=dict( - public_key=uuid.uuid4().hex, - prime=uuid.uuid4().hex, - base=uuid.uuid4().hex, - account_id=str(uuid.uuid4()), - ), - description=dict(en="Flanker", fr="Flanker"), - about=dict(en="Flanker", fr="Flanker"), - activities=[activity_flanker_data], - # Empty, but required - activity_flows=[], - ) - - response = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=create_data, - ) - assert response.status_code == 201, response.json() - - assert response.json()["result"]["activities"][0]["isPerformanceTask"] - assert response.json()["result"]["activities"][0]["performanceTaskType"] == "flanker" - - # Check that the 'get' after creating new applet returns correct data - response = await client.get( - self.applet_workspace_detail_url.format( - owner_id=tom.id, - pk=response.json()["result"]["id"], - ) - ) - assert response.status_code == 200 - assert response.json()["result"]["activities"][0]["isPerformanceTask"] - assert response.json()["result"]["activities"][0]["performanceTaskType"] == "flanker" - - async def test_applet_add_performance_task_to_the_applet(self, client, activity_flanker_data, tom): + @pytest.mark.parametrize( + "fixture_name, performance_task_type", + ( + ("activity_ab_trails_ipad_create", PerformanceTaskType.ABTRAILS), + ("activity_ab_trails_mobile_create", PerformanceTaskType.ABTRAILS), + ("activity_flanker_create", PerformanceTaskType.FLANKER), + ("actvitiy_cst_gyroscope_create", PerformanceTaskType.GYROSCOPE), + ("actvitiy_cst_touch_create", PerformanceTaskType.TOUCH), + ), + ) + async def test_create_applet_with_performance_task( + self, + client: TestClient, + tom: User, + applet_minimal_data: AppletCreate, + request: FixtureRequest, + fixture_name: str, + performance_task_type: PerformanceTaskType, + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") - - create_data = dict( - display_name="Add flanker to existing applet", - encryption=dict( - public_key=uuid.uuid4().hex, - prime=uuid.uuid4().hex, - base=uuid.uuid4().hex, - account_id=str(uuid.uuid4()), - ), - description=dict(en="Add flanker to existing applet"), - about=dict(en="Add flanker to existing applet"), - activities=[ - dict( - name="Morning activity", - key="577dbbda-3afc-4962-842b-8d8d11588bfe", - description=dict( - en="Understand morning feelings.", - ), - items=[ - dict( - name="activity_item_text", - question=dict( - en="How had you slept?", - ), - response_type="text", - response_values=None, - config=dict( - max_response_length=200, - correct_answer_required=False, - correct_answer=None, - numerical_response_required=False, - response_data_identifier=False, - response_required=False, - remove_back_button=False, - skippable_item=True, - ), - ), - ], - ), - ], - # Empty, but required - activity_flows=[], - ) - - response = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=create_data, - ) - assert response.status_code == 201 - activity = response.json()["result"]["activities"][0] - assert not activity["isPerformanceTask"] - assert not activity["performanceTaskType"] - # Test that get after creating new applet returns correct data - # Generaly we don't need to test, tested data, but for now let leave - # it here - response = await client.get( + data = applet_minimal_data.copy(deep=True) + activity = request.getfixturevalue(fixture_name) + data.activities = [activity] + resp = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) + assert resp.status_code == http.HTTPStatus.CREATED + activities = resp.json()["result"]["activities"] + assert len(activities) == 1 + assert activities[0]["isPerformanceTask"] + assert activities[0]["performanceTaskType"] == performance_task_type + # Check that the 'get' after creating new applet returns correct performance task type + resp = await client.get( self.applet_workspace_detail_url.format( owner_id=tom.id, - pk=response.json()["result"]["id"], + pk=resp.json()["result"]["id"], ) ) - assert response.status_code == 200 - activity = response.json()["result"]["activities"][0] - assert not activity["isPerformanceTask"] - assert not activity["performanceTaskType"] - - # Add flanker performance task - create_data["activities"].append(activity_flanker_data) - - response = await client.put( - self.applet_detail_url.format(pk=response.json()["result"]["id"]), - data=create_data, - ) - assert response.status_code == 200 - flanker = response.json()["result"]["activities"][1] - assert flanker["isPerformanceTask"] - assert flanker["performanceTaskType"] == "flanker" + assert resp.status_code == http.HTTPStatus.OK + assert resp.json()["result"]["activities"][0]["isPerformanceTask"] + assert resp.json()["result"]["activities"][0]["performanceTaskType"] == performance_task_type - # Check the 'get' method - response = await client.get( - self.applet_workspace_detail_url.format( - owner_id=tom.id, - pk=response.json()["result"]["id"], - ) - ) - assert response.status_code == 200 - flanker = response.json()["result"]["activities"][1] - assert flanker["isPerformanceTask"] - assert flanker["performanceTaskType"] == "flanker" - - # TODO: move all validation test to the activity domain test - async def test_create_applet_item_name_is_not_valid(self, client, applet_minimal_data, tom) -> None: + async def test_creating_applet_with_activity_items_condition( + self, + client: TestClient, + tom: User, + applet_minimal_data: AppletCreate, + activity_create_with_conditional_logic: ActivityCreate, + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") - data = applet_minimal_data.dict() - data["activities"][0]["items"][0]["name"] = "%name" - resp = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=data, - ) - assert resp.status_code == 422 - errors = resp.json()["result"] - assert len(errors) == 1 - assert errors[0]["message"] == activity_errors.IncorrectNameCharactersError.message + data = applet_minimal_data.copy(deep=True) + data.activities = [activity_create_with_conditional_logic] + response = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) + assert response.status_code == http.HTTPStatus.CREATED - async def test_create_applet_item_config_not_valid(self, client, applet_minimal_data, tom) -> None: + activity_id = response.json()["result"]["activities"][0]["id"] + response = await client.get(self.activity_detail_url.format(activity_id=activity_id)) + assert response.status_code == http.HTTPStatus.OK + assert isinstance(response.json()["result"]["items"][1]["conditionalLogic"], dict) + + async def test_creating_applet_with_activity_items_condition_not_valid_order_in_conditional_logic( + self, + client: TestClient, + tom: User, + applet_minimal_data: AppletCreate, + activity_create_with_conditional_logic: ActivityCreate, + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") - data = applet_minimal_data.dict() - del data["activities"][0]["items"][0]["config"]["add_scores"] - del data["activities"][0]["items"][0]["config"]["set_alerts"] - resp = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=data, - ) - assert resp.status_code == 422 - errors = resp.json()["result"] - assert len(errors) == 1 - assert errors[0]["message"] == activity_errors.IncorrectConfigError.message.format(type=SingleSelectionConfig) - - async def test_create_applet_not_valid_response_type(self, client, applet_minimal_data, tom) -> None: + data = applet_minimal_data.copy(deep=True) + data.activities = [activity_create_with_conditional_logic] + item_for_condition = activity_create_with_conditional_logic.items[0] + item_with_condition = activity_create_with_conditional_logic.items[1] + # Make wrong order. Item with condition must be after item which value is included in conditional logic + request_data = data.dict() + request_data["activities"][0]["items"] = [item_with_condition.dict(), item_for_condition.dict()] + response = await client.post(self.applet_create_url.format(owner_id=tom.id), data=request_data) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + result = response.json()["result"] + assert len(result) == 1 + assert result[0]["message"] == activity_errors.IncorrectConditionItemIndexError.message + + async def test_create_applet__activity_with_conditional_logic( + self, + client: TestClient, + tom: User, + applet_minimal_data: AppletCreate, + activity_create_with_conditional_logic: ActivityCreate, + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") - data = applet_minimal_data.dict() - data["activities"][0]["items"][0]["response_type"] = "NotValid" - resp = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=data, + data = applet_minimal_data.copy(deep=True) + data.activities = [activity_create_with_conditional_logic] + response = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) + assert response.status_code == http.HTTPStatus.CREATED + result = response.json()["result"] + item_create_with_logic = activity_create_with_conditional_logic.items[1] + assert item_create_with_logic.conditional_logic is not None + assert result["activities"][0]["items"][1]["conditionalLogic"] == item_create_with_logic.conditional_logic.dict( + by_alias=True ) - assert resp.status_code == 422 - errors = resp.json()["result"] - assert len(errors) == 1 - assert errors[0]["message"] == activity_errors.IncorrectResponseValueError.message.format(type=ResponseType) @pytest.mark.parametrize( - "value,error_msg", + "item_fixture_name", ( - ( - {}, - activity_errors.IncorrectResponseValueError.message.format(type=SingleSelectionValues), - ), - ( - None, - activity_errors.IncorrectResponseValueError.message.format(type=SingleSelectionValues), - ), + "single_select_item_create", + "multi_select_item_create", + "single_select_row_item_create", + "multi_select_row_item_create", ), ) - async def test_create_applet_not_valid_response_values( - self, client, applet_minimal_data, tom, value, error_msg - ) -> None: - await client.login(self.login_url, tom.email_encrypted, "Test1234!") - data = applet_minimal_data.dict() - data["activities"][0]["items"][0]["response_values"] = value - data["activities"][0]["items"][0]["response_type"] = ResponseType.SINGLESELECT - resp = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=data, - ) - assert resp.status_code == 422 - errors = resp.json()["result"] - assert len(errors) == 1 - assert errors[0]["message"] == error_msg - - async def test_create_applet_without_item_response_type(self, client, applet_minimal_data, tom) -> None: - await client.login(self.login_url, tom.email_encrypted, "Test1234!") - data = applet_minimal_data.dict() - del data["activities"][0]["items"][0]["response_type"] - resp = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=data, - ) - assert resp.status_code == 422 - errors = resp.json()["result"] - assert len(errors) == 1 - assert errors[0]["message"] == "field required" - - async def test_create_applet_single_select_add_scores_not_scores_in_response_values( - self, client, applet_minimal_data, tom - ) -> None: - await client.login(self.login_url, tom.email_encrypted, "Test1234!") - data = applet_minimal_data.dict() - data["activities"][0]["items"][0]["config"]["add_scores"] = True - data["activities"][0]["items"][0]["response_type"] = ResponseType.SINGLESELECT - resp = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=data, - ) - assert resp.status_code == 422 - errors = resp.json()["result"] - assert len(errors) == 1 - assert errors[0]["message"] == activity_errors.ScoreRequiredForResponseValueError.message - - async def test_create_applet_slider_response_values_add_scores_not_scores_in_response_values( - self, client, applet_minimal_data, slider_response_values, slider_config, tom - ) -> None: - await client.login(self.login_url, tom.email_encrypted, "Test1234!") - data = applet_minimal_data.dict() - slider_config.add_scores = True - data["activities"][0]["items"][0]["config"] = slider_config.dict() - data["activities"][0]["items"][0]["response_type"] = ResponseType.SLIDER - data["activities"][0]["items"][0]["response_values"] = slider_response_values.dict() - resp = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=data, - ) - assert resp.status_code == 422 - errors = resp.json()["result"] - assert len(errors) == 1 - assert errors[0]["message"] == activity_errors.NullScoreError.message - - async def test_create_applet_slider_response_values_add_scores_scores_not_for_all_values( - self, client, applet_minimal_data, slider_response_values, slider_config, tom - ) -> None: - await client.login(self.login_url, tom.email_encrypted, "Test1234!") - data = applet_minimal_data.dict() - slider_config.add_scores = True - min_val = slider_response_values.min_value - max_val = slider_response_values.max_value - scores = [i for i in range(max_val - min_val)] - slider_response_values_data = slider_response_values.dict() - slider_response_values_data["scores"] = scores - data["activities"][0]["items"][0]["config"] = slider_config.dict() - data["activities"][0]["items"][0]["response_type"] = ResponseType.SLIDER - data["activities"][0]["items"][0]["response_values"] = slider_response_values_data - resp = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=data, - ) - assert resp.status_code == 422 - errors = resp.json()["result"] - assert len(errors) == 1 - assert errors[0]["message"] == activity_errors.InvalidScoreLengthError.message - - async def test_create_applet_slider_rows_response_values_add_scores_true_no_scores( - self, client, applet_minimal_data, slider_rows_response_values, slider_rows_config, tom - ) -> None: + async def test_creating_activity_items_without_option_value( + self, + client: TestClient, + tom: User, + item_fixture_name: str, + request: FixtureRequest, + applet_minimal_data: AppletCreate, + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") - data = applet_minimal_data.dict() - slider_rows_config_data = slider_rows_config.dict() - slider_rows_response_values_data = slider_rows_response_values.dict() - slider_rows_config_data["add_scores"] = True - slider_rows_response_values_data["rows"][0]["scores"] = None - item = data["activities"][0]["items"][0] - item["config"] = slider_rows_config_data - item["response_type"] = ResponseType.SLIDERROWS - item["response_values"] = slider_rows_response_values_data - resp = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=data, - ) - assert resp.status_code == 422 - errors = resp.json()["result"] - assert len(errors) == 1 - assert errors[0]["message"] == activity_errors.NullScoreError.message - - async def test_create_applet_slider_rows_response_values_add_scores_true_scores_not_for_all_values( - self, client, applet_minimal_data, slider_rows_response_values, slider_rows_config, tom - ) -> None: - await client.login(self.login_url, tom.email_encrypted, "Test1234!") - data = applet_minimal_data.dict() - slider_rows_response_values_data = slider_rows_response_values.dict() - slider_rows_config_data = slider_rows_config.dict() - slider_rows_config_data["add_scores"] = True - min_val = slider_rows_response_values_data["rows"][0]["min_value"] - max_val = slider_rows_response_values_data["rows"][0]["max_value"] - slider_rows_response_values_data["rows"][0]["scores"] = [i for i in range(max_val - min_val)] - item = data["activities"][0]["items"][0] - item["config"] = slider_rows_config_data - item["response_type"] = ResponseType.SLIDERROWS - item["response_values"] = slider_rows_response_values_data - resp = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=data, - ) - assert resp.status_code == 422 - errors = resp.json()["result"] - assert len(errors) == 1 - assert errors[0]["message"] == activity_errors.InvalidScoreLengthError.message + item = request.getfixturevalue(item_fixture_name) + data = applet_minimal_data.copy(deep=True) + # row + if hasattr(item.response_values, "data_matrix"): + item.response_values.data_matrix[0].options[0].value = None + else: + item.response_values.options[0].value = None + data.activities[0].items = [item] + response = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) + assert response.status_code == http.HTTPStatus.CREATED + response_values = response.json()["result"]["activities"][0]["items"][0]["responseValues"] + assert response_values["options"][0]["value"] == 0 @pytest.mark.parametrize("response_type", (ResponseType.SINGLESELECT, ResponseType.MULTISELECT)) async def test_create_applet_single_multi_select_response_values_value_null_auto_set_value( self, client, applet_minimal_data, tom, response_type ) -> None: await client.login(self.login_url, tom.email_encrypted, "Test1234!") - data = applet_minimal_data.dict() + data = applet_minimal_data.copy(deep=True).dict() item = data["activities"][0]["items"][0] option = item["response_values"]["options"][0] del option["value"] @@ -2280,36 +270,16 @@ async def test_create_applet_single_multi_select_response_values_value_null_auto self.applet_create_url.format(owner_id=tom.id), data=data, ) - assert resp.status_code == 201 + assert resp.status_code == http.HTTPStatus.CREATED item = resp.json()["result"]["activities"][0]["items"][0] # We can use enumerate because we have 2 options and values should be # 0 and 1 for i, o in enumerate(item["responseValues"]["options"]): assert o["value"] == i - async def test_create_applet_single_select_rows_response_values_add_alerts_no_datamatrix( - self, client, applet_minimal_data, single_select_row_response_values, single_select_row_config, tom + async def test_create_applet_flow_wrong_activity_key( + self, client: TestClient, applet_minimal_data: AppletCreate, tom: User ) -> None: - await client.login(self.login_url, tom.email_encrypted, "Test1234!") - data = applet_minimal_data.dict() - single_select_row_config_data = single_select_row_config.dict() - single_select_row_response_values_data = single_select_row_response_values.dict() - single_select_row_config_data["set_alerts"] = True - single_select_row_response_values_data["data_matrix"] = None - item = data["activities"][0]["items"][0] - item["config"] = single_select_row_config_data - item["response_type"] = ResponseType.SINGLESELECTROWS - item["response_values"] = single_select_row_response_values_data - resp = await client.post( - self.applet_create_url.format(owner_id=tom.id), - data=data, - ) - assert resp.status_code == 422 - errors = resp.json()["result"] - assert len(errors) == 1 - assert errors[0]["message"] == activity_errors.DataMatrixRequiredError.message - - async def test_create_applet_flow_wrong_activity_key(self, client, applet_minimal_data, tom) -> None: await client.login(self.login_url, tom.email_encrypted, "Test1234!") data = applet_minimal_data.dict() activity_key = data["activities"][0]["key"] @@ -2336,10 +306,10 @@ async def test_create_applet_flow_wrong_activity_key(self, client, applet_minima self.applet_create_url.format(owner_id=tom.id), data=data, ) - assert resp.status_code == 201 + assert resp.status_code == http.HTTPStatus.CREATED async def test_update_applet_duplicated_activity_item_name_is_not_allowed( - self, client, applet_minimal_data, tom, applet_one + self, client: TestClient, applet_minimal_data: AppletCreate, tom: User, applet_one: AppletFull ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") data = AppletUpdate(**applet_minimal_data.dict(exclude_unset=True)).dict() @@ -2349,7 +319,206 @@ async def test_update_applet_duplicated_activity_item_name_is_not_allowed( self.applet_detail_url.format(pk=applet_one.id), data=data, ) - assert resp.status_code == 422 + assert resp.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY result = resp.json()["result"] assert len(result) == 1 assert result[0]["message"] == activity_errors.DuplicateActivityItemNameNameError.message + + @pytest.mark.parametrize( + "fixture_name", + ("single_select_item_create", "multi_select_item_create"), + ) + async def test_create_applet__item_single_multi_select_with_image( + self, + client: TestClient, + applet_minimal_data: AppletCreate, + tom: User, + fixture_name: str, + remote_image: str, + request: FixtureRequest, + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + data = applet_minimal_data.copy(deep=True) + item_create = request.getfixturevalue(fixture_name) + item_create.response_values.options[0].image = remote_image + data.activities[0].items = [item_create] + resp = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) + assert resp.status_code == http.HTTPStatus.CREATED + assert ( + resp.json()["result"]["activities"][0]["items"][0]["responseValues"]["options"][0]["image"] == remote_image + ) + + @pytest.mark.parametrize( + "fixture_name", + ("single_select_item_create", "multi_select_item_create"), + ) + async def test_create_applet__item_single_multi_select_with_color( + self, + client: TestClient, + applet_minimal_data: AppletCreate, + tom: User, + fixture_name: str, + request: FixtureRequest, + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + data = applet_minimal_data.copy(deep=True) + item_create = request.getfixturevalue(fixture_name) + color = Color("#ffffff") + item_create.response_values.options[0].color = color + data.activities[0].items = [item_create] + resp = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) + assert resp.status_code == http.HTTPStatus.CREATED + assert ( + resp.json()["result"]["activities"][0]["items"][0]["responseValues"]["options"][0]["color"] + == color.as_hex() + ) + + async def test_create_applet__item_slider_with_color( + self, + client: TestClient, + applet_minimal_data: AppletCreate, + tom: User, + remote_image: str, + slider_item_create: ActivityItemCreate, + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + data = applet_minimal_data.copy(deep=True) + slider_item_create.response_values = cast(SliderValues, slider_item_create.response_values) + slider_item_create.response_values.min_image = remote_image + slider_item_create.response_values.max_image = remote_image + data.activities[0].items = [slider_item_create] + resp = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) + assert resp.status_code == http.HTTPStatus.CREATED + assert resp.json()["result"]["activities"][0]["items"][0]["responseValues"]["minImage"] == remote_image + assert resp.json()["result"]["activities"][0]["items"][0]["responseValues"]["maxImage"] == remote_image + + async def test_create_applet__activity_with_subscale_settings__subscale_type_item( + self, + client: TestClient, + applet_minimal_data: AppletCreate, + tom: User, + subscale_setting: SubscaleSetting, + single_select_item_create_with_score: ActivityItemCreate, + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + data = applet_minimal_data.copy(deep=True) + sub_setting = subscale_setting.copy(deep=True) + # subscale item must have name from activity. So for test just update name in copied subscale item + sub_setting.subscales[0].items[0].name = single_select_item_create_with_score.name # type: ignore[index] + data.activities[0].items = [single_select_item_create_with_score] + data.activities[0].subscale_setting = sub_setting + resp = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) + assert resp.status_code == http.HTTPStatus.CREATED + result = resp.json()["result"] + assert result["activities"][0]["subscaleSetting"] == sub_setting.dict(by_alias=True) + + async def test_create_applet__activity_with_subscale_settings__subscale_type_subscale( + self, + client: TestClient, + applet_minimal_data: AppletCreate, + single_select_item_create_with_score: ActivityItemCreate, + tom: User, + subscale_setting: SubscaleSetting, + subscale_with_item_type_subscale: Subscale, + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + data = applet_minimal_data.copy(deep=True) + sub_settin = subscale_setting.copy(deep=True) + # subscale item must have name from activity. So for test just update name in copied subscale item + sub_settin.subscales[0].items[0].name = single_select_item_create_with_score.name # type: ignore[index] + # Add subscale type subscale which has subscale item pointing to the subscale above + sub_settin.subscales.append(subscale_with_item_type_subscale) # type: ignore[union-attr] + data.activities[0].items = [single_select_item_create_with_score] + data.activities[0].subscale_setting = sub_settin + resp = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) + assert resp.status_code == http.HTTPStatus.CREATED + result = resp.json()["result"] + assert result["activities"][0]["subscaleSetting"] == sub_settin.dict(by_alias=True) + + async def test_create_applet__activity_with_subscale_settings_with_total_score_table( + self, + client: TestClient, + applet_minimal_data: AppletCreate, + single_select_item_create_with_score: ActivityItemCreate, + tom: User, + subscale_setting: SubscaleSetting, + subscale_total_score_table: list[TotalScoreTable], + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + data = applet_minimal_data.copy(deep=True) + sub_setting = subscale_setting.copy(deep=True) + # subscale item must have name from activity. So for test just update name in copied subscale item + sub_setting.subscales[0].items[0].name = single_select_item_create_with_score.name # type: ignore[index] + sub_setting.total_scores_table_data = subscale_total_score_table + data.activities[0].items = [single_select_item_create_with_score] + data.activities[0].subscale_setting = sub_setting + resp = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) + assert resp.status_code == http.HTTPStatus.CREATED + result = resp.json()["result"] + assert result["activities"][0]["subscaleSetting"] == sub_setting.dict(by_alias=True) + + async def test_create_applet__activity_with_subscale_settings_with_subscale_lookup_table( + self, + client: TestClient, + applet_minimal_data: AppletCreate, + single_select_item_create_with_score: ActivityItemCreate, + tom: User, + subscale_setting: SubscaleSetting, + subscale_lookup_table: list[SubScaleLookupTable], + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + data = applet_minimal_data.copy(deep=True) + sub_setting = subscale_setting.copy(deep=True) + # subscale item must have name from activity. So for test just update name in copied subscale item + sub_setting.subscales[0].items[0].name = single_select_item_create_with_score.name # type: ignore[index] + sub_setting.subscales[0].subscale_table_data = subscale_lookup_table # type: ignore[index] + data.activities[0].items = [single_select_item_create_with_score] + data.activities[0].subscale_setting = sub_setting + resp = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) + assert resp.status_code == http.HTTPStatus.CREATED + result = resp.json()["result"] + assert result["activities"][0]["subscaleSetting"] == sub_setting.dict(by_alias=True) + + async def test_create_applet__activity_with_score_and_reports__score_and_section( + self, + client: TestClient, + applet_minimal_data: AppletCreate, + tom: User, + scores_and_reports: ScoresAndReports, + single_select_item_create_with_score: ActivityItemCreate, + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + data = applet_minimal_data.copy(deep=True) + reports_data = scores_and_reports.copy(deep=True) + reports_data.reports[0].items_print = [single_select_item_create_with_score.name] # type: ignore[index] + data.activities[0].items = [single_select_item_create_with_score] + data.activities[0].scores_and_reports = reports_data + resp = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) + assert resp.status_code == http.HTTPStatus.CREATED + result = resp.json()["result"] + assert result["activities"][0]["scoresAndReports"] == reports_data.dict(by_alias=True) + + async def test_create_applet__activity_with_score_and_reports__score_and_section_with_conditional_logic( + self, + client: TestClient, + applet_minimal_data: AppletCreate, + tom: User, + scores_and_reports: ScoresAndReports, + score_conditional_logic: ScoreConditionalLogic, + section_conditional_logic: SectionConditionalLogic, + single_select_item_create_with_score: ActivityItemCreate, + ): + await client.login(self.login_url, tom.email_encrypted, "Test1234!") + data = applet_minimal_data.copy(deep=True) + reports_data = scores_and_reports.copy(deep=True) + reports_data.reports[0].items_print = [single_select_item_create_with_score.name] # type: ignore[index] + reports_data.reports[1].items_print = [single_select_item_create_with_score.name] # type: ignore[index] + score_conditional_logic.items_print = [single_select_item_create_with_score.name] + reports_data.reports[0].conditional_logic = [score_conditional_logic] # type: ignore[index] + reports_data.reports[1].conditional_logic = section_conditional_logic # type: ignore[index] + data.activities[0].items = [single_select_item_create_with_score] + data.activities[0].scores_and_reports = reports_data + resp = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) + assert resp.status_code == http.HTTPStatus.CREATED + result = resp.json()["result"] + assert result["activities"][0]["scoresAndReports"] == reports_data.dict(by_alias=True) diff --git a/src/apps/applets/tests/test_applet_folder.py b/src/apps/applets/tests/test_applet_folder.py index c7c7879601b..26a18f07ffc 100644 --- a/src/apps/applets/tests/test_applet_folder.py +++ b/src/apps/applets/tests/test_applet_folder.py @@ -1,9 +1,14 @@ import http import uuid +from sqlalchemy.ext.asyncio import AsyncSession + +from apps.applets.domain.applet_full import AppletFull from apps.folders.crud import FolderCRUD from apps.folders.errors import FolderDoesNotExist from apps.shared.test import BaseTest +from apps.shared.test.client import TestClient +from apps.users.domain import User class TestAppletMoveToFolder(BaseTest): @@ -14,7 +19,7 @@ class TestAppletMoveToFolder(BaseTest): set_folder_url = "applets/set_folder" folders_applet_url = "applets/folders/{id}" - async def test_move_to_folder(self, session, client, tom, applet_one): + async def test_move_to_folder(self, session: AsyncSession, client: TestClient, tom: User, applet_one: AppletFull): await client.login(self.login_url, tom.email_encrypted, "Test1234!") data = dict( applet_id=str(applet_one.id), @@ -31,7 +36,7 @@ async def test_move_to_folder(self, session, client, tom, applet_one): assert len(folders_ids) == 1 assert str(folders_ids[0]) == "ecf66358-a717-41a7-8027-807374307731" - async def test_invalid_applet_move_to_folder(self, client, tom, uuid_zero): + async def test_invalid_applet_move_to_folder(self, client: TestClient, tom: User, uuid_zero: uuid.UUID): await client.login(self.login_url, tom.email_encrypted, "Test1234!") data = dict( applet_id=str(uuid_zero), @@ -41,7 +46,7 @@ async def test_invalid_applet_move_to_folder(self, client, tom, uuid_zero): response = await client.post(self.set_folder_url, data) assert response.status_code == http.HTTPStatus.NOT_FOUND - async def test_move_to_not_accessible_folder(self, client, tom, applet_one): + async def test_move_to_not_accessible_folder(self, client: TestClient, tom: User, applet_one: AppletFull): await client.login(self.login_url, tom.email_encrypted, "Test1234!") data = dict( applet_id=str(applet_one.id), @@ -52,7 +57,7 @@ async def test_move_to_not_accessible_folder(self, client, tom, applet_one): assert response.status_code == http.HTTPStatus.FORBIDDEN assert response.json()["result"][0]["message"] == "Access denied to folder." - async def test_move_not_accessible_applet_to_folder(self, client, user, applet_one): + async def test_move_not_accessible_applet_to_folder(self, client: TestClient, user: User, applet_one: AppletFull): await client.login(self.login_url, user.email_encrypted, "Test1234!") data = dict( applet_id=str(applet_one.id), @@ -63,7 +68,9 @@ async def test_move_not_accessible_applet_to_folder(self, client, user, applet_o assert response.status_code == http.HTTPStatus.FORBIDDEN assert response.json()["result"][0]["message"] == "Access denied to edit applet in current workspace." - async def test_remove_from_folder(self, session, client, tom, applet_one): + async def test_remove_from_folder( + self, session: AsyncSession, client: TestClient, tom: User, applet_one: AppletFull + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") data = dict(applet_id=str(applet_one.id), folder_id=None) @@ -73,7 +80,9 @@ async def test_remove_from_folder(self, session, client, tom, applet_one): folders_id = await FolderCRUD(session).get_applets_folder_id_in_workspace(tom.id, applet_one.id) assert len(folders_id) == 0 - async def test_move_to_folder__folder_does_not_exists(self, client, tom, applet_one, uuid_zero): + async def test_move_to_folder__folder_does_not_exists( + self, client: TestClient, tom: User, applet_one: AppletFull, uuid_zero: uuid.UUID + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") data = dict( applet_id=str(applet_one.id), @@ -84,7 +93,9 @@ async def test_move_to_folder__folder_does_not_exists(self, client, tom, applet_ assert response.status_code == http.HTTPStatus.BAD_REQUEST assert response.json()["result"][0]["message"] == FolderDoesNotExist.message - async def test_move_to_folder_applet_already_moved(self, session, client, tom, applet_one): + async def test_move_to_folder_applet_already_moved( + self, session: AsyncSession, client: TestClient, tom: User, applet_one: AppletFull + ): await client.login(self.login_url, tom.email_encrypted, "Test1234!") data = dict( applet_id=str(applet_one.id), diff --git a/src/apps/applets/tests/test_applet_link.py b/src/apps/applets/tests/test_applet_link.py index 79035fb1e93..c5b06dea880 100644 --- a/src/apps/applets/tests/test_applet_link.py +++ b/src/apps/applets/tests/test_applet_link.py @@ -1,4 +1,6 @@ +from apps.applets.domain.applet_full import AppletFull from apps.shared.test import BaseTest +from apps.shared.test.client import TestClient from config import settings @@ -6,7 +8,7 @@ class TestLink(BaseTest): login_url = "/auth/login" access_link_url = "applets/{applet_id}/access_link" - async def test_applet_access_link_create_by_admin(self, client, applet_one): + async def test_applet_access_link_create_by_admin(self, client: TestClient, applet_one: AppletFull): await client.login(self.login_url, "tom@mindlogger.com", "Test1234!") data = {"require_login": True} @@ -30,7 +32,7 @@ async def test_applet_access_link_create_by_admin(self, client, applet_one): ) assert response.status_code == 400 - async def test_applet_access_link_create_by_manager(self, client, applet_one_lucy_manager): + async def test_applet_access_link_create_by_manager(self, client: TestClient, applet_one_lucy_manager: AppletFull): await client.login(self.login_url, "lucy@gmail.com", "Test123") data = {"require_login": True} @@ -42,7 +44,9 @@ async def test_applet_access_link_create_by_manager(self, client, applet_one_luc assert response.status_code == 201 assert isinstance(response.json()["result"]["link"], str) - async def test_applet_access_link_create_by_coordinator(self, client, applet_one_lucy_coordinator): + async def test_applet_access_link_create_by_coordinator( + self, client: TestClient, applet_one_lucy_coordinator: AppletFull + ): await client.login(self.login_url, "lucy@gmail.com", "Test123") data = {"require_login": True} @@ -54,7 +58,7 @@ async def test_applet_access_link_create_by_coordinator(self, client, applet_one assert response.status_code == 201 assert isinstance(response.json()["result"]["link"], str) - async def test_applet_access_link_get(self, client, applet_one_with_link): + async def test_applet_access_link_get(self, client: TestClient, applet_one_with_link: AppletFull): await client.login(self.login_url, "tom@mindlogger.com", "Test1234!") response = await client.get(self.access_link_url.format(applet_id=applet_one_with_link.id)) @@ -63,13 +67,13 @@ async def test_applet_access_link_get(self, client, applet_one_with_link): url_path = settings.service.urls.frontend.private_link assert response.json()["result"]["link"] == f"https://{domain}/{url_path}/{applet_one_with_link.link}" - async def test_wrong_applet_access_link_get(self, client): + async def test_wrong_applet_access_link_get(self, client: TestClient): await client.login(self.login_url, "tom@mindlogger.com", "Test1234!") response = await client.get(self.access_link_url.format(applet_id="00000000-0000-0000-0000-000000000000")) assert response.status_code == 404 - async def test_applet_access_link_delete(self, client, applet_one_with_link): + async def test_applet_access_link_delete(self, client: TestClient, applet_one_with_link: AppletFull): await client.login(self.login_url, "tom@mindlogger.com", "Test1234!") response = await client.delete(self.access_link_url.format(applet_id=applet_one_with_link.id)) @@ -78,7 +82,7 @@ async def test_applet_access_link_delete(self, client, applet_one_with_link): response = await client.get(self.access_link_url.format(applet_id=applet_one_with_link.id)) assert response.status_code == 404 - async def test_applet_access_link_create_for_anonym(self, client, applet_one): + async def test_applet_access_link_create_for_anonym(self, client: TestClient, applet_one: AppletFull): resp = await client.login(self.login_url, "tom@mindlogger.com", "Test1234!") applet_id = applet_one.id data = {"require_login": False} diff --git a/src/apps/applets/tests/test_applet_settings.py b/src/apps/applets/tests/test_applet_settings.py index 8e087cdd1b0..c99616063ef 100644 --- a/src/apps/applets/tests/test_applet_settings.py +++ b/src/apps/applets/tests/test_applet_settings.py @@ -1,6 +1,8 @@ import http +from apps.applets.domain.applet_full import AppletFull from apps.shared.test import BaseTest +from apps.shared.test.client import TestClient class TestSettings(BaseTest): @@ -8,7 +10,7 @@ class TestSettings(BaseTest): applet_url = "applets/{applet_id}" data_retention = applet_url + "/retentions" - async def test_applet_set_data_retention(self, client, applet_one): + async def test_applet_set_data_retention(self, client: TestClient, applet_one: AppletFull): await client.login(self.login_url, "tom@mindlogger.com", "Test1234!") retention_data = dict( @@ -27,7 +29,7 @@ async def test_applet_set_data_retention(self, client, applet_one): assert response.json()["result"]["retentionPeriod"] == retention_data["period"] assert response.json()["result"]["retentionType"] == retention_data["retention"] - async def test_applet_set_data_retention_for_indefinite(self, client, applet_one): + async def test_applet_set_data_retention_for_indefinite(self, client: TestClient, applet_one: AppletFull): await client.login(self.login_url, "tom@mindlogger.com", "Test1234!") retention_data = dict( @@ -45,7 +47,7 @@ async def test_applet_set_data_retention_for_indefinite(self, client, applet_one assert response.json()["result"]["retentionPeriod"] is None assert response.json()["result"]["retentionType"] == retention_data["retention"] - async def test_applet_set_data_retention_for_indefinite_fail(self, client, applet_one): + async def test_applet_set_data_retention_for_indefinite_fail(self, client: TestClient, applet_one: AppletFull): await client.login(self.login_url, "tom@mindlogger.com", "Test1234!") retention_data = dict( diff --git a/src/apps/shared/test/client.py b/src/apps/shared/test/client.py index 7610ee5853e..32dfd95d26f 100644 --- a/src/apps/shared/test/client.py +++ b/src/apps/shared/test/client.py @@ -1,21 +1,19 @@ import json import urllib.parse from io import BytesIO -from typing import Any, Mapping, Type, TypeVar +from typing import Any, Mapping from httpx import AsyncClient, Response from pydantic import BaseModel -T = TypeVar("T", bound=BaseModel) - class TestClient: - def __init__(self, app): + def __init__(self, app) -> None: self.client = AsyncClient(app=app, base_url="http://test.com") - self.headers = {} + self.headers: dict[str, Any] = {} @staticmethod - def _prepare_url(url, query): + def _prepare_url(url, query) -> str: return f"{url}?{urllib.parse.urlencode(query)}" def _get_updated_headers(self, headers: dict | None = None) -> dict: @@ -25,7 +23,7 @@ def _get_updated_headers(self, headers: dict | None = None) -> dict: return headers_ @staticmethod - def _get_body(data: dict[str, Any] | Type[T] | None = None): + def _get_body(data: dict[str, Any] | BaseModel | None = None) -> str | None: if data: if isinstance(data, BaseModel): request_data = data.dict() @@ -37,7 +35,7 @@ def _get_body(data: dict[str, Any] | Type[T] | None = None): async def post( self, url: str, - data: dict[str, Any] | Type[T] | None = None, + data: dict[str, Any] | BaseModel | None = None, query: dict | None = None, headers: dict | None = None, files: Mapping[str, BytesIO] | None = None, @@ -55,7 +53,7 @@ async def post( async def put( self, url: str, - data: dict | None = None, + data: dict | BaseModel | None = None, query: dict | None = None, headers: dict | None = None, ) -> Response: @@ -96,7 +94,7 @@ async def delete( ) return response - async def login(self, url: str, email: str | None, password: str, device_id: str | None = None): + async def login(self, url: str, email: str | None, password: str, device_id: str | None = None) -> Response: # Just make password option to shut up mypy error when User.email_encrypted passed as arument assert password is not None response = await self.post( @@ -113,5 +111,5 @@ async def login(self, url: str, email: str | None, password: str, device_id: str self.headers["Authorization"] = f"{token_type} {access_token}" return response - async def logout(self): + async def logout(self) -> None: self.headers = {} From 414bd7aeec71001dd92ff47c856e0b12cfda2270 Mon Sep 17 00:00:00 2001 From: Carlos Chacon Date: Thu, 29 Feb 2024 11:33:52 -0600 Subject: [PATCH 12/16] M2-5284: add missing LICENSE.md --- LICENSE.MD | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 LICENSE.MD diff --git a/LICENSE.MD b/LICENSE.MD new file mode 100644 index 00000000000..0dabe8b20ec --- /dev/null +++ b/LICENSE.MD @@ -0,0 +1,173 @@ +# Common Public Attribution License Version 1.0 (CPAL-1.0) + +Full text also available through the Open Source Initiative: https://opensource.org/licenses/cpal_1.0 + +## 1. "Definitions" + +1.0.1 "Commercial Use" means distribution or otherwise making the Covered Code available to a third party. + +1.1 "Contributor" means each entity that creates or contributes to the creation of Modifications. + +1.2 "Contributor Version" means the combination of the Original Code, prior Modifications used by a Contributor, and the Modifications made by that particular Contributor. + +1.3 "Covered Code" means the Original Code or Modifications or the combination of the Original Code and Modifications, in each case including portions thereof. + +1.4 "Electronic Distribution Mechanism" means a mechanism generally accepted in the software development community for the electronic transfer of data. + +1.5 "Executable" means Covered Code in any form other than Source Code. + +1.6 "Initial Developer" means the individual or entity identified as the Initial Developer in the Source Code notice required by Exhibit A. + +1.7 "Larger Work" means a work which combines Covered Code or portions thereof with code not governed by the terms of this License. + +1.8 "License" means this document. +1.8.1 "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently acquired, any and all of the rights conveyed herein. + +1.9 "Modifications" means any addition to or deletion from the substance or structure of either the Original Code or any previous Modifications. When Covered Code is released as a series of files, a Modification is: +A. Any addition to or deletion from the contents of a file containing Original Code or previous Modifications. +B. Any new file that contains any part of the Original Code or previous Modifications. + +1.10 "Original Code" means Source Code of computer software code which is described in the Source Code notice required by Exhibit A as Original Code, and which, at the time of its release under this License is not already Covered Code governed by this License. +1.10.1 "Patent Claims" means any patent claim(s), now owned or hereafter acquired, including without limitation, method, process, and apparatus claims, in any patent Licensable by grantor. + +1.11 "Source Code" means the preferred form of the Covered Code for making modifications to it, including all modules it contains, plus any associated interface definition files, scripts used to control compilation and installation of an Executable, or source code differential comparisons against either the Original Code or another well known, available Covered Code of the Contributor’s choice. The Source Code can be in a compressed or archival form, provided the appropriate decompression or de-archiving software is widely available for no charge. + +1.12 "You" (or "Your") means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License or a future version of this License issued under Section 6.1. For legal entities, "You" includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. + +## 2. Source Code License. + +2.1 The Initial Developer Grant. +The Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims: +(a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer to use, reproduce, modify, display, perform, sublicense and distribute the Original Code (or portions thereof) with or without Modifications, and/or as part of a Larger Work; and +(b) under Patents Claims infringed by the making, using or selling of Original Code, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Code (or portions thereof). +(c) the licenses granted in this Section 2.1(a) and (b) are effective on the date Initial Developer first distributes Original Code under the terms of this License. +(d) Notwithstanding Section 2.1(b) above, no patent license is granted: 1) for code that You delete from the Original Code; 2) separate from the Original Code; or 3) for infringements caused by: i) the modification of the Original Code or ii) the combination of the Original Code with other software or devices. + +2.2 Contributor Grant. +Subject to third party intellectual property claims, each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license +(a) under intellectual property rights (other than patent or trademark) Licensable by Contributor, to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof) either on an unmodified basis, with other Modifications, as Covered Code and/or as part of a Larger Work; and +(b) under Patent Claims infringed by the making, using, or selling of Modifications made by that Contributor either alone and/or in combination with its Contributor Version (or portions of such combination), to make, use, sell, offer for sale, have made, and/or otherwise dispose of: 1) Modifications made by that Contributor (or portions thereof); and 2) the combination of Modifications made by that Contributor with its Contributor Version (or portions of such combination). +(c) the licenses granted in Sections 2.2(a) and 2.2(b) are effective on the date Contributor first makes Commercial Use of the Covered Code. +(d) Notwithstanding Section 2.2(b) above, no patent license is granted: 1) for any code that Contributor has deleted from the Contributor Version; 2) separate from the Contributor Version; 3) for infringements caused by: i) third party modifications of Contributor Version or ii) the combination of Modifications made by that Contributor with other software (except as part of the Contributor Version) or other devices; or 4) under Patent Claims infringed by Covered Code in the absence of Modifications made by that Contributor. + +## 3. Distribution Obligations. + +3.1 Application of License. +The Modifications which You create or to which You contribute are governed by the terms of this License, including without limitation Section 2.2. The Source Code version of Covered Code may be distributed only under the terms of this License or a future version of this License released under Section 6.1, and You must include a copy of this License with every copy of the Source Code You distribute. You may not offer or impose any terms on any Source Code version that alters or restricts the applicable version of this License or the recipients’ rights hereunder. However, You may include an additional document offering the additional rights described in Section 3.5. + +3.2 Availability of Source Code. +Any Modification which You create or to which You contribute must be made available in Source Code form under the terms of this License either on the same media as an Executable version or via an accepted Electronic Distribution Mechanism to anyone to whom you made an Executable version available; and if made available via Electronic Distribution Mechanism, must remain available for at least twelve (12) months after the date it initially became available, or at least six (6) months after a subsequent version of that particular Modification has been made available to such recipients. You are responsible for ensuring that the Source Code version remains available even if the Electronic Distribution Mechanism is maintained by a third party. + +3.3 Description of Modifications. +You must cause all Covered Code to which You contribute to contain a file documenting the changes You made to create that Covered Code and the date of any change. You must include a prominent statement that the Modification is derived, directly or indirectly, from Original Code provided by the Initial Developer and including the name of the Initial Developer in (a) the Source Code, and (b) in any notice in an Executable version or related documentation in which You describe the origin or ownership of the Covered Code. + +3.4 Intellectual Property Matters +(a) Third Party Claims. +If Contributor has knowledge that a license under a third party’s intellectual property rights is required to exercise the rights granted by such Contributor under Sections 2.1 or 2.2, Contributor must include a text file with the Source Code distribution titled "LEGAL" which describes the claim and the party making the claim in sufficient detail that a recipient will know whom to contact. If Contributor obtains such knowledge after the Modification is made available as described in Section 3.2, Contributor shall promptly modify the LEGAL file in all copies Contributor makes available thereafter and shall take other steps (such as notifying appropriate mailing lists or newsgroups) reasonably calculated to inform those who received the Covered Code that new knowledge has been obtained. +(b) Contributor APIs. +If Contributor’s Modifications include an application programming interface and Contributor has knowledge of patent licenses which are reasonably necessary to implement that API, Contributor must also include this information in the LEGAL file. +(c) Representations. +Contributor represents that, except as disclosed pursuant to Section 3.4(a) above, Contributor believes that Contributor’s Modifications are Contributor’s original creation(s) and/or Contributor has sufficient rights to grant the rights conveyed by this License. + +3.5 Required Notices. +You must duplicate the notice in Exhibit A in each file of the Source Code. If it is not possible to put such notice in a particular Source Code file due to its structure, then You must include such notice in a location (such as a relevant directory) where a user would be likely to look for such a notice. If You created one or more Modification(s) You may add your name as a Contributor to the notice described in Exhibit A. You must also duplicate this License in any documentation for the Source Code where You describe recipients’ rights or ownership rights relating to Covered Code. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Code. However, You may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear than any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer. + +3.6 Distribution of Executable Versions. +You may distribute Covered Code in Executable form only if the requirements of Section 3.1-3.5 have been met for that Covered Code, and if You include a notice stating that the Source Code version of the Covered Code is available under the terms of this License, including a description of how and where You have fulfilled the obligations of Section 3.2. The notice must be conspicuously included in any notice in an Executable version, related documentation or collateral in which You describe recipients’ rights relating to the Covered Code. You may distribute the Executable version of Covered Code or ownership rights under a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable version does not attempt to limit or alter the recipient’s rights in the Source Code version from the rights set forth in this License. If You distribute the Executable version under a different license You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer, Original Developer or any Contributor. You hereby agree to indemnify the Initial Developer, Original Developer and every Contributor for any liability incurred by the Initial Developer, Original Developer or such Contributor as a result of any such terms You offer. + +3.7 Larger Works. +You may create a Larger Work by combining Covered Code with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Code. + +## 4. Inability to Comply Due to Statute or Regulation. + +If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Code due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be included in the LEGAL file described in Section 3.4 and must be included with all distributions of the Source Code. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. + +## 5. Application of this License. + +This License applies to code to which the Initial Developer has attached the notice in Exhibit A and to related Covered Code. + +## 6. Versions of the License. + +6.1 New Versions. +Socialtext, Inc. ("Socialtext") may publish revised and/or new versions of the License from time to time. Each version will be given a distinguishing version number. + +6.2 Effect of New Versions. +Once Covered Code has been published under a particular version of the License, You may always continue to use it under the terms of that version. You may also choose to use such Covered Code under the terms of any subsequent version of the License published by Socialtext. No one other than Socialtext has the right to modify the terms applicable to Covered Code created under this License. + +6.3 Derivative Works. +If You create or use a modified version of this License (which you may only do in order to apply it to code which is not already Covered Code governed by this License), You must (a) rename Your license so that the phrases "Socialtext", "CPAL" or any confusingly similar phrase do not appear in your license (except to note that your license differs from this License) and (b) otherwise make it clear that Your version of the license contains terms which differ from the CPAL. (Filling in the name of the Initial Developer, Original Developer, Original Code or Contributor in the notice described in Exhibit A shall not of themselves be deemed to be modifications of this License.) + +## 7. DISCLAIMER OF WARRANTY. + +COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER, ORIGINAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. + +## 8. TERMINATION. + +8.1 This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. All sublicenses to the Covered Code which are properly granted shall survive any termination of this License. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive. + +8.2 If You initiate litigation by asserting a patent infringement claim (excluding declatory judgment actions) against Initial Developer, Original Developer or a Contributor (the Initial Developer, Original Developer or Contributor against whom You file such action is referred to as "Participant") alleging that: +(a) such Participant’s Contributor Version directly or indirectly infringes any patent, then any and all rights granted by such Participant to You under Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice from Participant terminate prospectively, unless if within 60 days after receipt of notice You either: (i) agree in writing to pay Participant a mutually agreeable reasonable royalty for Your past and future use of Modifications made by such Participant, or (ii) withdraw Your litigation claim with respect to the Contributor Version against such Participant. If within 60 days of notice, a reasonable royalty and payment arrangement are not mutually agreed upon in writing by the parties or the litigation claim is not withdrawn, the rights granted by Participant to You under Sections 2.1 and/or 2.2 automatically terminate at the expiration of the 60 day notice period specified above. +(b) any software, hardware, or device, other than such Participant’s Contributor Version, directly or indirectly infringes any patent, then any rights granted to You by such Participant under Sections 2.1(b) and 2.2(b) are revoked effective as of the date You first made, used, sold, distributed, or had made, Modifications made by that Participant. + +8.3 If You assert a patent infringement claim against Participant alleging that such Participant’s Contributor Version directly or indirectly infringes any patent where such claim is resolved (such as by license or settlement) prior to the initiation of patent infringement litigation, then the reasonable value of the licenses granted by such Participant under Sections 2.1 or 2.2 shall be taken into account in determining the amount or value of any payment or license. + +8.4 In the event of termination under Sections 8.1 or 8.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or any distributor hereunder prior to termination shall survive termination. + +## 9. LIMITATION OF LIABILITY. + +UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ORIGINAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY’S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. + +## 10. U.S. GOVERNMENT END USERS. + +The Covered Code is a "commercial item," as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer software" and "commercial computer software documentation," as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Code with only those rights set forth herein. + +## 11. MISCELLANEOUS. + +This License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by California law provisions (except to the extent applicable law, if any, provides otherwise), excluding its conflict-of-law provisions. With respect to disputes in which at least one party is a citizen of, or an entity chartered or registered to do business in the United States of America, any litigation relating to this License shall be subject to the jurisdiction of the Federal Courts of the Northern District of California, with venue lying in Santa Clara County, California, with the losing party responsible for costs, including without limitation, court costs and reasonable attorneys’ fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License. + +## 12. RESPONSIBILITY FOR CLAIMS. + +As between Initial Developer, Original Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer, Original Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability. + +## 13. MULTIPLE-LICENSED CODE. + +Initial Developer may designate portions of the Covered Code as Multiple-Licensed. Multiple-Licensed means that the Initial Developer permits you to utilize portions of the Covered Code under Your choice of the CPAL or the alternative licenses, if any, specified by the Initial Developer in the file described in Exhibit A. + +## 14. ADDITIONAL TERM: ATTRIBUTION + +(a) As a modest attribution to the organizer of the development of the Original Code ("Original Developer"), in the hope that its promotional value may help justify the time, money and effort invested in writing the Original Code, the Original Developer may include in Exhibit B ("Attribution Information") a requirement that each time an Executable and Source Code or a Larger Work is launched or initially run (which includes initiating a session), a prominent display of the Original Developer’s Attribution Information (as defined below) must occur on the graphic user interface employed by the end user to +access such Covered Code (which may include display on a splash screen), if any. The size of the graphic image should be consistent with the size of the other elements of the Attribution Information. If the access by the end user to the Executable and Source Code does not create a graphic user interface for access to the Covered Code, this obligation shall not apply. If the Original Code displays such Attribution Information in a particular form (such as in the form of a splash screen, notice at login, an "about" display, or dedicated attribution area on user interface screens), continued use of such form for +that Attribution Information is one way of meeting this requirement for notice. + +(b) Attribution information may only include a copyright notice, a brief phrase, graphic image and a URL ("Attribution Information") and is subject to the Attribution Limits as defined below. For these purposes, prominent shall mean display for sufficient duration to give reasonable notice to the user of the identity of the Original Developer and that if You include Attribution Information or similar information for other parties, You must ensure that the Attribution Information for the Original Developer shall be no less prominent than such Attribution Information or similar information for the other party. For greater certainty, the Original Developer may choose to specify in Exhibit B below that the above attribution requirement only applies to an Executable and Source Code resulting from the Original Code or any Modification, but not a Larger Work. The intent is to provide for reasonably modest attribution, therefore the Original Developer cannot require that You display, at any time, more than the following information as Attribution Information: (a) a copyright notice including the name of the Original Developer; (b) a word or one phrase +(not exceeding 10 words); (c) one graphic image provided by the Original Developer; and (d) a URL (collectively, the "Attribution Limits"). + +(c) If Exhibit B does not include any Attribution Information, then there are no requirements for You to display any Attribution Information of the Original Developer. + +(d) You acknowledge that all trademarks, service marks and/or trade names contained within the Attribution Information distributed with the Covered Code are the exclusive property of their owners and may only be used with the permission of their owners, or under circumstances otherwise permitted by law or as expressly set out in this License. + +## 15. ADDITIONAL TERM: NETWORK USE. + +The term "External Deployment" means the use, distribution, or communication of the Original Code or Modifications in any way such that the Original Code or Modifications may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Code or Modifications as a distribution under section 3.1 and make Source Code available under Section 3.2. + +## EXHIBIT A. Common Public Attribution License Version 1.0. + +The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://github.com/ChildMindInstitute/mindlogger-backend-refactor/blob/master/LICENSE.md. + +The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 have been added to cover use of software over a computer network and provide for limited attribution for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License. The Original Code is https://github.com/ChildMindInstitute/mindlogger-backend-refactor. + +The Original Developer is the Initial Developer. The Initial Developer of the Original Code is the MATTER Lab at the Child Mind Institute. All portions of the code written by the MATTER Lab at the Child Mind Institute are Copyright (c) 2017. All Rights Reserved. Contributor the MATTER Lab at the Child Mind Institute. + +[NOTE: The text of this Exhibit A may differ slightly from the text of the notices in the Source Code files of the Original Code. You should use the text of this Exhibit A rather than the text found in the Original Code Source Code for Your Modifications.] + +## EXHIBIT B. Attribution Information + +Attribution Copyright Notice: Copyright (c) 2017 MATTER Lab at the Child Mind Institute + +Attribution Phrase: Child Mind Institute product intended for building applications for good. + +Attribution URL: https://matter.childmind.org + +Graphic Image as provided in the Covered Code: + +Display of Attribution Information is required in Larger Works which are defined in the CPAL as a work which combines Covered Code or portions thereof with code not governed by the terms of the CPAL. From 4bb38db05189d943513098d67374efb7f4430ba5 Mon Sep 17 00:00:00 2001 From: Carlos Chacon Date: Thu, 29 Feb 2024 12:08:52 -0600 Subject: [PATCH 13/16] M2-5284: Add detail to README to run locally --- README.md | 308 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 210 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index a4c96d897f0..6872c6c90fa 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,197 @@ -# ChildMindInstitute - Mindlogger Backend API +# Child Mind Institute - MindLogger Backend API -This repository is used as a backend for the service MindLogger +This repository is used for the backend of the [MindLogger](https://mindlogger.org/) application stack. -## 1. Getting Started / Installation +## Getting Started -### 1.1 Create `.env` file for future needs +* MindLogger Admin - [GitHub Repo](https://github.com/ChildMindInstitute/mindlogger-admin) +* MindLogger Backend - **This Repo** +* MindLogger Mobile App - [GitHub Repo](https://github.com/ChildMindInstitute/mindlogger-app-refactor) +* MindLogger Web App - [GitHub Repo](https://github.com/ChildMindInstitute/mindlogger-web-refactor) -It is highly recommended to create an `.env` file as far as it is needed for setting up the project with Local and Docker approaches. + +## Contents +- [Features](#features) +- [Technologies](#technologies) +- [Application](#application-stack) + - [Prerequisites](#prerequisites) + - [Environment Variables](#environment-variables) +- [Installation](#installation) +- [Running the app](#running-the-app) + - [Running locally](#running-locally) + - [Running via docker](#running-via-docker) + - [Running using Makefile](#running-using-makefile) + - [Docker development](#docker-development) +- [Testing](#testing) +- [Scripts](#scripts) +- [Arbitrary setup](#arbitrary-setup) +- [License](#license) + +## Features + +See MindLogger's [Knowledge Base article](https://mindlogger.atlassian.net/servicedesk/customer/portal/3/topic/4d9a9ad4-c663-443b-b7fc-be9faf5d9383/article/337444910) to discover the MindLogger application stack's features. + +## Technologies + +- ✅ [Python3.10+](https://www.python.org/downloads/release/python-3108/) +- ✅ [Pipenv](https://pipenv.pypa.io/en/latest/) +- ✅ [FastAPI](https://fastapi.tiangolo.com) +- ✅ [Postgresql](https://www.postgresql.org/docs/14/index.html) +- ✅ [Redis](https://redis.io) +- ✅ [Docker](https://docs.docker.com/get-docker/) +- ✅ [Pydantic](https://pydantic-docs.helpmanual.io) +- ✅ [SQLAlchemy](https://www.sqlalchemy.org/) + +And + +- ✅ [The 12-Factor App](https://12factor.net) + +**Code quality tools:** + +- ✅ [ruff](https://github.com/astral-sh/ruff) +- ✅ [isort](https://github.com/PyCQA/isort) +- ✅ [mypy](https://github.com/python/mypy) +- ✅ [pytest](https://github.com/pytest-dev/pytest) + +## Application + +### Prerequisites + +- Python 3.10 - This project requires Python 3.10 as `aioredis` is [incompatible with 3.11+](https://github.com/aio-libs-abandoned/aioredis-py/issues/1409) +- [Docker](https://docs.docker.com/get-docker/) + +#### Recommended Extras + +Installing [pyenv](https://github.com/pyenv/pyenv) is recommended to automatically manage Python version in the virtual environment specified in the `Pipfile` + +Alternatively, on macOS you can use a tool like [Homebrew](https://brew.sh/) to install multiple versions and specify when creating the virtual environment: +```bash +pipenv --python /opt/homebrew/bin/python3.10 +``` + +### Environment Variables + +| Key | Default value | Description | +| ----------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | +| DATABASE\_\_HOST | postgres | Database Host | +| DATABASE\_\_USER | postgres | User name for Postgresql Database user | +| DATABASE\_\_PASSWORD | postgres | Password for Postgresql Database user | +| DATABASE\_\_DB | mindlogger_backend | Database name | +| CORS\_\_ALLOW\_ORIGINS | `*` | Represents the list of allowed origins. Set the `Access-Control-Allow-Origin` header. Example: `https://dev.com,http://localohst:8000` | +| CORS\_\_ALLOW\_ORIGINS\_REGEX | - | Regex pattern of allowed origins. | +| CORS\_\_ALLOW\_CREDENTIALS | true | Set the `Access-Control-Allow-Credentials` header | +| CORS\_\_ALLOW_METHODS | `*` | Set the `Access-Control-Allow-Methods` header | +| CORS\_\_ALLOW_HEADERS | `*` | Set the `Access-Control-Allow-Headers` header | +| AUTHENTICATION\_\_ACCESS\_TOKEN\_\_SECRET\_KEY | secret1 | Access token's salt | +| AUTHENTICATION\_\_REFRESH\_TOKEN\_\_SECRET\_KEY | secret2 | Refresh token salt | +| AUTHENTICATION\_\_ALGORITHM | HS256 | The JWT's algorithm | +| AUTHENTICATION\_\_ACCESS\_TOKEN\_\_EXPIRATION | 30 | Time in minutes after which the access token will stop working | +| AUTHENTICATION\_\_REFRESH\_TOKEN\_\_EXPIRATION | 30 | Time in minutes after which the refresh token will stop working | +| ADMIN_DOMAIN | - | Admin panel domain | +| RABBITMQ\_\_URL | rabbitmq | Rabbitmq service URL +| RABBITMQ\_\_USE_SSL | True | Rabbitmq ssl setting, turn false to local development +| MAILING\_\_MAIL\_\_USERNAME | mailhog | Mail service username +| MAILING\_\_MAIL\_\_PASSWORD | mailhog | Mail service password +| MAILING\_\_MAIL\_\_SERVER | mailhog | Mail service URL + +##### ✋ Mandatory: + +> You can see that some environment variables have double underscore (`__`) instead of `_`. +> +> As far as `pydantic` supports [nested settings models](https://pydantic-docs.helpmanual.io/usage/settings/) it uses to have cleaner code + +## Installation + +### Create `.env` file for future needs + +It is highly recommended to create an `.env` file as far as it is needed for setting up the project with Local and Docker approaches. +Use `.env.default` to get started:\ ```bash cp .env.default .env ``` -### 1.2 Generate secret keys, update .env with values +> 🛑 **NOTE:** Make sure to set `RABBITMQ__USE_SSL=False` for local development + +### Generate secret keys, update .env with values ```bash openssl rand -hex 32 ``` -### 1.3 Setup Redis +Generate a key and update `.env` values: -#### 1.3.1 Locally +* `AUTHENTICATION__ACCESS_TOKEN__SECRET_KEY` +* `AUTHENTICATION__REFRESH_TOKEN__SECRET_KEY` -✅ [🐧 Linux](https://redis.io/docs/getting-started/installation/install-redis-on-linux/) +### Required Services -✅ [ MacOs](https://redis.io/docs/getting-started/installation/install-redis-on-mac-os/) +- Postgres +- Redis +- RabbitMQ +- Mailhog - Only used for running mail services locally -#### 1.3.2 Install via Docker +Running required services using Docker is **highly** recommended even if you intend to run the app locally. -```bash -docker-compose up -d redis -``` +> 🛑 **NOTE:** Make sure to update your environment variables to point to the correct hostname and port for each service. -### 1.4. Install all project dependencies +#### Run services using Docker -Pipenv used as a default dependencies manager +- Run Postgres + ```bash + docker-compose up -d postgres + ``` + +- Run Redis + ```bash + docker-compose up -d redis + ``` + +- Run RabbitMQ + ```bash + docker-compose up -d rabbitmq + ``` + +- Alternatively, you can run all required services: + ```bash + docker-compose up + ``` +#### Run services manually + +For manual installation refer to each service's documentation: + +- [PostgreSQL Downloads](https://www.postgresql.org/download/) +- [Redis: Install Redis](https://redis.io/docs/install/install-redis/) +- [RabbitMQ documentation](https://rabbitmq-website.pages.dev/docs/download) + + +### Install all project dependencies + +Pipenv used as a default dependencies manager +Create your virtual environment: ```bash # Activate your environment pipenv shell +``` + +If `pyenv` is installed Python 3.10 should automatically be installed in the virtual environment, you can check the correct version of Python is active by running: +```bash +python --version +``` + +If the active version is **not** 3.10, you can manually specify a version while creating your virtual environment: +```bash +pipenv --python /opt/homebrew/bin/python3.10 +``` +Install all dependencies +```bash # Install all deps from Pipfile.lock # to install venv to current directory use `export PIPENV_VENV_IN_PROJECT=1` pipenv sync --dev ``` -
- > 🛑 **NOTE:** if you don't use `pipenv` for some reason remember that you will not have automatically exported variables from your `.env` file. > > 🔗 [Pipenv docs](https://docs.pipenv.org/advanced/#automatic-loading-of-env) @@ -68,11 +213,23 @@ set -o allexport; source .env; set +o allexport > 🛑 **NOTE:** Please do not forget about environment variables! Now all environment variables for the Postgres Database which runs in docker are already passed to docker-compose.yaml from the .env file. -
+## Running the app + +### Running locally -## 2. Usage +This option allows you to run the app for development purposes without having to manually build the Docker image. + +- Make sure all [required services](#required-services) are properly setup +- If you're running required services using Docker, disable the `app` service from `docker-compose` before running: + ```bash + docker-compose up -d + ``` + + Alternatively, you may run these services using [make](#running-using-makefile): + ```bash + make run_local + ``` -### 2.1 Running locally > 🛑 **NOTE:** Don't forget to set the `PYTHONPATH` environment variable, e.g: export PYTHONPATH=src/ @@ -86,8 +243,14 @@ P.S. You don't need to do this additional step if you run application via Docker uvicorn src.main:app --proxy-headers --port {PORT} --reload ``` -### 2.2 Running via docker +Alternatively, you may run the application using [make](#running-using-makefile): +```bash +make run +``` +### Running via docker +- [Build the application](#build-application-images) +- Run the app using Docker: ```bash docker-compose up ``` @@ -99,7 +262,7 @@ Additional `docker-compose up` flags that might be useful for development --no-recreate # If containers already exist, don't recreate them ``` -#### 2.2.1 Stop the application 🛑 +#### Stop the application 🛑 ```bash docker-compose down @@ -110,7 +273,7 @@ Additional `docker-compose down` flags that might be useful for development ```bash -v # Remove with all volumes ``` -### 2.3 Running using Makefile +### Running using Makefile You can use the `Makefile` to work with project (run the application / code quality tools / tests ...) @@ -129,9 +292,9 @@ make test # Check everything in one hop make check ``` -### 2.4 Docker development +### Docker development -#### 2.4.1. Build application images +#### Build application images ```bash docker-compose build @@ -143,60 +306,7 @@ docker-compose build 💡 If you would like to debug the application insode Docker comtainer make sure that you use `COMPOSE_FILE=docker-compose.dev.yaml` in `.env`. It has opened stdin and tty. - -## 3 Build With - -- ✅ [Python3.10+](https://www.python.org/downloads/release/python-3108/) -- ✅ [Pipenv](https://pipenv.pypa.io/en/latest/) -- ✅ [FastAPI](https://fastapi.tiangolo.com) -- ✅ [Postgresql](https://www.postgresql.org/docs/14/index.html) -- ✅ [Redis](https://redis.io) -- ✅ [Docker](https://docs.docker.com/get-docker/) -- ✅ [Pydantic](https://pydantic-docs.helpmanual.io) -- ✅ [SQLAlchemy](https://www.sqlalchemy.org/) - -And - -- ✅ [The 12-Factor App](https://12factor.net) - -
- -**Code quality tools:** - -- ✅ [ruff](https://github.com/astral-sh/ruff) -- ✅ [isort](https://github.com/PyCQA/isort) -- ✅ [mypy](https://github.com/python/mypy) -- ✅ [pytest](https://github.com/pytest-dev/pytest) - -
- -## 4. Environment Variables - -| Key | Default value | Description | -| ----------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | -| DATABASE\_\_HOST | postgres | Database Host | -| DATABASE\_\_USER | postgres | User name for Postgresql Database user | -| DATABASE\_\_PASSWORD | postgres | Password for Postgresql Database user | -| DATABASE\_\_DB | mindlogger_backend | Database name | -| CORS\_\_ALLOW_ORIGINS | `*` | Represents the list of allowed origins. Set the `Access-Control-Allow-Origin` header. Example: `https://dev.com,http://localohst:8000` | -| CORS__ALLOW_ORIGINS_REGEX | - | Regex pattern of allowed origins. | -| CORS\_\_ALLOW_CREDENTIALS | true | Set the `Access-Control-Allow-Credentials` header | -| CORS\_\_ALLOW_METHODS | `*` | Set the `Access-Control-Allow-Methods` header | -| CORS\_\_ALLOW_HEADERS | `*` | Set the `Access-Control-Allow-Headers` header | -| AUTHENTICATION**ACCESS_TOKEN**SECRET_KEY | secret1 | Access token's salt | -| AUTHENTICATION**REFRESH_TOKEN**SECRET_KEY | secret2 | Refresh token salt | -| AUTHENTICATION\_\_ALGORITHM | HS256 | The JWT's algorithm | -| AUTHENTICATION**ACCESS_TOKEN**EXPIRATION | 30 | Time in minutes after which the access token will stop working | -| AUTHENTICATION**REFRESH_TOKEN**EXPIRATION | 30 | Time in minutes after which the refresh token will stop working | -| ADMIN_DOMAIN | - | Admin panel domain | -| RABBITMQ__USE_SSL | True | Rabbitmq ssl setting, turn false to local development -##### ✋ Mandatory: - -> You can see that some environment variables have double underscore (`__`) instead of `_`. -> -> As far as `pydantic` supports [nested settings models](https://pydantic-docs.helpmanual.io/usage/settings/) it uses to have cleaner code - -## 5. Testing +## Testing The `pytest` framework is using in order to write unit tests. Currently postgresql is used as a database for tests with running configurations that are defined in `pyproject.toml` @@ -215,7 +325,7 @@ DATABASE__DB=test 127.0.0.1 postgres ``` -### 5.1. Adjust your database for using with tests +### Adjust your database for using with tests ⚠️️ Remember that you have to do this only once before the first test. @@ -240,7 +350,7 @@ psql# create user test; psql# alter user test with password 'test'; ``` -### 5.2. Test coverage +### Test coverage To correctly calculate test coverage, you need to run the coverage with the `--concurrency=thread,gevent` parameter: @@ -249,7 +359,7 @@ coverage run --concurrency=thread,gevent -m pytest coverage report -m ``` -### 5.3. Running test via docker +### Running test via docker (This is how tests are running on CI) @@ -264,9 +374,9 @@ make dtest make dcheck ``` -## 6. Scripts +## Scripts -### 6.1 Using pre-commit hooks +### Using pre-commit hooks It is a good practice to use Git hooks to provide better commits. @@ -284,21 +394,21 @@ make aws-scan 👉 Then all your staged cahnges will be checked via git hooks on every `git commit` -### 6.2 Alembic (migration) +### Alembic (migration) -#### 6.2.1 Add a new migrations file 🔨 +#### Add a new migrations file 🔨 ```bash alembic revision --autogenerate -m "Add a new field" ``` -#### 6.2.2. Upgrade to the latest migration 🔨 +#### Upgrade to the latest migration 🔨 ```bash alembic upgrade head ``` -#### 6.2.3. Downgrade to the specific one 🔨 +#### Downgrade to the specific one 🔨 ```bash alembic downgrade 0e43c346b90d @@ -306,13 +416,13 @@ alembic downgrade 0e43c346b90d ✅ This hash is taken from the generated file in the migrations folder -#### 6.2.3. Downgrade to the specific one 🔨 +#### Downgrade to the specific one 🔨 ```bash alembic downgrade 0e43c346b90d ``` -#### 6.2.4. Removing the migration 🔨 +#### Removing the migration 🔨 💡 Do not forget that alembic saves the migration version into the database. @@ -320,13 +430,13 @@ alembic downgrade 0e43c346b90d delete from alembic_version; ``` -#### 6.2.5. Upgrade arbitrary servers +#### Upgrade arbitrary servers ```bash alembic -c alembic_arbitrary.ini upgrade head ``` -#### 6.2.6. Database relation structure +#### Database relation structure ```mermaid @@ -579,11 +689,11 @@ Flow_histories }o--|| Applet_histories: "" ``` -## 7. Arbitrary setup +## Arbitrary setup You can connect arbitrary file storage and database by filling special fields in table `user_workspaces`. -### 7.1. PostgreSQL +### PostgreSQL Add your database connection string into `database_uri` In next format: @@ -592,14 +702,16 @@ In next format: postgresql+asyncpg://:@:port/database ``` -### 7.2. AWS S3 and GCP S3 +### AWS S3 and GCP S3 For AWS S3 bucket next fields are required: `storage_region`,`storage_bucket`, `storage_access_key`,`storage_secret_key`. -### 7.3. Azure Blob +### Azure Blob In case of Azure blob, specify your connection string into field `storage_secret_key` +## License +Common Public Attribution License Version 1.0 (CPAL-1.0) -## 8. License \ No newline at end of file +Refer to [LICENSE.md](./LICENSE.MD) \ No newline at end of file From abb55a1094e7fc8af25705e4e6ba22175eb0d831 Mon Sep 17 00:00:00 2001 From: Carlos Chacon Date: Thu, 29 Feb 2024 12:25:13 -0600 Subject: [PATCH 14/16] M2-5284: Add missing env keys to .env.default --- .env.default | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.default b/.env.default index 74800fc2ca1..0bb1396e63a 100644 --- a/.env.default +++ b/.env.default @@ -84,5 +84,6 @@ MONGO__AES_KEY= # RabbitMq # Uncommnent for local development # RABBITMQ__USE_SSL=False +RABBITMQ__URL=rabbitmq From 5d57d1cf83473b3f45c631304fc4fdeae03b6c1a Mon Sep 17 00:00:00 2001 From: Marty Date: Fri, 1 Mar 2024 06:58:30 -0800 Subject: [PATCH 15/16] M2-5512: update pr template (#1142) Co-authored-by: Marty <2614025+mbanting@users.noreply.github.com> --- .github/pull_request_template.md | 53 +++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 45be98d364f..d41872af8be 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,7 +1,52 @@ -resolves: M2-#### + + + -### Objective + -### Notes +- [ ] Tests for the changes have been added +- [ ] Related documentation has been added / updated +- [ ] OSS packages added to MindLogger [open source credit page](https://mindlogger.atlassian.net/jira/servicedesk/projects/MLA/knowledge/articles/340623543?spaceKey=MLA) -### Follow up tasks +### 📝 Description + + + + +🔗 [Jira Ticket M2-#](https://mindlogger.atlassian.net/browse/M2-#) + + + + + + +Changes include: + +- [Thing] +- [Other thing] +- [More things] + +### 🪤 Peer Testing + + + + + + + + +### ✏️ Notes + + From 813bf7887095650a0ccda3abc52fc9aa4c8a018a Mon Sep 17 00:00:00 2001 From: Kenroy Gobourne Date: Mon, 4 Mar 2024 12:59:46 -0500 Subject: [PATCH 16/16] M2-5595: Error message appears when trying to reset password (#1146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 📝 Description 🔗 [Jira Ticket M2-5595](https://mindlogger.atlassian.net/browse/M2-5595) This PR adds support for the mobile value of the `MindLogger-Content-Source` HTTP header. The PR https://github.com/ChildMindInstitute/mindlogger-backend-refactor/pull/1100 started making use of this HTTP header, but didn't account for the mobile value so this fixes that. I've also added a test case for it --- src/apps/users/tests/test_password.py | 66 +++++++++++++++++++++++++++ src/infrastructure/http/deps.py | 11 +++-- src/infrastructure/http/domain.py | 1 + 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/apps/users/tests/test_password.py b/src/apps/users/tests/test_password.py index ffbc25b17c2..b0105125c78 100644 --- a/src/apps/users/tests/test_password.py +++ b/src/apps/users/tests/test_password.py @@ -130,6 +130,72 @@ async def test_password_recovery_admin(self, client: TestClient, user_create: Us assert len(TestMail.mails) == 4 assert TestMail.mails[0].recipients[0] == password_recovery_request.email + async def test_password_recovery_mobile(self, client: TestClient, user_create: UserCreate): + # Password recovery + password_recovery_request: PasswordRecoveryRequest = PasswordRecoveryRequest(email=user_create.dict()["email"]) + + response = await client.post( + url=self.password_recovery_url, + data=password_recovery_request.dict(), + headers={"MindLogger-Content-Source": "mobile"}, + ) + + cache = RedisCache() + + assert response.status_code == status.HTTP_201_CREATED + keys = await cache.keys(key=f"PasswordRecoveryCache:{user_create.email}*") + assert len(keys) == 1 + assert password_recovery_request.email in keys[0] + assert len(TestMail.mails) == 5 + assert TestMail.mails[0].recipients[0] == password_recovery_request.email + assert TestMail.mails[0].subject == "MindLogger" + + response = await client.post( + url=self.password_recovery_url, + data=password_recovery_request.dict(), + ) + + assert response.status_code == status.HTTP_201_CREATED + + new_keys = await cache.keys(key=f"PasswordRecoveryCache:{user_create.email}*") + assert len(keys) == 1 + assert keys[0] != new_keys[0] + assert len(TestMail.mails) == 6 + assert TestMail.mails[0].recipients[0] == password_recovery_request.email + + async def test_password_recovery_invalid(self, client: TestClient, user_create: UserCreate): + # Password recovery + password_recovery_request: PasswordRecoveryRequest = PasswordRecoveryRequest(email=user_create.dict()["email"]) + + response = await client.post( + url=self.password_recovery_url, + data=password_recovery_request.dict(), + headers={"MindLogger-Content-Source": "invalid-content-source"}, + ) + + cache = RedisCache() + + assert response.status_code == status.HTTP_201_CREATED + keys = await cache.keys(key=f"PasswordRecoveryCache:{user_create.email}*") + assert len(keys) == 1 + assert password_recovery_request.email in keys[0] + assert len(TestMail.mails) == 7 + assert TestMail.mails[0].recipients[0] == password_recovery_request.email + assert TestMail.mails[0].subject == "MindLogger" + + response = await client.post( + url=self.password_recovery_url, + data=password_recovery_request.dict(), + ) + + assert response.status_code == status.HTTP_201_CREATED + + new_keys = await cache.keys(key=f"PasswordRecoveryCache:{user_create.email}*") + assert len(keys) == 1 + assert keys[0] != new_keys[0] + assert len(TestMail.mails) == 8 + assert TestMail.mails[0].recipients[0] == password_recovery_request.email + async def test_password_recovery_approve(self, client: TestClient, user_create: UserCreate): cache = RedisCache() diff --git a/src/infrastructure/http/deps.py b/src/infrastructure/http/deps.py index eb13befb07c..284cb0bc605 100644 --- a/src/infrastructure/http/deps.py +++ b/src/infrastructure/http/deps.py @@ -8,10 +8,13 @@ async def get_mindlogger_content_source( ) -> MindloggerContentSource: """Fetch the Mindlogger-Content-Source HTTP header.""" - return getattr( - MindloggerContentSource, - request.headers.get("mindlogger-content-source", MindloggerContentSource.web.name), - ) + try: + return getattr( + MindloggerContentSource, + request.headers.get("mindlogger-content-source", MindloggerContentSource.web.name), + ) + except AttributeError: + return MindloggerContentSource.web def get_language(request: Request) -> str: diff --git a/src/infrastructure/http/domain.py b/src/infrastructure/http/domain.py index b8a70c69c1e..a8b3741fa6e 100644 --- a/src/infrastructure/http/domain.py +++ b/src/infrastructure/http/domain.py @@ -6,3 +6,4 @@ class MindloggerContentSource(str, Enum): web = "web" admin = "admin" + mobile = "mobile"
- Hello {{receiver_name}}! + Hello and Welcome to MindLogger!
Create - your account and get started + style="padding: 0.4rem 1rem; font-size: 0.8rem; line-height: 1.5; border-radius: 100px; display: inline-block; font-weight: 400; text-align: center; vertical-align: middle; text-decoration: none; color: #ffffff; background-color: #00639A; border-color: #00639A;">Get + started
Créez - votre compte et commencez + style="padding: 0.4rem 1rem; font-size: 0.8rem; line-height: 1.5; border-radius: 100px; display: inline-block; font-weight: 400; text-align: center; vertical-align: middle; text-decoration: none; color: #ffffff; background-color: #00639A; border-color: #00639A;">Commencer