diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 990ebef171ab5a0d5ab3bccc12f0396ae8187467..5ce9f300f766ce4d709cfd3dee9b62c251622fca 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -23,9 +23,17 @@
 
 image: docker:24
 
-build:
+
+stages:
+  - "📦 build"
+  - "🩺 test"
+  - "🚀 deploy"
+  - "🔧 configure"
+
+
+"🐳 build":
   needs: []
-  stage: build
+  stage: "📦 build"
   variables:
     RUN_TEST: "1"
     POSTGRES_HOST: "db-test"
@@ -82,9 +90,9 @@ build:
 
 
 
-test-file-perms:
-  stage: test
-  needs: ["build"]
+"🩺 test-file-perms":
+  stage: "🩺 test"
+  needs: ["🐳 build"]
   variables:
     FILEPATH_TO_TEST: "/code/manage.py"
     PATH_TAG: "app"
@@ -104,8 +112,8 @@ test-file-perms:
 
 
 .deploy:
-  needs: ["build", "tailored-doc"]
-  stage: deploy
+  needs: ["🐳 build", "📝 tailored-doc"]
+  stage: "🚀 deploy"
   image: harbor.pasteur.fr/kube-system/helm-kubectl:3.13.3
   variables:
     CI_DEBUG_TRACE: "false"
@@ -116,7 +124,7 @@ test-file-perms:
   environment:
     name: "k8sdev-01-strass-dev/$CI_COMMIT_REF_SLUG"
     url: "https://strass-${CI_COMMIT_REF_SLUG}.dev.pasteur.cloud"
-    on_stop: stop_and_delete_in_dev
+    on_stop: "💥 stop_and_delete_in_dev"
   script:
     - kubectl delete secret registry-gitlab -n $NAMESPACE --ignore-not-found=true
     - kubectl create secret docker-registry -n $NAMESPACE registry-gitlab --docker-server=$CI_REGISTRY --docker-username=$DEPLOY_USER --docker-password=$DEPLOY_TOKEN --docker-email=$GITLAB_USER_EMAIL
@@ -146,7 +154,7 @@ test-file-perms:
 
 
 
-deploy_dev:
+"🚀 deploy_dev":
   extends: .deploy
   except:
     variables:
@@ -154,8 +162,12 @@ deploy_dev:
 
 
 
-deploy_prod:
+"🚀 deploy_prod":
   extends: .deploy
+  needs:
+    - "🐳 build"
+    - "📝 tailored-doc"
+    - "💯 checkCoverageMasterOrProd"
   only:
     variables:
       - $CI_COMMIT_REF_SLUG =~ /-prod$/
@@ -167,32 +179,34 @@ deploy_prod:
   environment:
     name: "k8sprod-02/strass-prod/$CI_COMMIT_REF_SLUG"
     url: "https://${CI_COMMIT_REF_SLUG::-5}.pasteur.cloud"
-    on_stop: stop_and_delete_in_prod
+    on_stop: "❗💥 stop_and_delete_in_prod"
   before_script:
     - export REL_NAME="${CI_COMMIT_REF_SLUG::-5}"
 
 
 
-open_public_access:
-  needs: ["deploy_prod"]
+"🔓🌐 open_public_access":
+  needs: ["🚀 deploy_prod"]
+  stage: "🔧 configure"
   when: manual
-  extends: deploy_prod
+  extends: "🚀 deploy_prod"
   variables:
     PUBLICLY_OPEN: "true"
 
 
 
-close_public_access:
-  needs: ["deploy_prod"]
+"🔒🌐 close_public_access":
+  needs: ["🚀 deploy_prod"]
+  stage: "🔧 configure"
   when: manual
-  extends: deploy_prod
+  extends: "🚀 deploy_prod"
   variables:
     PUBLICLY_OPEN: "false"
 
 
 
 .stop_and_delete:
-  stage: deploy
+  stage: "🔧 configure"
   when: manual
   image: harbor.pasteur.fr/kube-system/helm-kubectl:3.13.3
   variables:
@@ -209,8 +223,8 @@ close_public_access:
 
 
 
-stop_and_delete_in_dev:
-  needs: ["deploy_dev"]
+"💥 stop_and_delete_in_dev":
+  needs: ["🚀 deploy_dev"]
   extends: .stop_and_delete
   except:
     variables:
@@ -218,13 +232,13 @@ stop_and_delete_in_dev:
 
 
 
-stop_and_delete_in_prod:
-  needs: ["deploy_prod"]
+"❗💥 stop_and_delete_in_prod":
+  needs: ["🚀 deploy_prod"]
   extends: .stop_and_delete
   only:
     variables:
       - $CI_COMMIT_REF_SLUG =~ /-prod$/
-  stage: deploy
+  stage: "🔧 configure"
   when: manual
   variables:
     GIT_STRATEGY: none # important to not checkout source when branch is deleted
@@ -240,10 +254,10 @@ stop_and_delete_in_prod:
 
 
 
-build_pages:
+"📝 build_pages":
   needs: []
   image: python:3.11
-  stage: build
+  stage: "📦 build"
   script:
   - pip install -r doc/requirements.txt
   - cd doc
@@ -256,8 +270,8 @@ build_pages:
 
 
 pages:
-  needs: [build_pages]
-  stage: build
+  needs: ["📝 build_pages"]
+  stage: "📦 build"
   script:
     - ls -lah
   artifacts:
@@ -268,9 +282,9 @@ pages:
 
 
 
-tailored-doc:
-  needs: [build_pages]
-  stage: build
+"📝 tailored-doc":
+  needs: ["📝 build_pages"]
+  stage: "📦 build"
   before_script:
     - i=0; while [ "$i" -lt 60 ]; do docker info && break; sleep 1; i=$(( i + 1 )) ; done
     - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin
@@ -305,10 +319,10 @@ tailored-doc:
 
 
 
-black:
+"🎨 black":
   needs: []
   image: python:3.11
-  stage: build
+  stage: "📦 build"
   script:
     - python -m pip install --upgrade pip
     - cd src/strass/
@@ -319,7 +333,8 @@ black:
 
 # credits https://rpadovani.com/gitlab-code-coverage
 .checkCoverage:
-    needs: ["build"]
+    needs: ["🐳 build"]
+    stage: "🩺 test"
     image: alpine:latest
     variables:
         JOB_NAME: build
@@ -335,28 +350,25 @@ black:
         - if  [ "$CURRENT_COVERAGE" -lt "$TARGET_COVERAGE" ]; then echo "Coverage decreased from ${TARGET_COVERAGE} to ${CURRENT_COVERAGE}" && exit 1; fi;
 
 
-
-checkCoverageMasterOrProd:
+"💯 checkCoverageMasterOrProd":
   extends: .checkCoverage
   only:
     variables:
       - $CI_COMMIT_REF_SLUG =~ /-prod$/
       - $CI_COMMIT_REF_SLUG == "master"
-  stage: test
 
 
 
-checkCoverageOther:
+"💯 checkCoverageOther":
   extends: .checkCoverage
   except:
     variables:
       - $CI_COMMIT_REF_SLUG =~ /-prod$/
       - $CI_COMMIT_REF_SLUG == "master"
