Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Connexion: Nouveau parcours pour les candidats [GEN-2012] #5288

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion itou/static/img/login.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 25 additions & 1 deletion itou/static/img/peamu_btn.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions itou/templates/account/login_existing_user.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
{% elif login_provider == "FC" %}
<p class="h4">Se connecter avec FranceConnect</p>
{% if show_france_connect %}
<p>
<strong>Votre adresse e-mail utilise une connexion via FranceConnect. Veuillez vous connecter en utilisant le bouton ci-dessous.</strong>
</p>
<p>
FranceConnect est la solution proposée par l’État pour sécuriser et simplifier la connexion à vos services en ligne.
</p>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

J'ai l'impression qu'une partie de mise à jour de wording (et d'image) pourrait facilement être sortie dans un commit séparé (voir même une PR ?) .

<div class="mt-4">{% include "signup/includes/france_connect_button.html" %}</div>
{% else %}
<div class="alert alert-info" role="status">
Expand All @@ -31,6 +37,9 @@
{% elif login_provider == "PEC" %}
<p class="h4">Se connecter avec France Travail</p>
{% if show_peamu %}
<p>
<strong>Votre adresse e-mail utilise une connexion via France Travail. Veuillez vous connecter en utilisant le bouton ci-dessous.</strong>
</p>
<div class="mt-4 text-center">{% include "signup/includes/peamu_button.html" %}</div>
{% else %}
<div class="alert alert-info" role="status">
Expand Down
55 changes: 34 additions & 21 deletions itou/templates/account/login_job_seeker.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{# django-allauth template override. #}
{% extends "layout/base.html" %}
{% load static %}
{% load django_bootstrap5 %}
{% load redirection_fields %}
{% load buttons_form %}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔡


{% block title %}Connexion candidat {{ block.super }}{% endblock %}

Expand All @@ -12,31 +15,41 @@
<div class="s-section__row row">
<div class="s-section__col col-12 col-lg-6">
<div class="c-form mb-5">
<p class="h4">Se connecter avec FranceConnect</p>
{% if show_france_connect %}
<div class="mt-4">{% include "signup/includes/france_connect_button.html" %}</div>
{% else %}
<div class="alert alert-info" role="status">
<p class="mb-0">FranceConnect est désactivé.</p>
</div>
{% endif %}
<hr class="my-5" data-it-text="ou">
<p class="h4 text-primary">Quelle adresse mail utilisez-vous ?</p>
<p class="text-secondary fs-sm">
Si vous avec un compte <strong>France Travail</strong> ou <strong>FranceConnect</strong> et que vous souhaitez vous connecter avec ce compte,
merci de renseigner dans ce champs l’adresse e-mail qui y est associé.
</p>

<form method="post" class="js-prevent-multiple-submit">
{% csrf_token %}
{% redirection_input_field value=redirect_field_value %}

<p class="h4">Se connecter avec France Travail</p>
{% if show_peamu %}
<div class="row mt-3">
<div class="col-sm">
<div class="text-center">{% include "signup/includes/peamu_button.html" %}</div>
<div class="form-group mb-1 form-group-required text-primary">
{% bootstrap_label "Adresse e-mail" label_for="id_email" %}
{% bootstrap_field form.email wrapper_class="form-group mb-0" show_label=False %}
<div class="form-text mb-3">
<div class="text-end mt-1">
<a href="#" class="fs-sm text-secondary" data-bs-toggle="modal" data-bs-target="#no-email-modal">Pas d'adresse e-mail ?</a>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pour contrôler une modale, plutôt utiliser un <button type="button">

</div>
{% include "signup/includes/no_email_link.html" with exclude_button=True only %}
</div>
</div>
{% else %}
<div class="alert alert-info" role="status">
<p class="mb-0">France Travail est désactivé.</p>
</div>
{% endif %}
<hr class="my-5" data-it-text="ou">
{% url 'home:hp' as reset_url %}
{% itou_buttons_form primary_label="Suivant" reset_url=reset_url %}

{% include "account/includes/login_form.html" %}
<div class="mt-5 mb-5 text-end text-primary">
<p>
Vous n'avez pas de compte ? <a href="{% url 'signup:job_seeker_situation' %}">Inscription</a>
</p>
</div>
</form>
</div>
</div>
<!-- Hide left column on small devices. -->
<div class="d-none d-lg-inline-flex align-items-center col-lg-6 justify-content-center">
<div class="w-75">
<img class="img-fluid img-fitcover" src="{% static 'img/login.svg' %}" alt="">
</div>
</div>
</div>
Expand Down
19 changes: 19 additions & 0 deletions itou/templates/utils/modal_includes/login_failure.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% load static %}
{% load theme_inclusion %}
{% load django_bootstrap5 %}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔤


