Merge branch 'feat-optional-mail-phone' into 'develop'

feat: Allow email/phone to be optional, required, or disabled

See merge request la-chariotte/la-chariotte!134
This commit is contained in:
selfhoster1312 ACAB 2025-05-03 16:44:39 +00:00
commit dc41ece1cb
9 changed files with 499 additions and 20 deletions

View file

@ -18,9 +18,26 @@ class GroupedOrderForm(forms.ModelForm):
widget=forms.TimeInput(attrs={"type": "time"}), widget=forms.TimeInput(attrs={"type": "time"}),
initial=datetime.time(hour=23, minute=59, second=59), initial=datetime.time(hour=23, minute=59, second=59),
) )
is_phone_mandatory = forms.BooleanField(
label="Numéro de téléphone obligatoire pour les participants", required_phone = forms.TypedChoiceField(
label="Numéro de téléphone",
required=False, required=False,
choices=[
(0, "Désactivé"),
(1, "Facultatif"),
(2, "Obligatoire"),
],
coerce=int,
)
required_email = forms.TypedChoiceField(
label="Adresse email",
required=False,
choices=[
(0, "Désactivé"),
(1, "Facultatif"),
(2, "Obligatoire"),
],
coerce=int,
) )
class Meta: class Meta:
@ -33,7 +50,8 @@ class GroupedOrderForm(forms.ModelForm):
"delivery_slot", "delivery_slot",
"place", "place",
"description", "description",
"is_phone_mandatory", "required_phone",
"required_email",
] ]
widgets = { widgets = {
"name": forms.TextInput( "name": forms.TextInput(

View file

@ -0,0 +1,55 @@
# Generated by Django 4.2.19 on 2025-03-06 16:37
import django.core.validators
from django.db import migrations, models
def migrate_from_is_phone_mandatory(apps, schema_editor):
"""For continuity, move is_phone_mandatory to the new required_phone field, and set
required_email=2 for the orders that were created so far."""
GroupedOrder = apps.get_model("order", "GroupedOrder")
for grouped_order in GroupedOrder.objects.all():
grouped_order.required_email = 2
grouped_order.required_phone = 2 if grouped_order.is_phone_mandatory else 1
grouped_order.save()
class Migration(migrations.Migration):
dependencies = [
("order", "0029_set_phone_mandatory_for_existing_orders"),
]
operations = [
migrations.AddField(
model_name="groupedorder",
name="required_email",
field=models.IntegerField(
verbose_name="Adresse email",
blank=True,
default=2,
validators=[
django.core.validators.MaxValueValidator(2),
django.core.validators.MinValueValidator(0),
],
),
),
migrations.AddField(
model_name="groupedorder",
name="required_phone",
field=models.IntegerField(
verbose_name="Numéro de téléphone",
blank=True,
default=2,
validators=[
django.core.validators.MaxValueValidator(2),
django.core.validators.MinValueValidator(0),
],
),
),
migrations.RunPython(migrate_from_is_phone_mandatory),
migrations.RemoveField(
model_name="groupedorder",
name="is_phone_mandatory",
),
]

View file

@ -2,6 +2,7 @@ import random
import base36 import base36
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
@ -26,8 +27,28 @@ class GroupedOrder(models.Model):
) )
description = models.TextField("Description", null=True, blank=True) description = models.TextField("Description", null=True, blank=True)
code = models.CharField(auto_created=True) code = models.CharField(auto_created=True)
is_phone_mandatory = models.BooleanField(
default=False, verbose_name="Numéro de téléphone obligatoire" # Whether phone/email registration is possible/necessary for this grouped order
# 0 = disabled
# 1 = optional
# 2 = required
required_phone = models.IntegerField(
verbose_name="Numéro de téléphone",
default=2,
blank=True,
validators=[
MaxValueValidator(2),
MinValueValidator(0),
],
)
required_email = models.IntegerField(
verbose_name="Adresse email",
default=2,
blank=True,
validators=[
MaxValueValidator(2),
MinValueValidator(0),
],
) )
def create_code_from_pk(self): def create_code_from_pk(self):

View file

@ -157,16 +157,22 @@
<input id="last_name" type="text" name="last_name" placeholder="Votre nom" <input id="last_name" type="text" name="last_name" placeholder="Votre nom"
value="{{ order_author.last_name }}" required></p> value="{{ order_author.last_name }}" required></p>
</div> </div>
{% if required_phone or required_email %}
<div class="column"> <div class="column">
<p><label for="phone">Numéro de téléphone {% if not is_phone_mandatory %}<em>(facultatif)</em> {% endif %}:</label> {% if required_phone != 0 %}
<p><label for="phone">Numéro de téléphone {% if required_phone == 1 %}<em>(facultatif)</em> {% endif %}:</label>
<input id="phone" type="tel" pattern="[0-9]{10}" <input id="phone" type="tel" pattern="[0-9]{10}"
placeholder="0601020304" name="phone" placeholder="0601020304" name="phone"
value="{{ order_author.phone }}" value="{{ order_author.phone }}"
{% if is_phone_mandatory %}required{% endif %}></p> {% if required_phone == 2 %}required{% endif %}></p>
<p><label for="email">Adresse mail : </label> {% endif %}
{% if required_email != 0 %}
<p><label for="email">Adresse mail {% if required_email == 1 %}<em>(facultatif)</em> {%endif %}: </label>
<input id="email" type="email" placeholder="exemple@mail.fr" name="email" <input id="email" type="email" placeholder="exemple@mail.fr" name="email"
value="{{ order_author.email }}" required></p> value="{{ order_author.email }}" {% if required_email == 2 %}required{% endif %}></p>
{% endif %}
</div> </div>
{% endif %}
</div> </div>
<p><label for="note">Note à l'organisateur·ice<em> (facultatif)</em> :</label> <p><label for="note">Note à l'organisateur·ice<em> (facultatif)</em> :</label>
<textarea id="note" rows=3 name="note">{{ note }}</textarea></p> <textarea id="note" rows=3 name="note">{{ note }}</textarea></p>

View file

@ -134,8 +134,9 @@ class GroupedOrderDetailView(generic.DetailView):
"prices_dict": json.dumps(prices_dict, cls=DjangoJSONEncoder), "prices_dict": json.dumps(prices_dict, cls=DjangoJSONEncoder),
"remaining_qty": remaining_qty, "remaining_qty": remaining_qty,
"order_author": order_author, "order_author": order_author,
# Used to set if the phone is required in the form # Used to set if the phone/email is required/optional/disabled in the form
"is_phone_mandatory": grouped_order.is_phone_mandatory, "required_phone": grouped_order.required_phone,
"required_email": grouped_order.required_email,
} }
) )
return context return context