-  stage: deploy
 
 
-test-migrations-and-lang-ar-up-to-date:
-  stage: test
+"🩺 test-migrations-and-lang-ar-up-to-date":
+  stage: "🩺 test"
   needs: []
   image: python:3.11
   variables:
@@ -377,9 +389,9 @@ test-migrations-and-lang-ar-up-to-date:
 
 
 ssi-test:
-  needs: ["build"]
+  needs: ["🐳 build"]
   when: manual
-  stage: test
+  stage: "🩺 test"
   before_script:
     - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
   cache:
diff --git a/doc/_static/merge-request-pipelines-all-cut-dev.png b/doc/_static/merge-request-pipelines-all-cut-dev.png
new file mode 100644
index 0000000000000000000000000000000000000000..92c98b25e2b1fd0c6bed2eb3d526afc0b598fc3a
Binary files /dev/null and b/doc/_static/merge-request-pipelines-all-cut-dev.png differ
diff --git a/doc/_static/merge-request-pipelines-all-cut.png b/doc/_static/merge-request-pipelines-all-cut.png
deleted file mode 100644
index a298ec2f9d99d9c529ecc6b13b31bf42253dbffe..0000000000000000000000000000000000000000
Binary files a/doc/_static/merge-request-pipelines-all-cut.png and /dev/null differ
diff --git a/doc/_static/merge-request-pipelines-all.png b/doc/_static/merge-request-pipelines-all.png
deleted file mode 100644
index 38e84e27ab77476ad0a04f97faba5948fe9dc797..0000000000000000000000000000000000000000
Binary files a/doc/_static/merge-request-pipelines-all.png and /dev/null differ
diff --git a/doc/_static/merge-request-pipelines-delete-dev.png b/doc/_static/merge-request-pipelines-delete-dev.png
index eda7aa35d1e60097dd30e8d0a3e87779624eb792..cccf3ec0ed458305b3e5fce813de2bb22d9e6e40 100644
Binary files a/doc/_static/merge-request-pipelines-delete-dev.png and b/doc/_static/merge-request-pipelines-delete-dev.png differ
diff --git a/doc/_static/merge-request-pipelines-delete.png b/doc/_static/merge-request-pipelines-delete.png
index 70b73e781138b84fb31bb35dc748c5537c5aa273..f19deb188386f5c16d07f8782ecb098498bc1b9d 100644
Binary files a/doc/_static/merge-request-pipelines-delete.png and b/doc/_static/merge-request-pipelines-delete.png differ
diff --git a/doc/_static/merge-request-pipelines.png b/doc/_static/merge-request-pipelines.png
index 448eb544c92b5a8eaad3dd3e74ea3db1521ee564..6044d0dbee3cb61b7e6b7c53e03af9063b2ab3fe 100644
Binary files a/doc/_static/merge-request-pipelines.png and b/doc/_static/merge-request-pipelines.png differ
diff --git a/doc/configure_instance.rst b/doc/configure_instance.rst
index 5c52e5de3a93b962833a256dfa30efabc0b404c5..3d76978a8c3a4cf9939a3b7d1fad3eca1066feb5 100644
--- a/doc/configure_instance.rst
+++ b/doc/configure_instance.rst
@@ -173,8 +173,9 @@ A profiles can be "Single cell" "Metagenomic", "Web development", ... There can
 .. warning::
     Deleting a profile would result in un-associating all existing candidate to this profile without notifying them. To prevent mis-deletion, this possibility is not possible through the recruitment management interface. It is only possible in the admin interface https://strass-master.dev.pasteur.cloud/admin/strass_app/profile/, and only for superusers.
 
+.. _language override module:
 
-Using STRASS for a workshop
+Using STRASS for a workshop (a.k.a the Language override module)
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 If you use STRASS to recruit students for a workshop, the term `Profile` does not suite the need and you probably want to rename it to `Workshop` for example. Here is how to do it:
@@ -358,6 +359,12 @@ Reviewers can express their conflicts of interest
 Reviewers can indicate that they have conflict of interest when reviewing a candidate.
 They can also indicate that they don't want to review a candidate. To do se the application have to be configured so they see the candidate list in https://strass-master.dev.pasteur.cloud/setup/#review-settings .
 
+Reminding reviewers to indicate their profiles
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Reviewers should have already indicated which profile they accepte to review.
+If some have forgotten it, you can use the :ref:`Mass Mailing` to automatically select this subset of reviewers and remind them to do it.
+
 Assign reviewers
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 Link: https://strass-master.dev.pasteur.cloud/setup/review/overview/
@@ -396,7 +403,10 @@ Assign reviewers, again
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 Link: https://strass-master.dev.pasteur.cloud/setup/review/overview/
 
-You can still assign and re-assign reviewers. A reviewer can be automatically unassigned if zhe was not manually assigned and have not started yet to provide a review
+.. note::
+    You can re-run reviewers assignation **without risk** of data loss.
+
+You can still assign and re-assign reviewers. A reviewer can be automatically unassigned only if zhe was not manually assigned and have not started yet to provide a review. Reviews that have been already started are kept.
 
 **Use case: new conflicts of interest**
 
diff --git a/doc/jm_documentation.rst b/doc/jm_documentation.rst
index e966b38b70303a39a5d0571db3be1877364177aa..acf72257a37998d3f2724aacc4e36bf14e36d7a9 100644
--- a/doc/jm_documentation.rst
+++ b/doc/jm_documentation.rst
@@ -12,4 +12,5 @@ Jury Manager Doc
     reviewer_assignation
     mass_mailing
     question_system
-    overall_score_system
\ No newline at end of file
+    overall_score_system
+    security_feature
\ No newline at end of file
diff --git a/doc/life_cycle.rst b/doc/life_cycle.rst
index ec82c500d6f404fae2f961de5d67e5de3e8929ac..161f5970b864933c185ecc44bc540892699d03fb 100644
--- a/doc/life_cycle.rst
+++ b/doc/life_cycle.rst
@@ -78,8 +78,9 @@ all pipelines are now green.
   :alt: Pipelines all in green
 
 
-On the right part, the third green checkmark (named A) allows you to see all job related to
-the deployment of the instance. You see the `deploy_prod` went well. To open the public access just click on the play
+On the right part, the fourth circle (named A) allows you to see all job related to
+the deployment of the instance.
+To open the public access just click on the play
 button next to `open_public_access` (named B) and wait for the job completion.
 Similarly, to close the public access juste click on the play
 button next to `close_public_access` (named C) and wait for the job completion.
diff --git a/doc/life_cycle_demo.rst b/doc/life_cycle_demo.rst
index b84c5c68916f600bd54c31ed50a2948293b0eec2..7c4c529d955fac79fe008f4ecbe19ddd2a6bb729 100644
--- a/doc/life_cycle_demo.rst
+++ b/doc/life_cycle_demo.rst
@@ -48,14 +48,16 @@ Load the demo ?
 -------------------------------------------------------------------------------
 
 You can load the demo, but you need kubectl installed, and you need to be granted on the dev namespace.
