diff --git a/backend/api/grants/mutations.py b/backend/api/grants/mutations.py index 5178179046..435403c170 100644 --- a/backend/api/grants/mutations.py +++ b/backend/api/grants/mutations.py @@ -90,6 +90,7 @@ def validate(self, conference: Conference, user: User) -> GrantErrors: "python_usage", "been_to_other_events", "why", + "grant_type", ) for field in non_empty_fields: @@ -110,7 +111,7 @@ class SendGrantInput(BaseGrantInput): age_group: AgeGroup gender: str occupation: Occupation - grant_type: GrantType + grant_type: list[GrantType] python_usage: str been_to_other_events: str community_contribution: str @@ -149,7 +150,7 @@ class UpdateGrantInput(BaseGrantInput): age_group: AgeGroup gender: str occupation: Occupation - grant_type: GrantType + grant_type: list[GrantType] python_usage: str been_to_other_events: str community_contribution: str diff --git a/backend/api/grants/types.py b/backend/api/grants/types.py index 2a399d9ecb..742028d53a 100644 --- a/backend/api/grants/types.py +++ b/backend/api/grants/types.py @@ -22,7 +22,7 @@ class Grant: age_group: Optional[AgeGroup] gender: str occupation: Occupation - grant_type: GrantType + grant_type: list[GrantType] python_usage: str community_contribution: str been_to_other_events: str @@ -46,7 +46,7 @@ def from_model(cls, grant: GrantModel) -> Grant: age_group=AgeGroup(grant.age_group) if grant.age_group else None, gender=grant.gender, occupation=Occupation(grant.occupation), - grant_type=GrantType(grant.grant_type), + grant_type=[GrantType(g) for g in grant.grant_type], python_usage=grant.python_usage, community_contribution=grant.community_contribution, been_to_other_events=grant.been_to_other_events, diff --git a/backend/grants/migrations/0023_alter_grant_grant_type.py b/backend/grants/migrations/0023_alter_grant_grant_type.py new file mode 100644 index 0000000000..d57511884b --- /dev/null +++ b/backend/grants/migrations/0023_alter_grant_grant_type.py @@ -0,0 +1,50 @@ +# Generated by Django 5.1.1 on 2024-12-01 17:59 + +from django.db import migrations, models +import json + +def forwards_func(apps, schema_editor): + Grant = apps.get_model('grants', 'Grant') + for grant in Grant.objects.all(): + old_value = grant.grant_type + # Convert the old string value into a list + grant.grant_type_json = [old_value] if old_value else [] + grant.save(update_fields=['grant_type_json']) + +def reverse_func(apps, schema_editor): + Grant = apps.get_model('grants', 'Grant') + for grant in Grant.objects.all(): + value_list = grant.grant_type + # Convert the list back to a single string + if value_list: + grant.grant_type = value_list[0] + else: + grant.grant_type = '' + grant.save(update_fields=['grant_type']) + +class Migration(migrations.Migration): + dependencies = [ + ("grants", "0022_grant_departure_city_grant_nationality_and_more"), + ] + + operations = [ + # Step 1: Add a temporary JSONField + migrations.AddField( + model_name='grant', + name='grant_type_json', + field=models.JSONField(default=list, verbose_name="grant type"), + ), + # Step 2: Backfill data into the temporary field + migrations.RunPython(forwards_func, reverse_func), + # Step 3: Remove the old field + migrations.RemoveField( + model_name='grant', + name='grant_type', + ), + # Step 4: Rename the temporary field to grant_type + migrations.RenameField( + model_name='grant', + old_name='grant_type_json', + new_name='grant_type', + ), + ] diff --git a/backend/grants/models.py b/backend/grants/models.py index 2d3a5ddc16..ef4304c745 100644 --- a/backend/grants/models.py +++ b/backend/grants/models.py @@ -97,9 +97,7 @@ class ApprovedType(models.TextChoices): ) # Your Grant Section - grant_type = models.CharField( - _("grant type"), choices=GrantType.choices, max_length=10 - ) + grant_type = models.JSONField(_("grant type"), default=list) departure_country = models.CharField( _("Departure Country"), max_length=100, diff --git a/backend/grants/tests/factories.py b/backend/grants/tests/factories.py index f03a67f053..bd716665f2 100644 --- a/backend/grants/tests/factories.py +++ b/backend/grants/tests/factories.py @@ -8,6 +8,7 @@ from countries import countries from participants.tests.factories import ParticipantFactory from participants.models import Participant +import random class GrantFactory(DjangoModelFactory): @@ -22,7 +23,12 @@ class Meta: age_group = factory.fuzzy.FuzzyChoice(Grant.AgeGroup) gender = factory.fuzzy.FuzzyChoice([gender[0] for gender in GENDERS]) occupation = factory.fuzzy.FuzzyChoice(Grant.Occupation) - grant_type = factory.fuzzy.FuzzyChoice(Grant.GrantType) + grant_type = factory.LazyFunction( + lambda: random.sample( + [choice[0] for choice in Grant.GrantType.choices], + k=random.randint(1, len(Grant.GrantType.choices)), + ) + ) python_usage = factory.Faker("text") been_to_other_events = factory.Faker("text") diff --git a/backend/reviews/templates/grant-review.html b/backend/reviews/templates/grant-review.html index dee1a6f1be..09c8840c26 100644 --- a/backend/reviews/templates/grant-review.html +++ b/backend/reviews/templates/grant-review.html @@ -112,7 +112,11 @@

