From 5839a9a7c610bbe9582a8c7e02204cf1e6f2d69d Mon Sep 17 00:00:00 2001
From: Julien <120013023+albatrousse@users.noreply.github.com>
Date: Thu, 26 Oct 2023 17:46:51 +0200
Subject: [PATCH] feat: enable user to edit their preferred language and the
country they live in (#8826)
The goal is to add, when signing up and in the user profile, a field for the user to inform their preferred language and the country they live. When signing up, the fields will be prefill with the country associated with the website (looking at the country code) the user uses and the same goes for the language.
The country list function is also cached and reused for country.pl
### Related issue(s) and discussion
- Fixes #8748 and #8749
---
cgi/countries.pl | 38 +--
cgi/user.pl | 31 ++-
lib/ProductOpener/TestDefaults.pm | 2 +
lib/ProductOpener/Users.pm | 7 +-
lib/ProductOpener/Web.pm | 69 ++++-
po/common/common.pot | 6 +-
po/common/en.po | 4 +
po/common/fr.po | 4 +
.../pages/user_form/user_form_page.tt.html | 13 +-
tests/integration/countries.t | 31 +++
tests/integration/create_pro_user.t | 31 ++-
tests/integration/create_user.t | 2 +-
.../countries/list-countries-filtered-fr.json | 8 +
.../countries/list-countries-french.json | 254 ++++++++++++++++++
.../countries/list-countries-world.json | 254 ++++++++++++++++++
.../user-after-subscription.json | 2 +
tests/integration/modify_user.t | 4 +
17 files changed, 708 insertions(+), 52 deletions(-)
create mode 100644 tests/integration/countries.t
create mode 100644 tests/integration/expected_test_results/countries/list-countries-filtered-fr.json
create mode 100644 tests/integration/expected_test_results/countries/list-countries-french.json
create mode 100644 tests/integration/expected_test_results/countries/list-countries-world.json
diff --git a/cgi/countries.pl b/cgi/countries.pl
index aa5b610211953..7d08ea2ddff1a 100644
--- a/cgi/countries.pl
+++ b/cgi/countries.pl
@@ -27,16 +27,13 @@
use ProductOpener::Config qw/:all/;
use ProductOpener::Store qw/:all/;
-use ProductOpener::Index qw/:all/;
use ProductOpener::Display qw/:all/;
-use ProductOpener::Users qw/:all/;
-use ProductOpener::URL qw/:all/;
use ProductOpener::Lang qw/:all/;
use ProductOpener::Tags qw/:all/;
+use ProductOpener::Web qw/get_countries_options_list/;
use CGI qw/:cgi :form escapeHTML/;
use URI::Escape::XS;
-use Storable qw/dclone/;
use Encode;
use JSON::PP;
@@ -47,30 +44,17 @@
my $term = decode utf8 => single_param('term');
+my @options = @{get_countries_options_list($lang, undef)};
+if (defined $term and $term ne '') {
+ # filter by term
+ @options = grep {$_->{label} =~ /$term/i} @options;
+}
my %result = ();
-foreach my $country (
- sort {
- ( get_string_id_for_lang("no_language", $translations_to{countries}{$a}{$lang})
- || get_string_id_for_lang("no_language", $translations_to{countries}{$a}{'en'}))
- cmp( get_string_id_for_lang("no_language", $translations_to{countries}{$b}{$lang})
- || get_string_id_for_lang("no_language", $translations_to{countries}{$b}{'en'}))
- }
- keys %{$properties{countries}}
- )
-{
-
- my $cc = country_to_cc($country);
- if (not(defined $cc)) {
- next;
- }
-
- my $tag = display_taxonomy_tag($lang, 'countries', $country);
- if ( (not defined $term)
- or ($term eq '')
- or ($tag =~ /$term/i))
- {
- $result{$cc} = $tag;
- }
+# transform to simple dict and use codes
+foreach my $option (@options) {
+ my $code = country_to_cc($option->{value});
+ next if not defined $code;
+ $result{$code} = $option->{prefixed};
}
my $data = encode_json(\%result);
diff --git a/cgi/user.pl b/cgi/user.pl
index 8f56643bfb1a9..d0b91ff417720 100644
--- a/cgi/user.pl
+++ b/cgi/user.pl
@@ -26,6 +26,7 @@
use ProductOpener::Store qw/:all/;
use ProductOpener::Index qw/:all/;
use ProductOpener::Display qw/:all/;
+use ProductOpener::Web qw/:all/;
use ProductOpener::Users qw/:all/;
use ProductOpener::Lang qw/:all/;
use ProductOpener::Orgs qw/:all/;
@@ -169,6 +170,12 @@
$template_data_ref->{sections} = [];
if ($user_ref) {
+ my $selected_language = $user_ref->{preferred_language}
+ // (remove_tags_and_quote(single_param('preferred_language')) || "$lc");
+ my $selected_country = $user_ref->{country} // (remove_tags_and_quote(single_param('country')) || $country);
+ if ($selected_country eq "en:world") {
+ $selected_country = "";
+ }
push @{$template_data_ref->{sections}}, {
id => "user",
fields => [
@@ -193,6 +200,20 @@
type => "password",
label => "password_confirm"
},
+ {
+ field => "preferred_language",
+ type => "select",
+ label => "preferred_language",
+ selected => $selected_language,
+ options => get_languages_options_list($lc),
+ },
+ {
+ field => "country",
+ type => "select",
+ label => "select_country",
+ selected => $selected_country,
+ options => get_countries_options_list($lc),
+ },
{
# this is a honeypot to detect scripts, that fills every fields
# this one is hidden in a div and user won't see it
@@ -203,6 +224,10 @@
]
};
+ # news letter subscription value
+ my $newsletter = $user_ref->{newsletter} // (remove_tags_and_quote(single_param('newsletter')) || "on");
+ push @{$template_data_ref->{newsletter}}, $newsletter;
+
# Professional account
push @{$template_data_ref->{sections}},
{
@@ -257,6 +282,8 @@
}
# Contributor section
+ my $display_barcode = $user_ref->{display_barcode} || remove_tags_and_quote(single_param('display_barcode'));
+ my $edit_link = $user_ref->{edit_link} || remove_tags_and_quote(single_param('edit_link'));
my $contributor_section_ref = {
id => "contributor_settings",
name => lang("contributor_settings") . " (" . lang("optional") . ")",
@@ -266,13 +293,13 @@
field => "display_barcode",
type => "checkbox",
label => display_icon("barcode") . lang("display_barcode_in_search"),
- value => $user_ref->{display_barcode} && "on",
+ value => $display_barcode && "on",
},
{
field => "edit_link",
type => "checkbox",
label => display_icon("edit") . lang("edit_link_in_search"),
- value => $user_ref->{edit_link} && "on",
+ value => $edit_link && "on",
},
]
};
diff --git a/lib/ProductOpener/TestDefaults.pm b/lib/ProductOpener/TestDefaults.pm
index caee6817f22e2..d105a97d415fe 100644
--- a/lib/ProductOpener/TestDefaults.pm
+++ b/lib/ProductOpener/TestDefaults.pm
@@ -65,6 +65,8 @@ A basic user.
team_1 => "",
team_2 => "",
team_3 => "",
+ preferred_language => "en",
+ country => "en:united-states",
action => "process",
type => "add"
);
diff --git a/lib/ProductOpener/Users.pm b/lib/ProductOpener/Users.pm
index 2071855a7c0b9..de21925aa3207 100644
--- a/lib/ProductOpener/Users.pm
+++ b/lib/ProductOpener/Users.pm
@@ -403,6 +403,10 @@ sub check_user_form ($type, $user_ref, $errors_ref) {
$user_ref->{email} = $email;
}
+ # Country and preferred language
+ $user_ref->{preferred_language} = remove_tags_and_quote(single_param("preferred_language"));
+ $user_ref->{country} = remove_tags_and_quote(single_param("country"));
+
# Is there a checkbox to make a professional account
if (defined single_param("pro_checkbox")) {
@@ -684,7 +688,8 @@ sub process_user_form ($type, $user_ref, $request_ref) {
# Fetch the HTML mail template corresponding to the user language, english is the
# default if the translation is not available
- my $email_content = get_html_email_content("user_welcome.html", $user_ref->{initial_lc});
+ my $language = $user_ref->{preferred_language} || $user_ref->{initial_lc};
+ my $email_content = get_html_email_content("user_welcome.html", $language);
my $user_name = $user_ref->{name};
# Replace placeholders by user values
$email_content =~ s/\{\{USERID\}\}/$userid/g;
diff --git a/lib/ProductOpener/Web.pm b/lib/ProductOpener/Web.pm
index e2ad0d946af36..62ae9d150e926 100644
--- a/lib/ProductOpener/Web.pm
+++ b/lib/ProductOpener/Web.pm
@@ -49,6 +49,7 @@ use ProductOpener::Images qw(:all);
use Template;
use Log::Log4perl;
+use Unicode::Collate;
BEGIN {
use vars qw(@ISA @EXPORT_OK %EXPORT_TAGS);
@@ -58,12 +59,15 @@ BEGIN {
&display_data_quality_description
&display_knowledge_panel
&get_languages_options_list
+ &get_countries_options_list
); #the fucntions which are called outside this file
%EXPORT_TAGS = (all => [@EXPORT_OK]);
}
use vars @EXPORT_OK;
+my $unicode_collate = Unicode::Collate->new();
+
=head1 FUNCTIONS
=head2 display_field ( $product_ref, $field )
@@ -282,6 +286,9 @@ sub display_knowledge_panel ($product_ref, $panels_ref, $panel_id) {
return $html;
}
+# cache for languages_options_list
+my %lang_options_cache = ();
+
=head2 get_languages_options_list( $target_lc )
Generates a data structure containing the list of languages and their translation in a target language.
@@ -291,27 +298,79 @@ The data structured can be passed to HTML templates to construction a list of op
sub get_languages_options_list ($target_lc) {
+ return $lang_options_cache{$target_lc} if (defined $lang_options_cache{$target_lc});
+
my @lang_options = ();
my %lang_labels = ();
foreach my $l (@Langs) {
- $lang_labels{$l} = display_taxonomy_tag($target_lc, 'languages', $language_codes{$l});
+ my $label = display_taxonomy_tag($target_lc, 'languages', $language_codes{$l});
+ # remove eventual language prefix
+ $label =~ s/^\w\w://;
+ $lang_labels{$l} = $label;
}
- my @lang_values = sort {$lang_labels{$a} cmp $lang_labels{$b}} @Langs;
+ my @lang_values = sort {$unicode_collate->cmp($lang_labels{$a}, $lang_labels{$b})} @Langs;
- foreach my $l (@lang_values) {
+ foreach my $lang_code (@lang_values) {
push(
@lang_options,
{
- value => $l,
- label => $lang_labels{$l},
+ value => $lang_code,
+ label => $lang_labels{$lang_code},
}
);
}
+ # cache
+ $lang_options_cache{$target_lc} = \@lang_options;
+
return \@lang_options;
}
+# cache for get_countries
+my %countries_options_lists = ();
+
+=head2 get_countries_options_list( $target_lc )
+
+Generates all the countries name in the $target_lc language suitable for an select option list
+
+=head3 Arguments
+
+=head4 $target_lc - language code for labels
+
+=head4 $exclude_world - boolean to exclude 'World' from list
+
+=head3 Return value
+
+A reference to a list of hashes with every country code and their label in the $lc language
+[{value => "fr", label => "France"},…]
+
+=cut
+
+sub get_countries_options_list ($target_lc, $exclude_world = 1) {
+ # if already computed send it back
+ if (defined $countries_options_lists{$target_lc}) {
+ return $countries_options_lists{$target_lc};
+ }
+ # compute countries list
+ my @countries_list = ();
+ my @tags_list = get_all_taxonomy_entries("countries");
+ foreach my $tag (@tags_list) {
+ next if $exclude_world and ($tag eq 'en:world');
+ my $country = display_taxonomy_tag($target_lc, "countries", $tag);
+ # remove eventual language prefix
+ my $country_no_code = $country;
+ $country_no_code =~ s/^\w\w://;
+ # Adding to the list the modified string
+ push @countries_list, {value => $tag, label => $country_no_code, prefixed => $country};
+ }
+ # sort by name
+ @countries_list = sort {$unicode_collate->cmp($a->{label}, $b->{label})} @countries_list;
+ # cache
+ $countries_options_lists{$target_lc} = \@countries_list;
+ return \@countries_list;
+}
+
1;
diff --git a/po/common/common.pot b/po/common/common.pot
index abdad2650b016..df3e4a3963d34 100644
--- a/po/common/common.pot
+++ b/po/common/common.pot
@@ -6595,6 +6595,10 @@ msgctxt "relative_to_all_products"
msgid "relative to all products"
msgstr "relative to all products"
+msgctxt "preferred_language"
+msgid "Preferred Language"
+msgstr "Preferred Language"
+
msgctxt "packagings_n_p"
msgid "Numbers of packaging components"
msgstr "Numbers of packaging components"
@@ -6617,4 +6621,4 @@ msgstr "Weight per 100g of product"
msgctxt "weight_percent"
msgid "Weight percent"
-msgstr "Weight percent"
\ No newline at end of file
+msgstr "Weight percent"
diff --git a/po/common/en.po b/po/common/en.po
index 2f3d469ad7bc3..86aacc9255498 100644
--- a/po/common/en.po
+++ b/po/common/en.po
@@ -4699,6 +4699,10 @@ msgctxt "preference_mandatory"
msgid "Mandatory"
msgstr "Mandatory"
+msgctxt "preferred_language"
+msgid "Preferred Language"
+msgstr "Preferred Language"
+
msgctxt "packaging_alt"
msgid "Recycling instructions and/or packaging information"
msgstr "Recycling instructions and/or packaging information"
diff --git a/po/common/fr.po b/po/common/fr.po
index 4b01a2957741e..064d620436ec1 100644
--- a/po/common/fr.po
+++ b/po/common/fr.po
@@ -4697,6 +4697,10 @@ msgctxt "preference_mandatory"
msgid "Mandatory"
msgstr "Obligatoire"
+msgctxt "preferred_language"
+msgid "Preferred Language"
+msgstr "Langue préférée"
+
msgctxt "packaging_alt"
msgid "Recycling instructions and/or packaging information"
msgstr "Instruction de recyclage et/ou informations d'emballage"
diff --git a/templates/web/pages/user_form/user_form_page.tt.html b/templates/web/pages/user_form/user_form_page.tt.html
index 850948932376c..345bd6b9b0631 100644
--- a/templates/web/pages/user_form/user_form_page.tt.html
+++ b/templates/web/pages/user_form/user_form_page.tt.html
@@ -88,6 +88,14 @@
[% ELSIF field.type == 'email' %]
+ [% ELSIF field.type == 'select' %]
+
+
[% ELSIF field.type == 'checkbox' %]
[% IF accepted_organization && section.id == "professional" %]
[% f_lang('f_this_is_a_pro_account_for_org', {'org' => accepted_organization}) %]
@@ -108,7 +116,6 @@
[% END %]
-
[% END %]
@@ -146,13 +153,13 @@
[% IF type == 'add' %]
[% lang("unsubscribe_info") %]
[% END %]
- [% IF userid %]
+ [% IF userid and (type != 'add') %]