-If you understand this requirementsadd, then see :ref:`load_demo_k8s` on know how to load the demo in kubernetes
+If you understand this requirements, then see :ref:`load_demo_k8s` on know how to load the demo in kubernetes
 
 Stop it
 -------------------------------------------------------------------------------
 
+It is automatically done when you merge your branch, but If you just close it you need to stop it first.
+
 In the pipelines (A), see the job "stop_and_delete_instance" (D)
 
-.. image:: _static/merge-request-pipelines-all-cut.png
+.. image:: _static/merge-request-pipelines-all-cut-dev.png
   :width: 100%
   :alt: Pipelines all in green
 
diff --git a/doc/mass_mailing.rst b/doc/mass_mailing.rst
index ea7a1a5579e5cedc70e4e9b0e4b78b00b512f8d6..782da9a450fcf3b9ea9363ad8667f262f74586f7 100644
--- a/doc/mass_mailing.rst
+++ b/doc/mass_mailing.rst
@@ -44,6 +44,9 @@ There are multiple recipients group, some such as ``Reviewers`` are explicit eno
 **Reviewers with editable reviews not completed**:
 The reviewers that are associated to a review that is both empty and in a review stage that still allows to provide a review.
 
+**Reviewers that haven't indicated reviewable profiles**:
+The reviewers that haven't specified the candidate's profiles they are willing to review.
+
 **Pending reviewer**:
 Users that have accepted to be reviewer, but have not been yet confirmed as reviewer, or refused.
 