View file

@ -40,15 +40,67 @@ def place_order(request, code):
) )
orders_dict[i] = value orders_dict[i] = value
# create an order error_message = None
first_name = request.POST["first_name"] first_name = request.POST["first_name"]
last_name = request.POST["last_name"] last_name = request.POST["last_name"]
phone = request.POST["phone"]
email = request.POST["email"]
note = request.POST["note"] note = request.POST["note"]
# check if required/optional phone is supplied
# When not supplied, use "" because the order_author table phone has a NOT NULL statement
error_message = None
if grouped_order.required_phone == 0:
phone = ""
else:
if "phone" in request.POST and request.POST["phone"] != "":
# Phone is supplied, whether required or not
phone = request.POST["phone"]
elif grouped_order.required_phone == 2:
# Phone is not supplied, but was required
error_message = (
"Le numéro de téléphone est obligatoire pour cette commande groupée"
)
phone = ""
else:
# Phone is not supplied, but was not mandatory
phone = ""
# check if required/optional email is supplied
# When not supplied, use "" because the order_author table email has a NOT NULL statement
if grouped_order.required_email == 0:
email = ""
else:
if "email" in request.POST and request.POST["email"] != "":
# email is supplied, whether required or not
email = request.POST["email"]
elif grouped_order.required_email == 2:
# email is not supplied, but was required
error_message = (
"L'adresse email est obligatoire pour cette commande groupée"
)
email = ""
else:
# email is not supplied, but was not mandatory
email = ""
# create an author
author = OrderAuthor.objects.create( author = OrderAuthor.objects.create(
first_name=first_name, last_name=last_name, email=email, phone=phone first_name=first_name, last_name=last_name, email=email, phone=phone
) )
if error_message:
author.delete()
return render(
request,
"order/grouped_order_detail.html",
{
"grouped_order": grouped_order,
"error_message": error_message,
"note": note,
"author": author,
},
)
# create an order
order = Order.objects.create( order = Order.objects.create(
author=author, author=author,
grouped_order=grouped_order, grouped_order=grouped_order,
@ -57,7 +109,6 @@ def place_order(request, code):
) )
# add items to the order # add items to the order
error_message = None
for key, quantity in orders_dict.items(): for key, quantity in orders_dict.items():
quantity = int(quantity) quantity = int(quantity)
item = grouped_order.item_set.get(pk=key.split("_")[1]) item = grouped_order.item_set.get(pk=key.split("_")[1])

