diff --git a/counter/forms.py b/counter/forms.py index a5950eea..c43de059 100644 --- a/counter/forms.py +++ b/counter/forms.py @@ -2,6 +2,7 @@ from ajax_select import make_ajax_field from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField from django import forms from django.utils.translation import gettext_lazy as _ +from phonenumber_field.widgets import RegionalPhoneNumberWidget from core.views.forms import NFCTextInput, SelectDate, SelectDateTime from counter.models import ( @@ -28,6 +29,9 @@ class BillingInfoForm(forms.ModelForm): "country", "phone_number", ] + widgets = { + "phone_number": RegionalPhoneNumberWidget, + } class StudentCardForm(forms.ModelForm): diff --git a/counter/tests/test_counter.py b/counter/tests/test_counter.py index 85ea1dfa..02ae0074 100644 --- a/counter/tests/test_counter.py +++ b/counter/tests/test_counter.py @@ -428,6 +428,24 @@ class TestBillingInfo: assert infos.phone_number.as_national == "06123 45678" assert infos.phone_number.country_code == 49 + @pytest.mark.parametrize( + "phone_number", ["061234567a", "06 12 34 56", "061234567879", "azertyuiop"] + ) + def test_invalid_phone_number( + self, client: Client, payload: dict, phone_number: str + ): + """Test that invalid phone numbers are rejected.""" + user = subscriber_user.make() + client.force_login(user) + payload["phone_number"] = phone_number + response = client.put( + reverse("api:put_billing_info", args=[user.id]), + json.dumps(payload), + content_type="application/json", + ) + assert response.status_code == 422 + assert not BillingInfo.objects.filter(customer__user=user).exists() + class TestBarmanConnection(TestCase): @classmethod diff --git a/eboutic/schemas.py b/eboutic/schemas.py index 940d6ddb..a8766f7e 100644 --- a/eboutic/schemas.py +++ b/eboutic/schemas.py @@ -1,6 +1,11 @@ +from typing import Annotated + from ninja import ModelSchema, Schema from pydantic import Field, NonNegativeInt, PositiveInt, TypeAdapter +# from phonenumber_field.phonenumber import PhoneNumber +from pydantic_extra_types.phone_numbers import PhoneNumber, PhoneNumberValidator + from counter.models import BillingInfo @@ -35,4 +40,4 @@ class BillingInfoSchema(ModelSchema): # for reasons described in the model, BillingInfo.phone_number # in nullable, but null values shouldn't be actually allowed, # so we force the field to be required - phone_number: str + phone_number: Annotated[PhoneNumber, PhoneNumberValidator(default_region="FR")] diff --git a/eboutic/static/eboutic/js/makecommand.js b/eboutic/static/eboutic/js/makecommand.js index e43a571f..3f0fc6ab 100644 --- a/eboutic/static/eboutic/js/makecommand.js +++ b/eboutic/static/eboutic/js/makecommand.js @@ -42,7 +42,16 @@ document.addEventListener("alpine:init", () => { this.req_state = res.ok ? BillingInfoReqState.SUCCESS : BillingInfoReqState.FAILURE; - if (res.ok) { + if (res.status === 422) { + const errors = (await res.json())["detail"].map((err) => err["loc"]).flat(); + Array.from(form.querySelectorAll("input")) + .filter((elem) => errors.includes(elem.name)) + .forEach((elem) => { + elem.setCustomValidity(gettext("Incorrect value")); + elem.reportValidity(); + elem.oninput = () => elem.setCustomValidity(""); + }); + } else if (res.ok) { Alpine.store("billing_inputs").fill(); } }, diff --git a/eboutic/views.py b/eboutic/views.py index 1bbd9630..9687bb51 100644 --- a/eboutic/views.py +++ b/eboutic/views.py @@ -144,7 +144,7 @@ class EbouticCommand(LoginRequiredMixin, TemplateView): elif default_billing_info.phone_number is None: kwargs["billing_infos_state"] = BillingInfoState.MISSING_PHONE_NUMBER else: - kwargs["billing_infos_state"] = BillingInfoState.EMPTY + kwargs["billing_infos_state"] = BillingInfoState.VALID if kwargs["billing_infos_state"] == BillingInfoState.VALID: # the user has already filled all of its billing_infos, thus we can # get it without expecting an error diff --git a/locale/fr/LC_MESSAGES/djangojs.po b/locale/fr/LC_MESSAGES/djangojs.po index 5c16a03e..e86bc171 100644 --- a/locale/fr/LC_MESSAGES/djangojs.po +++ b/locale/fr/LC_MESSAGES/djangojs.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-09-03 15:22+0200\n" +"POT-Creation-Date: 2024-09-27 22:32+0200\n" "PO-Revision-Date: 2024-09-17 11:54+0200\n" "Last-Translator: Sli \n" "Language-Team: AE info \n" @@ -16,9 +16,24 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: core/static/user/js/family_graph.js:230 + +#: core/static/user/js/family_graph.js:233 msgid "family_tree.%(extension)s" msgstr "arbre_genealogique.%(extension)s" -#: sas/static/sas/js/picture.js:52 + +#: core/static/user/js/user_edit.js:93 +#, javascript-format +msgid "captured.%s" +msgstr "capture.%s" + +#: eboutic/static/eboutic/js/makecommand.js:50 +msgid "Incorrect value" +msgstr "Valeur incorrecte" + +#: sas/static/sas/js/viewer.js:196 +msgid "Couldn't moderate picture" +msgstr "Echec de la suppression de la photo" + +#: sas/static/sas/js/viewer.js:209 msgid "Couldn't delete picture" msgstr "Echec de la suppression de la photo" diff --git a/poetry.lock b/poetry.lock index b49aa74f..a2ba919f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1813,6 +1813,33 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pydantic-extra-types" +version = "2.9.0" +description = "Extra Pydantic types." +optional = false +python-versions = ">=3.8" +files = [] +develop = false + +[package.dependencies] +pydantic = ">=2.5.2" +typing-extensions = "*" + +[package.extras] +all = ["pendulum (>=3.0.0,<4.0.0)", "phonenumbers (>=8,<9)", "pycountry (>=23)", "python-ulid (>=1,<2)", "python-ulid (>=1,<3)", "pytz (>=2024.1)", "semver (>=3.0.2)", "semver (>=3.0.2,<3.1.0)", "tzdata (>=2024.1)"] +pendulum = ["pendulum (>=3.0.0,<4.0.0)"] +phonenumbers = ["phonenumbers (>=8,<9)"] +pycountry = ["pycountry (>=23)"] +python-ulid = ["python-ulid (>=1,<2)", "python-ulid (>=1,<3)"] +semver = ["semver (>=3.0.2)"] + +[package.source] +type = "git" +url = "https://github.com/pydantic/pydantic-extra-types.git" +reference = "HEAD" +resolved_reference = "58db4b096d7c90566d3d48d51b4665c01a591df6" + [[package]] name = "pygments" version = "2.18.0" @@ -2626,4 +2653,4 @@ filelock = ">=3.4" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "b6202203d272cecdb607ea8ebc1ba12dd8369e4f387f65692c4a9681915e6f48" +content-hash = "c9c49497cc576b24c96ea914b74ef5c3a0c2981c488a599752f05aabb575f8d8" diff --git a/pyproject.toml b/pyproject.toml index 2a4329fb..448887d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,10 @@ dict2xml = "^1.7.3" Sphinx = "^5" # Needed for building xapian tomli = "^2.0.1" django-honeypot = "^1.2.1" +# When I introduced pydantic-extra-types, I needed *right now* +# the PhoneNumberValidator class which was on the master branch but not released yet. +# Once it's released, switch this to a regular version. +pydantic-extra-types = { git = "https://github.com/pydantic/pydantic-extra-types.git", rev = "58db4b0" } [tool.poetry.group.prod.dependencies] # deps used in prod, but unnecessary for development