<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title" id="message-modal-{{ forloop.counter }}-label">
{% if "email_does_not_exist" in message.extra_tags %}
Adresse e-mail inconnue
{% else %}
Le connexion a échoué
{% endif %}
</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
{{ message }}
</div>
</div>
2 changes: 2 additions & 0 deletions itou/templates/utils/modals.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
{% include "utils/modal_includes/sso_email_conflict_registration_failure.html" %}
{% elif "registration_failure" in message.extra_tags %}
{% include "utils/modal_includes/registration_failure.html" %}
{% elif "login_failure" in message.extra_tags %}
{% include "utils/modal_includes/login_failure.html" %}
{% endif %}
</div>

Expand Down
44 changes: 44 additions & 0 deletions itou/www/login/forms.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,57 @@
from allauth.account.forms import LoginForm
from django import forms
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.urls import reverse
from django.utils.html import format_html
from django.utils.safestring import mark_safe

from itou.openid_connect.errors import format_error_modal_content
from itou.users.enums import IdentityProvider
from itou.users.models import User


class FindExistingUserViaEmailForm(forms.Form):
"""
Validates only the email field. Displays a modal to user if email not in use
"""

email = forms.EmailField(
label="Adresse e-mail",
required=True,
widget=forms.TextInput(
attrs={"type": "email", "placeholder": "[email protected]", "autocomplete": "email", "autofocus": True}
),
)

def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request", None)
Comment on lines +28 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request", None)
def __init__(self, *args, *, request=None, **kwargs):
self.request = request

super().__init__(*args, **kwargs)

def clean_email(self):
email = self.cleaned_data.get("email")
self.user = User.objects.filter(email__iexact=email).first()
if self.user is None:
messages.error(
self.request,
format_error_modal_content(
mark_safe(
"<p>Cette adresse e-mail est inconnue de nos services.</p>"
"<p>Si vous êtes déjà inscrit(e), "
"assurez-vous de saisir correctement votre adresse e-mail.</p>"
"<p>Si vous n'êtes pas encore inscrit(e), "
"nous vous invitons à cliquer sur Inscription pour créer votre compte.</p>"
),
reverse("signup:job_seeker_situation"),
"Inscription",
),
extra_tags="modal login_failure email_does_not_exist",
)
raise ValidationError("Cette adresse e-mail est inconnue. Veuillez soumettre une autre, ou vous inscrire.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
raise ValidationError("Cette adresse e-mail est inconnue. Veuillez soumettre une autre, ou vous inscrire.")
raise ValidationError("Cette adresse e-mail est inconnue. Veuillez en saisir une autre, ou vous inscrire.")

return email


class ItouLoginForm(LoginForm):
# Hidden field allowing demo prescribers and employers to log in using the banner form
demo_banner_account = forms.BooleanField(widget=forms.HiddenInput(), required=False)
Expand Down
2 changes: 1 addition & 1 deletion itou/www/login/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
path("prescriber", views.PrescriberLoginView.as_view(), name="prescriber"),
path("employer", views.EmployerLoginView.as_view(), name="employer"),
path("labor_inspector", views.LaborInspectorLoginView.as_view(), name="labor_inspector"),
path("job_seeker", views.JobSeekerLoginView.as_view(), name="job_seeker"),
path("job_seeker", views.JobSeekerPreLoginView.as_view(), name="job_seeker"),
path("existing/<uuid:user_public_id>", views.ExistingUserLoginView.as_view(), name="existing_user"),
]
48 changes: 34 additions & 14 deletions itou/www/login/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.views.generic.edit import FormView

