diff --git a/.gitignore b/.gitignore
index a41b38cd12c489f319f94d58ac21566d382e1323..54ee305acbb0d48a1ffe5b10f756ef818da2c4dc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,4 +12,8 @@ __pycache__
 htmlcov/
 .storage/
 doc/_build/
+doc/build/
+src/strass/.static.shared.docker
+
 /tokens.sh
+/src/strass/persistent_volume/coverage.xml
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 35b5d9c6b3e3f3f164cda875123e91ab7b02adb0..c76a765089bcbfdf51d6f6433d6516270aa71dd6 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -198,7 +198,7 @@ stop_and_delete_in_prod:
 
 build_pages:
   needs: []
-  image: python:3.7
+  image: python:3.11
   stage: build
   script:
   - pip install -r doc/requirements.txt
@@ -263,7 +263,7 @@ tailored-doc:
 
 black:
   needs: []
-  image: python:3.7
+  image: python:3.11
   stage: build
   script:
     - python -m pip install --upgrade pip
@@ -313,7 +313,7 @@ checkCoverageOther:
 test-migrations-and-lang-ar-up-to-date:
   stage: test
   needs: []
-  image: python:3.9-bullseye
+  image: python:3.11
   variables:
     POSTGRES_HOST: db-local
     POSTGRES_DBNAME: postgres
diff --git a/chart/templates/config-map-app.yaml b/chart/templates/config-map-app.yaml
index 5be354362a554d81f67e3105793bbaa1717aa0bf..0ba631d191c6d9f1aaf6632f313f4de8ab7f72be 100644
--- a/chart/templates/config-map-app.yaml
+++ b/chart/templates/config-map-app.yaml
@@ -5,6 +5,7 @@ metadata:
   labels:
     {{- include "chart.labels" . | nindent 4 }}
 data:
+  ADMIN_EMAIL: {{ .Values.admin_email | quote }}
   ALLOWED_HOSTS: {{ printf "strass-%s.dev.pasteur.cloud,%s-internal.pasteur.cloud,%s.pasteur.cloud" $.Release.Name $.Release.Name $.Release.Name | quote }}
   DEBUG: {{ .Values.debug | quote }}
   PROJECT_NAME: "strass"
diff --git a/chart/values.prod.yaml b/chart/values.prod.yaml
index 5a031d8c0aa264e3df5777fd0652c0caaaec46c7..c296be870abf9c0ca54e749f86facd0180bf4128 100644
--- a/chart/values.prod.yaml
+++ b/chart/values.prod.yaml
@@ -3,7 +3,7 @@ resources:
     memory: "2Gi"
     cpu: "2000m"
 
-
+admin_email: "strass-admin@pasteur.fr"
 isProd: true
 publiclyOpen: true
 storageClassName: ceph-block
\ No newline at end of file
diff --git a/chart/values.yaml b/chart/values.yaml
index 73c1bf9b8b85fee23127ada1068c340d606d1444..017a5dfa82a36178c73ac523c41076e3036234a6 100644
--- a/chart/values.yaml
+++ b/chart/values.yaml
@@ -7,6 +7,7 @@ resources:
     cpu: 1
 
 
+admin_email: ""
 isProd: false
 publiclyOpen: false
 #storageClassName: isilon
diff --git a/doc/_static/candidate-set-status.png b/doc/_static/candidate-set-status.png
new file mode 100644
index 0000000000000000000000000000000000000000..d7f70f0819f69b4c7af919a046f114db5bde675a
Binary files /dev/null and b/doc/_static/candidate-set-status.png differ
diff --git a/doc/_static/jury-manager-accept.png b/doc/_static/jury-manager-accept.png
new file mode 100644
index 0000000000000000000000000000000000000000..83cb2cf92cc3a48bbfe91ee35d83dc4681411919
Binary files /dev/null and b/doc/_static/jury-manager-accept.png differ
diff --git a/doc/_static/question-create-choices.png b/doc/_static/question-create-choices.png
index ba0e6253cf6013708dee12c23b93272ea13b666f..e9df22ae1146d1e410cbe92bd92ebaf4f9a2116f 100644
Binary files a/doc/_static/question-create-choices.png and b/doc/_static/question-create-choices.png differ
diff --git a/doc/_static/question-create-question.png b/doc/_static/question-create-question.png
index 189cd6e0bcb645e923db50cc1785c31bfdc2296f..2ea59db4ead0a1e0a0867cb6ce66845946fc9cb9 100644
Binary files a/doc/_static/question-create-question.png and b/doc/_static/question-create-question.png differ
diff --git a/doc/_static/question-create-range.png b/doc/_static/question-create-range.png
index c306c85a2864ee3882ac2c2894532d8eb5a8b7d5..aec8524b724ce6fe20e736304a813f4bed77d6bb 100644
Binary files a/doc/_static/question-create-range.png and b/doc/_static/question-create-range.png differ
diff --git a/doc/_static/question-create-text.png b/doc/_static/question-create-text.png
index 311aef5baab9620273b779b3b38c5a60ec050120..9248be1d602235c043ff8c46ffaa5b5587dff872 100644
Binary files a/doc/_static/question-create-text.png and b/doc/_static/question-create-text.png differ
diff --git a/doc/_static/question-create.png b/doc/_static/question-create.png
index fd1a5dd4e9e05a48528afa335f73127bfd0687a1..63e4969e46347d7f0534129284b61b6c380705c2 100644
Binary files a/doc/_static/question-create.png and b/doc/_static/question-create.png differ
diff --git a/doc/admin_FAQ.rst b/doc/admin_FAQ.rst
index 6dc24653e6546f09d511cd0e604d32dd9cdee977..5e53936d5ff86ab5c74b2e2b55460db585bc73e8 100644
--- a/doc/admin_FAQ.rst
+++ b/doc/admin_FAQ.rst
@@ -29,3 +29,110 @@ How to load the demo in k8s
     kubectl config use-context strass-dev
     BACKEND_POD=$(kubectl get po -l branch=branch-${CI_COMMIT_REF_SLUG},role=front --output jsonpath='{.items[0].metadata.name}')
     kubectl exec $BACKEND_POD --container django-container -- python manage.py load_demo