diff --git a/doc/security_feature.rst b/doc/security_feature.rst
new file mode 100644
index 0000000000000000000000000000000000000000..6433cd879acc858cdcea2b57bc114c619214c560
--- /dev/null
+++ b/doc/security_feature.rst
@@ -0,0 +1,29 @@
+.. note::
+    **Audience:** Jury Manager to adapt, all for information.
+
+.. _Overall score:
+
+Security considerations and features
+===============================================================================
+
+The application is thoroughly tested with 100% test coverage, ensuring that every part of the application and every scenario is tested. Before testing all scenarios, all role-based permissions and controls are tested separately, with a required coverage of 100%, ensuring that this key feature is well tested.
+
+The application's source code is hosted on GitLab, a software forge. Continuous integration has been set up to test the application at each commit. Unit tests act as a safeguard, preventing new versions from being deployed if they introduce regressions. Reducing test coverage is also prohibited for the master branch and production instances.
+
+The application runs in a "hardened" Docker container. This means that several security measures have been implemented to prevent code injection and ensure stability. The source code is set to read-only within the container, preventing unauthorized modifications or tampering. Additionally, it runs as a non-root user, limiting access to critical system resources and preventing unauthorized modifications or tampering with the system and sources. As a consequence, patching the application during execution is impossible, changes must be committed to the source repository where authorship is unquestionable and changes are thoroughly tested.
+
+These combined measures provide a secure and stable environment for our users.
+
+Protection from attacks using PDF
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A CV can be provided in a PDF file. This file format comes with features that can be exploited to attack the application, such as stealing session cookies or extracting application content. To prevent this, all executable code is stripped out from uploaded PDF files.
+
+Uploading a CV in PDF file can be disabled in https://strass-master.dev.pasteur.cloud/setup/#candidate-settings. If disabled, you may need to adapt the introduction text presented to applicants on how to apply. This text is visible at https://strass-master.dev.pasteur.cloud/candidate/apply/Intro/. To do so, use the :ref:`Language override module <language override module>`.
+
+Protection from attacks using Markdown
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Markdown is used to format motivation letters, calls, and more. This standard is exposed to JavaScript or CSS injection from attackers. A strict whitelist approach has been adopted, preventing almost all use of HTML within Markdown.
+
+Markdown can be disabled for the entire application in https://strass-master.dev.pasteur.cloud/setup/#misc-settings. If disabled, all content is escaped, and line breaks are replaced by ``<br>``.
diff --git a/src/strass/live_settings/__init__.py b/src/strass/live_settings/__init__.py
deleted file mode 100644
index a48cb26e8cbef542ba65e9d7d126e6a9ca3b8f24..0000000000000000000000000000000000000000
--- a/src/strass/live_settings/__init__.py
+++ /dev/null
@@ -1,77 +0,0 @@
-#
-#  STRASS
-#  Copyright (C) 2025  The STRASS Authors
-#
-#  This program is free software: you can redistribute it and/or modify
-#  it under the terms of the GNU Affero General Public License as published by
-#  the Free Software Foundation, either version 3 of the License, or
-#  (at your option) any later version.
-#
-#  This program is distributed in the hope that it will be useful,
-#  but WITHOUT ANY WARRANTY; without even the implied warranty of
-#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#  GNU Affero General Public License for more details.
-#
-#  You should have received a copy of the GNU Affero General Public License
-#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-
-from datetime import datetime
-
-from django.core.cache import cache
-from django.db import OperationalError, ProgrammingError
-
-__LIVE_SETTING_EDITOR = "LiveSettingEditor"
-
-
-def get_live_settings_editor_group_name():
-    return __LIVE_SETTING_EDITOR
-
-
-class LiveSettings(object):
-    def __setattr__(self, key, value, *args, **kwargs):
-        try:
-            from live_settings.models import LiveSettings
-
-            if value is None:
-                LiveSettings.objects.filter(key=key).delete()
-            else:
-                LiveSettings.objects.update_or_create(key=key, defaults=dict(value=str(value)))
-        except (ProgrammingError, OperationalError):
-            return super().__setattr__(key, value)
-
-    def __getattribute__(self, key):
-        as_int = False
-        as_bool = False
-        as_date = False
-        if key.endswith("__int"):
-            key = key[:-5]
-            as_int = True
-        if key.endswith("__bool"):
-            key = key[:-6]
-            as_bool = True
-        if key.endswith("__date"):
-            key = key[:-6]
-            as_date = True
-        live_settings_dict = cache.get("live_settings_dict")
-        if live_settings_dict is None:
-            try:
-                from live_settings.models import LiveSettings
-
-                live_settings_dict = dict(LiveSettings.objects.values_list("key", "value"))
-                cache.set("live_settings_dict", live_settings_dict)
-            except (ProgrammingError, OperationalError):
-                return super().__getattribute__(key)
-        value = live_settings_dict.get(key, None)
-        if value is None:
-            return value
-        if as_int:
-            return int(value)
-        if as_bool:
-            return str(value).upper() == "TRUE"
-        if as_date:
-            return datetime.strptime(str(value), "%Y-%m-%d").date()
-        return value
-
-
-live_settings = LiveSettings()
diff --git a/src/strass/live_settings/admin.py b/src/strass/live_settings/admin.py
deleted file mode 100644
index 5a3cc397208f650956aabc649c493476da294bf8..0000000000000000000000000000000000000000
--- a/src/strass/live_settings/admin.py
+++ /dev/null
@@ -1,29 +0,0 @@
-#
-#  STRASS
-#  Copyright (C) 2025  The STRASS Authors
-#
-#  This program is free software: you can redistribute it and/or modify
-#  it under the terms of the GNU Affero General Public License as published by
-#  the Free Software Foundation, either version 3 of the License, or
-#  (at your option) any later version.
-#
-#  This program is distributed in the hope that it will be useful,
-#  but WITHOUT ANY WARRANTY; without even the implied warranty of
-#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#  GNU Affero General Public License for more details.
-#
-#  You should have received a copy of the GNU Affero General Public License
-#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-
-from django.contrib import admin
-
-from live_settings import models
-
-
-class LiveSettingsAdmin(admin.ModelAdmin):
-    list_display = ("key", "last_edition_date")
-    readonly_fields = ["last_edition_date"]
-
-
-admin.site.register(models.LiveSettings, LiveSettingsAdmin)
diff --git a/src/strass/live_settings/context_processors.py b/src/strass/live_settings/context_processors.py
deleted file mode 100644
index 62b6bb2974acf502c97ef780ba715a2a4ec80fae..0000000000000000000000000000000000000000
--- a/src/strass/live_settings/context_processors.py
+++ /dev/null
@@ -1,23 +0,0 @@
-#
-#  STRASS
-#  Copyright (C) 2025  The STRASS Authors
-#
-#  This program is free software: you can redistribute it and/or modify
-#  it under the terms of the GNU Affero General Public License as published by
-#  the Free Software Foundation, either version 3 of the License, or
-#  (at your option) any later version.
-#
-#  This program is distributed in the hope that it will be useful,
-#  but WITHOUT ANY WARRANTY; without even the implied warranty of
-#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#  GNU Affero General Public License for more details.
-#
-#  You should have received a copy of the GNU Affero General Public License
-#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-
-from live_settings import live_settings
-
-
-def processors(request):
-    return dict(live_settings=live_settings)
diff --git a/src/strass/live_settings/forms.py b/src/strass/live_settings/forms.py
deleted file mode 100644
index b98c948623cbf8e1f0db40d1ecf3f64ae7159e3d..0000000000000000000000000000000000000000
--- a/src/strass/live_settings/forms.py
+++ /dev/null
@@ -1,30 +0,0 @@
-#
-#  STRASS
-#  Copyright (C) 2025  The STRASS Authors
-#
-#  This program is free software: you can redistribute it and/or modify
-#  it under the terms of the GNU Affero General Public License as published by
-#  the Free Software Foundation, either version 3 of the License, or
-#  (at your option) any later version.
-#
-#  This program is distributed in the hope that it will be useful,
-#  but WITHOUT ANY WARRANTY; without even the implied warranty of
-#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#  GNU Affero General Public License for more details.
-#
-#  You should have received a copy of the GNU Affero General Public License
-#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-
-from django import forms
-
-from live_settings import models
-
-
-class LiveSettingsForm(forms.ModelForm):
-    next = forms.CharField(widget=forms.HiddenInput(), required=True)
-
-    class Meta:
-        model = models.LiveSettings
-        fields = ("value",)
-        widgets = {"value": forms.HiddenInput()}
diff --git a/src/strass/live_settings/migrations/0001_initial.py b/src/strass/live_settings/migrations/0001_initial.py
deleted file mode 100644
index ac147870d5dd46e793378ef1ea01e900650bc16b..0000000000000000000000000000000000000000
--- a/src/strass/live_settings/migrations/0001_initial.py
+++ /dev/null
@@ -1,42 +0,0 @@
-#
-#  STRASS
-#  Copyright (C) 2025  The STRASS Authors
-#
-#  This program is free software: you can redistribute it and/or modify
-#  it under the terms of the GNU Affero General Public License as published by
-#  the Free Software Foundation, either version 3 of the License, or
-#  (at your option) any later version.
-#
-#  This program is distributed in the hope that it will be useful,
-#  but WITHOUT ANY WARRANTY; without even the implied warranty of
-#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#  GNU Affero General Public License for more details.
-#
-#  You should have received a copy of the GNU Affero General Public License
-#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-
-# Generated by Django 2.2.5 on 2019-11-20 10:20
-
-import django.core.validators
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    initial = True
-
-    dependencies = [
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='LiveSettings',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('key', models.CharField(max_length=256, unique=True, validators=[django.core.validators.RegexValidator(code='invalid_live_settings_key', message='LiveSettings key must must be Alphanumeric and start with a letter: <code>^[a-zA-Z][\\w]*$</code>.', regex='^[a-zA-Z][\\w]*$')])),
-                ('value', models.TextField(blank=True, null=True)),
-                ('last_edition_date', models.DateTimeField()),
-            ],
-        ),
-    ]
diff --git a/src/strass/live_settings/migrations/0002_create_group_settings_changer.py b/src/strass/live_settings/migrations/0002_create_group_settings_changer.py
deleted file mode 100644
index cb41bcd036a2a7ee88ef5c6e3ba3ab8a644d44d2..0000000000000000000000000000000000000000
--- a/src/strass/live_settings/migrations/0002_create_group_settings_changer.py
+++ /dev/null
@@ -1,57 +0,0 @@
-#
-#  STRASS
-#  Copyright (C) 2025  The STRASS Authors
-#
-#  This program is free software: you can redistribute it and/or modify
-#  it under the terms of the GNU Affero General Public License as published by
-#  the Free Software Foundation, either version 3 of the License, or
-#  (at your option) any later version.
-#
-#  This program is distributed in the hope that it will be useful,
-#  but WITHOUT ANY WARRANTY; without even the implied warranty of
-#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#  GNU Affero General Public License for more details.
-#
-#  You should have received a copy of the GNU Affero General Public License
-#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-from django.contrib.auth import get_permission_codename
-from django.contrib.auth.models import Permission, Group
-from django.contrib.contenttypes.models import ContentType
-from django.db import migrations
-
-
-def migration_code(apps, schema_editor):
-    # inspired by django.contrib.auth.management.create_permissions
-    group, _ = Group.objects.get_or_create(name="LiveSettingEditor")
-
-    for klass in [
-        apps.get_model("live_settings", "LiveSettings"),
-    ]:
-        ct = ContentType.objects.get_for_model(klass)
-        opts = klass._meta
-
-        for action in klass._meta.default_permissions:
-            perm, _ = Permission.objects.get_or_create(
-                codename=get_permission_codename(action, opts),
-                name='Can %s %s' % (action, opts.verbose_name_raw),
-                content_type=ct)
-            group.permissions.add(perm)
-
-
-def reverse_code(apps, schema_editor):
-    Group.objects.filter(name="LiveSettingEditor").delete()
-
-
-class Migration(migrations.Migration):
-    dependencies = [
-        ('live_settings', '0001_initial'),
-    ]
-
-    operations = [
-        migrations.RunPython(migration_code, reverse_code=reverse_code),
-    ]
diff --git a/src/strass/live_settings/migrations/0003_auto_20200311_1037.py b/src/strass/live_settings/migrations/0003_auto_20200311_1037.py
deleted file mode 100644
index aaccf2e590a1bf2bd9964e25296efd0ace657d8f..0000000000000000000000000000000000000000
--- a/src/strass/live_settings/migrations/0003_auto_20200311_1037.py
+++ /dev/null
@@ -1,35 +0,0 @@
-#
-#  STRASS
-#  Copyright (C) 2025  The STRASS Authors
-#
-#  This program is free software: you can redistribute it and/or modify
-#  it under the terms of the GNU Affero General Public License as published by
-#  the Free Software Foundation, either version 3 of the License, or
-#  (at your option) any later version.
-#
-#  This program is distributed in the hope that it will be useful,
-#  but WITHOUT ANY WARRANTY; without even the implied warranty of
-#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#  GNU Affero General Public License for more details.
-#
-#  You should have received a copy of the GNU Affero General Public License
-#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-
-# Generated by Django 2.2.5 on 2020-03-11 09:37
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('live_settings', '0002_create_group_settings_changer'),
-    ]
-
-    operations = [
-        migrations.AlterModelOptions(
-            name='livesettings',
-            options={'verbose_name': 'Live setting', 'verbose_name_plural': 'Live settings'},
-        ),
-    ]
diff --git a/src/strass/live_settings/migrations/0004_alter_livesettings_id.py b/src/strass/live_settings/migrations/0004_alter_livesettings_id.py
deleted file mode 100644
index 5f0191e33046b63b124c015e36132a794d2da324..0000000000000000000000000000000000000000
--- a/src/strass/live_settings/migrations/0004_alter_livesettings_id.py
+++ /dev/null
@@ -1,36 +0,0 @@
-#
-#  STRASS
-#  Copyright (C) 2025  The STRASS Authors
-#
-#  This program is free software: you can redistribute it and/or modify
-#  it under the terms of the GNU Affero General Public License as published by
-#  the Free Software Foundation, either version 3 of the License, or
-#  (at your option) any later version.
-#
-#  This program is distributed in the hope that it will be useful,
-#  but WITHOUT ANY WARRANTY; without even the implied warranty of
-#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#  GNU Affero General Public License for more details.
-#
-#  You should have received a copy of the GNU Affero General Public License
-#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-
-# Generated by Django 3.2.4 on 2021-07-01 09:15
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('live_settings', '0003_auto_20200311_1037'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='livesettings',
-            name='id',
-            field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
-        ),
-    ]
diff --git a/src/strass/live_settings/migrations/__init__.py b/src/strass/live_settings/migrations/__init__.py
deleted file mode 100644
index 596267674969f0d7622c9f00c2431e4cd07b25dd..0000000000000000000000000000000000000000
--- a/src/strass/live_settings/migrations/__init__.py
+++ /dev/null
@@ -1,18 +0,0 @@
-#
-#  STRASS
-#  Copyright (C) 2025  The STRASS Authors
-#
-#  This program is free software: you can redistribute it and/or modify
-#  it under the terms of the GNU Affero General Public License as published by
-#  the Free Software Foundation, either version 3 of the License, or
-#  (at your option) any later version.
-#
-#  This program is distributed in the hope that it will be useful,
-#  but WITHOUT ANY WARRANTY; without even the implied warranty of
-#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#  GNU Affero General Public License for more details.
-#
-#  You should have received a copy of the GNU Affero General Public License
-#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-
diff --git a/src/strass/live_settings/models.py b/src/strass/live_settings/models.py
deleted file mode 100644
index df680e1e99cd4c6c92d6ff0343653d8ece9f3e3d..0000000000000000000000000000000000000000
--- a/src/strass/live_settings/models.py
+++ /dev/null
@@ -1,63 +0,0 @@
-#
-#  STRASS
-#  Copyright (C) 2025  The STRASS Authors
-#
-#  This program is free software: you can redistribute it and/or modify
-#  it under the terms of the GNU Affero General Public License as published by
-#  the Free Software Foundation, either version 3 of the License, or
-#  (at your option) any later version.
-#
-#  This program is distributed in the hope that it will be useful,
-#  but WITHOUT ANY WARRANTY; without even the implied warranty of
-#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#  GNU Affero General Public License for more details.
-#
-#  You should have received a copy of the GNU Affero General Public License
-#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-
-from __future__ import unicode_literals
-
-from django.core.cache import cache
-from django.core.validators import RegexValidator
-from django.db import models
-from django.db.models.signals import post_save, post_delete
-from django.dispatch import receiver
-from django.utils import timezone
-from django.utils.safestring import mark_safe
-
-
-class LiveSettings(models.Model):
-    class Meta:
-        verbose_name = "Live setting"
-        verbose_name_plural = "Live settings"
-
-    regex = "^[a-zA-Z][\\w]*$"
-    key = models.CharField(
-        max_length=256,
-        unique=True,
-        validators=(
-            RegexValidator(
-                regex=regex,
-                message=mark_safe(
-                    "LiveSettings key must must be Alphanumeric and start with a letter: " "<code>%s</code>." % regex
-                ),
-                code="invalid_live_settings_key",
-            ),
-        ),
-    )
-    value = models.TextField(
-        blank=True,
-        null=True,
-    )
-    last_edition_date = models.DateTimeField()
-
-    def save(self, *args, **kwargs):
-        self.last_edition_date = timezone.now()
-        super().save(*args, **kwargs)
-
-
-@receiver(post_save, sender=LiveSettings)
-@receiver(post_delete, sender=LiveSettings)
-def flush_live_settings_in_cache(*args, **kwargs):
-    cache.delete("live_settings_dict")
diff --git a/src/strass/live_settings/templates/admin/index.html b/src/strass/live_settings/templates/admin/index.html
deleted file mode 100644
index ff93bb55311fe33270af1e21b89df56c02b7045a..0000000000000000000000000000000000000000
--- a/src/strass/live_settings/templates/admin/index.html
+++ /dev/null
@@ -1,57 +0,0 @@
-{% extends "admin/index.html" %}
-{% load i18n basetheme_bootstrap %}
-
-{% block extrastyle %}
-{{ block.super }}
-<style>
-#content-related{
-    background: none;
-}
-#content-related .module{
-    background: var(--darkened-bg);
-}
-#site-wide-commands form{
-    margin:15px 7px;
-}
-</style>
-{% endblock %}
-
-{% block sidebar %}
-<div id="content-related">
-    {% if perms.live_settings.change_livesettings %}
-    <div class="module" id="site-wide-commands">
-        <h2>{% trans 'Site wide settings' %}</h2>
-        {%include_if_exists "live_settings/sitewidecommands.html" %}
-    </div>
-    {%endif%}
-
-    <div class="module" id="recent-actions-module">
-        <h2>{% trans 'Recent actions' %}</h2>
-        <h3>{% trans 'My actions' %}</h3>
-            {% load log %}
-            {% get_admin_log 10 as admin_log for_user user %}
-            {% if not admin_log %}
-            <p>{% trans 'None available' %}</p>
-            {% else %}
-            <ul class="actionlist">
-            {% for entry in admin_log %}
-            <li class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}">
-                {% if entry.is_deletion or not entry.get_admin_url %}
-                    {{ entry.object_repr }}
-                {% else %}
-                    <a href="{{ entry.get_admin_url }}">{{ entry.object_repr }}</a>
-                {% endif %}
-                <br>
-                {% if entry.content_type %}
-                    <span class="mini quiet">{% filter capfirst %}{{ entry.content_type }}{% endfilter %}</span>
-                {% else %}
-                    <span class="mini quiet">{% trans 'Unknown content' %}</span>
-                {% endif %}
-            </li>
-            {% endfor %}
-            </ul>
-            {% endif %}
-        <hr/>
-    </div>
-</div>
-{% endblock %}
\ No newline at end of file
diff --git a/src/strass/live_settings/tests.py b/src/strass/live_settings/tests.py
deleted file mode 100644
index 00a996e9bb7fd68f7ebe4d9b052a784ff5013af2..0000000000000000000000000000000000000000
--- a/src/strass/live_settings/tests.py
+++ /dev/null
@@ -1,98 +0,0 @@
-#
-#  STRASS
-#  Copyright (C) 2025  The STRASS Authors
-#
-#  This program is free software: you can redistribute it and/or modify
-#  it under the terms of the GNU Affero General Public License as published by
-#  the Free Software Foundation, either version 3 of the License, or
-#  (at your option) any later version.
-#
-#  This program is distributed in the hope that it will be useful,
-#  but WITHOUT ANY WARRANTY; without even the implied warranty of
-#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#  GNU Affero General Public License for more details.
-#
-#  You should have received a copy of the GNU Affero General Public License
-#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-
-import logging
-from unittest import TestCase
-
-from django.contrib.auth import get_user_model
-from django.contrib.auth.models import Permission
-from django.contrib.contenttypes.models import ContentType
-from django.core import management
-from django.core.cache import cache
-from django.test import TestCase as DjangoTestCase
-from django.urls import reverse
-
-from live_settings import live_settings, models
-from live_settings.tools import set_default_live_setting
-
-logger = logging.getLogger(__name__)
-
-
-class LiveSettingsTestCase(DjangoTestCase):
-    def setUp(self) -> None:
-        super().setUp()
-        ################################################################################
-        self.user = get_user_model().objects.create(
-            username="root",
-        )
-
-    def test_get_value(self):
-        self.assertIsNone(live_settings.toto)
-        models.LiveSettings.objects.create(key="toto", value="tata")
-        self.assertIsNotNone(live_settings.toto)
-        self.assertEqual(live_settings.toto, "tata")
-
-    def test_get_set(self):
-        live_settings.tralala = 1
-        self.assertEqual(str(live_settings.tralala), "1")
-        live_settings.tralala = None
-        self.assertIsNone(live_settings.tralala)
-
-    def test_view_works(self):
-        form_data = dict(value="titi", next="/")
-        url = reverse('live_settings:update', args=["toto"])
-
-        response = self.client.post(url, form_data)
-        self.assertEqual(response.status_code, 302)
-        self.assertNotEqual(live_settings.toto, form_data["value"])
-
-        self.client.force_login(self.user)
-
-        response = self.client.post(url, form_data)
-        self.assertEqual(response.status_code, 302)
-        self.assertNotEqual(live_settings.toto, form_data["value"])
-
-        change = Permission.objects.get(
-            content_type=ContentType.objects.get_for_model(models.LiveSettings), codename__startswith="change"
-        )
-        self.user.user_permissions.add(change)
-
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 403)
-        self.assertNotEqual(live_settings.toto, form_data["value"])
-
-        response = self.client.post(url, dict(value=form_data["value"]))
-        self.assertEqual(response.status_code, 400)
-        self.assertNotEqual(live_settings.toto, form_data["value"])
-
-        response = self.client.post(url, form_data)
-        self.assertEqual(response.status_code, 302)
-        self.assertEqual(live_settings.toto, form_data["value"])
-
-
-class LiveSettingsNoDBTestCase(TestCase):
-    def test_get_value(self):
-        management.call_command("migrate", "live_settings", "zero", no_input=True, skip_checks=True)
-        cache.delete("live_settings_dict")
-        try:
-            ex = live_settings.tralala
-        except Exception as e:
-            ex = e
-        assert isinstance(ex, AttributeError)
-        live_settings.tralala = 1
-        set_default_live_setting("tralala", 2)
diff --git a/src/strass/live_settings/tools.py b/src/strass/live_settings/tools.py
deleted file mode 100644
index bfa216df197942607f1d08c80a6ff7f88486a8c0..0000000000000000000000000000000000000000
--- a/src/strass/live_settings/tools.py
+++ /dev/null
@@ -1,30 +0,0 @@
-#
-#  STRASS
-#  Copyright (C) 2025  The STRASS Authors
-#
-#  This program is free software: you can redistribute it and/or modify
-#  it under the terms of the GNU Affero General Public License as published by
-#  the Free Software Foundation, either version 3 of the License, or
-#  (at your option) any later version.
-#
-#  This program is distributed in the hope that it will be useful,
-#  but WITHOUT ANY WARRANTY; without even the implied warranty of
-#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#  GNU Affero General Public License for more details.
-#
-#  You should have received a copy of the GNU Affero General Public License
-#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-
-from django.core.cache import cache
-from django.db import OperationalError, ProgrammingError
-
-
-def set_default_live_setting(key, value):
-    try:
-        from live_settings.models import LiveSettings
-
-        LiveSettings.objects.get_or_create(key=key, defaults=dict(value=str(value)))
-        cache.delete("live_settings_dict")
-    except (ProgrammingError, OperationalError):
-        pass
diff --git a/src/strass/live_settings/urls.py b/src/strass/live_settings/urls.py
deleted file mode 100644
index 77be4ad4bbf8e3eaa861341c9950afeecd9d0496..0000000000000000000000000000000000000000
--- a/src/strass/live_settings/urls.py
+++ /dev/null
@@ -1,24 +0,0 @@
-#
-#  STRASS
-#  Copyright (C) 2025  The STRASS Authors
-#
-#  This program is free software: you can redistribute it and/or modify
-#  it under the terms of the GNU Affero General Public License as published by
-#  the Free Software Foundation, either version 3 of the License, or
-#  (at your option) any later version.
-#
-#  This program is distributed in the hope that it will be useful,
-#  but WITHOUT ANY WARRANTY; without even the implied warranty of
-#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#  GNU Affero General Public License for more details.
-#
-#  You should have received a copy of the GNU Affero General Public License
-#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-
-from django.urls import re_path
-
-from live_settings import views
-
-app_name = "live_settings"
-urlpatterns = [re_path(r"^update/(?P<slug>[a-zA-Z][\w]*)/$", views.change_value, name="update")]
diff --git a/src/strass/live_settings/views.py b/src/strass/live_settings/views.py
deleted file mode 100644
index 95764b56649038a1b2f3f10b41fb6d9c01133efa..0000000000000000000000000000000000000000
--- a/src/strass/live_settings/views.py
+++ /dev/null
@@ -1,38 +0,0 @@
-#
-#  STRASS
-#  Copyright (C) 2025  The STRASS Authors
-#
-#  This program is free software: you can redistribute it and/or modify
-#  it under the terms of the GNU Affero General Public License as published by
-#  the Free Software Foundation, either version 3 of the License, or
-#  (at your option) any later version.
-#
-#  This program is distributed in the hope that it will be useful,
-#  but WITHOUT ANY WARRANTY; without even the implied warranty of
-#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#  GNU Affero General Public License for more details.
-#
-#  You should have received a copy of the GNU Affero General Public License
-#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-
-from django.contrib.auth.decorators import permission_required
-from django.http import HttpResponseForbidden, HttpResponseBadRequest
-from django.shortcuts import redirect
-
-from live_settings import forms
-from live_settings import live_settings
-
-
-@permission_required("live_settings.change_livesettings")
-def change_value(request, slug):
-    if request.method != "POST":
-        return HttpResponseForbidden()
-
-    form = forms.LiveSettingsForm(data=request.POST)
-    if not form.is_valid():
-        return HttpResponseBadRequest()
-
-    setattr(live_settings, slug, form.cleaned_data["value"])
-
-    return redirect(form.cleaned_data["next"])
diff --git a/src/strass/requirements.txt b/src/strass/requirements.txt
index b8ab71654ba5c54aed45c171732f60e984f8b1c8..2c890cf702b2486b123df0e90ba6852edfe3ca86 100644
--- a/src/strass/requirements.txt
+++ b/src/strass/requirements.txt
@@ -4,11 +4,13 @@ django-crispy-forms~=2.0
 crispy-bootstrap4
 python-decouple
 --extra-index-url https://gitlab.pasteur.fr/api/v4/projects/bbrancot%2Fdjango-basetheme-bootstrap/packages/pypi/simple