from itou.openid_connect.inclusion_connect.enums import InclusionConnectChannel
from itou.users.enums import MATOMO_ACCOUNT_TYPE, IdentityProvider, UserKind
from itou.users.models import User
from itou.utils.urls import add_url_params, get_safe_url, get_url_param_value
from itou.www.login.forms import ItouLoginForm
from itou.www.login.forms import FindExistingUserViaEmailForm, ItouLoginForm


class ItouLoginView(LoginView):
class UserKindLoginMixin:
"""
Generic authentication entry point.
This view is used only in one case:
when a user confirms its email after updating it.
Allauth magic is complicated to debug.
Mixin class which adds functionality relating to the different IdentityProviders,
configured to be used according to UserKind (certain identity providers accessible only to certain user kinds).
django-allauth provides the login behaviour, extended by views for each UserKind.
"""

form_class = ItouLoginForm
Expand Down Expand Up @@ -90,6 +90,12 @@ def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)


class ItouLoginView(UserKindLoginMixin, LoginView):
"""Generic authentication entry point."""

pass


class PrescriberLoginView(ItouLoginView):
template_name = "account/login_generic.html"
user_kind = UserKind.PRESCRIBER
Expand Down Expand Up @@ -138,20 +144,34 @@ def get_context_data(self, **kwargs):
return context | extra_context


class JobSeekerLoginView(ItouLoginView):
class JobSeekerPreLoginView(UserKindLoginMixin, FormView):
"""
JobSeeker's do not log in directly.
Instead they enter their email and they are redirected to the login method configured on their account.
"""

template_name = "account/login_job_seeker.html"
user_kind = UserKind.JOB_SEEKER
form_class = FindExistingUserViaEmailForm

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
extra_context = {
"show_france_connect": bool(settings.FRANCE_CONNECT_BASE_URL),
"show_peamu": bool(settings.PEAMU_AUTH_BASE_URL),
}
return context | extra_context
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["request"] = self.request
return kwargs

def form_valid(self, form):
self.user = form.user
return super().form_valid(form)

def get_success_url(self):
return f'{reverse("login:existing_user", args=(self.user.public_id,))}?back_url={reverse("login:job_seeker")}'


class ExistingUserLoginView(ItouLoginView):
"""
Allows a user to login with the provider configured on their account.
"""

template_name = "account/login_existing_user.html"

def setup(self, request, *args, **kwargs):
Expand Down
1 change: 0 additions & 1 deletion itou/www/signup/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,6 @@ def __init__(self, prior_cleaned_data, *args, **kwargs):
self.fields["birthdate"].initial = prior_cleaned_data.get("birthdate")
self.fields["nir"].initial = prior_cleaned_data.get("nir")

# self.fields["password1"].help_text = CnilCompositionPasswordValidator().get_help_text()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀

for password_field in [self.fields["password1"], self.fields["password2"]]:
password_field.widget.attrs["placeholder"] = "**********"
self.fields["password1"].help_text = CnilCompositionPasswordValidator().get_help_text()
Expand Down
3 changes: 1 addition & 2 deletions tests/www/dashboard/test_edit_user_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,13 @@ def test_update_email(self, client, mailoutbox):

# User cannot log in with his old address
post_data = {"login": old_email, "password": DEFAULT_PASSWORD}
url = reverse("login:job_seeker")
url = reverse("login:existing_user", args=(user.public_id,))
response = client.post(url, data=post_data)
assert response.status_code == 200
assert not response.context_data["form"].is_valid()

# User cannot log in until confirmation
post_data = {"login": new_email, "password": DEFAULT_PASSWORD}
url = reverse("login:job_seeker")
response = client.post(url, data=post_data)
assert response.status_code == 302
assert response.url == reverse("account_email_verification_sent")
Expand Down
12 changes: 12 additions & 0 deletions tests/www/login/__snapshots__/tests.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@