+
+
+How to theme STRASS
+-------------------------------------------------------------------------------
+
+Some instance need specific logo, specific colors, ... To comply to those needs a theme have to be made.
+It consists in creating a python package which will place in the application various html fragments and files in various folders.
+An example can be found at https://gitlab.pasteur.fr/bbrancot/django-basetheme-bootstrap-ebaii-theme
+
+To create a new theme :
+
+* Fork the project
+* Rename ``ebaii`` to ``yourtheme`` (tips: using lower case without ``-`` or ``_`` will save you many issues)
+* Update setup.cfg (name, version, description)
+* Adapte/remove html fragments in ``yourtheme/templates/strass_app/``
+* Create/edit css in ``yourtheme/static/css/``, to include more file adapte ``yourtheme/templates/strass_app/extra_in_header.html``
+* Images can be shipped in ``yourtheme/static/img/``
+
+Once push to https://gitlab.pasteur.fr/api/v4/projects/alovelace%2Fdjango-basetheme-bootstrap-mytheme
+you need to use the theme in the application like this:
+
+.. code-block:: diff
+
+    diff --git a/src/strass/strass/settings.py b/src/strass/strass/settings.py
+    index 0d6c462d..889acf67 100644
+    --- a/src/strass/strass/settings.py
+    +++ b/src/strass/strass/settings.py
+    @@ -53,7 +53,6 @@ INSTALLED_APPS = [
+         'crispy_bootstrap4',
+         'django_kubernetes_probes',
+         'strass_app',
+    +    'yourtheme',
+     ]
+
+     if DEBUG or config('USE_DJANGO_EXTENSIONS', default=False, cast=bool):
+    index 94b6c670..14ebfb78 100644
+    --- a/src/strass/requirements.txt
+    +++ b/src/strass/requirements.txt
+    @@ -7,8 +7,6 @@ python-decouple
+     django-basetheme-bootstrap>=1.6.1
+     --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/alovelace%2Fdjango-basetheme-bootstrap-mytheme/packages/pypi/simple
+    +django-basetheme-bootstrap-mytheme>=1.0.0
+     django-formtools
+     csscompressor
+     coverage
+
+How to remove a language
+-------------------------------------------------------------------------------
+
+French and English language cannot be removed as the internationalization system uses this two languages to check it soundness.
+For the other language, just remove the language from ``LANGUAGES`` in ``src/strass/strass/settings.py``.
+
+.. _add_lang:
+
+How to add a new language
+-------------------------------------------------------------------------------
+
+For the call
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The first step to add a new language is not make it available for Django, do it in ``src/strass/strass/settings.py``.
+Once added the language will be available in the language menu in the top right, and call will be also proposed in this new language.
+
+.. code-block:: diff
+
+    diff --git a/src/strass/strass/settings.py b/src/strass/strass/settings.py
+    index 889acf67..275e09d5 100644
+    --- a/src/strass/strass/settings.py
+    +++ b/src/strass/strass/settings.py
+    @@ -134,6 +134,7 @@ LOCALE_PATHS = [
+     LANGUAGES = [
+         ('en', 'English'),
+         ('fr', 'Français'),
+    +    ('de', 'German'),
+     ]
+
+     TIME_ZONE = 'CET'
+
+In all the application
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. note::
+   Translating the application in a new language implies to translate ~800 text entries.
+
+To translate the whole application is a new language the code base have to be locally cloned, virtualenv created and dependencies installed.
+
+.. code-block:: shell
+
+   # create the translation file for django itself
+   python manage.py makemessages -l de --no-location
+   # create the translation file for internationalization in javascript
+   python manage.py makemessages -d djangojs -l de --no-location
+
+Then edit the following file :
+
+* /src/strass/strass_app/locale/de/LC_MESSAGES/django.po
+* /src/strass/strass_app/locale/de/LC_MESSAGES/djangojs.po
+* /src/strass/locale/de/LC_MESSAGES/django.po
+
+To validate your file, regularly run the following command.
+
+.. code-block:: shell
+
+   python manage.py compilemessages --ignore *venv*
+
diff --git a/doc/conf.py b/doc/conf.py
index 73ea40a70cd7d516f0c17d977460ffccf09071cf..558f183e4fe6f3b5a2ef8cccf5f45550f3350567 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -76,7 +76,6 @@ html_context = {
 html_theme_options = {
     "use_edit_page_button": True,
     "secondary_sidebar_items": [
-        "sidebar-nav-bs",
         "page-toc",
         "sourcelink",
         "edit-this-page",
diff --git a/doc/configure_instance.rst b/doc/configure_instance.rst
index 70a7a125d9a9a84a48ad089d46682cf428a5c261..3cebb58eb10da5243f82df8784f61307fc3bf98f 100644
--- a/doc/configure_instance.rst
+++ b/doc/configure_instance.rst
@@ -97,6 +97,25 @@ Link: https://strass-master.dev.pasteur.cloud/setup/#mail-settings
 
 User can contact you at ``/contact/``, but only if you provided the email on which they can contact you. Note that the email is not rendered to the user.
 
+Invite other jury manager
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Link: https://strass-master.dev.pasteur.cloud/setup/users/
+
+.. note::
+   The role of "Jury manager" is essential. This role allows to control all settings of your recruitment process, such as open/close application, and edit questions asked to candidate/reviewer/referee. It is good practice to not be the only one that have this role in case you are unavailable (holidays, sick leave, account issue, ...).
+
+Generate invitation to become a jury manager in the application (button "Invite user to be..." on the top right), and send it to the persons you want them to become jury manager.
+
+Once a person have accepted to be a jury manager, you still have to validate that you want this person to actually be a jury manager by going to Link: https://strass-master.dev.pasteur.cloud/setup/users/
+
+.. image:: _static/jury-manager-accept.png
+  :width: 100%
+  :align: center
+  :alt: Review allocation
+
+Once validated, this person receive an email notification, so does all other jury managers because of the permissions that come with this role.
+No email notification is sent when downgrading status.
+
 Provide the call
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  * Link: https://strass-master.dev.pasteur.cloud/setup/#call-fr
@@ -106,6 +125,25 @@ The call have to be provided in all the available languages of the instance. To
 
 In the homepage, the call is rendered, by clicking on the pencil, you can edit the call in the current language of the application. To edit the call in another language switch between language, the switch is located on the top right of the web site.
 
+It is not possible to remove english nor french due to the unit test that check the soundness of the code base.
+It is possible to provide the call in other language, please contact the dev team to do so and/or read :ref:`add_lang`.
+
+
+.. _open close call:
+
+Define how candidates can apply to the call
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+https://strass-master.dev.pasteur.cloud/setup/#candidate-settings
+
+
+Define when candidate can start applying, until when they can, and until when they can edit their application. Define here if you want to ask for:
+
+* a CV (in pdf), or not.
+* a motivation letter (written in plain text/Markdown in the app), or not.
+* maximum number of profile a candidate can apply for. Set to 0 for no limitation.
+* min and max number of referee. To disable the referee module, set the maximum to 0.
+* opening and closing date for applying
+
 
 Define the profiles
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -116,24 +154,41 @@ 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.
 
-Define application status
+
+Using STRASS for a workshop
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Link: https://strass-master.dev.pasteur.cloud/setup/#candidate-status
 
+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:
 
-Application statuses will help you managing the application, and the different step of your recruitment. The first status is when the application have just been submitted. You have to define this status before opening the instance to candidate. Accent and space are allowed in the name, markdown in the description. When creating/editing a status, you can define if this status is the default one associated to every new application **when they are submitted**, thus choose it before candidate can apply.
+* Go to https://strass-master.dev.pasteur.cloud/setup/#lang-override and enable the debug mode.
+  This will render a message everywhere a string can be override.
 
-Define how candidates can apply to the call
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-https://strass-master.dev.pasteur.cloud/setup/#candidate-settings
+* Repetitively browse to the following URL in all the language available:
 
+  * https://strass-master.dev.pasteur.cloud/setup/
 
-Define when candidate can start applying, until when they can, and until when they can edit their application. Define here if you want to ask for:
+  * https://strass-master.dev.pasteur.cloud/about/
+
+  * https://strass-master.dev.pasteur.cloud/candidate/apply/Intro/ (using anonymous navigation, after opening for application temporarily cf :ref:`open close call`)
+
+  * https://strass-master.dev.pasteur.cloud/candidate/apply/Information/ (again using anonymous navigation, with application temporarily open)
+
+* Go back to https://strass-master.dev.pasteur.cloud/setup/#lang-override and disable the debug mode.
+
+* Do not forget to restore the correct date for application submission
+
+* Go to https://strass-master.dev.pasteur.cloud/admin/language_override/languageoverride/
+
+* All text fragments that can be renamed are now present, edit the value field one by one, uncheck `is_draft` and save.
+
+* A summary of what translations are made and pending can be seen in the footer of the highlighted card at https://strass-master.dev.pasteur.cloud/setup/#lang-override
+
+Define application status
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Link: https://strass-master.dev.pasteur.cloud/setup/#candidate-status
 
-* a CV (in pdf)
-* a motivation letter (written in plain text/Markdown in the app)
-* maximum number of profile a candidate can apply for
-* min and max number of referee. To disable the referee module, set the maximum to 0.
+
+Application statuses will help you managing the application, and the different step of your recruitment. The first status is when the application have just been submitted. You have to define this status before opening the instance to candidate. Accent and space are allowed in the name, markdown in the description. When creating/editing a status, you can define if this status is the default one associated to every new application **when they are submitted**, thus choose it before candidate can apply.
 
 
 Define candidate questions
@@ -142,6 +197,8 @@ Link: https://strass-master.dev.pasteur.cloud/setup/candidate/question/
 
 Define what are the question you want the candidate to answer when zhe apply. Questions are asked in a dedicated step during application, after the Information step and before asking for referee (if enabled).
 
+More help on the question system can be found :ref:`here <Question system>`.
+
 Define recommendation settings
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 Link: https://strass-master.dev.pasteur.cloud/setup/#recommendation-settings
@@ -151,6 +208,8 @@ When submitting an application the app will send an invitation to provide a reco
 so does when adding new referees.
 Define until when referee will be able to submit an recommandation, and how many referee a candidate can indicate.
 
+More help on the question system can be found :ref:`here <Question system>`.
+
 Define recommendation questions
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 Link: https://strass-master.dev.pasteur.cloud/setup/recommendation/question/
@@ -249,6 +308,8 @@ Link: https://strass-master.dev.pasteur.cloud/setup/review/question/
 
 Define what are the question you can the reviewer to answer. Numerical scale answer can be used in overall score to give you an overview of the evaluation of the candidates. Free text area are also allowed. A mix of both is a good idea, including numerical scale with a text area to justify the grade.
 
+More help on the question system can be found :ref:`here <Question system>`.
+
 Invite reviewers
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  * Link: https://strass-master.dev.pasteur.cloud/setup/#reviewers-and-invite
@@ -329,7 +390,29 @@ A reviewer, named Alice, is having to much review to do so you searched for more
 Review period is closes
 -------------------------------------------------------------------------------
 
-*Reviewers cannot edit the review anymore, here come the interview session stage*
+*Reviewers cannot edit the review anymore, select the candidate, create your juries*
+
+Sort candidate between selected, waiting list, rejected, ...
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Link: https://strass-master.dev.pasteur.cloud/candidates/
+
+Once reviews phase is completed, select which candidates will be interviewed.
+
+.. image:: _static/candidate-set-status.png
+  :width: 100%
+  :align: center
+  :alt: Review allocation
+
+You may need for the selection process to export candidates along with their reviews, this can be done in one pdf, or with an archive with one pdf per candidate. In a given file you will find the application, cv, motivation letter, candidate answers, recommendations, reviews.
+
+You may also need a csv export with all information (billing, badges for the face to face interviews, statistics, ...).
+
+.. note::
+    Whatever your need is, you have to comply to `GDPR <https://www.cnil.fr/fr/reglement-europeen-protection-donnees>`_. A guide focused on recruitment is available at https://www.cnil.fr/fr/le-guide-du-recrutement. Hare are some take away from this document (as of sept. 2024):
+
+    * Candidat data should be kept up to three months after the end of the recruitment process in order to motivate decision made for this recruitment (p50).
+    * If refused candidate accepts, their data can be kept up to two years (p50), and only for futur job position.
+
 
 .. _Juror settings:
 
@@ -471,12 +554,9 @@ Recruitment is done
 
 Export application settings
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Link: https://gitlab.pasteur.fr/hub/strass/-/issues/130
-
-.. warning::
-    Feature not implemented yet, see https://gitlab.pasteur.fr/hub/strass/-/issues/130
+Link: https://strass-master.dev.pasteur.cloud/setup/#io-config
 
-Export your recruitment settings. It includes questions to reviewers and referee, profiles, candidates status, ... It can contains all information that are not related to individuals.
+Export your recruitment settings. It includes questions to candidates reviewers and referee, profiles, candidates status, language override ... It can contains all information that are not related to individuals.
 
 
 
diff --git a/doc/k8s.rst b/doc/k8s.rst
index 7d040bc2aafa7be705b31bd9d30203fd7dd85a9b..626759f09a595f393ce1acd8b46134cbdb0bc9b1 100644
--- a/doc/k8s.rst
+++ b/doc/k8s.rst
@@ -35,7 +35,7 @@ Get the running pods
 
 You get
 
-.. code-block:: log
+.. code-block:: text
 
     NAME                                                          READY   STATUS    RESTARTS   AGE
     doc-pod-master                                                1/1     Running   3          6d16h
@@ -66,7 +66,7 @@ To get the log, run the following command, do not forget to specify which contai
 
 You get:
 
-.. code-block:: log
+.. code-block:: text
 
     Copy static files to shared directory
     cp -rf /code/.static/* /code/.static.shared
diff --git a/doc/overall_score_system.rst b/doc/overall_score_system.rst
index 1064b3ad1bee896e72b12033167fa60848c1b070..035f50017da30ddc688c8e82eee67375a6c5bf5e 100644
--- a/doc/overall_score_system.rst
+++ b/doc/overall_score_system.rst
@@ -39,7 +39,7 @@ Textual range
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 For a question answered with a text range such as ``No;Neutral;Yes``.
 We use the position of the answer in the list of possible answer, and then convert it as for numerical range.
-In this example, ``No`` is ``0%``, ``Neutral`` is ``50%`` and ``Yes`` is ``100%``.
+In this example, ``No`` is ``0%``, ``Neutral`` is ``50%`` and ``Yes`` is ``100%`` (cf :ref:`question system text range`)
 
 Aggregating the answer
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/doc/question_system.rst b/doc/question_system.rst
index fe242adf894adcd324ab61bee14be0614108ca92..1bbd7b86eb60f36bed79786442dcd66aedba63ed 100644
--- a/doc/question_system.rst
+++ b/doc/question_system.rst
@@ -23,6 +23,8 @@ Question
 
 Provide here the question
 
+.. _question system text range:
+
 Answer with a range
 -------------------------------------------------------------------------------
 
@@ -31,10 +33,10 @@ Answer with a range
   :align: center
 
 You can ask the user to answer with a range, it can be directly numerical,
-or an ordered list of work such as "no;neutral;yes" for answering to "Would you work with this person again.
+or an ordered list of work such as "no;neutral;yes" for answering to "Would you work with this person again?"
 
 Whether it is numerical of text range, the answer can be used in the overall score (cf :ref:`Overall score`).
-When it is a text range, the first choice is translated to 0, the second to 1 and so on.
+When it is a text range, choices are converted in percentage from 0% to 100% such as in the example here above.
 
 Answer with one or many choices
 -------------------------------------------------------------------------------
diff --git a/doc/user_documentation.rst b/doc/user_documentation.rst
index c0b8b803a83887f60773c19f84519d9adefa5b38..c0f371c8e0b7f1ce50b5184e74e261bd429d94c8 100644
--- a/doc/user_documentation.rst
+++ b/doc/user_documentation.rst
@@ -6,5 +6,4 @@ User Doc
     :maxdepth: 3
 
     how_to_recommend
-    how_to_review
-    user_FAQ
\ No newline at end of file
+    how_to_review
\ No newline at end of file
diff --git a/src/strass/Dockerfile b/src/strass/Dockerfile
index 537fa889291afd89b3749ad166c2da92296a46d8..dc3bf61c9965925c3286639d9f90b64ff533aa3e 100644
--- a/src/strass/Dockerfile
+++ b/src/strass/Dockerfile
@@ -1,4 +1,4 @@
-FROM python:3.8-slim
+FROM python:3.11-slim
 ENV PYTHONUNBUFFERED 1
 ENV PROJECT_NAME strass
 
@@ -19,6 +19,7 @@ RUN addgroup --gid 1000 kiwi \
         pkg-config \
         gcc \
         g++ \
+        libmagic1 \
  && rm -rf /var/lib/apt/lists/* \
  && python -m pip install --upgrade pip \
  && pip install csscompressor gunicorn
diff --git a/src/strass/cspmailreports/__init__.py b/src/strass/cspmailreports/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/strass/cspmailreports/apps.py b/src/strass/cspmailreports/apps.py
new file mode 100644
index 0000000000000000000000000000000000000000..18813633f313f54b8193e9e186300a29f684a810
--- /dev/null
+++ b/src/strass/cspmailreports/apps.py
@@ -0,0 +1,29 @@
+from django.apps import AppConfig
+import django.core.checks
+
+from cspmailreports.conf import app_settings
+
+
+class CspmailreportsConfig(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'cspmailreports'
+
+
+@django.core.checks.register()
+def check_settings(app_configs, **kwargs):
+    errors = []
+    if app_settings.dos_cooldown <= 0:
+        errors.append(
+            django.core.checks.Error(
+                "CSP_MAIL_REPORTS_COOLDOWN_IN_SECONDS must be greater than 0",
+                id="cspmailreports.E001",
+            )
+        )
+    if app_settings.max_report_before_cooldown <= 0:
+        errors.append(
+            django.core.checks.Error(
+                "CSP_MAIL_REPORTS_MAX_BEFORE_COOLDOWN must be greater than 0",
+                id="cspmailreports.E002",
+            )
+        )
+    return errors
diff --git a/src/strass/cspmailreports/conf.py b/src/strass/cspmailreports/conf.py
new file mode 100644
index 0000000000000000000000000000000000000000..c2b1186dc58f9e17512d506c609ba9985cc8d2dd
--- /dev/null
+++ b/src/strass/cspmailreports/conf.py
@@ -0,0 +1,32 @@
+from django.conf import settings
+
+
+class Settings:
+    def __init__(self):
+        self.__print_in_log = None
+        self.__max_report_before_cooldown = None
+        self.__dos_cooldown = None
+
+    def _reset_cache(self):
+        self.__init__()
+
+    @property
+    def print_in_log(self) -> bool:
+        if self.__print_in_log is None:
+            self.__print_in_log = getattr(settings, "CSP_MAIL_REPORTS_LOG", True)
+        return self.__print_in_log
+
+    @property
+    def dos_cooldown(self) -> int:
+        if self.__dos_cooldown is None:
+            self.__dos_cooldown = getattr(settings, "CSP_MAIL_REPORTS_COOLDOWN_IN_SECONDS", 30 * 60)
+        return self.__dos_cooldown
+
+    @property
+    def max_report_before_cooldown(self) -> bool:
+        if self.__max_report_before_cooldown is None:
+            self.__max_report_before_cooldown = getattr(settings, "CSP_MAIL_REPORTS_MAX_BEFORE_COOLDOWN", 50)
+        return self.__max_report_before_cooldown
+
+
+app_settings = Settings()
diff --git a/src/strass/cspmailreports/tests.py b/src/strass/cspmailreports/tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..f178678b9193f30bc4799f8eec02b871ec7a9a56
--- /dev/null
+++ b/src/strass/cspmailreports/tests.py
@@ -0,0 +1,141 @@
+import json
+import logging
+from typing import Dict
+
+from django.core import mail
+from django.test import TestCase, override_settings
+from django.urls import reverse
+from django.urls import reverse_lazy
+
+import cspmailreports.apps
+import cspmailreports.conf
+
+logger = logging.getLogger(__name__)
+
+
+class CSPTooledTestCase(TestCase):
+    url = reverse_lazy('cspmailreports:csp-report')
+
+    def setUp(self):
+        super().setUp()
+        cspmailreports.conf.app_settings._reset_cache()
+
+    @staticmethod
+    def fake_report(referrer="http://127.0.0.1:8080") -> Dict:
+        return {
+            "csp-report": {
+                "blocked-uri": "inline",
+                "disposition": "enforce",
+                "document-uri": f"{referrer}/about/",
+                "effective-directive": "script-src-elem",
+                "line-number": 215,
+                "original-policy": "default-src 'self' *; script-src 'self' cdn.datatables.net",
+                "referrer": referrer,
+                "script-sample": "",
+                "source-file": f"{referrer}/about/",
+                "status-code": 200,
+                "violated-directive": "script-src-elem",
+            }
+        }
+
+    def report(self, report=None):
+        if report is None:
+            report = self.fake_report()
+        return self.client.post(self.url, data=json.dumps(report), content_type='application/csp-report')
+
+
+class TestMain(CSPTooledTestCase):
+    def test_works(self):
+        url = reverse('cspmailreports:csp-report')
+        # check get ko
+        self.assertNotIn(self.client.get(url).status_code, [200])
+        # check post works
+        self.assertIn(self.report().status_code, [200])
+
+    def test_invalid_mime_type_refused(self):
+        self.assertNotIn(self.client.post(self.url, data=self.fake_report()).status_code, [200])
+
+    def test_invalid_data_accepted(self):
+        self.assertIn(
+            self.client.generic(
+                "POST",
+                self.url,
+                'zerzerz!sdf{',
+                'application/csp-report',
+            ).status_code,
+            [200],
+        )
+
+
+@override_settings(
+    CSP_MAIL_REPORTS_MAX_BEFORE_COOLDOWN=10,
+    ADMIN=(('ada', 'ada@pasteur.fr'),),
+    DEBUG=False,
+)
+class TestDOS(CSPTooledTestCase):
+    def test_it(self):
+        mail_count = len(mail.outbox)
+        self.client.defaults['REMOTE_ADDR'] = '1.2.3.4'
+        # trigger dos
+        for i in range(cspmailreports.conf.app_settings.max_report_before_cooldown):
+            self.assertIn(self.report().status_code, [200])
+            mail_count += 1
+        self.assertEqual(mail_count, len(mail.outbox))
+        # check blocked
+        self.assertIn(self.report().status_code, [429])
+        self.assertEqual(mail_count, len(mail.outbox))
+        # check other is not blocked
+        self.client.defaults['REMOTE_ADDR'] = '1.2.3.5'
+        self.assertIn(self.report().status_code, [200])
+        mail_count += 1
+        self.assertEqual(mail_count, len(mail.outbox))
+
+
+@override_settings(
+    ADMINS=(('ada', 'ada@pasteur.fr'),),
+)
+class TestMailAdmin(CSPTooledTestCase):
+    def test_it(self):
+        mail_count = len(mail.outbox)
+        self.assertIn(self.report().status_code, [200])
+        mail_count += 1
+        self.assertEqual(mail_count, len(mail.outbox))
+
+
+@override_settings(
+    ADMINS=(),
+)
+class TestMailNoAdmin(CSPTooledTestCase):
+    def test_it(self):
+        mail_count = len(mail.outbox)
+        self.assertIn(self.report().status_code, [200])
+        mail_count += 0  # in debug not mail to admin is sent
+        self.assertEqual(mail_count, len(mail.outbox))
+
+
+@override_settings(
+    CSP_MAIL_REPORTS_MAX_BEFORE_COOLDOWN=-1,
+)
+class TestCheck1(CSPTooledTestCase):
+    def test_it(self):
+        cspmailreports.conf.app_settings._reset_cache()
+        self.assertEqual(len(cspmailreports.apps.check_settings(None)), 1)
+
+
+@override_settings(
+    CSP_MAIL_REPORTS_COOLDOWN_IN_SECONDS=-1,
+)
+class TestCheck2(CSPTooledTestCase):
+    def test_it(self):
+        cspmailreports.conf.app_settings._reset_cache()
+        self.assertEqual(len(cspmailreports.apps.check_settings(None)), 1)
+
+
+@override_settings(
+    CSP_MAIL_REPORTS_MAX_BEFORE_COOLDOWN=-1,
+    CSP_MAIL_REPORTS_COOLDOWN_IN_SECONDS=-1,
+)
+class TestCheckAll(CSPTooledTestCase):
+    def test_it(self):
+        cspmailreports.conf.app_settings._reset_cache()
+        self.assertEqual(len(cspmailreports.apps.check_settings(None)), 2)
diff --git a/src/strass/cspmailreports/urls.py b/src/strass/cspmailreports/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..5c0ab9d08a19b2cae922b3b8071907246bf5a456
--- /dev/null
+++ b/src/strass/cspmailreports/urls.py
@@ -0,0 +1,8 @@
+from django.urls import re_path
+
+import cspmailreports.views
+
+app_name = 'cspmailreports'
+urlpatterns = [
+    re_path(r'^report/$', cspmailreports.views.report_csp, name='csp-report'),
+]
diff --git a/src/strass/cspmailreports/utils.py b/src/strass/cspmailreports/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..59043a4284731284084c86d67e91528a8a062945
--- /dev/null
+++ b/src/strass/cspmailreports/utils.py
@@ -0,0 +1,33 @@
+import json
+import logging
+
+from django.core.cache import cache
+from cspmailreports.conf import app_settings
+
+logger = logging.getLogger(__name__)
+
+
+def is_flagged_as_dos(request) -> bool:
+    ip_address = request.META.get("REMOTE_ADDR")
+    cache_key = f"csp_report_attempts:{ip_address}"
+    csp_report_attempts = cache.get(cache_key, 0) + 1
+    cache.set(cache_key, csp_report_attempts, app_settings.dos_cooldown)
+    return csp_report_attempts > app_settings.max_report_before_cooldown
+
+
+def create_report(request) -> str:
+    agent = request.META.get('HTTP_USER_AGENT', '')
+    report = request.body
+    if isinstance(report, bytes):
+        report = report.decode('utf-8')
+    try:
+        report = json.dumps(
+            json.loads(report),
+            indent=4,
+            sort_keys=True,
+        )
+    except ValueError as e:
+        print(e)
+        ip = request.META.get("REMOTE_ADDR")
+        logger.warning(f'Invalid CSP report violation from "{ip}": "{report[:20]}..."')
+    return f"HTTP user agent :\n{agent}\n\nReport:\n{report}"
diff --git a/src/strass/cspmailreports/views.py b/src/strass/cspmailreports/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..4acfa6c605f43487cfeca5d30793bcd6c1ab42f6
--- /dev/null
+++ b/src/strass/cspmailreports/views.py
@@ -0,0 +1,25 @@
+import logging
+
+from django.core.mail import mail_admins
+from django.http import HttpResponse
+from django.views.decorators.csrf import csrf_exempt
+from django.views.decorators.http import require_POST
+
+import cspmailreports.conf
+import cspmailreports.utils
+
+logger = logging.getLogger(__name__)
+
+
+@require_POST
+@csrf_exempt
+def report_csp(request):
+    if request.content_type != 'application/csp-report':
+        return HttpResponse(status=415)
+    if cspmailreports.utils.is_flagged_as_dos(request):
+        return HttpResponse(status=429, headers=[("Retry-After", cspmailreports.conf.app_settings.dos_cooldown)])
+    report = cspmailreports.utils.create_report(request)
+    if cspmailreports.conf.app_settings.print_in_log:
+        logger.warning(f'CSP violation report:\n{report}')
+    mail_admins("CSP violation report", report)
+    return HttpResponse()
diff --git a/src/strass/requirements.txt b/src/strass/requirements.txt
index 94b6c67086d4bda1ba067f9be953759b633e41a4..bbbc6f82ed76be72fdc37d5bce791b2a024344e7 100644
--- a/src/strass/requirements.txt
+++ b/src/strass/requirements.txt
@@ -4,7 +4,7 @@ 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.6.1
+django-basetheme-bootstrap>=1.8.2
 --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/bbrancot%2Fdjango-basetheme-bootstrap-ebaii-theme/packages/pypi/simple
@@ -14,7 +14,7 @@ csscompressor
 coverage
 Faker>=5
 pdfkit
-PyPDF2~=3.0.1
+pypdf
 pdftotext
 freezegun
 markdown
@@ -25,4 +25,6 @@ tblib # needed when running django tests in parallele to get the error when fail
 tqdm
 gunicorn
 qrcode
+python-magic # to check mime type
+django-csp
 #END OF FILE
diff --git a/src/strass/strass/settings.py b/src/strass/strass/settings.py
index 0d6c462d5b8f3fd40cd41bd289e4c25de8ffec04..365fc82398903bd814f3e14db01c772722a9323f 100644
--- a/src/strass/strass/settings.py
+++ b/src/strass/strass/settings.py
@@ -15,6 +15,7 @@ from socket import gethostname, gethostbyname
 
 import errno
 from decouple import config
+from django.urls import reverse_lazy
 
 # Build paths inside the project like this: BASE_DIR / 'subdir'.
 BASE_DIR = Path(__file__).resolve().parent.parent
@@ -54,6 +55,8 @@ INSTALLED_APPS = [
     'django_kubernetes_probes',
     'strass_app',
     'ebaii',
+    'csp',
+    'cspmailreports',
 ]
 
 if DEBUG or config('USE_DJANGO_EXTENSIONS', default=False, cast=bool):
@@ -68,6 +71,7 @@ MIDDLEWARE = [
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
+    'csp.middleware.CSPMiddleware',
 ]
 
 ROOT_URLCONF = 'strass.urls'
@@ -113,6 +117,9 @@ AUTH_PASSWORD_VALIDATORS = [
     },
     {
         'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+        'OPTIONS': {
+            'min_length': 12,
+        },
     },
     {
         'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
@@ -120,6 +127,21 @@ AUTH_PASSWORD_VALIDATORS = [
     {
         'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
     },
+    {
+        'NAME': 'strass_app.password_validation.HasSymbolCharValidator',
+    },
+    {
+        'NAME': 'strass_app.password_validation.HasCapitalLetterCharValidator',
+    },
+    {
+        'NAME': 'strass_app.password_validation.HasDigitLetterCharValidator',
+    },
+    {
+        'NAME': 'strass_app.password_validation.HasNoSubPartFromListValidator',
+    },
+    {
+        'NAME': 'strass_app.password_validation.HasNoAccentedLetterValidator',
+    },
 ]
 
 AUTH_USER_MODEL = 'strass_app.User'
@@ -168,12 +190,16 @@ except OSError as e:
 
 DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
 DATA_UPLOAD_MAX_NUMBER_FIELDS = 10000
+ADMINS = (('admin', config('ADMIN_EMAIL', 'strass-admin@pasteur.fr')),)
+SERVER_EMAIL = 'strass-admin@pasteur.fr'
+EMAIL_SUBJECT_PREFIX = '[STRASS] '
 
 ################################################################################
 # django-crispy-forms
 ################################################################################
 CRISPY_TEMPLATE_PACK = 'bootstrap4'
 CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap4"
+SESSION_COOKIE_SECURE = True
 
 ################################################################################
 # basetheme_bootstrap
@@ -206,3 +232,38 @@ FILE_UPLOAD_HANDLERS = [
 FILE_UPLOAD_MAX_MEMORY_SIZE = 10485760  # 10Mo
 
 ################################################################################
+# DJANGO CSP, and DJANGO CSP REPORTS
+################################################################################
+CSP_DEFAULT_SRC = [
+    "'self'",
+    "*",
+]
+CSP_SCRIPT_SRC = [
+    "'self'",
+    "cdn.datatables.net",
+    "cdnjs.cloudflare.com",
+    "code.jquery.com",
+    "plausible.pasteur.cloud",
+    "stackpath.bootstrapcdn.com",
+    "cdn.jsdelivr.net",
+    "www.googletagmanager.com",
+    "www.google-analytics.com",
+]
+CSP_STYLE_SRC = [
+    "'self'",
+    "'unsafe-inline'",
+    '*',
+]
+CSP_IMG_SRC = [
+    "'self'",
+    "*",
+    "data:",
+]
+CSP_EXCLUDE_URL_PREFIXES = (
+    "/candidate/apply/Information/",
+    "/candidate/edit/Information/",
+    "/setup/settings/import/",
+)
+CSP_REPORT_URI = reverse_lazy('cspmailreports:csp-report')
+
+################################################################################
diff --git a/src/strass/strass/urls.py b/src/strass/strass/urls.py
index 6934f048e329c6e6554c731a8dc3728ed3adf24a..cd7386ffbab35f377c748c665cdd35f3e25ce583 100644
--- a/src/strass/strass/urls.py
+++ b/src/strass/strass/urls.py
@@ -26,6 +26,7 @@ urlpatterns = [
     # path('i18n/', include('django.conf.urls.i18n')),
     path('', include('django_kubernetes_probes.urls')),
     path('', include('basetheme_bootstrap.urls')),
+    path('csp/', include('cspmailreports.urls')),
     path('live_settings/', include("live_settings.urls")),
     path('', include('strass_app.urls')),
     path('admin/', admin.site.urls),
diff --git a/src/strass/strass_app/admin.py b/src/strass/strass_app/admin.py
index afbb5d8f7d3b9d484623fc10934abeddded4784d..0ea42d9a3ddbc134ff8198704fad84c35f7efc59 100644
--- a/src/strass/strass_app/admin.py
+++ b/src/strass/strass_app/admin.py
@@ -84,7 +84,7 @@ class CandidateAdmin(ViewOnSiteModelAdmin):
             )
 
         name = 'mark_%s' % re.sub('[\W]+', '_', str(status))
-        return name, (_action, name, 'Set status to "%s"' % status)
+        return name, (_action, name, _('Set status to "%s"') % status)
 
     def get_actions(self, request):
         actions = super().get_actions(request=request)
diff --git a/src/strass/strass_app/exports.py b/src/strass/strass_app/exports.py
index e1eaea6786ec4513f84c1bc010e26ae51b17e2fb..3797a91cae443a021fec48c42e85c86e7d604405 100644
--- a/src/strass/strass_app/exports.py
+++ b/src/strass/strass_app/exports.py
@@ -5,14 +5,12 @@ import os
 import pathlib
 import re
 import tempfile
-from functools import reduce
 from tempfile import NamedTemporaryFile
 from zipfile import ZipFile
 
 import pdfkit
-from PyPDF2 import PdfReader, PdfWriter
+from pypdf import PdfReader, PdfWriter
 from django.conf import settings
-from django.db.models import Q
 from django.utils.text import slugify
 
 from strass_app import models, business_logic
@@ -99,12 +97,17 @@ _static_to_file_path = re.compile(r'( href="| src=")/static([^?"]*)[?]?[A-Za-z\d
 # {% url 'javascript-catalog' %}, but also run the manage.py compilejsi18n to have the file compiled and in static.
 # I thus preferred not, and simple remove this url when exporting (and breaking datatables).
 _get_lang_from_script = re.compile(r'<script src="/([^/]*)/jsi18n/"></script>')
+# In order to remove in-html js script definition, the google-tracker-id has been moved to and on-the-fly generated js
+# file. As wkhtmltopdf download all js file, and cannot access source from current host, we juste remove it.
+_remove_google_tracker = re.compile(r'<script src="/google-tracker-id/"></script>')
+
 _wkhtmltopdf_temp_dir = os.path.join(tempfile.gettempdir(), "wkhtmltopdf")
 
 
 def write_html_to_pdf(f, html):
     # replace all relative static url with filepath
     assert os.path.exists(settings.STATIC_ROOT), "you forgot to run python manage.py collectstatic --link"
+    html = _remove_google_tracker.sub('', html)
     html = _static_to_file_path.sub(r'\1{}\2\3'.format(settings.STATIC_ROOT), html)
     # remove <script language="fr" src="/fr/jsi18n/"></script>
     html = _get_lang_from_script.sub('', html)
@@ -207,24 +210,6 @@ def get_candidate_dumped_to_pdf(
     return filepath
 
 
-__BANNED_STRING_FROM_QUESTION_EXPORT = [
-    'first name',
-    'last name',
-    'family name',
-    'gender',
-    'phone number',
-    'your phone',
-    'email',
-    'prénom',
-    'nom de famille',
-    'genre',
-    'numéros de téléphone',
-    'numéro de téléphone',
-    'courriel',
-    'couriel',
-]
-
-
 def export_candidates(
     request,
     candidates,
@@ -236,26 +221,15 @@ def export_candidates(
         candidates = candidates.prefetch_related('profiles', 'groups', 'candidateanswer_set')
         profiles = list(models.Profile.objects.all())
         groups = list(models.CandidateGroup.objects.all())
-        banning_filters = []
-        for s1 in __BANNED_STRING_FROM_QUESTION_EXPORT:
-            for s in [
-                s1,
-                s1.replace("é", "e"),
-                s1.replace("é", "&eacute;"),
-            ]:
-                banning_filters.append(Q(question__icontains=s))
-                banning_filters.append(Q(question__icontains=s.replace(" ", "")))
-                banning_filters.append(Q(question__icontains=s.replace(" ", "-")))
-                banning_filters.append(Q(question__icontains=s.replace(" ", "--")))
-                banning_filters.append(Q(question__icontains=s.replace(" ", "_")))
-                banning_filters.append(Q(question__icontains=s.replace(" ", "__")))
-                banning_filters.append(Q(question__icontains=s.replace(" ", "&nbsp;")))
-        questions = list(models.CandidateQuestion.objects.exclude(reduce(lambda x, y: x | y, banning_filters)))
+        questions = list(models.CandidateQuestion.objects.all())
         with open(out_filepath, 'w', newline='') as csvfile:
             writer = csv.writer(csvfile, quotechar='"', quoting=csv.QUOTE_MINIMAL)
             header = [
                 models.Candidate._meta.get_field('uid').verbose_name,
                 models.Candidate._meta.get_field('lang').verbose_name,
+                models.Candidate._meta.get_field('first_name').verbose_name,
+                models.Candidate._meta.get_field('last_name').verbose_name,
+                models.Candidate._meta.get_field('email').verbose_name,
             ]
             for p in profiles:
                 header.append(p.name)
@@ -268,6 +242,9 @@ def export_candidates(
                 row = [
                     c.uid,
                     c.lang,
+                    c.first_name,
+                    c.last_name,
+                    c.email,
                 ]
                 for p in profiles:
                     row.append(p in c.profiles.all())
diff --git a/src/strass/strass_app/forms.py b/src/strass/strass_app/forms.py
index d026cd028b52c1073a7ad9bef29ceda3e6c99482..b5ea2b95969ae3057bea79c9599cc43a45d6e4d0 100644
--- a/src/strass/strass_app/forms.py
+++ b/src/strass/strass_app/forms.py
@@ -11,7 +11,9 @@ from django.contrib import messages
 from django.contrib.auth import get_user_model
 from django.contrib.auth.models import Group
 from django.core.exceptions import ValidationError
+from django.core.files.uploadedfile import SimpleUploadedFile
 from django.core.mail import EmailMultiAlternatives
+from django.core.validators import RegexValidator
 from django.db import transaction
 from django.db.models import Q, Case, When, Value, BooleanField
 from django.forms import formset_factory, modelform_factory, inlineformset_factory, modelformset_factory
@@ -19,6 +21,7 @@ from django.forms import widgets
 from django.template.defaultfilters import linebreaksbr, date
 from django.urls import reverse
 from django.utils import timezone, translation
+from django.utils.regex_helper import _lazy_re_compile
 from django.utils.safestring import mark_safe
 from django.utils.translation import gettext_lazy as _, gettext, ngettext
 
@@ -27,7 +30,7 @@ from live_settings import live_settings
 from strass_app import models, business_logic, misc, data_io
 from strass_app.custom_layout_object import Formset
 from strass_app.templatetags.strass_tags import markdown
-from strass_app.utils import get_email_backend, validate_multiple_email
+from strass_app.utils import get_email_backend, validate_multiple_email, safe_pdf
 
 
 class BoostrapSelectMultiple(forms.SelectMultiple):
@@ -61,7 +64,7 @@ class RefereeForm(forms.Form):
         required=True,
         widget=widgets.EmailInput(
             attrs={
-                "onkeyup": "check_institutional_email(this)",
+                "class": "check-institutional-email-target",
             },
         ),
     )
@@ -360,6 +363,14 @@ class CandidateForm(ModelFormWithReadOnly):
         u = get_user_model().objects.filter(email=cleaned_data['email']).first()
         if u and business_logic.is_jury_manager(u):
             raise ValidationError({'email': _("The email cannot be used to apply")})
+
+        if live_settings.cv_enabled__bool:
+            cleaned_data['cv'] = SimpleUploadedFile(
+                "cv.pdf",
+                safe_pdf(cleaned_data['cv']).read(),
+                content_type="application/pdf",
+            )
+
         return cleaned_data
 
 
@@ -514,11 +525,21 @@ class AppSettingsForm(forms.Form):
         label=_("AppSettingsForm.google_tracker_id.label"),
         help_text=_("AppSettingsForm.google_tracker_id.help_text"),
         required=False,
+        validators=[
+            RegexValidator(
+                _lazy_re_compile(r"^[\-a-zA-Z0-9_]+\Z"),
+            )
+        ],
     )
     plausible_data_domain = forms.CharField(
         label=_("AppSettingsForm.plausible_data_domain.label"),
         help_text=_("AppSettingsForm.plausible_data_domain.help_text"),
         required=False,
+        validators=[
+            RegexValidator(
+                _lazy_re_compile(r"^[\-a-zA-Z0-9.]+\Z"),
+            )
+        ],
     )
     cv_enabled = forms.BooleanField(
         label=_("AppSettingsForm.cv_enabled.label"),
@@ -1173,27 +1194,41 @@ class ReviewQuestionModelForm(forms.ModelForm):
                 ),
                 layout.Row(
                     layout.Div(
-                        'range_min',
-                        css_class="col-lg col-12",
-                    ),
-                    layout.Div(
-                        'range_max',
-                        css_class="col-lg col-12",
-                    ),
-                    style="display:None;",
-                ),
-                layout.Row(
-                    layout.Div(
-                        'mapping_range_to_sentence_str',
-                        'text_range',
-                        css_class="col-12",
+                        layout.Div(
+                            layout.Row(
+                                layout.Div(
+                                    'range_min',
+                                    css_class="col-lg col-12",
+                                ),
+                                layout.Div(
+                                    'range_max',
+                                    css_class="col-lg col-12",
+                                ),
+                                style="display:None;",
+                            ),
+                            layout.Row(
+                                layout.Div(
+                                    'mapping_range_to_sentence_str',
+                                    'text_range',
+                                    css_class="col-12",
+                                ),
+                                style="display:None;",
+                            ),
+                            layout.Row(
+                                layout.Div(
+                                    'use_in_global_grade',
+                                    css_class="col",
+                                ),
+                            ),
+                        ),
+                        css_class="col d-flex align-items-center",
                     ),
-                    style="display:None;",
-                ),
-                layout.Row(
                     layout.Div(
-                        'use_in_global_grade',
-                        css_class="col-12",
+                        layout.Div(
+                            layout.HTML('<table class="range-in-global-grade-dest table table-sm"></table>'),
+                            # css_class="multi-choices-dest",
+                        ),
+                        css_class="col-auto d-flex align-items-center range-in-global-grade-settings",
                     ),
                 ),
                 css_class="card-body",
@@ -1923,8 +1958,9 @@ class ImportSettingsForm(forms.Form):
                 # that related instance should either be already present, or imported.
                 _("%(html_before)s message about import and its danger %(html_after)s")
                 % dict(
-                    html_before='<p class="alert alert-warning" role="alert">'
-                    '<i class="fas fa-exclamation-triangle text-warning"></i> ',
+                    html_before='<p class="alert alert-%(css_cls)s" role="alert">'
+                    '<i class="fas fa-exclamation-triangle text-%(css_cls)s"></i> '
+                    % dict(css_cls="danger" if data_io.presence_of_data_in_importable_models() else "warning"),
                     html_after='</p>',
                 )
             ),
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 bff41c006a9b772c7e372bdff0db3deddd596d7f..3c579caec2b7a2188de275f64019be02e14bdb3e 100644
--- a/src/strass/strass_app/locale/en/LC_MESSAGES/django.po
+++ b/src/strass/strass_app/locale/en/LC_MESSAGES/django.po
@@ -1,6 +1,5 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
 # This file is distributed under the same license as the PACKAGE package.
+# Created using gettext (GNU gettext-runtime) 0.21 on Python 3.11.10.
 #
 # refgexp to clean up fuzzy:
 # #, fuzzy\n.*\n(.*)\n.*\n\n
@@ -11,7 +10,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-03-01 17:30+0100\n"
+"POT-Creation-Date: 2024-09-11 15:33+0200\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"
@@ -27,6 +26,10 @@ msgid_plural "Status \"%(s)s\" was successfully defined for %(d)d candidates."
 msgstr[0] ""
 msgstr[1] ""
 
+#, python-format
+msgid "Set status to \"%s\""
+msgstr ""
+
 msgid "Remove empty automatically assigned reviews"
 msgstr ""
 
@@ -486,7 +489,11 @@ msgid "AppSettingsForm.plausible_data_domain.label"
 msgstr "Plausible \"data-domain\""
 
 msgid "AppSettingsForm.plausible_data_domain.help_text"
-msgstr "If you wish to measure the audience with the Plausible hosted at the Institut Pasteur, indicate here the data-domain provided by Plausible. It is generally the same as the domain name. This audience measurement does not require a cookie banner, in accordance with the GDPR."
+msgstr ""
+"If you wish to measure the audience with the Plausible hosted at the "
+"Institut Pasteur, indicate here the data-domain provided by Plausible. It is "
+"generally the same as the domain name. This audience measurement does not "
+"require a cookie banner, in accordance with the GDPR."
 
 msgid "AppSettingsForm.cv_enabled.label"
 msgstr "CV enabled"
@@ -860,13 +867,12 @@ msgid "notify_candidate_edited__subject"
 msgstr "Your application have been edited"
 
 #, python-format
-msgid ""
-"notify_candidate_edited__body%(first_name)s%(last_name)s%(candidate_link)s"
+msgid "notify_candidate_edited__body%(first_name)s%(last_name)s%(candidate_link)s"
 msgstr ""
 "<p>Dear %(first_name)s %(last_name)s, </p>\n"
 "<p>Changes made to your application have been saved. It can be viewed and "
-"edited with the following link: <a href=\"%(candidate_link)s\">"
-"%(candidate_link)s</a>.</p>\n"
+"edited with the following link: <a "
+"href=\"%(candidate_link)s\">%(candidate_link)s</a>.</p>\n"
 "<p>Best regards</p>"
 
 msgid "notify_candidate_edited__message"
@@ -876,8 +882,7 @@ msgid "notify_candidate_submitted__subject"
 msgstr "Your application have been saved"
 
 #, python-format
-msgid ""
-"notify_candidate_submitted__body%(first_name)s%(last_name)s%(candidate_link)s"
+msgid "notify_candidate_submitted__body%(first_name)s%(last_name)s%(candidate_link)s"
 msgstr ""
 "<p>Dear %(first_name)s %(last_name)s, </p>\n"
 "<p>We received you application. It can be viewed and edited with the "
@@ -895,9 +900,7 @@ msgstr "Interview session booking"
 
 #. Translators: This message is sent when the user can register to a session
 #, python-format
-msgid ""
-"notify_interview_session_open__body%(first_name)s%(last_name)s"
-"%(view_edit_link)s%(session)s"
+msgid "notify_interview_session_open__body%(first_name)s%(last_name)s%(view_edit_link)s%(session)s"
 msgstr ""
 "<p>Dear %(first_name)s %(last_name)s</p>\n"
 "<p>You have been selected for interviews (\"<b>%(session)s</b>\").</p> "
@@ -908,14 +911,12 @@ msgid "notify_interview_session_added_by_candidate__subject"
 msgstr "Interview session selected"
 
 #, python-format
-msgid ""
-"notify_interview_session_added_by_candidate__body%(first_name)s%(last_name)s"
-"%(view_edit_link)s%(session)s"
+msgid "notify_interview_session_added_by_candidate__body%(first_name)s%(last_name)s%(view_edit_link)s%(session)s"
 msgstr ""
 "<p>Dear %(first_name)s %(last_name)s, </p>\n"
-"<p>Your choice for an interview session have been saved (\"<b>%(session)s</b>"
-"\"). It can be edited with the following link: <a href=\"%(view_edit_link)s"
-"\">%(view_edit_link)s</a>.</p>\n"
+"<p>Your choice for an interview session have been saved (\"<b>%(session)s</"
+"b>\"). It can be edited with the following link: <a "
+"href=\"%(view_edit_link)s\">%(view_edit_link)s</a>.</p>\n"
 "<p>Best regards</p>"
 
 msgid "notify_interview_session_added_by_candidate__message"
@@ -925,14 +926,12 @@ msgid "notify_interview_session_added_by_other__subject"
 msgstr "Interview session selected"
 
 #, python-format
-msgid ""
-"notify_interview_session_added_by_other__body%(first_name)s%(last_name)s"
-"%(view_edit_link)s%(session)s"
+msgid "notify_interview_session_added_by_other__body%(first_name)s%(last_name)s%(view_edit_link)s%(session)s"
 msgstr ""
 "<p>Dear %(first_name)s %(last_name)s, </p>\n"
 "<p>You have been assigned to an interview session (\"<b>%(session)s</b>\"). "
-"This can be edited with the following link: <a href=\"%(view_edit_link)s\">"
-"%(view_edit_link)s</a>.</p>\n"
+"This can be edited with the following link: <a "
+"href=\"%(view_edit_link)s\">%(view_edit_link)s</a>.</p>\n"
 "<p>Best regards</p>"
 
 msgid "notify_removed_from_interview_session__subject"
@@ -940,15 +939,13 @@ msgstr "Removed from an interview session"
 
 #. Translators: This message is also sent when the user removed zimself, the link is to the candidate profile
 #, python-format
-msgid ""
-"notify_removed_from_interview_session__body%(first_name)s%(last_name)s"
-"%(view_edit_link)s%(session)s"
+msgid "notify_removed_from_interview_session__body%(first_name)s%(last_name)s%(view_edit_link)s%(session)s"
 msgstr ""
 "<p>Dear %(first_name)s %(last_name)s, </p>\n"
-"<p>Your have been removed from the interview session (\"<b>%(session)s</b>"
-"\"). If you just changed of session please ignore this email. If not, we "
-"recommend you to visit your profile with the following link: <a href="
-"\"%(view_edit_link)s\">%(view_edit_link)s</a>, and check if the newly "
+"<p>Your have been removed from the interview session (\"<b>%(session)s</"
+"b>\"). If you just changed of session please ignore this email. If not, we "
+"recommend you to visit your profile with the following link: <a "
+"href=\"%(view_edit_link)s\">%(view_edit_link)s</a>, and check if the newly "
 "selected session fits you, or chose a new one.</p>\n"
 "<p>Best regards</p>"
 
@@ -965,33 +962,28 @@ msgid "notify_referee_submitted__subject"
 msgstr "Reference letter submitted"
 
 #, python-format
-msgid ""
-"notify_referee_submitted__body%(first_name)s%(last_name)s"
-"%(recommendation_update_link)s%(candidate_first_name)s%(candidate_last_name)s"
-"%(close_recommendation_after)s"
+msgid "notify_referee_submitted__body%(first_name)s%(last_name)s%(recommendation_update_link)s%(candidate_first_name)s%(candidate_last_name)s%(close_recommendation_after)s"
 msgstr ""
 "<p>Dear %(first_name)s %(last_name)s</p>\n"
 "<p>A recommendation letter for %(candidate_first_name)s "
 "%(candidate_last_name)s has been submitted in your name. Recommendation can "
 "be provided and edited until %(close_recommendation_after)s included.</p>\n"
-"<p>For more details, please use the following link <a href="
-"\"%(recommendation_update_link)s\">%(recommendation_update_link)s</a>.</p>"
+"<p>For more details, please use the following link <a "
+"href=\"%(recommendation_update_link)s\">%(recommendation_update_link)s</a>.</"
+"p>"
 
 #. Translators : Email sent to referee when he used the link but has expired, or with copy paste error
 msgid "notify_referee_requested__subject"
 msgstr "Reference letter request"
 
 #, python-format
-msgid ""
-"notify_referee_requested__body%(first_name)s%(last_name)s"
-"%(recommendation_update_link)s%(candidate_first_name)s%(candidate_last_name)s"
-"%(close_recommendation_after)s"
+msgid "notify_referee_requested__body%(first_name)s%(last_name)s%(recommendation_update_link)s%(candidate_first_name)s%(candidate_last_name)s%(close_recommendation_after)s"
 msgstr ""
 "<p>Dear %(first_name)s %(last_name)s</p>\n"
 "<p>A recommendation letter request has been made by %(candidate_first_name)s "
 "%(candidate_last_name)s.</p>\n"
-"<p>For more details, please use the following link <a href="
-"\"%(recommendation_update_link)s\">%(recommendation_update_link)s</a>. "
+"<p>For more details, please use the following link <a "
+"href=\"%(recommendation_update_link)s\">%(recommendation_update_link)s</a>. "
 "Recommendation can be provided and edited until "
 "%(close_recommendation_after)s included.</p>\n"
 "<p>An account has been create allowing you to easily access recommendation "
@@ -1014,9 +1006,7 @@ msgstr "New application submitted"
 
 #. Translators: This message is send to reviewers whether it's a new or edited application
 #, python-format
-msgid ""
-"notify_reviewer_new_candidate_apply__body%(first_name)s%(last_name)s"
-"%(view_candidate_link)s"
+msgid "notify_reviewer_new_candidate_apply__body%(first_name)s%(last_name)s%(view_candidate_link)s"
 msgstr ""
 "<p>Dear %(first_name)s %(last_name)s, </p>\n"
 "<p>A new application have been submitted. It can be viewed with the "
@@ -1025,9 +1015,7 @@ msgstr ""
 "<p>Best regards</p>"
 
 #, python-format
-msgid ""
-"notify_on_accepted_invitation__first_one__body%(first_name)s%(last_name)s"
-"%(link)s%(for_what)s%(by_first_name)s%(by_last_name)s"
+msgid "notify_on_accepted_invitation__first_one__body%(first_name)s%(last_name)s%(link)s%(for_what)s%(by_first_name)s%(by_last_name)s"
 msgstr ""
 "<p>Dear %(first_name)s %(last_name)s, </p>\n"
 "<p>An invitation for being \"%(for_what)s\" has just been accepted by "
@@ -1040,9 +1028,7 @@ msgstr ""
 "<p>Best regards</p>"
 
 #, python-format
-msgid ""
-"notify_on_accepted_invitation__each_time__body%(first_name)s%(last_name)s"
-"%(link)s%(for_what)s%(by_first_name)s%(by_last_name)s"
+msgid "notify_on_accepted_invitation__each_time__body%(first_name)s%(last_name)s%(link)s%(for_what)s%(by_first_name)s%(by_last_name)s"
 msgstr ""
 "<p>Dear %(first_name)s %(last_name)s, </p>\n"
 "<p>An invitation for being \"%(for_what)s\" has just been accepted "
@@ -1056,9 +1042,7 @@ msgid "notify_on_accepted_invitation__subject%(for_what)s"
 msgstr "Invitation to become %(for_what)s accepted"
 
 #, python-format
-msgid ""
-"notify_user_invitation_validated%(first_name)s%(last_name)s%(link)s"
-"%(for_what)s"
+msgid "notify_user_invitation_validated%(first_name)s%(last_name)s%(link)s%(for_what)s"
 msgstr ""
 "<i><p>Dear %(first_name)s %(last_name)s, </p>\n"
 "<p>You have just been accepted/defined as \"%(for_what)s\".<p>\n"
@@ -1759,6 +1743,28 @@ msgstr ""
 "and all reviewers allowed to read the review of this candidate by the "
 "reviewer. Markdown is allowed here."
 
+#, python-format
+msgid "Your password must contain at least %(min_count)d %(kind)s."
+msgstr ""
+
+msgid "symbol(s)"
+msgstr ""
+
+msgid "capital letter(s)"
+msgstr ""
+
+msgid "digit(s)"
+msgstr ""
+
+msgid "Your password must not contain accented char."
+msgstr ""
+"Your password must only use latin alphabet with non accentuated letters"
+
+msgid "Your password must not contain public reference."
+msgstr ""
+"Your password must not contain publicly available references (Institut "
+"Pasteur, current year, date of birth, first/last name)."
+
 msgid "About"
 msgstr ""
 
@@ -1861,8 +1867,9 @@ msgstr ""
 #, python-format
 msgid ""
 "Registration for interview sessions of stage <i>%(stage_name)s</i> are "
-"opened but you have not yet chosen a session. Please follow <a href="
-"\"%(stage_url)s\">this link</a> befor %(closing_date)s to select a session."
+"opened but you have not yet chosen a session. Please follow <a "
+"href=\"%(stage_url)s\">this link</a> befor %(closing_date)s to select a "
+"session."
 msgstr ""
 
 msgid "Updating status"
@@ -1973,9 +1980,9 @@ msgstr ""
 msgid "No profile declared for a reviewer message%(url)s"
 msgstr ""
 "You have not declared yet what profiles you accept to review. Please go to "
-"your <a href=\"%(url)s\">account settings, in \"Reviewer settings\" <i class="
-"\"fa fa-link\" aria-hidden=\"true\"></i></a>, and define which profiles you "
-"accept."
+"your <a href=\"%(url)s\">account settings, in \"Reviewer settings\" <i "
+"class=\"fa fa-link\" aria-hidden=\"true\"></i></a>, and define which "
+"profiles you accept."
 
 #, python-format
 msgid ""
@@ -2236,9 +2243,6 @@ msgstr ""
 "will still be available at <a href=\"./#io-config\">the bottom of this page</"
 "a>."
 
-msgid "Import"
-msgstr ""
-
 msgid "Import configuration"
 msgstr ""
 
@@ -2395,9 +2399,6 @@ msgstr ""
 msgid "Email me the configuration"
 msgstr ""
 
-msgid "Import and overwrite"
-msgstr ""
-
 #. Translators: Explain that now that there are candidate, bulk import of question, profiles, ... could lead to serious data lose, and is thus not allowed.
 msgid "Import is not allowed anymore"
 msgstr ""
@@ -2557,13 +2558,19 @@ msgstr ""
 msgid "The email address \"%(mail)s\" is invalid."
 msgstr ""
 
+#, python-format
+msgid ""
+"MIME type \"%(mime_type)s\" is not allowed. Allowed MIME types are: "
+"%(allowed_mime_types)s."
+msgstr ""
+
 #, python-format
 msgid ""
 "You have been redirected as your are the only jury manager and also user in "
 "the db.This interface can be access in <span class=\"blk\">%(menu)s</"
 "span>><span class=\"blk\">%(submenu)s</span>. Note that the documentation on "
-"how to setup an instance is available <a href=\"%(url_doc)s\" target=\"_blank"
-"\">here</a>."
+"how to setup an instance is available <a href=\"%(url_doc)s\" "
+"target=\"_blank\">here</a>."
 msgstr ""
 
 msgid "Easy setup menu entry"
@@ -2703,10 +2710,14 @@ msgstr ""
 msgid "Recommandation could not be saved:"
 msgstr ""
 
+msgid "Import and overwrite"
+msgstr ""
+
+msgid "Import"
+msgstr ""
+
 #, python-format
-msgid ""
-"export_settings_email_body%(first_name)s%(last_name)s%(scope_as_ul)s"
-"%(instance_name)s"
+msgid "export_settings_email_body%(first_name)s%(last_name)s%(scope_as_ul)s%(instance_name)s"
 msgstr ""
 "<p>Dear %(first_name)s %(last_name)s, </p>\n"
 "<p>Please find attached an export of the configuration of the STRASS "
@@ -2772,25 +2783,21 @@ msgid "No answer associated"
 msgstr ""
 
 #, python-format
-msgid ""
-"generated_invitation_link%(expires_after)s%(invitation_to_be)s"
-"%(invitation_link)s"
+msgid "generated_invitation_link%(expires_after)s%(invitation_to_be)s%(invitation_link)s"
 msgstr ""
 "Here is an invitation allowing to join as a <b>%(invitation_to_be)s</b>. "
 "This invitation will expire after <b>%(expires_after)s</b>.<br/><br/>\n"
-"To accept this invitation follow this link: <a href=\"%(invitation_link)s\">"
-"%(invitation_link)s</a>.<br/><br/>\n"
+"To accept this invitation follow this link: <a "
+"href=\"%(invitation_link)s\">%(invitation_link)s</a>.<br/><br/>\n"
 "Best regards"
 
 #, python-format
-msgid ""
-"generated_invitation_link%(expires_after)s%(invitation_to_be)s"
-"%(invitation_link)s%(base64qr)s"
+msgid "generated_invitation_link%(expires_after)s%(invitation_to_be)s%(invitation_link)s%(base64qr)s"
 msgstr ""
 "Here is an invitation allowing to join as a <b>%(invitation_to_be)s</b>. "
 "This invitation will expire after <b>%(expires_after)s</b>.<br/><br/>\n"
-"To accept this invitation follow this link: <a href=\"%(invitation_link)s\">"
-"%(invitation_link)s</a>.<br/><br/>\n"
+"To accept this invitation follow this link: <a "
+"href=\"%(invitation_link)s\">%(invitation_link)s</a>.<br/><br/>\n"
 "Here is a QR code allowing to do the same:<br/><img src=\"data:image/png;"
 "base64, %(base64qr)s\" alt=\"QR Code\" width=\"200px\"/><br/><br/>\n"
 "Best regards"
@@ -2886,10 +2893,3 @@ msgstr ""
 "You are about to delete all the reviews done by this reviewer for this "
 "candidate. The conflict itself and its comment will remain in the database, "
 "and will prevent future assignation of this reviewer to this candidate."
-
-#, fuzzy
-#~| msgid "expires_after capped to today for jury manager invitation"
-#~ msgid "expires_after capped to today"
-#~ msgstr ""
-#~ "For security reasons, the expiration date for being Jury Manager limited "
-#~ "to the current day, as the status is associated to many rights."
diff --git a/src/strass/strass_app/locale/en/LC_MESSAGES/djangojs.po b/src/strass/strass_app/locale/en/LC_MESSAGES/djangojs.po
index 8a08c406f542f6d2c5019cd749b4605024cd5553..ba55d8804e677e70c7a5374088b618b5cc78ff4f 100644
--- a/src/strass/strass_app/locale/en/LC_MESSAGES/djangojs.po
+++ b/src/strass/strass_app/locale/en/LC_MESSAGES/djangojs.po
@@ -1,14 +1,11 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
 # This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-#
+# Created using gettext (GNU gettext-runtime) 0.21 on Python 3.11.10.
 #, fuzzy
 msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-01-21 17:34+0100\n"
+"POT-Creation-Date: 2024-09-11 11:48+0200\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"
@@ -18,6 +15,9 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
+msgid "It is recommended to provide an institutional email."
+msgstr ""
+
 msgid "Selection"
 msgstr ""
 
@@ -96,3 +96,9 @@ msgid_plural ""
 "application."
 msgstr[0] ""
 msgstr[1] ""
+
+msgid "Choice"
+msgstr ""
+
+msgid "Score"
+msgstr ""
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 4bc40805241b170db59a43570f197d376e955efc..d86f85b105104431e4a12840e3fec5b27aff3856 100644
--- a/src/strass/strass_app/locale/fr/LC_MESSAGES/django.po
+++ b/src/strass/strass_app/locale/fr/LC_MESSAGES/django.po
@@ -1,6 +1,5 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
 # This file is distributed under the same license as the PACKAGE package.
+# Created using gettext (GNU gettext-runtime) 0.21 on Python 3.11.10.
 #
 # refgexp to clean up fuzzy:
 # #, fuzzy\n.*\n(.*)\n.*\n\n
@@ -11,7 +10,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-03-01 17:30+0100\n"
+"POT-Creation-Date: 2024-09-11 15:36+0200\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"
@@ -27,6 +26,10 @@ msgid_plural "Status \"%(s)s\" was successfully defined for %(d)d candidates."
 msgstr[0] "Le status \"%(s)s\" a été appliqué à %(d)d candidat."
 msgstr[1] "Le status \"%(s)s\" a été appliqué à %(d)d candidats."
 
+#, python-format
+msgid "Set status to \"%s\""
+msgstr "Donner le statut \"%s\""
+
 msgid "Remove empty automatically assigned reviews"
 msgstr "Supprimer les reviews automatiquement assignées qui sont vides"
 
@@ -64,10 +67,10 @@ msgid "Export selection in one pdf"
 msgstr "Exporter la sélection en un pdf unique"
 
 msgid "Export selection in one archive"
-msgstr "Exporter la selection en une archive unique"
+msgstr "Exporter la sélection en une archive unique"
 
 msgid "Export selection in csv"
-msgstr "Exporter la selection dans un fichier csv"
+msgstr "Exporter la sélection dans un fichier csv"
 
 #, python-format
 msgid "Candidates (%(now)s).%(ext)s"
@@ -149,7 +152,7 @@ msgid "None"
 msgstr "Aucun"
 
 msgid "Accepted statuses"
-msgstr "Status acceptés"
+msgstr "Statut acceptés"
 
 msgid "Accepted groups"
 msgstr "Groupes acceptés"
@@ -519,8 +522,10 @@ msgstr "Identifiant Plausible \"data-domain\""
 
 msgid "AppSettingsForm.plausible_data_domain.help_text"
 msgstr ""
-"Si vous souhaitez mesurer l'audience avec le Plausible hebergé à l'Institut Pasteur, indiquez ici le data-domain fournis par Plausible. Il est généralement le même que le nom de domaine. Cette mesure d'audience ne nécessite pas de banière cookie, conformement à la RGPD."
-
+"Si vous souhaitez mesurer l'audience avec le Plausible hebergé à l'Institut "
+"Pasteur, indiquez ici le data-domain fournis par Plausible. Il est "
+"généralement le même que le nom de domaine. Cette mesure d'audience ne "
+"nécessite pas de banière cookie, conformement à la RGPD."
 
 msgid "AppSettingsForm.cv_enabled.label"
 msgstr "CV requis"
@@ -795,7 +800,7 @@ msgstr "Jurés potentiels (%i)"
 
 #, python-format
 msgid "Pick candidate(s) from a list (%i)"
-msgstr "Selection manuelle de candidats (%i)"
+msgstr "sélection manuelle de candidats (%i)"
 
 #, python-format
 msgid "Candidates %(status)s (%(count)i)"
@@ -914,8 +919,7 @@ msgid "notify_candidate_edited__subject"
 msgstr "Mise à jours de votre candidature"
 
 #, python-format
-msgid ""
-"notify_candidate_edited__body%(first_name)s%(last_name)s%(candidate_link)s"
+msgid "notify_candidate_edited__body%(first_name)s%(last_name)s%(candidate_link)s"
 msgstr ""
 "<p>Bonjour %(first_name)s %(last_name)s</p>\n"
 "<p>Les modifications que vous avez apporté à votre candidature ont été "
@@ -930,8 +934,7 @@ msgid "notify_candidate_submitted__subject"
 msgstr "Dépôt de candidature"
 
 #, python-format
-msgid ""
-"notify_candidate_submitted__body%(first_name)s%(last_name)s%(candidate_link)s"
+msgid "notify_candidate_submitted__body%(first_name)s%(last_name)s%(candidate_link)s"
 msgstr ""
 "<p>Bonjour %(first_name)s %(last_name)s</p>\n"
 "<p>Votre candidature a bien été enregistrée.</p>\n"
@@ -946,28 +949,24 @@ msgstr "Réservation ouverte pour des session d'entretien"
 
 #. Translators: This message is sent when the user can register to a session
 #, python-format
-msgid ""
-"notify_interview_session_open__body%(first_name)s%(last_name)s"
-"%(view_edit_link)s%(session)s"
+msgid "notify_interview_session_open__body%(first_name)s%(last_name)s%(view_edit_link)s%(session)s"
 msgstr ""
 "<p>Bonjour %(first_name)s %(last_name)s</p>\n"
-"<p>Votre candidature a été retenue pour une session d'entretiens (\"<b>"
-"%(session)s</b>\").</p> <p>Vous avez la possibilité de choisir une session "
-"en fonction des places disponibles en utilisant le lien suivant</p><a  href="
-"\"%(view_edit_link)s\">%(view_edit_link)s</>"
+"<p>Votre candidature a été retenue pour une session d'entretiens "
+"(\"<b>%(session)s</b>\").</p> <p>Vous avez la possibilité de choisir une "
+"session en fonction des places disponibles en utilisant le lien suivant</"
+"p><a  href=\"%(view_edit_link)s\">%(view_edit_link)s</>"
 
 msgid "notify_interview_session_added_by_candidate__subject"
 msgstr "Choix d'une session d'entretiens enregistré"
 
 #, python-format
-msgid ""
-"notify_interview_session_added_by_candidate__body%(first_name)s%(last_name)s"
-"%(view_edit_link)s%(session)s"
+msgid "notify_interview_session_added_by_candidate__body%(first_name)s%(last_name)s%(view_edit_link)s%(session)s"
 msgstr ""
 "<p>Bonjour %(first_name)s %(last_name)s, </p>\n"
-"<p>Votre choix pour la session d'entretiens à été sauvegardé (\"<b>"
-"%(session)s</b>\"). Ce choix peut être modifié à l'adresse suivante : <a "
-"href=\"%(view_edit_link)s\">%(view_edit_link)s</a>.</p>\n"
+"<p>Votre choix pour la session d'entretiens à été sauvegardé "
+"(\"<b>%(session)s</b>\"). Ce choix peut être modifié à l'adresse suivante : "
+"<a href=\"%(view_edit_link)s\">%(view_edit_link)s</a>.</p>\n"
 "<p>Cordialement,</p>"
 
 msgid "notify_interview_session_added_by_candidate__message"
@@ -977,14 +976,12 @@ msgid "notify_interview_session_added_by_other__subject"
 msgstr "Choix d'une session d'entretiens enregistré"
 
 #, python-format
-msgid ""
-"notify_interview_session_added_by_other__body%(first_name)s%(last_name)s"
-"%(view_edit_link)s%(session)s"
+msgid "notify_interview_session_added_by_other__body%(first_name)s%(last_name)s%(view_edit_link)s%(session)s"
 msgstr ""
 "<p>Bonjour %(first_name)s %(last_name)s, </p>\n"
-"<p>Vous avez été placé dans une session d'entretiens (\"<b>%(session)s</b>"
-"\"). Ce choix peut être modifier à l'adresse suivante : <a href="
-"\"%(view_edit_link)s\">%(view_edit_link)s</a>.</p>\n"
+"<p>Vous avez été placé dans une session d'entretiens (\"<b>%(session)s</"
+"b>\"). Ce choix peut être modifier à l'adresse suivante : <a "
+"href=\"%(view_edit_link)s\">%(view_edit_link)s</a>.</p>\n"
 "<p>Cordialement,</p>"
 
 msgid "notify_removed_from_interview_session__subject"
@@ -992,9 +989,7 @@ msgstr "Annulation session d'entretien"
 
 #. Translators: This message is also sent when the user removed zimself, the link is to the candidate profile
 #, python-format
-msgid ""
-"notify_removed_from_interview_session__body%(first_name)s%(last_name)s"
-"%(view_edit_link)s%(session)s"
+msgid "notify_removed_from_interview_session__body%(first_name)s%(last_name)s%(view_edit_link)s%(session)s"
 msgstr ""
 "<p>Bonjour %(first_name)s %(last_name)s, </p>\n"
 "<p>Vous avez été retiré d'une session d'entretiens (\"<b>%(session)s</b>\"). "
@@ -1018,14 +1013,11 @@ msgid "notify_referee_submitted__subject"
 msgstr "Demande de référence soumise"
 
 #, python-format
-msgid ""
-"notify_referee_submitted__body%(first_name)s%(last_name)s"
-"%(recommendation_update_link)s%(candidate_first_name)s%(candidate_last_name)s"
-"%(close_recommendation_after)s"
+msgid "notify_referee_submitted__body%(first_name)s%(last_name)s%(recommendation_update_link)s%(candidate_first_name)s%(candidate_last_name)s%(close_recommendation_after)s"
 msgstr ""
 "<p>Bonjour %(first_name)s %(last_name)s</p>\n"
 "<p>Une lettre de recommandation pour %(candidate_first_name)s "
-"%(candidate_last_name)sa été soumise en votre nom.</p>\n"
+"%(candidate_last_name)s a été soumise en votre nom.</p>\n"
 "<p>Pour consulter cette demande, vous pouvez utiliser le lien suivant <a "
 "href=\"%(recommendation_update_link)s\">%(recommendation_update_link)s</a>. "
 "Noter que le dépôt et l'édition de recommandation est ouvert jusqu'au "
@@ -1036,23 +1028,21 @@ msgid "notify_referee_requested__subject"
 msgstr "Demande de référence"
 
 #, python-format
-msgid ""
-"notify_referee_requested__body%(first_name)s%(last_name)s"
-"%(recommendation_update_link)s%(candidate_first_name)s%(candidate_last_name)s"
-"%(close_recommendation_after)s"
+msgid "notify_referee_requested__body%(first_name)s%(last_name)s%(recommendation_update_link)s%(candidate_first_name)s%(candidate_last_name)s%(close_recommendation_after)s"
 msgstr ""
 "<p>Bonjour %(first_name)s %(last_name)s</p>\n"
 "<p>Une demande de recommandation a été faite par %(candidate_first_name)s "
 "%(candidate_last_name)s.</p>\n"
 "<p>Pour consulter cette demande et envoyer votre recommandation, vous pouvez "
-"utiliser le lien suivant <a href=\"%(recommendation_update_link)s\">"
-"%(recommendation_update_link)s</a>. Le dépôt et l'édition de recommandation "
-"est ouvert jusqu'au %(close_recommendation_after)s inclus.</p>\n"
+"utiliser le lien suivant <a "
+"href=\"%(recommendation_update_link)s\">%(recommendation_update_link)s</a>. "
+"Le dépôt et l'édition de recommandation est ouvert jusqu'au "
+"%(close_recommendation_after)s inclus.</p>\n"
 "<p>Un compte vous a été créé pour que vous puissiez accéder rapidement à la "
 "ou les demandes de recommendations sur ce courriel. Si vous souhaitre "
-"utiliser ce compte, suivez ce <a href=\"%(recommendation_update_link)s"
-"\">lien</a>, faites \"Connexion\" puis \"Réinitialiser mon mot de passe\".</"
-"p>"
+"utiliser ce compte, suivez ce <a "
+"href=\"%(recommendation_update_link)s\">lien</a>, faites \"Connexion\" puis "
+"\"Réinitialiser mon mot de passe\".</p>"
 
 msgid "<i>Date not specified</i>"
 msgstr "<i>Date non spécifiée</i>"
@@ -1069,19 +1059,15 @@ msgstr "Nouvelle candidature soumise"
 
 #. Translators: This message is send to reviewers whether it's a new or edited application
 #, python-format
-msgid ""
-"notify_reviewer_new_candidate_apply__body%(first_name)s%(last_name)s"
-"%(view_candidate_link)s"
+msgid "notify_reviewer_new_candidate_apply__body%(first_name)s%(last_name)s%(view_candidate_link)s"
 msgstr ""
 "<p>Bonjour %(first_name)s %(last_name)s</p>\n"
 "<p>Un nouvelle candidature a été soumise.</p>\n"
-"<p> Vous pouvez la consulter avec le lien suivant : <br/><a href="
-"\"%(view_candidate_link)s\">%(view_candidate_link)s</a></p>"
+"<p> Vous pouvez la consulter avec le lien suivant : <br/><a "
+"href=\"%(view_candidate_link)s\">%(view_candidate_link)s</a></p>"
 
 #, python-format
-msgid ""
-"notify_on_accepted_invitation__first_one__body%(first_name)s%(last_name)s"
-"%(link)s%(for_what)s%(by_first_name)s%(by_last_name)s"
+msgid "notify_on_accepted_invitation__first_one__body%(first_name)s%(last_name)s%(link)s%(for_what)s%(by_first_name)s%(by_last_name)s"
 msgstr ""
 "<p>Bonjour %(first_name)s %(last_name)s</p>\n"
 "<p>Une invitation à être \"%(for_what)s\" vient d'être acceptée par "
@@ -1093,9 +1079,7 @@ msgstr ""
 "p><p>Cordialement,</p>"
 
 #, python-format
-msgid ""
-"notify_on_accepted_invitation__each_time__body%(first_name)s%(last_name)s"
-"%(link)s%(for_what)s%(by_first_name)s%(by_last_name)s"
+msgid "notify_on_accepted_invitation__each_time__body%(first_name)s%(last_name)s%(link)s%(for_what)s%(by_first_name)s%(by_last_name)s"
 msgstr ""
 "<p>Bonjour %(first_name)s %(last_name)s</p>\n"
 "<p>Une invitation à être \"%(for_what)s\" vient d'être acceptée par "
@@ -1108,9 +1092,7 @@ msgid "notify_on_accepted_invitation__subject%(for_what)s"
 msgstr "Invitation à devenir %(for_what)s acceptée"
 
 #, python-format
-msgid ""
-"notify_user_invitation_validated%(first_name)s%(last_name)s%(link)s"
-"%(for_what)s"
+msgid "notify_user_invitation_validated%(first_name)s%(last_name)s%(link)s%(for_what)s"
 msgstr ""
 "<p>Bonjour %(first_name)s %(last_name)s</p>\n"
 "<p>Vous avez été promu.e/défini.e \"%(for_what)s\".</p>\n"
@@ -1274,8 +1256,8 @@ msgstr "Review finalisée"
 
 msgid "review.is_completed.help_text"
 msgstr ""
-"Si cette case n'est pas cochée, la review est considérée comme un \"brouillon"
-"\"."
+"Si cette case n'est pas cochée, la review est considérée comme un "
+"\"brouillon\"."
 
 msgid "You must select to which stage of review this review is associated to."
 msgstr "Vous devez associer cette review à une phase de review."
@@ -1305,7 +1287,7 @@ msgid "application_status.description.help_text"
 msgstr "Description du statut, visible par les candidats."
 
 msgid "Default status for newly created application"
-msgstr "Status par défaut des nouvelles candidatures"
+msgstr "Statut par défaut des nouvelles candidatures"
 
 msgid ""
 "When a new application is created should this status be associated to the "
@@ -1828,6 +1810,29 @@ msgstr ""
 "de recrutement, et toute personne pouvant lire les reviews de ce reviewer "
 "sur ce candidat. Vous pouvez écrire en markdown."
 
+#, python-format
+msgid "Your password must contain at least %(min_count)d %(kind)s."
+msgstr "Votre mot de passe doit contenir au minimum %(min_count)d %(kind)s."
+
+msgid "symbol(s)"
+msgstr "symbole(s)"
+
+msgid "capital letter(s)"
+msgstr "lettre(s) capitale(s)"
+
+msgid "digit(s)"
+msgstr "chiffre(s)"
+
+msgid "Your password must not contain accented char."
+msgstr ""
+"Votre mot de passe doit uniquement utiliser l'alphabet latin sans lettres "
+"accentuées."
+
+msgid "Your password must not contain public reference."
+msgstr ""
+"Votre mot de passe ne doit pas contenir de références publiques (Institut "
+"Pasteur, l'année en cours, date de naissance, prénom, nom)."
+
 msgid "About"
 msgstr "À propos"
 
@@ -1881,7 +1886,7 @@ msgid "The numerical answer is used to for computing the global grade."
 msgstr "La valeur numérique est utilisée dans le calcul du score global."
 
 msgid "No choice selected"
-msgstr "Aucun choix selectionné"
+msgstr "Aucun choix sélectionné"
 
 msgid "Not answered"
 msgstr "Non répondu"
@@ -1928,13 +1933,14 @@ msgid "Information"
 msgstr "Informations"
 
 msgid "Selecting an interview session"
-msgstr "Selection d'une session d'entretiens"
+msgstr "sélection d'une session d'entretiens"
 
 #, python-format
 msgid ""
 "Registration for interview sessions of stage <i>%(stage_name)s</i> are "
-"opened but you have not yet chosen a session. Please follow <a href="
-"\"%(stage_url)s\">this link</a> befor %(closing_date)s to select a session."
+"opened but you have not yet chosen a session. Please follow <a "
+"href=\"%(stage_url)s\">this link</a> befor %(closing_date)s to select a "
+"session."
 msgstr ""
 "Les inscriptions aux sessions d'entretiens de l'étape <i>%(stage_name)s</i> "
 "sont ouvertes et vous n'avez pas encore choisi de session. Veuillez suivre "
@@ -2057,8 +2063,8 @@ msgid "No profile declared for a reviewer message%(url)s"
 msgstr ""
 "Vous n'avez pas encore déclaré quels profils vous acceptez d'évaluer. "
 "Veuillez vous rendre dans <a href=\"%(url)s\">les paramètres de votre "
-"compte, dans \"Paramètres du reviewer\" <i class=\"fa fa-link\" aria-hidden="
-"\"true\"></i></a></a>, et définir les profils que vous acceptez."
+"compte, dans \"Paramètres du reviewer\" <i class=\"fa fa-link\" aria-"
+"hidden=\"true\"></i></a></a>, et définir les profils que vous acceptez."
 
 #, python-format
 msgid ""
@@ -2082,7 +2088,7 @@ msgid "Interview"
 msgstr "Session d'entretiens"
 
 msgid "No session selected"
-msgstr "Aucun choix selectionné"
+msgstr "Aucun choix sélectionné"
 
 msgid "Stage"
 msgstr "Phase"
@@ -2252,8 +2258,8 @@ msgid ""
 "Sent on <span class=\"blk text-nowrap\">%(when)s</span> by <span class=\"blk "
 "text-nowrap\">%(who)s</span>."
 msgstr ""
-"Envoyée le <span class=\"blk text-nowrap\">%(when)s</span> par <span class="
-"\"blk text-nowrap\">%(who)s</span>."
+"Envoyée le <span class=\"blk text-nowrap\">%(when)s</span> par <span "
+"class=\"blk text-nowrap\">%(who)s</span>."
 
 msgid "Email subject:"
 msgstr "Objet : "
@@ -2327,11 +2333,8 @@ msgid "Message about importing configuration in an empty instance"
 msgstr ""
 "L'instance STRASS actuelle est vide, vous pouvez importer la configuration "
 "d'une autre instance. Une fois importée, ce message sera supprimé. La "
-"fonctionnalité d'importation sera toujours disponible <a href=\"./#io-config"
-"\">en bas de cette page</a>."
-
-msgid "Import"
-msgstr "Importer"
+"fonctionnalité d'importation sera toujours disponible <a href=\"./#io-"
+"config\">en bas de cette page</a>."
 
 msgid "Import configuration"
 msgstr "Importer la configuration"
@@ -2504,9 +2507,6 @@ msgstr "Me l'envoyer par courriel"
 msgid "Email me the configuration"
 msgstr "M'envoyer la configuration par courriel"
 
-msgid "Import and overwrite"
-msgstr ""
-
 #. Translators: Explain that now that there are candidate, bulk import of question, profiles, ... could lead to serious data lose, and is thus not allowed.
 msgid "Import is not allowed anymore"
 msgstr ""
@@ -2600,10 +2600,10 @@ msgstr ""
 "Voici les utilisateurs avec un statut avancé tel que <i>Reviewer</i>, "
 "<i>Juré</i> ou <i>Jury Manager</i>. Vous trouverez également les "
 "utilisateurs qui ont accepté une invitation et pour lesquels vous devez soit "
-"les accepter, soit annuler leur invitation.<br/>Le retrait du status \"juré"
-"\" d'un utilisateur entraîne son retrait des jurys dont il faisait partie. "
-"Le retrait du status \"reviewer\" concerve les reviews redigées, mais "
-"supprime celles qui sont <b>vide</b>."
+"les accepter, soit annuler leur invitation.<br/>Le retrait du status "
+"\"juré\" d'un utilisateur entraîne son retrait des jurys dont il faisait "
+"partie. Le retrait du status \"reviewer\" concerve les reviews redigées, "
+"mais supprime celles qui sont <b>vide</b>."
 
 msgid "Finalizing invitation"
 msgstr "Finalisation de l'invitation"
@@ -2621,7 +2621,7 @@ msgid "Revoke"
 msgstr "Refuser"
 
 msgid "Remove status"
-msgstr "Retirer un status"
+msgstr "Retirer un statut"
 
 msgid "Remove from:"
 msgstr "Retirer de:"
@@ -2676,13 +2676,21 @@ msgstr "Aucune action sélectionnée"
 msgid "The email address \"%(mail)s\" is invalid."
 msgstr "L'adresse de courriel \"%(mail)s\" est invalide."
 
+#, python-format
+msgid ""
+"MIME type \"%(mime_type)s\" is not allowed. Allowed MIME types are: "
+"%(allowed_mime_types)s."
+msgstr ""
+"Le type MIME \"%(mime_type)s\" est interdit. Les types autorisés sont : "
+"%(allowed_mime_types)s."
+
 #, python-format
 msgid ""
 "You have been redirected as your are the only jury manager and also user in "
 "the db.This interface can be access in <span class=\"blk\">%(menu)s</"
 "span>><span class=\"blk\">%(submenu)s</span>. Note that the documentation on "
-"how to setup an instance is available <a href=\"%(url_doc)s\" target=\"_blank"
-"\">here</a>."
+"how to setup an instance is available <a href=\"%(url_doc)s\" "
+"target=\"_blank\">here</a>."
 msgstr ""
 "Vous avez été redirigé car vous êtes le seul jury manager et également "
 "utilisateur dans la base de données. Cette interface est accessible dans "
@@ -2831,10 +2839,14 @@ msgstr ""
 msgid "Recommandation could not be saved:"
 msgstr "Votre recommandation n'a pas pu être enregistrée:"
 
+msgid "Import and overwrite"
+msgstr "Importer, en écrasant"
+
+msgid "Import"
+msgstr "Importer"
+
 #, python-format
-msgid ""
-"export_settings_email_body%(first_name)s%(last_name)s%(scope_as_ul)s"
-"%(instance_name)s"
+msgid "export_settings_email_body%(first_name)s%(last_name)s%(scope_as_ul)s%(instance_name)s"
 msgstr ""
 "<p>Bonjour %(first_name)s %(last_name)s</p>\n"
 "<p>Veuillez trouvez ce joint un export de la configurationn de l'instance "
@@ -2881,8 +2893,8 @@ msgstr "Lien invalide"
 #, python-format
 msgid "You are not longer eligible for selecting an interview session in %s."
 msgstr ""
-"Vous n'avez plus la possibilité de sélection une session d'entretiens dans "
-"%s."
+"Vous n'avez plus la possibilité de sélectionner une session d'entretiens "
+"dans %s."
 
 msgid "Editing a question"
 msgstr "Modifier une question"
@@ -2905,25 +2917,21 @@ msgid "No answer associated"
 msgstr "Aucune réponse associée"
 
 #, python-format
-msgid ""
-"generated_invitation_link%(expires_after)s%(invitation_to_be)s"
-"%(invitation_link)s"
+msgid "generated_invitation_link%(expires_after)s%(invitation_to_be)s%(invitation_link)s"
 msgstr ""
 "Voici une invitation vous permettant de devenir <b>%(invitation_to_be)s</b>. "
 "Cette invitation expirera après le <b>%(expires_after)s</b>.<br/><br/>\n"
-"Pour accepter cette invitation, suivez ce lien: <a href=\"%(invitation_link)s"
-"\">%(invitation_link)s</a>.<br/><br/>\n"
+"Pour accepter cette invitation, suivez ce lien: <a "
+"href=\"%(invitation_link)s\">%(invitation_link)s</a>.<br/><br/>\n"
 "Cordialement"
 
 #, python-format
-msgid ""
-"generated_invitation_link%(expires_after)s%(invitation_to_be)s"
-"%(invitation_link)s%(base64qr)s"
+msgid "generated_invitation_link%(expires_after)s%(invitation_to_be)s%(invitation_link)s%(base64qr)s"
 msgstr ""
 "Voici une invitation vous permettant de devenir <b>%(invitation_to_be)s</b>. "
 "Cette invitation expirera après le <b>%(expires_after)s</b>.<br/><br/>\n"
-"Pour accepter cette invitation, suivez ce lien: <a href=\"%(invitation_link)s"
-"\">%(invitation_link)s</a>.<br/><br/>\n"
+"Pour accepter cette invitation, suivez ce lien: <a "
+"href=\"%(invitation_link)s\">%(invitation_link)s</a>.<br/><br/>\n"
 "Voici un code QR le permettant aussi :<br/><img src=\"data:image/png;base64, "
 "%(base64qr)s\" alt=\"QR Code\" width=\"200px\"/><br/><br/>\n"
 "Cordialement"
@@ -3023,11 +3031,3 @@ msgstr ""
 "reviewer pour ce candidat. Le conflit lui-même et son commentaire resteront "
 "dans la base de données et empêcheront l'attribution future de ce reviewer à "
 "ce candidat."
-
-#, fuzzy
-#~| msgid "expires_after capped to today for jury manager invitation"
-#~ msgid "expires_after capped to today"
-#~ msgstr ""
-#~ "Par mesure de sécurité, la date limite d'un invitation à devenir Jury "
-#~ "Manager est limité au jour courant, du au fait que ce status est associé "
-#~ "à beaucoup de droits."
diff --git a/src/strass/strass_app/locale/fr/LC_MESSAGES/djangojs.po b/src/strass/strass_app/locale/fr/LC_MESSAGES/djangojs.po
index 7d30e83402af77dcb1b07a8bc669e4aef3aedad9..20e496ab56259100ce90e73b69e701c92996f504 100644
--- a/src/strass/strass_app/locale/fr/LC_MESSAGES/djangojs.po
+++ b/src/strass/strass_app/locale/fr/LC_MESSAGES/djangojs.po
@@ -1,14 +1,11 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
 # This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-#
+# Created using gettext (GNU gettext-runtime) 0.21 on Python 3.11.10.
 #, fuzzy
 msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-01-21 17:34+0100\n"
+"POT-Creation-Date: 2024-09-11 11:48+0200\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"
@@ -18,8 +15,11 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n > 1);\n"
 
+msgid "It is recommended to provide an institutional email."
+msgstr "Il est recommandé d'utiliser un courriel institutionnel."
+
 msgid "Selection"
-msgstr ""
+msgstr "Sélection"
 
 msgid "All"
 msgstr "Tous"
@@ -120,3 +120,9 @@ msgstr[0] ""
 msgstr[1] ""
 "Il reste %(hours)s heures et %(minutes)s minutes avant la cloture des "
 "candidature."
+
+msgid "Choice"
+msgstr "Choix"
+
+msgid "Score"
+msgstr ""
diff --git a/src/strass/strass_app/migrations/0041_alter_candidate_cv.py b/src/strass/strass_app/migrations/0041_alter_candidate_cv.py
new file mode 100644
index 0000000000000000000000000000000000000000..ffdba6aac56d384b1d0ad3800df484b4ff33fdfc
--- /dev/null
+++ b/src/strass/strass_app/migrations/0041_alter_candidate_cv.py
@@ -0,0 +1,29 @@
+# Generated by Django 4.2.15 on 2024-08-30 14:49
+
+import django.core.validators
+from django.db import migrations, models
+import strass_app.models
+import strass_app.validators
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('strass_app', '0040_alter_candidate_former_statuses'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='candidate',
+            name='cv',
+            field=models.FileField(
+                help_text='Candidate.cv.help_text',
+                upload_to=strass_app.models.candidate_dependant_upload_to,
+                validators=[
+                    django.core.validators.FileExtensionValidator(['pdf']),
+                    strass_app.validators.FileMimeTypeValidator(['application/pdf']),
+                ],
+                verbose_name='Candidate.cv.verbose_name',
+            ),
+        ),
+    ]
diff --git a/src/strass/strass_app/models.py b/src/strass/strass_app/models.py
index 26ed05f42d6e50c07984b293027573e44b6f84b8..d7af7bcc51aad96c5b78f94b21306891229d21e9 100644
--- a/src/strass/strass_app/models.py
+++ b/src/strass/strass_app/models.py
@@ -30,7 +30,7 @@ from django.db.models import (
     FloatField,
 )
 from django.db.models.functions import Upper, Coalesce
-from django.db.models.signals import m2m_changed, post_save, pre_delete, post_delete, pre_save
+from django.db.models.signals import m2m_changed, post_save, post_delete, pre_save
 from django.dispatch import receiver
 from django.template.defaultfilters import date as date_tag
 from django.urls import reverse
@@ -41,6 +41,7 @@ from django.utils.translation import gettext_lazy as _, gettext
 
 from strass_app import business_logic
 from strass_app.utils import build_counter_sub_query
+from strass_app.validators import FileMimeTypeValidator
 
 
 class NotificationLogEntry(models.Model):
@@ -572,6 +573,7 @@ class Candidate(models.Model):
         upload_to=candidate_dependant_upload_to,
         validators=[
             FileExtensionValidator(['pdf']),
+            FileMimeTypeValidator(['application/pdf']),
         ],
     )
     groups = models.ManyToManyField(
diff --git a/src/strass/strass_app/password_validation.py b/src/strass/strass_app/password_validation.py
new file mode 100644
index 0000000000000000000000000000000000000000..0884240ebfe9b6a8d246cbdd2c890d64a90d4be4
--- /dev/null
+++ b/src/strass/strass_app/password_validation.py
@@ -0,0 +1,104 @@
+import abc
+import unicodedata
+
+from django.core.exceptions import ValidationError
+from django.utils import timezone
+from django.utils.translation import gettext as _
+
+
+class _MetaCharValidator(metaclass=abc.ABCMeta):
+    def __init__(self, min_count: int = 1):
+        self.min_count = min_count
+
+    @staticmethod
+    @abc.abstractmethod
+    def check_char(char: str):  # pragma: no cover
+        pass
+
+    @property
+    @abc.abstractmethod
+    def kind(self):  # pragma: no cover
+        pass
+
+    def validate(self, password, user=None):
+        count = sum(1 if self.check_char(char) else 0 for char in password)
+        if count < self.min_count:
+            raise ValidationError(
+                _("Your password must contain at least %(min_count)d %(kind)s."),
+                code='password_not_diverse',
+                params={'min_count': self.min_count, 'kind': self.kind},
+            )
+
+    def get_help_text(self):
+        return _("Your password must contain at least %(min_count)d %(kind)s.") % {
+            'min_count': self.min_count,
+            'kind': self.kind,
+        }
+
+
+class HasSymbolCharValidator(_MetaCharValidator):
+    @property
+    def kind(self):
+        return _('symbol(s)')
+
+    @staticmethod
+    def check_char(char: str):
+        return not char.isalnum()
+
+
+class HasCapitalLetterCharValidator(_MetaCharValidator):
+    @property
+    def kind(self):
+        return _('capital letter(s)')
+
+    @staticmethod
+    def check_char(char: str):
+        return char.isupper()
+
+
+class HasDigitLetterCharValidator(_MetaCharValidator):
+    @property
+    def kind(self):
+        return _('digit(s)')
+
+    @staticmethod
+    def check_char(char: str):
+        return char.isdigit()
+
+
+class HasNoAccentedLetterValidator:
+    def validate(self, password, user=None):
+        if unicodedata.normalize('NFKD', password) != password:
+            raise ValidationError(
+                _("Your password must not contain accented char."),
+                code='password_refused',
+            )
+
+    def get_help_text(self):
+        return _("Your password must not contain accented char.")
+
+
+class HasNoSubPartFromListValidator:
+    _banned_list = None
+    _default_banned_list = [
+        'institut',
+        'pasteur',
+    ]
+
+    def __init__(self):
+        self._banned_list = [
+            *self._default_banned_list,
+            timezone.now().strftime("%Y"),
+        ]
+
+    def validate(self, password, user=None):
+        password = password.lower()
+        for banned_work in self._banned_list:
+            if banned_work in password:
+                raise ValidationError(
+                    _("Your password must not contain public reference."),
+                    code='password_refused',
+                )
+
+    def get_help_text(self):
+        return _("Your password must not contain public reference.")
diff --git a/src/strass/strass_app/static/css/base.css b/src/strass/strass_app/static/css/base.css
index 1647452399c971503bc0bfd969904ce0e09e5631..768a02ded77e200ca7037437de8aa87d2e08a5cf 100644
--- a/src/strass/strass_app/static/css/base.css
+++ b/src/strass/strass_app/static/css/base.css
@@ -130,6 +130,10 @@
         flex: 0 0 100%;
         max-width: 100%;
     }
+    .slider-handle, .slider-tick.in-selection, .tick-slider-selection{
+        background-image: none !important;
+        background-repeat: no-repeat !important;
+    }
     .no-print{display: none !important;}
 }
 @media screen{
diff --git a/src/strass/strass_app/static/js/apply_referee.js b/src/strass/strass_app/static/js/apply_referee.js
index 86640bd282c73620508621b00c5297cb278b0886..c667473db108bea3b345f0873aef5c36dbc544f9 100644
--- a/src/strass/strass_app/static/js/apply_referee.js
+++ b/src/strass/strass_app/static/js/apply_referee.js
@@ -31,4 +31,10 @@ function check_institutional_email(elt){
             .insertAfter($(hint));
 
     }
-}
\ No newline at end of file
+}
+
+$(document).ready(function(){
+    $('.check-institutional-email-target').on('keyup', function(e){
+        check_institutional_email(e.target);
+    });
+});
\ No newline at end of file
diff --git a/src/strass/strass_app/static/js/datatables.url.en.js b/src/strass/strass_app/static/js/datatables.url.en.js
index 87144598f38a20f897cec6d45d751e38a9969983..d6cb22249f3c5ce81886557e68a2812d4c082d8b 100644
--- a/src/strass/strass_app/static/js/datatables.url.en.js
+++ b/src/strass/strass_app/static/js/datatables.url.en.js
@@ -1 +1 @@
-var datatables_language_url = "https://cdn.datatables.net/plug-ins/1.10.21/i18n/English.json";
\ No newline at end of file
+var datatables_language_url = "https://cdn.datatables.net/plug-ins/1.13.10/i18n/English.json";
\ No newline at end of file
diff --git a/src/strass/strass_app/static/js/datatables.url.fr.js b/src/strass/strass_app/static/js/datatables.url.fr.js
index b568a2a7d2d29f18db9436fa7b8361b9ba3656a2..2f72f327026f95361547ddad64eb0686410e486d 100644
--- a/src/strass/strass_app/static/js/datatables.url.fr.js
+++ b/src/strass/strass_app/static/js/datatables.url.fr.js
@@ -1 +1 @@
-var datatables_language_url = "https://cdn.datatables.net/plug-ins/1.10.21/i18n/French.json";
\ No newline at end of file
+var datatables_language_url = "https://cdn.datatables.net/plug-ins/1.13.10/i18n/French.json";
\ No newline at end of file
diff --git a/src/strass/strass_app/static/js/language_menu.js b/src/strass/strass_app/static/js/language_menu.js
new file mode 100644
index 0000000000000000000000000000000000000000..b254a62375f3d66aa5d3685af809b6d80027a2ec
--- /dev/null
+++ b/src/strass/strass_app/static/js/language_menu.js
@@ -0,0 +1,5 @@
+$(document).ready(function(){
+    $('.language-menu .language-item').on('click', function(e) {
+        $('#lg>[name=\'language\']').val($(e.target).data("lang-code")).parent().submit();
+    });
+});
\ No newline at end of file
diff --git a/src/strass/strass_app/static/js/netsted_formset.js b/src/strass/strass_app/static/js/netsted_formset.js
index afc2ac041351a7f54246d7d11d0c2f223c2a7128..6f422fb7d41ec4f9777d86a7299992cb3950a066 100644
--- a/src/strass/strass_app/static/js/netsted_formset.js
+++ b/src/strass/strass_app/static/js/netsted_formset.js
@@ -39,6 +39,9 @@ $(document).ready(function(){
             $formset_container.find(".formset-new-item").hide();
          }
     });
+    $('.formset-new-item').on('click', function(e){
+        add_form_to_nested_formset(e.target);
+    });
 });
 
 function add_keyup_on_input(event){
diff --git a/src/strass/strass_app/static/js/question_update.js b/src/strass/strass_app/static/js/question_update.js
index ce9a50927652870d22bba6365727114be89e1c93..85c1e4ce4a4eed89582d262dbbb4514ce395b186 100644
--- a/src/strass/strass_app/static/js/question_update.js
+++ b/src/strass/strass_app/static/js/question_update.js
@@ -14,9 +14,15 @@ function init_question_update(origin){
     origin.find('form [name=has_multi_choices]').change(has_multi_choices_changed);
     origin.find('form [name=multi_choices_str]').change(has_multi_choices_changed);
     origin.find('form [name=multi_choices_str]').keyup(has_multi_choices_changed);
+    origin.find('form [name=text_range]').keyup(has_range_graded_changed);
+    origin.find('form [name=range_min]').change(has_range_graded_changed);
+    origin.find('form [name=range_max]').change(has_range_graded_changed);
+    origin.find('form [name=range_kind]').change(has_range_graded_changed);
+    origin.find('form [name=use_in_global_grade]').change(has_range_graded_changed);
     range_kind_changed({target:$("[name=range_kind]:checked")[0]});
     show_text_area_changed({target:$("[name=show_text_area]")[0]});
     has_multi_choices_changed({target:$("[name=has_multi_choices]")[0]});
+    has_range_graded_changed({target:$("[name=use_in_global_grade]")[0]});
 };
 
 function range_kind_changed(event){
@@ -25,12 +31,15 @@ function range_kind_changed(event){
     let value = event.target.value;
     $("[name=range_min]").closest(".form-row").hide();
     $("[name=text_range]").closest(".form-row").hide();
+    $("[name=use_in_global_grade]").closest(".form-row").show();
+    $(".range-in-global-grade-settings").closest(".form-row").show();
     if (value == 'int'){
         $("[name=range_min]").closest(".form-row").show();
     }else if (value == 'str'){
         $("[name=text_range]").closest(".form-row").show();
     }else if (value == 'none'){
-
+        $("[name=use_in_global_grade]").closest(".form-row").hide();
+        $(".range-in-global-grade-settings").closest(".form-row").hide();
     }
 }
 
@@ -62,6 +71,34 @@ function has_multi_choices_changed(event){
     }
 }
 
+function has_range_graded_changed(event){
+    if (typeof event.target == "undefined")
+        return;
+    if($("[name=use_in_global_grade]").prop("checked")){
+        $("[name=text_range]").closest(".card-body").find(".range-in-global-grade-settings").addClass("d-flex");
+        $("[name=text_range]").closest(".card-body").find(".range-in-global-grade-settings").removeClass("d-none");
+    }else{
+        $("[name=text_range]").closest(".card-body").find(".range-in-global-grade-settings").addClass("d-none");
+        $("[name=text_range]").closest(".card-body").find(".range-in-global-grade-settings").removeClass("d-flex");
+    }
+    $(".range-in-global-grade-dest")[0].innerHTML = '';
+    if ($("[name=range_kind]:checked")[0].value == 'str'){
+        var elements = $("[name=text_range]")[0].value.split(';');
+    }else{
+        var elements = [];
+        for (let i = $("[name=range_min]")[0].value; i <= $("[name=range_max]")[0].value; i++) {
+            elements.push(i)
+        }
+    }
+    $(' <tr><th>'+gettext('Choice')+'</th><th>'+gettext('Score')+'</th></tr>').appendTo($(".range-in-global-grade-dest"));
+    for (let i = 0; i < elements.length; i++) {
+        let element=elements[i];
+        if (element==='')
+            continue
+        $('<tr><th>'+element+'</th><td>'+Math.ceil((i/(elements.length-1))*100)+'%</td></tr>').appendTo($(".range-in-global-grade-dest"));
+    }
+}
+
 function question_new_order(event) {
     $("#order_form input[name=value]").val(
         $.map($(event.target).children(), function(el) {return $(el).data("question");}).join(',')
diff --git a/src/strass/strass_app/templates/strass_app/base.html b/src/strass/strass_app/templates/strass_app/base.html
index b7b119b984206feb822b9cbfd04df6570eed1ff1..8d83312f68e759b3ad9a152e99a3dc14ece7a82f 100644
--- a/src/strass/strass_app/templates/strass_app/base.html
+++ b/src/strass/strass_app/templates/strass_app/base.html
@@ -31,9 +31,9 @@
         <button class="btn dropdown-toggle" type="button" id="dropdownMenuButtonLg" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
             <i class="fa fa-globe fa-lg"></i> [{{LANGUAGE_CODE}}]
         </button>
-        <div class="dropdown-menu" aria-labelledby="dropdownMenuButtonLg">
+        <div class="dropdown-menu language-menu" aria-labelledby="dropdownMenuButtonLg">
             {% for language in languages %}
-            <a  class="dropdown-item" href="#" onclick="$('#lg>[name=\'language\']').val('{{ language.code }}').parent().submit();">{{ language.name_local }} ({{ language.code }})</a>
+            <a class="dropdown-item language-item" data-lang-code="{{ language.code }}" href="#">{{ language.name_local }} ({{ language.code }})</a>
             {% endfor %}
         </div>
     </div>
@@ -42,7 +42,7 @@
 
 {% block upper_nav_bar_right_form %}
 <ul class="nav navbar-nav navbar-right">
-    <li class="nav-item dropdown {{li_additional_class}}">
+    <li class="nav-item dropdown language-menu {{li_additional_class}}">
         {% get_current_language as LANGUAGE_CODE %}
         {% get_available_languages as LANGUAGES %}
         {% get_language_info_list for LANGUAGES as languages %}
@@ -56,7 +56,7 @@
         ><i class="fa fa-globe fa-lg"></i> [{{LANGUAGE_CODE}}]</a>
         <div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdown-user{{suffix_id}}">
         {% for language in languages %}
-            <a  class="dropdown-item" href="#" onclick="$('#lg>[name=\'language\']').val('{{ language.code }}').parent().submit();">{{ language.name_local }} ({{ language.code }})</a>
+            <a class="dropdown-item language-item" data-lang-code="{{ language.code }}" href="#">{{ language.name_local }} ({{ language.code }})</a>
         {% endfor %}
         </div>
     </li>
@@ -109,6 +109,7 @@
 <script src="https://code.jquery.com/ui/1.13.1/jquery-ui.js"></script>
 {% get_current_language as LANGUAGE_CODE %}
 <script src="{% url 'javascript-catalog' %}"></script>
+<script src="{% url 'strass:google-tracker-id' %}"></script>
 {% with '/js/datatables.url.'|add:LANGUAGE_CODE|add:".js" as datatables_url %}
 <script src="{% sstatic datatables_url %}"></script>
 {% endwith %}
@@ -117,8 +118,6 @@
 <script src="{% sstatic '/js/base.js' %}"></script>
 <script src="{% sstatic '/js/bootstrap_multiselect_bs4.js' %}"></script>
 <script src="{% sstatic '/js/cookie-consent.js' %}"></script>
-<script>
-    const google_tracker_id='{{google_tracker_id|default:''}}';
-</script>
+<script src="{% sstatic '/js/language_menu.js' %}"></script>
 <script defer="defer" data-domain="{{plausible_data_domain|default:''}}" src="https://plausible.pasteur.cloud/js/script.js"></script>
 {% endblock %}
diff --git a/src/strass/strass_app/templates/strass_app/candidate_detail.html b/src/strass/strass_app/templates/strass_app/candidate_detail.html
index e6fe4da84c8d24248af8defbb57ab8dd26786120..e55c6f40a2c82a6121cdad5c07e7e442738ecc41 100644
--- a/src/strass/strass_app/templates/strass_app/candidate_detail.html
+++ b/src/strass/strass_app/templates/strass_app/candidate_detail.html
@@ -15,7 +15,7 @@
 {% elif is_jury_manager or is_reviewer  %}
 <div>
     {% if request.user.id %}
-    <a href="{%url 'strass:conflict-update' request.user.id object.uid %}" role="button" class="btn btn-warning"> {%trans "Declare a conflict of interest" %}</a>
+    <a href="{%url 'strass:conflict-update' request.user.id object.uid %}" role="button" class="btn btn-warning no-print"> {%trans "Declare a conflict of interest" %}</a>
     {% endif %}
     {% if is_jury_manager %}
     <a href="{%url 'strass:candidate-contact' object.uid %}" role="button" class="btn btn-primary d-print-none"><i class="fa fa-envelope"></i> {%trans "Contact candidate" %}</a>
diff --git a/src/strass/strass_app/templates/strass_app/interviewsession_grid.html b/src/strass/strass_app/templates/strass_app/interviewsession_grid.html
index addba56b3ea1a7df4c67f94090667853f1adc19f..abb767d4dc1c0b7bf6335dd3f3976e5cbc9bcc52 100644
--- a/src/strass/strass_app/templates/strass_app/interviewsession_grid.html
+++ b/src/strass/strass_app/templates/strass_app/interviewsession_grid.html
@@ -8,7 +8,7 @@
 
         {% if field.label %}
             <label {% if field.id_for_label %}for="{{ field.id_for_label }}" {% endif %}class="{{ label_class }}{% if not inline_class %} col-form-label{% endif %}{% if field.field.required %} requiredField{% endif %}">
-                {{ field.label|safe }}{% if field.field.required %}<span class="asteriskField">*</span>{% endif %}
+                {{ field.label }}{% if field.field.required %}<span class="asteriskField">*</span>{% endif %}
             </label>
         {% endif %}
 
diff --git a/src/strass/strass_app/templates/strass_app/review_detail.html b/src/strass/strass_app/templates/strass_app/review_detail.html
index ccf251d861671bada60e4e1e6ccb029ddbfadf1c..8ff81064d599be93ee20e4b3980494cc4f798954 100644
--- a/src/strass/strass_app/templates/strass_app/review_detail.html
+++ b/src/strass/strass_app/templates/strass_app/review_detail.html
@@ -57,7 +57,7 @@
         {%endif%}
     </dl>
 {% if is_jury_manager or object.reviewer == request.user  %}
-    <a href="{%url 'strass:conflict-update' object.reviewer.id object.candidate.uid %}" role="button" class="btn btn-warning text-nowrap d-none d-xl-block w-100 mb-4"> {%trans "Declare a conflict of interest" %}</a>
+    <a href="{%url 'strass:conflict-update' object.reviewer.id object.candidate.uid %}" role="button" class="btn btn-warning text-nowrap d-none d-xl-block w-100 mb-4 no-print"> {%trans "Declare a conflict of interest" %}</a>
 {%endif%}
 {% if object.can_edit%}
     <a href="{%url 'strass:review-update' object.id %}" role="button" class="btn btn-primary text-nowrap d-none d-xl-block w-100"> {%trans "Edit it" %}</a>
diff --git a/src/strass/strass_app/templates/strass_app/setup.html b/src/strass/strass_app/templates/strass_app/setup.html
index 721b144ad54ebea97d0dc566da73d675cd0b89d5..2d90d0f4ca1aff4bb25702d8aed021e2eb7bd86c 100644
--- a/src/strass/strass_app/templates/strass_app/setup.html
+++ b/src/strass/strass_app/templates/strass_app/setup.html
@@ -34,14 +34,8 @@
             {% trans "Message about importing configuration in an empty instance" %}
             <br/>
             <a role="button"
-               href=""
+               href="{%url 'strass:import-settings' %}"
                class="btn btn-primary mt-0 float-right"
-               data-modal-action-url="{%url 'strass:import-settings' %}"
-               data-toggle="modal"
-               data-target="#modalForm"
-               data-modal-title="{% trans 'Import recruitment configuration' %}"
-               data-modal-submit="{%trans 'Import'%}"
-               data-modal-css-class="modal-dialog modal-lg"
             >
             <i class="fa fa-file-import"></i> {%trans "Import configuration" %}</a>
         </div>
@@ -321,19 +315,8 @@
             </div>
             <div class="col-6 col-md-12 p-2 d-flex {% if not is_import_allowed %}cursor-na{%endif%}">
             <a role="button"
-               href=""
+               href="{%url 'strass:import-settings' %}"
                class="btn {% if presence_of_data_in_importable_models %}btn-warning{%else%}btn-primary{%endif%} {% if not is_import_allowed %}disabled{%endif%} mt-0 flex-fill"
-               data-modal-action-url="{%url 'strass:import-settings' %}"
-               data-toggle="modal"
-               data-target="#modalForm"
-               data-modal-title="{% trans 'Import recruitment configuration' %}"
-               data-modal-css-class="modal-dialog modal-lg"
-               {% if presence_of_data_in_importable_models %}
-               data-modal-submit-css-class="btn btn-warning"
-               data-modal-submit="{%trans 'Import and overwrite'%}"
-               {%else%}
-               data-modal-submit="{%trans 'Import'%}"
-               {% endif %}
             >
             <i class="fa fa-file-import"></i> {%trans "Import configuration" %}</a>
             </div>
diff --git a/src/strass/strass_app/templates/strass_app/wizard_form.html b/src/strass/strass_app/templates/strass_app/wizard_form.html
index 53cab3fad22796b903f9b179c3e48f946bc9c102..f6caae365eb295139453cd1cf02a222c1cd25c11 100644
--- a/src/strass/strass_app/templates/strass_app/wizard_form.html
+++ b/src/strass/strass_app/templates/strass_app/wizard_form.html
@@ -47,8 +47,7 @@
         <div class="formset-new-item pb-4 text-center">
             <input type="button"
                    class="btn btn-primary"
-                   value="{%trans 'Add a referee'%}"
-                   onclick="add_form_to_nested_formset(this);"/>
+                   value="{%trans 'Add a referee'%}"/>
         </div>
         {% endif %}
         {% else %}
diff --git a/src/strass/strass_app/tests/test_admin.py b/src/strass/strass_app/tests/test_admin.py
index a95271f23a5d012c6ea016d0538819349e3575b1..7032affcf0ba560486db718d84991a0e00e80f9e 100644
--- a/src/strass/strass_app/tests/test_admin.py
+++ b/src/strass/strass_app/tests/test_admin.py
@@ -4,7 +4,7 @@ import re
 import zipfile
 from datetime import timedelta
 
-from PyPDF2 import PdfReader
+from pypdf import PdfReader
 from django.contrib.auth import get_user_model
 from django.contrib.auth.models import Group
 from django.contrib.contenttypes.models import ContentType
diff --git a/src/strass/strass_app/tests/test_app_settings.py b/src/strass/strass_app/tests/test_app_settings.py
index 3fde127f2105019d7e579649a4affd81adc2b8a5..c875129b1321bda477f07d98d78b3456d51a2b7d 100644
--- a/src/strass/strass_app/tests/test_app_settings.py
+++ b/src/strass/strass_app/tests/test_app_settings.py
@@ -292,6 +292,22 @@ class ViewsTestCase(BaseTestCase):
             self.assertFalse(getattr(live_settings, f'{f_name}__bool'), f_name)
             self.assertFalse(forms.AppSettingsForm().initial[f_name], f_name)
 
+        self.set_to("google_tracker_id", "'; alert(\"hacked\");a='", works=False)
+        self.assertNotIn("; alert(", live_settings.google_tracker_id)
+        self.set_to("google_tracker_id", "';<script...", works=False)
+        self.assertNotIn("<script", live_settings.google_tracker_id)
+        self.set_to("google_tracker_id", "azerty")
+        self.assertIn("azerty", live_settings.google_tracker_id)
+
+        self.set_to("plausible_data_domain", "'; alert(\"hacked\");a='", works=False)
+        self.assertNotIn("; alert(", live_settings.plausible_data_domain)
+        self.set_to("plausible_data_domain", "';<script...", works=False)
+        self.assertNotIn("<script", live_settings.plausible_data_domain)
+        self.set_to("plausible_data_domain", "invalide_domain.pasteur.cloud", works=False)
+        self.assertNotIn("_", live_settings.plausible_data_domain)
+        self.set_to("plausible_data_domain", "valide-domain.pasteur.cloud")
+        self.assertIn("valide-domain.pasteur.cloud", live_settings.plausible_data_domain)
+
 
 class FormTestCase(TooledTestCase):
     def test_setup_next_hash(self):
diff --git a/src/strass/strass_app/tests/test_candidate_apply.py b/src/strass/strass_app/tests/test_candidate_apply.py
index 0a628939d04320fd62cee8cf93824924576f0459..0235029122af91f22f7c29014b759f3751c76ad4 100644
--- a/src/strass/strass_app/tests/test_candidate_apply.py
+++ b/src/strass/strass_app/tests/test_candidate_apply.py
@@ -4,6 +4,7 @@ import logging
 import os
 import shutil
 from datetime import date, datetime, timedelta
+from tempfile import NamedTemporaryFile
 from typing import List, Union
 
 from basetheme_bootstrap.user_preferences_utils import get_user_preferences_for_user
@@ -21,7 +22,7 @@ from django.utils import timezone
 from faker import Faker
 
 from live_settings import live_settings
-from strass_app import models, views, business_logic, forms
+from strass_app import models, views, business_logic, forms, utils
 from strass_app.management.commands import load_demo
 from strass_app.tests.test_base_test_case import TooledTestCase
 
@@ -257,7 +258,11 @@ class TestCandidateApply(TooledTestCase):
             set(get_user_model().objects.filter(recommendation__candidate__pk=ada.pk).values_list('email', flat=True)),
             set(v.lower() for k, v in steps[3].form_data.items() if "email" in k) - {'', 'a@aa.aa'},
         )
-        self.assertFileEqual(ada.cv.path, os.path.join(self.test_data, "cv.pdf"))
+        with NamedTemporaryFile(prefix="strass-", suffix=".pdf") as temp_cv:
+            cv = open(os.path.join(self.test_data, "cv.pdf"), "rb")
+            with open(temp_cv.name, 'wb') as f:
+                f.write(utils.safe_pdf(cv).read())
+            self.assertFileEqual(ada.cv.path, temp_cv.name)
         self.assertEqual(ada.status, models.ApplicationStatus.objects.filter(default=True).first())
 
         # redo up to step 1 to check that duplicate are prevented
@@ -427,7 +432,11 @@ class TestCandidateApply(TooledTestCase):
             set(get_user_model().objects.filter(recommendation__candidate__pk=ada.pk).values_list('email', flat=True)),
             emails_in_form_data,
         )
-        self.assertFileEqual(ada.cv.path, os.path.join(self.test_data, "cv-larger.pdf"))
+        with NamedTemporaryFile(prefix="strass-", suffix=".pdf") as temp_cv:
+            cv = open(os.path.join(self.test_data, "cv-larger.pdf"), "rb")
+            with open(temp_cv.name, 'wb') as f:
+                f.write(utils.safe_pdf(cv).read())
+            self.assertFileEqual(ada.cv.path, temp_cv.name)
         self.assertTrue(models.Recommendation.objects.filter(pk=other_recommendation.pk))
         self.assertEqual(models.Recommendation.objects.count(), len(emails_in_form_data) + 1)
 
diff --git a/src/strass/strass_app/tests/test_candidate_pdf_safe.py b/src/strass/strass_app/tests/test_candidate_pdf_safe.py
new file mode 100644
index 0000000000000000000000000000000000000000..526246faab6165f3d879f28836346bddea693fb7
--- /dev/null
+++ b/src/strass/strass_app/tests/test_candidate_pdf_safe.py
@@ -0,0 +1,56 @@
+import io
+
+from django.core.exceptions import ValidationError
+from django.core.files.uploadedfile import SimpleUploadedFile
+
+from strass_app import models
+from strass_app.management.commands import load_demo
+from strass_app.tests.test_base_test_case import TooledTestCase
+import os
+
+
+class CandidatePDFSafeTestCase(TooledTestCase):
+    def test_invalid_uuid_length(self):
+        # this is done to check that clean_field work as expected
+        cv = open(os.path.join(self.test_data, "cv.pdf"), "rb")
+        load_demo.create_application_statuses()
+        c = models.Candidate(
+            email='ada.lovelace@pasteur.fr',
+            first_name='ada',
+            last_name='lovelace',
+            motivation="maybe",
+            uid='A',
+            status=models.ApplicationStatus.objects.first(),
+        )
+        self.assertRaises(ValidationError, c.clean_fields)
+
+    def test_with_good_file(self):
+        cv = open(os.path.join(self.test_data, "cv.pdf"), "rb")
+        load_demo.create_application_statuses()
+        c = models.Candidate(
+            email='ada.lovelace@pasteur.fr',
+            lang='en',
+            first_name='ada',
+            last_name='lovelace',
+            motivation="maybe",
+            uid='A' * models.Candidate.UID_LENGTH,
+            status=models.ApplicationStatus.objects.first(),
+            cv=SimpleUploadedFile(cv.name, cv.read()),
+        )
+        c.clean_fields()
+        c.clean_fields()  # check a second time to see if stream is still usable
+
+    def test_with_invalide_file(self):
+        load_demo.create_application_statuses()
+        f = io.StringIO("clearly not a pdf file")
+        c = models.Candidate(
+            email='ada.lovelace@pasteur.fr',
+            lang='en',
+            first_name='ada',
+            last_name='lovelace',
+            motivation="maybe",
+            uid='A' * models.Candidate.UID_LENGTH,
+            status=models.ApplicationStatus.objects.first(),
+            cv=SimpleUploadedFile("attacking.js.pdf", f.getvalue().encode()),
+        )
+        self.assertRaises(ValidationError, c.clean_fields)
diff --git a/src/strass/strass_app/tests/test_data_io.py b/src/strass/strass_app/tests/test_data_io.py
index 919719578caf9f87d2c064126800bdbc24d298ae..430abc83a0d402760b53fae3f49948e024208406 100644
--- a/src/strass/strass_app/tests/test_data_io.py
+++ b/src/strass/strass_app/tests/test_data_io.py
@@ -52,6 +52,21 @@ class TestViews(BaseTestCase):
         self.do_test_get_post([reverse('strass:import-settings')], [200])
 
 
+class TestViewsWithNoData(TooledTestCase):
+    def test_access_and_update(self):
+        u = get_user_model().objects.create(
+            username="user_pk_1",
+            first_name="user_pk_1",
+            last_name="user_pk_1",
+            email="user_pk_1@pasteur.fr",
+        )
+        business_logic.set_jury_manager(u, True)
+        self.client.force_login(u)
+        r = self.client.get(reverse('strass:export-settings'))
+        self.do_test_get_post([reverse('strass:export-settings')], [200])
+        self.do_test_get_post([reverse('strass:import-settings')], [200])
+
+
 class TestExportViews(BaseTestCase):
     def test_export(self):
         live_settings.show_email_as_message = False
diff --git a/src/strass/strass_app/tests/test_invitation.py b/src/strass/strass_app/tests/test_invitation.py
index 44410e30744f132907f5d96c4de2c3352747715a..e69bde08e6bd041392f32cf9c736c8ab97929c50 100644
--- a/src/strass/strass_app/tests/test_invitation.py
+++ b/src/strass/strass_app/tests/test_invitation.py
@@ -6,6 +6,7 @@ from django.contrib.auth import get_user_model
 from django.contrib.auth.models import Group
 from django.contrib.messages import get_messages
 from django.core import mail
+from django.test import override_settings
 from django.urls import reverse
 from django.utils import timezone
 from freezegun import freeze_time
@@ -93,6 +94,9 @@ class ViewsTestCase(BaseTestCase):
             notify_on_accepted_invitation=models.MyUserPreferences.notify_on_accepted_invitation_NEVER
         )
 
+    @override_settings(
+        ADMINS=(),
+    )
     def test_all_at_once_to_spare_resource(self):
         #######################################################################
         # def test_access_and_update(self):
diff --git a/src/strass/strass_app/tests/test_password_validation.py b/src/strass/strass_app/tests/test_password_validation.py
new file mode 100644
index 0000000000000000000000000000000000000000..4a9e9ec60075cd7270fe6c958059633d5d56fc87
--- /dev/null
+++ b/src/strass/strass_app/tests/test_password_validation.py
@@ -0,0 +1,197 @@
+from django.core.exceptions import ValidationError
+from django.utils import timezone
+from freezegun import freeze_time
+
+from strass_app import password_validation
+from strass_app.tests.test_base_test_case import TooledTestCase
+
+
+class HasSymbolCharValidatorTestCase(TooledTestCase):
+    def test_passing(self):
+        for p in [
+            'azerty@',
+            '@',
+            ']',
+            '[',
+            '~',
+            '!',
+            '?',
+            '@',
+            '#',
+            '$',
+            '%',
+            '^',
+            '&',
+            '*',
+            '(',
+            ')',
+            '_',
+            '+',
+            '{',
+            '}',
+            '"',
+            ':',
+            ';',
+            '\'',
+        ]:
+            self.assertIsNone(password_validation.HasSymbolCharValidator().validate(p))
+
+    def test_failing(self):
+        for p in [
+            'azerty',
+            'azert1',
+            'Azerty',
+            'AZERTY',
+        ]:
+            self.assertRaises(ValidationError, password_validation.HasSymbolCharValidator().validate, p)
+
+
+class HasNoAccentedLetterValidatorTestCase(TooledTestCase):
+    validator = password_validation.HasNoAccentedLetterValidator
+
+    def test_passing(self):
+        for p in [
+            'azerty@',
+            '@',
+            ']',
+            '[',
+            '~',
+            '!',
+            '?',
+            '@',
+            '#',
+            '$',
+            '%',
+            '^',
+            '&',
+            '*',
+            '(',
+            ')',
+            '_',
+            '+',
+            '{',
+            '}',
+            '"',
+            ':',
+            ';',
+            '\'',
+        ]:
+            pwd = f'{p}aZ1'
+            self.assertIsNone(self.validator().validate(pwd), pwd)
+
+    def test_failing(self):
+        for p in [
+            'î',
+            'ï',
+            'í',
+            'Ä«',
+            'į',
+            'ì',
+            'azérty',
+            'ç',
+        ]:
+            self.assertRaises(ValidationError, self.validator().validate, p)
+
+
+class HasDigitLetterCharValidatorTestCase(TooledTestCase):
+    def test_passing(self):
+        for p in [
+            'azert1',
+            '1',
+            '123.5',
+        ]:
+            self.assertIsNone(password_validation.HasDigitLetterCharValidator().validate(p))
+
+    def test_failing(self):
+        for p in [
+            'azerty',
+            'Azerty',
+            'azerty@',
+            '@',
+            'AZERTY',
+        ]:
+            self.assertRaises(ValidationError, password_validation.HasDigitLetterCharValidator().validate, p)
+
+
+class HasCapitalLetterCharValidatorTestCase(TooledTestCase):
+    def test_passing(self):
+        for p in [
+            'Azerty',
+            'AZERTY',
+        ]:
+            self.assertIsNone(password_validation.HasCapitalLetterCharValidator().validate(p))
+
+    def test_failing(self):
+        for p in [
+            'azert1',
+            '1',
+            '123.5',
+            'azerty',
+            'azerty@',
+            '@',
+            '{',
+            '}',
+            '"',
+        ]:
+            self.assertRaises(ValidationError, password_validation.HasCapitalLetterCharValidator().validate, p)
+
+
+class WronglyInheritedMetaCharValidatorTestCase(TooledTestCase):
+    def test_failing_both_not_implemented(self):
+        class InvalidCharValidator(password_validation._MetaCharValidator):
+            pass
+
+        self.assertRaises(TypeError, InvalidCharValidator)
+
+    def test_ok_with_kind_and_check_char_implemented(self):
+        class InvalidCharValidator(password_validation._MetaCharValidator):
+            @property
+            def kind(self):
+                return "e"
+
+            def check_char(self, char: str):
+                return True
+
+        v = InvalidCharValidator()
+        self.assertIsNotNone(v.kind)
+        v.check_char('r')
+        self.assertTrue(True, " Instanciation have worked, no new method")
+
+
+class HasNoSubPartFromListValidatorTestCase(TooledTestCase):
+
+    validator = password_validation.HasNoSubPartFromListValidator
+
+    def test_passing(self):
+        for p in [
+            'azert1',
+            '1955',
+        ]:
+            self.assertIsNone(self.validator().validate(p))
+
+    def test_failing(self):
+
+        for p in [
+            'InStitut',
+            'Pasteur',
+            timezone.now().strftime("%Y "),
+        ]:
+            self.assertRaises(ValidationError, self.validator().validate, p)
+
+    @freeze_time("1955-11-12")
+    def test_passing1955(self):
+        for p in [
+            'azert1',
+            '2001',
+        ]:
+            self.assertIsNone(self.validator().validate(p))
+
+    @freeze_time("1955-11-12")
+    def test_failing1955(self):
+
+        for p in [
+            'InStitut',
+            'Pasteur',
+            '1955',
+        ]:
+            self.assertRaises(ValidationError, self.validator().validate, p)
diff --git a/src/strass/strass_app/tests/test_pdf_cache.py b/src/strass/strass_app/tests/test_pdf_cache.py
index c9ead1a2761bf269f6381348cfe2e410b774d824..137d03547a3f189510eed35fed4b054ac89772b2 100644
--- a/src/strass/strass_app/tests/test_pdf_cache.py
+++ b/src/strass/strass_app/tests/test_pdf_cache.py
@@ -4,6 +4,7 @@ import logging
 import os
 import pathlib
 import shutil
+from tempfile import NamedTemporaryFile
 
 from django.conf import settings
 from django.contrib.auth import get_user_model
@@ -14,7 +15,7 @@ from django.test import override_settings
 from django.urls import reverse
 
 from live_settings import live_settings
-from strass_app import models, business_logic
+from strass_app import models, business_logic, utils
 from strass_app.tests.test_base_test_case import TooledTestCase
 
 MEDIA_DIR = f'.media_for_{__name__.split(".")[-1]}'
@@ -196,7 +197,11 @@ class TestCacheOK(TooledTestCase):
             set(get_user_model().objects.filter(recommendation__candidate__pk=ada.pk).values_list('email', flat=True)),
             set(v.lower() for k, v in steps[2].form_data.items() if "email" in k) - {'', 'a@aa.aa'},
         )
-        self.assertFileEqual(ada.cv.path, os.path.join(self.test_data, "cv.pdf"))
+        with NamedTemporaryFile(prefix="strass-", suffix=".pdf") as temp_cv:
+            cv = open(os.path.join(self.test_data, "cv.pdf"), "rb")
+            with open(temp_cv.name, 'wb') as f:
+                f.write(utils.safe_pdf(cv).read())
+            self.assertFileEqual(ada.cv.path, temp_cv.name)
         self.assertEqual(ada.status, models.ApplicationStatus.objects.filter(default=True).first())
 
         # redo up to step 1 to check that duplicate are prevented
diff --git a/src/strass/strass_app/tests/test_sanitize_pdf.py b/src/strass/strass_app/tests/test_sanitize_pdf.py
new file mode 100644
index 0000000000000000000000000000000000000000..fab619886722ffa0fae7d638dba9ba79f7bd1564
--- /dev/null
+++ b/src/strass/strass_app/tests/test_sanitize_pdf.py
@@ -0,0 +1,23 @@
+import os
+from io import StringIO
+
+from strass_app import utils
+from strass_app.tests.test_base_test_case import TooledTestCase
+
+
+class SafePDFTestCase(TooledTestCase):
+    def check_no_js(self, my_io):
+        self.assertNotIn('ICanSubmitTheContentOfThisFileAnywhere', str(my_io.read()))
+        my_io.seek(0)
+
+    def test_small_file_ok(self):
+        cv = open(os.path.join(self.test_data, "cv.pdf"), "rb")
+        my_io = utils.safe_pdf(cv)
+        self.check_no_js(my_io)
+
+    def test_file_with_js(self):
+        cv = open(os.path.join(self.test_data, "cv-with-js.pdf"), "rb")
+        self.assertIn('ICanSubmitTheContentOfThisFileAnywhere', str(cv.read()))
+        cv.seek(0)
+        my_io = utils.safe_pdf(cv)
+        self.check_no_js(my_io)
diff --git a/src/strass/strass_app/tests/test_setup.py b/src/strass/strass_app/tests/test_setup.py
index 27fa3fbd60c670fe4ff658fbdb01151bc08e33e5..b4574563a19dd027f162b27cf7be9eb8f1c29164 100644
--- a/src/strass/strass_app/tests/test_setup.py
+++ b/src/strass/strass_app/tests/test_setup.py
@@ -25,8 +25,8 @@ class JuryRegisterTestCase(TooledTestCase):
                     email='user@pasteur.fr',
                     first_name='user',
                     last_name='nop',
-                    password1='basetheme_bootstrap',
-                    password2='basetheme_bootstrap',
+                    password1='basetheme_bootstrapA&1',
+                    password2='basetheme_bootstrapA&1',
                 ),
                 follow=True,
             ),
@@ -41,8 +41,8 @@ class JuryRegisterTestCase(TooledTestCase):
                     email='user2@pasteur.fr',
                     first_name='user2',
                     last_name='nop2',
-                    password1='basetheme_bootstrap',
-                    password2='basetheme_bootstrap',
+                    password1='basetheme_bootstrapA&1',
+                    password2='basetheme_bootstrapA&1',
                 ),
             ).status_code,
             200,
diff --git a/src/strass/strass_app/tests/test_validators.py b/src/strass/strass_app/tests/test_validators.py
new file mode 100644
index 0000000000000000000000000000000000000000..beff583f1d48234c12c3fdb9577ab108a41fa76a
--- /dev/null
+++ b/src/strass/strass_app/tests/test_validators.py
@@ -0,0 +1,67 @@
+import io
+import types
+
+from django.core.exceptions import ValidationError
+from django.core.files.uploadedfile import SimpleUploadedFile
+
+from strass_app import models
+from strass_app.management.commands import load_demo
+from strass_app.tests.test_base_test_case import TooledTestCase
+import os
+
+from strass_app.validators import FileMimeTypeValidator
+
+
+class FileMimeTypeValidatorTestCase(TooledTestCase):
+    """
+    Inspired from https://github.com/django/django/blob/main/tests/validators/tests.py
+    """
+
+    def test_validator(self):
+        for validator, value, expected in [
+            (FileMimeTypeValidator(['application/pdf']), open(os.path.join(self.test_data, "cv.pdf"), "rb"), None),
+            (FileMimeTypeValidator(['application/pdf']), io.StringIO("clearly not a pdf file"), ValidationError),
+            (
+                FileMimeTypeValidator(['text/plain']),
+                open(os.path.join(self.test_data, "cv.pdf"), "rb"),
+                ValidationError,
+            ),
+            (FileMimeTypeValidator(['text/plain']), io.StringIO("clearly not a pdf file"), None),
+        ]:
+            name = validator.__name__ if isinstance(validator, types.FunctionType) else validator.__class__.__name__
+            exception_expected = expected is not None and issubclass(expected, Exception)
+            with self.subTest(name, value=value):
+                if exception_expected:
+                    with self.assertRaises(expected):
+                        validator(value)
+                else:
+                    self.assertEqual(expected, validator(value))
+
+    def test_mime_type_extension_equality(self):
+        self.assertEqual(FileMimeTypeValidator(), FileMimeTypeValidator())
+        self.assertEqual(FileMimeTypeValidator(['application/pdf']), FileMimeTypeValidator(['application/pdf']))
+        self.assertEqual(FileMimeTypeValidator(['APPLICATION/PDF']), FileMimeTypeValidator(['application/pdf']))
+        self.assertEqual(
+            FileMimeTypeValidator(['APPLICATION/pdf', 'application/json']),
+            FileMimeTypeValidator(['application/pdf', 'application/json']),
+        )
+        self.assertEqual(
+            FileMimeTypeValidator(['APPLICATION/pdf', 'application/json', 'text/plain']),
+            FileMimeTypeValidator(['text/plain', 'application/json', 'application/pdf']),
+        )
+        self.assertEqual(
+            FileMimeTypeValidator(['application/pdf']),
+            FileMimeTypeValidator(["application/pdf"], code="invalid_mime_type"),
+        )
+        self.assertNotEqual(FileMimeTypeValidator(['application/pdf']), FileMimeTypeValidator(["image/png"]))
+        self.assertNotEqual(
+            FileMimeTypeValidator(['application/pdf']), FileMimeTypeValidator(["image/png", "image/jpg"])
+        )
+        self.assertNotEqual(
+            FileMimeTypeValidator(['application/pdf']),
+            FileMimeTypeValidator(["txt"], code="custom_code"),
+        )
+        self.assertNotEqual(
+            FileMimeTypeValidator(['application/pdf']),
+            FileMimeTypeValidator(["txt"], message="custom error message"),
+        )
diff --git a/src/strass/strass_app/tests/test_views_candidate.py b/src/strass/strass_app/tests/test_views_candidate.py
index 2d93caa0cef19bc8443d03b887f3ae3c79f12920..0878e97ccc359514fccc3fb11e3118e4cb3816b9 100644
--- a/src/strass/strass_app/tests/test_views_candidate.py
+++ b/src/strass/strass_app/tests/test_views_candidate.py
@@ -2,7 +2,7 @@ import csv
 import io
 from datetime import timedelta
 
-from PyPDF2 import PdfReader
+from pypdf import PdfReader
 from django.contrib.auth import get_user_model
 from django.urls import reverse
 from django.utils import timezone
@@ -121,29 +121,24 @@ class ViewsTestCase(BaseTestCase):
         q_to_del.delete()
         del q_to_del
 
-        # check that question too personal are filtered out
-        for tried_banned_string in [
-            "what is your first name",
-            "what is your FirSt Name",
-            "what is your FirStName",
-            "what is your FirSt-Name",
-            "what is your FirSt_Name",
-            "what is your gender",
-            "what is your phone",
-            "Give a second email",
-            "Un autre couriel",
-            "votre prénom",
-            "votre prenom",
-            "votre pr&eacute;nom",
-        ]:
-            q = models.CandidateQuestion.objects.first()
-            q.question = tried_banned_string
-            q.save()
-            response = self.client.post(url, form_data, follow=False)
-            self.assertEqual(response.status_code, 200)
-            content = response.content.decode("UTF-8")
-            self.assertNotIn(q.question, content)
-            self.assertNotIn("FirSt Name", content)
+        # test csv content
+        form_data = {
+            ActionFormListView.context_object_name: [
+                self.candidate_with_account.get_associated_candidate().pk,
+                self.candidates.first().pk,
+                self.candidates.exclude(pk=self.candidates.last().pk).last().pk,
+            ],
+            'action': [k for k in admin.CandidateAdmin.get_export_actions().keys() if "csv" in k][0],
+        }
+        response = self.client.post(url, form_data, follow=False)
+        self.assertEqual(response.status_code, 200)
+        content = response.content.decode("UTF-8")
+        candidate_not_in = self.candidates.exclude(pk=self.candidates.first().pk).first()
+        assert candidate_not_in.pk not in form_data[ActionFormListView.context_object_name]
+        self.assertIn(self.candidate_with_account.first_name, content)
+        self.assertIn(self.candidate_with_account.last_name, content)
+        self.assertNotIn(candidate_not_in.first_name, content)
+        self.assertNotIn(candidate_not_in.last_name, content)
 
         # test pdf export
         form_data = {
diff --git a/src/strass/strass_app/tests/test_views_others.py b/src/strass/strass_app/tests/test_views_others.py
index ab22214b1a3f5539ecbe70dbe7ebd9e24c110cf9..cafbd4a33b35814b1cd02e6d9bbf16800db0e510 100644
--- a/src/strass/strass_app/tests/test_views_others.py
+++ b/src/strass/strass_app/tests/test_views_others.py
@@ -7,7 +7,7 @@ import shutil
 import zipfile
 from tempfile import TemporaryDirectory
 
-from PyPDF2 import PdfReader
+from pypdf import PdfReader
 from django.conf import settings
 from django.contrib.auth import get_user_model
 from django.contrib.auth.models import AnonymousUser
@@ -722,3 +722,24 @@ class OtherWithoutDataTestCase(TooledTestCase):
         self.assertEqual(self.client.get(reverse("basetheme_bootstrap:about_page")).status_code, 200)
         live_settings.contact_us = "a@a.fr"
         self.assertEqual(self.client.get(reverse("basetheme_bootstrap:about_page")).status_code, 200)
+
+
+class GoogleIdViewTestCase(TestCase):
+    def test_is_ok(self):
+        def fcn():
+            return self.client.get(reverse('strass:google-tracker-id')).content.decode("UTF-8")
+
+        live_settings.google_tracker_id = ""
+        self.assertIn("google_tracker_id='';", fcn())
+
+        live_settings.google_tracker_id = "UA-0000-0"
+        self.assertIn("UA-0000-0", fcn())
+
+        live_settings.google_tracker_id = "UA-0000-1"
+        self.assertIn("UA-0000-1", fcn())
+
+        live_settings.google_tracker_id = ""
+        self.assertIn("google_tracker_id='';", fcn())
+
+        live_settings.google_tracker_id = "'; alert(\"hacked\");a='"
+        self.assertEqual(len(fcn().split(';')), 2, fcn())
diff --git a/src/strass/strass_app/urls.py b/src/strass/strass_app/urls.py
index 9f1b131f46fdb95d00b839967b9c248054530ef6..b9b8959c74670514c8af0b14de467b463b642fb8 100644
--- a/src/strass/strass_app/urls.py
+++ b/src/strass/strass_app/urls.py
@@ -321,4 +321,5 @@ urlpatterns = [
         name='autocomplete-mail-view',
     ),
     path('doc/', RedirectView.as_view(url='http://hub.pages.pasteur.fr/strass/')),
+    path('google-tracker-id/', views.google_tracker_id_view, name='google-tracker-id'),
 ]
diff --git a/src/strass/strass_app/utils.py b/src/strass/strass_app/utils.py
index 26bc740e0c3050b66debd2931c3f935578207239..e0dfdde81bd8c80a318d924bb36722aa6474f5ab 100644
--- a/src/strass/strass_app/utils.py
+++ b/src/strass/strass_app/utils.py
@@ -1,6 +1,8 @@
 import functools
 import logging
 from abc import abstractmethod
+from io import BytesIO
+from typing import IO, Any
 
 from django.contrib import messages
 from django.contrib.admin import helpers as admin_helpers
@@ -14,6 +16,7 @@ from django.utils import translation
 from django.utils.translation import gettext_lazy as _
 from django.views.generic import ListView
 from django.views.generic.edit import FormMixin
+from pypdf import PdfWriter
 
 from live_settings import live_settings
 
@@ -265,3 +268,28 @@ def validate_multiple_email(email):
                 e.code,
                 e.params,
             )
+
+
+def _pdf_object_cleanup(pdf_file, obj):
+    for k in list(obj):
+        if k in [
+            '/JavaScript',
+            '/S',
+        ]:
+            incriminated_obj = pdf_file.get_object(obj[k]['/Names'][1].idnum)
+            keys = list(incriminated_obj.keys())
+            for k_to_del in keys:
+                del incriminated_obj[k_to_del]
+            del obj[k]
+
+
+def safe_pdf(my_stream: IO[Any]):
+    writer = PdfWriter(clone_from=my_stream)
+    _pdf_object_cleanup(writer, writer.root_object.get('/Names', {}))
+
+    for page in writer.pages:
+        _pdf_object_cleanup(writer, page.get_object())
+    myio = BytesIO()
+    writer.write(myio)
+    myio.seek(0)
+    return myio
diff --git a/src/strass/strass_app/validators.py b/src/strass/strass_app/validators.py
new file mode 100644
index 0000000000000000000000000000000000000000..1c4e83420722d06e2ed79cc448a42e3073ed1268
--- /dev/null
+++ b/src/strass/strass_app/validators.py
@@ -0,0 +1,42 @@
+from gettext import gettext as _
+
+import magic
+from django.core.exceptions import ValidationError
+from django.utils.deconstruct import deconstructible
+
+
+@deconstructible
+class FileMimeTypeValidator:
+    message = _("MIME type \"%(mime_type)s\" is not allowed. Allowed MIME types are: %(allowed_mime_types)s.")
+    code = "invalid_mime_type"
+
+    def __init__(self, allowed_mime_types=None, message=None, code=None):
+        if allowed_mime_types is not None:
+            allowed_mime_types = [allowed_extension.lower() for allowed_extension in allowed_mime_types]
+        self.allowed_mime_types = allowed_mime_types
+        if message is not None:
+            self.message = message
+        if code is not None:
+            self.code = code
+
+    def __call__(self, value):
+        mime = magic.from_buffer(value.read(1024), mime=True)
+        value.seek(0)
+        if self.allowed_mime_types is not None and mime not in self.allowed_mime_types:
+            raise ValidationError(
+                self.message,
+                code=self.code,
+                params={
+                    "mime_type": mime,
+                    "allowed_mime_types": ", ".join(self.allowed_mime_types),
+                    "value": value,
+                },
+            )
+
+    def __eq__(self, other):
+        return (
+            isinstance(other, self.__class__)
+            and set(self.allowed_mime_types or []) == set(other.allowed_mime_types or [])
+            and self.message == other.message
+            and self.code == other.code
+        )
diff --git a/src/strass/strass_app/views.py b/src/strass/strass_app/views.py
index bf1911029b1009ea98e47b6812ec26914367ce8b..f0f9dd238e6a54c7f63a28e86425286cc7fadbc8 100644
--- a/src/strass/strass_app/views.py
+++ b/src/strass/strass_app/views.py
@@ -9,6 +9,7 @@ import os
 import pathlib
 import posixpath
 import random
+import re
 import string
 from email.mime.image import MIMEImage
 from functools import reduce
@@ -72,6 +73,7 @@ from strass_app.templatetags.strass_tags import markdown
 from strass_app.utils import get_email_backend, ActionFormListView, ModelAdminWrapper, ModalFormMixin, use_target_lang
 
 logger = logging.getLogger(__name__)
+_google_tracker_id_view_re = re.compile(r'[^\-a-zA-Z0-9]')
 
 
 class CreateUpdateView(UpdateView):
@@ -161,6 +163,15 @@ def index(request):
     )
 
 
+def google_tracker_id_view(request):
+    traker_id = live_settings.google_tracker_id or ''
+    traker_id = _google_tracker_id_view_re.sub("_", traker_id)
+    return HttpResponse(
+        f"const google_tracker_id='{traker_id}';",
+        content_type='text/javascript',
+    )
+
+
 class AbstractCandidateDetailView(DetailView):
     class Meta:
         abstract = True
@@ -1092,14 +1103,29 @@ class AppSettingsView(mixins.OnlyJuryManagerMixin, ModalFormMixin, FormView):
         return super().form_valid(form)
 
 
-class ImportSettingsView(mixins.OnlyJuryManagerMixin, mixins.NoCandidateAppliedMixin, ModalFormMixin, FormView):
+class ImportSettingsView(mixins.OnlyJuryManagerMixin, mixins.NoCandidateAppliedMixin, FormView):
+    template_name = 'basetheme_bootstrap/small_form_host.html'
     form_class = forms.ImportSettingsForm
     initial = dict(
         scope=[k for k, v in data_io.get_models_as_choices()],
     )
 
+    def get_context_data(self, *, object_list=None, **kwargs):
+        context = super().get_context_data(object_list=object_list, **kwargs)
+        context["page_title"] = _("Import recruitment configuration")
+        context["title"] = _("Import recruitment configuration")
+        context["custom_css_width"] = "col-xl-8 offset-xl-2"
+        context["cancel_url"] = self.get_success_url()
+        context["btn_cancel_classes"] = "btn btn-secondary"
+        if data_io.presence_of_data_in_importable_models():
+            context["submit_text"] = _("Import and overwrite")
+            context["btn_classes"] = "btn btn-danger"
+        else:
+            context["submit_text"] = _("Import")
+        return context
+
     def get_success_url(self):
-        return reverse('strass:setup')
+        return f"{reverse('strass:setup')}#io-config"
 
     def form_valid(self, form):
         data_io.import_dumped_data(
diff --git a/src/strass/test_data/README.md b/src/strass/test_data/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..5204054da0ad4564071ef40ecd426da809c2a3f2
--- /dev/null
+++ b/src/strass/test_data/README.md
@@ -0,0 +1,15 @@
+Here is how pdf with js file is generated
+
+```python
+from io import BytesIO
+from typing import IO, Any
+
+from pypdf import PdfWriter, PdfReader
+
+writer = PdfWriter(clone_from="cv.pdf")
+writer.add_js('app.alert("ICanSubmitTheContentOfThisFileAnywhere.");')
+with open("cv-with-js.pdf", "wb") as fp:
+    writer.write(fp)
+
+writer = PdfWriter(clone_from="cv-with-js.pdf")
+```
\ No newline at end of file
diff --git a/src/strass/test_data/cv-with-js.pdf b/src/strass/test_data/cv-with-js.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..79d4e2983ef52af175622fc88833111a4fc6d46c
Binary files /dev/null and b/src/strass/test_data/cv-with-js.pdf differ