-django-basetheme-bootstrap>=1.8.4
+django-basetheme-bootstrap>=1.8.7
 --extra-index-url https://gitlab.pasteur.fr/api/v4/projects/hub%2Fdjango-kubernetes-probes/packages/pypi/simple
 django-kubernetes-probes>=1.1
 --extra-index-url https://gitlab.pasteur.fr/api/v4/projects/hub%2Fdjango-csp-mail-reports/packages/pypi/simple
 django-csp-mail-reports>=1.0
+--extra-index-url https://gitlab.pasteur.fr/api/v4/projects/hub%2Fdjango-live-settings/packages/pypi/simple
+django-live-settings>=1.0.1
 django-formtools
 csscompressor
 coverage
diff --git a/src/strass/strass_app/forms.py b/src/strass/strass_app/forms.py
index ffe4c3836e4877deae2c8261b45df594c378e7b3..6e922d16e9155f222eda0e2219458d5bf8fd4e6f 100644
--- a/src/strass/strass_app/forms.py
+++ b/src/strass/strass_app/forms.py
@@ -1649,6 +1649,7 @@ class MassMailingForm(forms.Form):
         for key, text in [
             ('reviewers', _("Reviewers (%i)")),
             ('reviewers_with_review_todo', _("Reviewers with editable reviews not completed (%i)")),
+            ('reviewers_without_profile', _("Reviewers with no associated profile (%i)")),
             ('pending_reviewers', _("Pending reviewers (%i)")),
             ('jurors', _("Jurors (%i)")),
             ('pending_jurors', _("Pending jurors (%i)")),
@@ -1689,6 +1690,10 @@ class MassMailingForm(forms.Form):
                 .filter(is_open_for_edition=True, is_completed=False)
                 .values_list('reviewer__pk', flat=True)
             )