View file

@ -362,7 +362,7 @@ class TestGroupedOrderDetailView:
"first_name": {authenticated_user_with_name.first_name}, "first_name": {authenticated_user_with_name.first_name},
"last_name": {authenticated_user_with_name.last_name}, "last_name": {authenticated_user_with_name.last_name},
"phone": "0645632569", "phone": "0645632569",
"email": {authenticated_user_with_name.email}, "email": {authenticated_user_with_name.username},
"note": "test note", "note": "test note",
}, },
) )
@ -670,7 +670,7 @@ class TestGroupedOrderDetailView:
name="gr order test", name="gr order test",
orga_user=other_user, orga_user=other_user,
) )
assert grouped_order.is_phone_mandatory == True assert grouped_order.required_phone == 2
item = models.Item.objects.create( item = models.Item.objects.create(
name="test item 1", grouped_order=grouped_order, price=1, max_limit=2 name="test item 1", grouped_order=grouped_order, price=1, max_limit=2
) )
@ -686,12 +686,53 @@ class TestGroupedOrderDetailView:
assert ( assert (
"Numéro de téléphone <em>(facultatif)</em>" not in response.content.decode() "Numéro de téléphone <em>(facultatif)</em>" not in response.content.decode()
) )
grouped_order.is_phone_mandatory = False
grouped_order.required_phone = 1
grouped_order.save() grouped_order.save()
response = client.get(detail_url) response = client.get(detail_url)
assert "gr order test" in response.content.decode() assert "gr order test" in response.content.decode()
assert "Numéro de téléphone <em>(facultatif)</em>" in response.content.decode() assert "Numéro de téléphone <em>(facultatif)</em>" in response.content.decode()
grouped_order.required_phone = 0
grouped_order.save()
response = client.get(detail_url)
assert "gr order test" in response.content.decode()
assert '<label for="phone">' not in response.content.decode()
def test_email_not_required_display(self, client, other_user):
"""a user orders something without entering phone when it is required"""
grouped_order = create_grouped_order(
days_before_delivery_date=5,
days_before_deadline=2,
name="gr order test",
orga_user=other_user,
)
assert grouped_order.required_email == 2
item = models.Item.objects.create(
name="test item 1", grouped_order=grouped_order, price=1, max_limit=2
)
detail_url = reverse(
"order:grouped_order_detail",
kwargs={
"code": grouped_order.code,
},
)
response = client.get(detail_url)
assert response.status_code == 200
assert "gr order test" in response.content.decode()
assert "Adresse mail <em>(facultatif)</em>" not in response.content.decode()
grouped_order.required_email = 1
grouped_order.save()
response = client.get(detail_url)
assert "gr order test" in response.content.decode()
assert "Adresse mail <em>(facultatif)</em>" in response.content.decode()
grouped_order.required_email = 0
grouped_order.save()
response = client.get(detail_url)
assert "gr order test" in response.content.decode()
assert '<label for="email">' not in response.content.decode()
class TestGroupedOrderOverview: class TestGroupedOrderOverview:
def test_get_overview(self, client_log): def test_get_overview(self, client_log):