<p class="h4">Se connecter avec FranceConnect</p>

<p>
<strong>Votre adresse e-mail utilise une connexion via FranceConnect. Veuillez vous connecter en utilisant le bouton ci-dessous.</strong>
</p>
<p>
FranceConnect est la solution proposée par l’État pour sécuriser et simplifier la connexion à vos services en ligne.
</p>
<div class="mt-4">


Expand Down Expand Up @@ -143,6 +149,9 @@

<p class="h4">Se connecter avec France Travail</p>

<p>
<strong>Votre adresse e-mail utilise une connexion via France Travail. Veuillez vous connecter en utilisant le bouton ci-dessous.</strong>
</p>
<div class="mt-4 text-center">


Expand Down Expand Up @@ -230,3 +239,6 @@
</div>
'''
# ---
# name: TestJobSeekerPreLogin.test_pre_login_email_unknown
'<div class="modal-body"><p>Cette adresse e-mail est inconnue de nos services.</p><p>Si vous êtes déjà inscrit(e), assurez-vous de saisir correctement votre adresse e-mail.</p><p>Si vous n\'êtes pas encore inscrit(e), nous vous invitons à cliquer sur Inscription pour créer votre compte.</p></div><div class="modal-footer"><button type="button" class="btn btn-sm btn-link" data-bs-dismiss="modal">Retour</button><a href="/signup/job_seeker/situation" class="btn btn-sm btn-primary">Inscription</a></div>'
# ---
44 changes: 37 additions & 7 deletions tests/www/login/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,19 +208,36 @@ def test_login(self, client):
assertRedirects(response, reverse("account_email_verification_sent"))


class TestJopbSeekerLogin:
def test_login(self, client):
class TestJobSeekerPreLogin:
def test_pre_login_email_invalid(self, client):
form_data = {"email": "emailinvalid"}
response = client.post(reverse("login:job_seeker"), data=form_data)
assert response.status_code == 200
assert response.context["form"].errors["email"] == ["Saisissez une adresse e-mail valide."]

def test_pre_login_redirects_to_existing_user(self, client):
user = JobSeekerFactory()
url = reverse("login:job_seeker")
response = client.get(url)
assert response.status_code == 200

form_data = {
"login": user.email,
"password": DEFAULT_PASSWORD,
}
form_data = {"email": user.email}
response = client.post(url, data=form_data)
assertRedirects(response, reverse("account_email_verification_sent"))
assertRedirects(response, f'{reverse("login:existing_user", args=(user.public_id,))}?back_url={url}')

def test_pre_login_email_unknown(self, client, snapshot):
url = reverse("login:job_seeker")
response = client.get(url)

form_data = {"email": "[email protected]"}
response = client.post(url, data=form_data)
assert response.status_code == 200

assert response.context["form"].errors["email"] == [
"Cette adresse e-mail est inconnue. Veuillez soumettre une autre, ou vous inscrire."
]
assertMessages(response, [messages.Message(messages.ERROR, snapshot)])
assertContains(response, reverse("signup:job_seeker_situation"))

@respx.mock
@override_settings(
Expand Down Expand Up @@ -290,6 +307,19 @@ def test_login(self, client, snapshot, identity_provider):
assertNotContains(response, self.UNSUPPORTED_IDENTITY_PROVIDER_TEXT)
assert str(parse_response_to_soup(response, selector=".c-form")) == snapshot

def test_login_django(self, client):
user = JobSeekerFactory(identity_provider=IdentityProvider.DJANGO)
url = reverse("login:existing_user", args=(user.public_id,))
response = client.get(url)
assert response.status_code == 200

form_data = {
"login": user.email,
"password": DEFAULT_PASSWORD,
}
response = client.post(url, data=form_data)
assertRedirects(response, reverse("account_email_verification_sent"))

@pytest.mark.parametrize(
"identity_provider",
[
Expand Down
Loading
Loading