+        if recipients == "reviewers_without_profile":
+            return business_logic.get_reviewers().exclude(
+                pk__in=business_logic.get_reviewers().filter(user_preferences__profiles__pk__isnull=False)
+            )
         if recipients == "jurors":
             return business_logic.get_jurors()
         if recipients == "pending_jurors":
diff --git a/src/strass/strass_app/locale/en/LC_MESSAGES/django.po b/src/strass/strass_app/locale/en/LC_MESSAGES/django.po
index 6798b5673e49554ab2091271009e76468c52063f..d728cb6481b18e6218fc3002f93d588577164cc1 100644
--- a/src/strass/strass_app/locale/en/LC_MESSAGES/django.po
+++ b/src/strass/strass_app/locale/en/LC_MESSAGES/django.po
@@ -10,7 +10,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2025-02-12 18:54+0100\n"
+"POT-Creation-Date: 2025-02-28 09:44+0100\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -728,7 +728,9 @@ msgid "MassMailingForm.body.placeholder"
 msgstr ""
 "Dear [first_name] [last_name],\n"
 "\n"
-"The email we have for you is [email], if you are a candidate your "
+"The email we have for you is [email].\n"
+"\n"
+"If you are a candidate your "
 "application is accessible at [application_url].\n"
 "\n"
 "If you are a reviewer, your editable reviews are:\n"