View file

@ -268,3 +268,287 @@ class TestOrder:
assert grouped_order.order_set.all().count() == 2 assert grouped_order.order_set.all().count() == 2
assert grouped_order.total_price == 35 - order_price assert grouped_order.total_price == 35 - order_price
assert item.ordered_nb == 1 assert item.ordered_nb == 1
def test_order_phone_and_email__required(self, client_log):
"""Someone places an order where email/phone is required"""
grouped_order = create_grouped_order(
days_before_delivery_date=5,
days_before_deadline=3,
name="gr order test",
orga_user=auth.get_user(client_log),
)
item = models.Item.objects.create(
name="test item 1", grouped_order=grouped_order, price=2
)
# some items are ordered
order_url = reverse(
"order:order",
kwargs={
"code": grouped_order.code,
},
)
# Phone and mail supplied
response = client_log.post(
order_url,
{
f"quantity_{item.pk}": [4, 0],
"first_name": "Prénom",
"last_name": "Nom",
"phone": "0645632569",
"email": "test@mail.fr",
"note": "",
},
)
assert response.status_code == 302
# Phone and mail not supplied
response = client_log.post(
order_url,
{
f"quantity_{item.pk}": [4, 0],
"first_name": "Prénom",
"last_name": "Nom",
"note": "",
},
)
assert response.status_code == 200
assert (
"L&#x27;adresse email est obligatoire pour cette commande groupée"
in response.content.decode()
)
# Phone and mail empty
response = client_log.post(
order_url,
{
f"quantity_{item.pk}": [4, 0],
"first_name": "Prénom",
"last_name": "Nom",
"phone": "",
"email": "",
"note": "",
},
)
assert response.status_code == 200
assert (
"L&#x27;adresse email est obligatoire pour cette commande groupée"
in response.content.decode()
)
# Only phone supplied
response = client_log.post(
order_url,
{
f"quantity_{item.pk}": [4, 0],
"first_name": "Prénom",
"last_name": "Nom",
"phone": "0645632569",
"email": "",
"note": "",
},
)
assert response.status_code == 200
assert (
"L&#x27;adresse email est obligatoire pour cette commande groupée"
in response.content.decode()
)
# Only email supplied
response = client_log.post(
order_url,
{
f"quantity_{item.pk}": [4, 0],
"first_name": "Prénom",
"last_name": "Nom",
"phone": "",
"email": "foo@bar.com",
"note": "",
},
)
assert response.status_code == 200
assert (
"Le numéro de téléphone est obligatoire pour cette commande groupée"
in response.content.decode()
)
def test_order_phone_and_email__optional(self, client_log):
"""Someone places an order where email/phone is optional"""
grouped_order = create_grouped_order(
days_before_delivery_date=5,
days_before_deadline=3,
name="gr order test",
orga_user=auth.get_user(client_log),
required_phone=1,
required_email=1,
)
item = models.Item.objects.create(
name="test item 1", grouped_order=grouped_order, price=2
)
# some items are ordered
order_url = reverse(
"order:order",
kwargs={
"code": grouped_order.code,
},
)
# Phone and mail supplied
response = client_log.post(
order_url,
{
f"quantity_{item.pk}": [4, 0],
"first_name": "Prénom",
"last_name": "Nom",
"phone": "0645632569",
"email": "test@mail.fr",
"note": "",
},
)
assert response.status_code == 302
# Phone and mail not supplied
response = client_log.post(
order_url,
{
f"quantity_{item.pk}": [4, 0],
"first_name": "Prénom",
"last_name": "Nom",
"note": "",
},
)
assert response.status_code == 302
# Phone and mail empty
response = client_log.post(
order_url,
{
f"quantity_{item.pk}": [4, 0],
"first_name": "Prénom",
"last_name": "Nom",
"phone": "",
"email": "",
"note": "",
},
)
assert response.status_code == 302
# Only phone supplied
response = client_log.post(
order_url,
{
f"quantity_{item.pk}": [4, 0],
"first_name": "Prénom",
"last_name": "Nom",
"phone": "0645632569",
"email": "",
"note": "",
},
)
assert response.status_code == 302
# Only email supplied
response = client_log.post(
order_url,
{
f"quantity_{item.pk}": [4, 0],
"first_name": "Prénom",
"last_name": "Nom",
"phone": "",
"email": "foo@bar.com",
"note": "",
},
)
assert response.status_code == 302
def test_order_phone_and_email__disabled(self, client_log):
"""Someone places an order where email/phone is disabled"""
grouped_order = create_grouped_order(
days_before_delivery_date=5,
days_before_deadline=3,
name="gr order test",
orga_user=auth.get_user(client_log),
required_phone=0,
required_email=0,
)
item = models.Item.objects.create(
name="test item 1", grouped_order=grouped_order, price=2
)
# some items are ordered
order_url = reverse(
"order:order",
kwargs={
"code": grouped_order.code,
},
)
# Phone and mail supplied
response = client_log.post(
order_url,
{
f"quantity_{item.pk}": [4, 0],
"first_name": "Prénom",
"last_name": "Nom",
"phone": "0645632569",
"email": "test@mail.fr",
"note": "",
},
)
assert response.status_code == 302
# Phone and mail not supplied
response = client_log.post(
order_url,
{
f"quantity_{item.pk}": [4, 0],
"first_name": "Prénom",
"last_name": "Nom",
"note": "",
},
)
assert response.status_code == 302
# Phone and mail empty
response = client_log.post(
order_url,
{
f"quantity_{item.pk}": [4, 0],
"first_name": "Prénom",
"last_name": "Nom",
"phone": "",
"email": "",
"note": "",
},
)
assert response.status_code == 302
# Only phone supplied
response = client_log.post(
order_url,
{
f"quantity_{item.pk}": [4, 0],
"first_name": "Prénom",
"last_name": "Nom",
"phone": "0645632569",
"email": "",
"note": "",
},
)
assert response.status_code == 302
# Only email supplied
response = client_log.post(
order_url,
{
f"quantity_{item.pk}": [4, 0],
"first_name": "Prénom",
"last_name": "Nom",
"phone": "",
"email": "foo@bar.com",
"note": "",
},
)
assert response.status_code == 302

View file

@ -13,7 +13,8 @@ def create_grouped_order(
days_before_deadline, days_before_deadline,
name, name,
orga_user, orga_user,
is_phone_mandatory=True, required_phone=2,
required_email=2,
): ):
date = timezone.now().date() + datetime.timedelta(days=days_before_delivery_date) date = timezone.now().date() + datetime.timedelta(days=days_before_delivery_date)
deadline = timezone.now() + datetime.timedelta(days=days_before_deadline) deadline = timezone.now() + datetime.timedelta(days=days_before_deadline)
@ -22,7 +23,8 @@ def create_grouped_order(
orga=orga_user, orga=orga_user,
delivery_date=date, delivery_date=date,
deadline=deadline, deadline=deadline,
is_phone_mandatory=is_phone_mandatory, required_phone=required_phone,
required_email=required_email,
) )
grouped_order.create_code_from_pk() grouped_order.create_code_from_pk()
grouped_order.save() grouped_order.save()