Grant

Grant type -
{{grant.get_grant_type_display}}
+
+ {% for type_ in grant.grant_type %} + {{type_}} + {% endfor %} +
diff --git a/backend/reviews/templates/grants-recap.html b/backend/reviews/templates/grants-recap.html index 03393b2eaf..b0df97d1ca 100644 --- a/backend/reviews/templates/grants-recap.html +++ b/backend/reviews/templates/grants-recap.html @@ -507,7 +507,11 @@

  • Grant type: - {{ item.get_grant_type_display }} + + {% for type_ in item.grant_type %} + {{type_}} + {% endfor %} +
  • Needs: diff --git a/frontend/src/components/grant-form/index.tsx b/frontend/src/components/grant-form/index.tsx index 01116de7a7..6e817bbcb0 100644 --- a/frontend/src/components/grant-form/index.tsx +++ b/frontend/src/components/grant-form/index.tsx @@ -58,7 +58,7 @@ export type GrantFormFields = ParticipantFormFields & { ageGroup: AgeGroup; gender: string; occupation: Occupation; - grantType: GrantType; + grantType: GrantType[]; pythonUsage: string; communityContribution: string; beenToOtherEvents: string; @@ -458,21 +458,27 @@ export const GrantForm = ({ } > - + + + + {getErrors("grantType").join(", ")} + }> - + + {grantType.map((type, index) => ( + + + + ))} + }> diff --git a/frontend/src/locale/index.ts b/frontend/src/locale/index.ts index 7bea044a82..6259783064 100644 --- a/frontend/src/locale/index.ts +++ b/frontend/src/locale/index.ts @@ -696,6 +696,7 @@ We look forward to reading about you and hope to see you at PyCon Italia 2024! "grants.form.yourGrant": "Your grant", "grants.form.travel": "Travel", "grants.form.youAndPython": "You and Python", + "grants.form.validationErrors": "The submitted form is not correct", "grants.form.optionalInformation": "Optional information", "grants.form.optionalInformation.description": @@ -730,9 +731,10 @@ We look forward to reading about you and hope to see you at PyCon Italia 2024! "grants.form.fields.occupation.values.researcher": "Researcher", "grants.form.fields.occupation.values.unemployed": "Unemployed", "grants.form.fields.occupation.values.other": "Other", - "grants.form.fields.grantType": "What type of grant are you applying for?", - "grants.form.fields.grantType.description": - "Note: If you have submitted a talk/workshop proposal, you do not need to apply for a grant to receive a refund. If your proposal is accepted, we will contact you regarding the ticket refund.", + "grants.form.fields.grantType": + "Select all grant categories that apply to you", + "grants.form.fields.grantType.description": `You can choose more than one option if applicable. +Note: If you have submitted a talk/workshop proposal, you don't need to apply for a speaker grant to get your conference ticket refunded. We will contact you about refunding your ticket if your proposal is accepted.`, "grants.form.fields.grantType.values.diversity": "Diversity", "grants.form.fields.grantType.values.unemployed": "Unemployed", "grants.form.fields.grantType.values.speaker": "Speaker", @@ -1456,9 +1458,10 @@ Non vediamo l'ora di leggere la tua storia e speriamo di vederti a PyCon Italia "grants.form.fields.occupation.values.researcher": "Ricerca", "grants.form.fields.occupation.values.unemployed": "Disoccupato/a", "grants.form.fields.occupation.values.other": "Altro", - "grants.form.fields.grantType": "Che tipo di grant stai richiedendo?", - "grants.form.fields.grantType.description": - "Nota: Se hai inviato una proposta di talk/workshop, non è necessario fare domanda per un grant per ricevere un rimborso. Se la tua proposta viene accettata, ti contatteremo riguardo al rimborso del biglietto.", + "grants.form.fields.grantType": + "Seleziona tutte le categorie di grant che si applicano a te", + "grants.form.fields.grantType.description": `Puoi scegliere più di un'opzione se necessario. +Nota: Se hai inviato una proposta di talk/workshop, non è necessario richiedere un grant come speaker per ottenere il rimborso del biglietto della conferenza. Ti contatteremo per il rimborso del biglietto se la tua proposta viene accettata.`, "grants.form.fields.grantType.values.diversity": "Diversity", "grants.form.fields.grantType.values.unemployed": "Disoccupato", "grants.form.fields.grantType.values.speaker": "Speaker",