@@ -744,6 +746,10 @@ msgstr ""
 msgid "Reviewers with editable reviews not completed (%i)"
 msgstr ""
 
+#, python-format
+msgid "Reviewers with no associated profile (%i)"
+msgstr "Reviewers that haven't indicated reviewable profiles (%i)"
+
 #, python-format
 msgid "Pending reviewers (%i)"
 msgstr ""
@@ -2840,6 +2846,16 @@ msgstr ""
 "automatically assigned. Any review that is either manually assigned or "
 "already answered by the author will be kept."
 
+msgid "ComputeAutoReviewView information about"
+msgstr ""
+"Re-"
+"running the assignation is useful when new reviewers arrive or new conflicts "
+"of interest declared (cf <a href=\"/doc/configure_instance.html#assign-"
+"reviewers-again\" target=\"_blank\">documentation</a> for more).<br/>"
+"Note that data loss is prevented when re-running assignation. Any review that is "
+"either manually assigned or already answered by the author will be kept, i.e "
+"a reviewer will not be unsigned if zhe have started to review."
+
 msgid "MassMailingWizard.Prepare.title"
 msgstr "Preparing the mass mailing"
 
diff --git a/src/strass/strass_app/locale/fr/LC_MESSAGES/django.po b/src/strass/strass_app/locale/fr/LC_MESSAGES/django.po
index f88d736dd13f6f0c62ff6411f54934ba8634f163..f9fdc50c16836cc985730bc1e9986f552b307618 100644
--- a/src/strass/strass_app/locale/fr/LC_MESSAGES/django.po
+++ b/src/strass/strass_app/locale/fr/LC_MESSAGES/django.po
@@ -10,7 +10,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2025-02-12 18:54+0100\n"
+"POT-Creation-Date: 2025-02-28 09:47+0100\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -779,9 +779,10 @@ msgid "MassMailingForm.body.placeholder"
 msgstr ""
 "Bonjour [prenom] [nom]\n"
 "\n"
-"Votre courriel est [mail]. Si vous êtes un.e candidat.e, vous pouvez "
-"consulter et modifier votre candidature en utilisant le lien suivant visible "
-"à [url_candidature].\n"
+"Votre courriel est [mail].\n"
+"\n"
+"Si vous êtes un.e candidat.e, vous pouvez "
+"consulter et modifier votre candidature en utilisant le lien suivant [url_candidature].\n"
 "\n"
 "Si vous êtes un reviewer, vos reviews editable sont :\n"
 "[liste_reviews_editable]\n"
@@ -796,6 +797,10 @@ msgstr "Reviewers (%i)"
 msgid "Reviewers with editable reviews not completed (%i)"
 msgstr "Reviewers avec des reviews éditables et pas encore finalisées (%i)"
 
+#, python-format
+msgid "Reviewers with no associated profile (%i)"
+msgstr "Reviewers qui n'ont pas indiqués leur profiles évaluables (%i)"
+
 #, python-format
 msgid "Pending reviewers (%i)"
 msgstr "Reviewers potentiels (%i)"
@@ -2969,6 +2974,17 @@ msgstr ""
 "vides et assignées automatiquement. Une review qui est donc soit "
 "manuellement assignée, soit déjà remplie sera gardée."
 
+msgid "ComputeAutoReviewView information about"
+msgstr ""
+"Relancer l'assignation est utile en cas de "
+"nouveau reviewer, ou de nouveau conflit d'intérêt (cf <a href=\"/doc/"
+"configure_instance.html#assign-reviewers-again\" "
+"target=\"_blank\">documentation</a>).<br/>"
+"Vous ne pouvez pas perdre de donnée en relançant l'assignation. Toutes les "
+"reviews attribuées manuellement ou déjà commencées seront conservées. En "
+"d'autre terme un(e) reviewer ne sera pas dé-assigné d'un candidat si iel à "
+"commencé à écrire la review."
+
 msgid "MassMailingWizard.Prepare.title"
 msgstr "Préparer le message"
 
diff --git a/src/strass/strass_app/templates/strass_app/review_allocation.html b/src/strass/strass_app/templates/strass_app/review_allocation.html
index 5f4d10cadb5a0991c098d33f069d6ae7450b5fc3..4081e72304576cc9b7beca05e0a2aca6ac0c08ca 100644
--- a/src/strass/strass_app/templates/strass_app/review_allocation.html
+++ b/src/strass/strass_app/templates/strass_app/review_allocation.html
@@ -41,6 +41,7 @@
      data-target="#modalForm"
      data-modal-title="{%trans 'Automatized allocation of reviewer'%}"
      data-modal-submit="{%trans 'Compute automatic allocation'%}"
+     data-modal-css-class="modal-dialog modal-lg"
   ><i class="fa fa-cogs"></i> {%trans "Compute automatic allocation" %}...</a>
 </div>
 {% endblock page_title_right%}
diff --git a/src/strass/strass_app/tests/test_mass_mailing.py b/src/strass/strass_app/tests/test_mass_mailing.py
index fbbf6bb63551706c0b2ce3e84753b111a1997a47..bf2c12b4cf64bca8c341954af4868d1e54256ceb 100644
--- a/src/strass/strass_app/tests/test_mass_mailing.py
+++ b/src/strass/strass_app/tests/test_mass_mailing.py
@@ -550,6 +550,74 @@ class TestViews2(BaseTestCase):
                     self.assertIn(f'http://testserver{reverse("strass:review-detail", args=[pk])}', m.body)
 
 
+@override_settings(
+    MEDIA_ROOT=os.path.join(
+        settings.BASE_DIR,
+        'persistent_volume',
+        f'{MEDIA_DIR}_3',
+    ),
+    STORAGE_ROOT=os.path.join(
+        settings.BASE_DIR,
+        'persistent_volume',
+        '.storage_for_test_3',
+    ),
+)
+class TestViews3(BaseTestCase):
+    def test_reviewers_without_profils(self):
+        steps = list()
+        live_settings.show_email_as_message = False
+        mail_count = len(mail.outbox)
+        self.client.force_login(self.jury_manager)
+        business_logic.get_reviewers().first().profiles.clear()
+        business_logic.get_reviewers().last().profiles.clear()
+        self.assertTrue(models.Profile.objects.first().myuserpreferences_set.all().exists())
+        self.assertFalse(business_logic.get_reviewers().first().profiles.exists())
+
+        #######################################################################
+        # Use user group, also send a test email
+        #######################################################################
+        url = reverse('strass:mass-mailing')
+        response_home = self.client.get(url, follow=True)
+        target = response_home.redirect_chain[0][0]
+        step_name = target.split("/")[-2]
+        form_data = {
+            step_name + "-sender": self.jury_manager.pk,
+            step_name + "-recipients": "reviewers_without_profile",
+            step_name + "-subject": "mail subject",
+            step_name + "-body": "hi [first_name] x [last_name] !",
+            "mass_mailing_wizard-current_step": step_name,
+        }
+        response = self.client.post(target, form_data, follow=True)
+        self.assertEqual(response.status_code, 200)
+        steps.append(WizardStep(target=target, response=response, form_data=form_data, step_name=step_name))
+        self.write_in_tmp_file(response)
+        del target, response, form_data, step_name
+
+        target = steps[-1].response.redirect_chain[0][0]
+        step_name = target.split("/")[-2]
+        form_data = {
+            step_name + "-agree": 'on',
+            "mass_mailing_wizard-current_step": step_name,
+        }
+        response = self.client.post(target, form_data, follow=True)
+        self.assertEqual(response.status_code, 200)
+        steps.append(WizardStep(target=target, response=response, form_data=form_data, step_name=step_name))
+        del target, response, form_data, step_name
+
+        recipients = set()
+        for m in mail.outbox[mail_count:]:
+            recipients |= set(m.recipients())
+
+        self.assertEqual(
+            recipients,
+            {
+                business_logic.get_reviewers().get(pk=1).email,
+                business_logic.get_reviewers().first().email,
+                business_logic.get_reviewers().last().email,
+            },
+        )
+
+
 class _TestViews(BaseTestCase):
     def _test_with_reviewers(self):
         steps = list()
diff --git a/src/strass/strass_app/views.py b/src/strass/strass_app/views.py
index 37cc3e6ed4af2ba3734dfa83f6e1b0f5d42826b5..3c426076a2c8dcaa9919e68cde71684f7701ec70 100644
--- a/src/strass/strass_app/views.py
+++ b/src/strass/strass_app/views.py
@@ -2120,6 +2120,22 @@ class ReviewAllocationFormView(mixins.OnlyJuryManagerMixin, ModalFormMixin, Form
     success_url = reverse_lazy('strass:review-allocation')
     form_class = forms.ReviewPerCandidateForm
 
+    def get_form(self, form_class=None):
+        form = super().get_form()
+        helper = FormHelper()
+        helper.form_tag = False
+        helper.layout = layout.Layout(
+            layout.HTML('<p>' + gettext("ComputeAutoReviewView information about") + '</p>'),
+            'stage',
+            'count',
+            'match',
+            'reviewer_groups',
+            'candidate_groups',
+            'refused_candidate_groups',
+        )
+        form.helper = helper
+        return form
+
     def form_valid(self, form):
         review_allocation.compute_allocation(
             min_review_per_candidate=form.cleaned_data["count"],