Commit 573b31ab authored by bojanz's avatar bojanz

Issue #2834627: Promotion percentage can't be provided in the locale-specific format

parent c0d91f6b
......@@ -3,7 +3,7 @@
* Admin styling.
*/
.form-type-commerce-price .form-type-textfield, .form-type-commerce-price .form-type-select {
.form-type-commerce-price .form-type-commerce-number, .form-type-commerce-price .form-type-select {
display: inline;
}
......
<?php
namespace Drupal\commerce_price\Element;
use CommerceGuys\Intl\Formatter\NumberFormatterInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\FormElement;
use Drupal\Core\Render\Element;
/**
* Provides a number form element with support for language-specific input.
*
* The #default_value is given in the generic, language-agnostic format, which
* is then formatted into the language-specific format on element display.
* During element validation the input is converted back into to the generic
* format, to allow the returned value to be stored.
*
* Usage example:
* @code
* $form['number'] = [
* '#type' => 'commerce_number',
* '#title' => t('Number'),
* '#default_value' => '18.99',
* '#required' => TRUE,
* ];
* @endcode
*
* @FormElement("commerce_number")
*/
class Number extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#min_fraction_digits' => NULL,
'#max_fraction_digits' => NULL,
'#min' => 0,
'#max' => NULL,
'#size' => 10,
'#maxlength' => 128,
'#default_value' => NULL,
'#element_validate' => [
[$class, 'validateNumber'],
],
'#process' => [
[$class, 'processElement'],
[$class, 'processAjaxForm'],
[$class, 'processGroup'],
],
'#pre_render' => [
[$class, 'preRenderNumber'],
[$class, 'preRenderGroup'],
],
'#input' => TRUE,
'#theme' => 'input__textfield',
'#theme_wrappers' => ['form_element'],
];
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input !== FALSE && $input !== NULL) {
if (!is_scalar($input)) {
$input = '0';
}
if ($input === '') {
$input = '0';
}
return $input;
}
elseif (!empty($element['#default_value'])) {
// Convert the stored number to the local format. For example, "9.99"
// becomes "9,99" in many locales. This also strips any extra zeroes.
$number_formatter = self::getNumberFormatter($element);
return $number_formatter->format($element['#default_value']);
}
return NULL;
}
/**
* Builds the commerce_number form element.
*
* @param array $element
* The initial commerce_number form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*
* @return array
* The built commerce_number form element.
*/
public static function processElement(array $element, FormStateInterface $form_state, &$complete_form) {
// Provide an example to the end user so that they know which decimal
// separator to use. This is the same pattern Drupal core uses.
$number_formatter = self::getNumberFormatter($element);
$element['#placeholder'] = $number_formatter->format('9.99');
return $element;
}
/**
* Validates the number element.
*
* Converts the number back to the standard format (e.g. "9,99" -> "9.99").
*
* @param array $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public static function validateNumber(array $element, FormStateInterface $form_state) {
$value = $form_state->getValue($element['#parents']);
$title = empty($element['#title']) ? $element['#parents'][0] : $element['#title'];
$number_formatter = self::getNumberFormatter($element);
$value = $number_formatter->parse($value);
if ($value === FALSE) {
$form_state->setError($element, t('%title must be a number.', [
'%title' => $title,
]));
return;
}
if (isset($element['#min']) && $value < $element['#min']) {
$form_state->setError($element, t('%title must be higher than or equal to %min.', [
'%title' => $title,
'%min' => $element['#min'],
]));
return;
}
if (isset($element['#max']) && $value > $element['#max']) {
$form_state->setError($element, t('%title must be lower than or equal to %max.', [
'%title' => $title,
'%max' => $element['#max'],
]));
return;
}
$form_state->setValueForElement($element, $value);
}
/**
* Prepares a #type 'commerce_number' render element for input.html.twig.
*
* @param array $element
* An associative array containing the properties of the element.
* Properties used: #title, #value, #description, #size, #maxlength,
* #placeholder, #required, #attributes.
*
* @return array
* The $element with prepared variables ready for input.html.twig.
*/
public static function preRenderNumber($element) {
// We're not using the "number" type because it won't accept
// language-specific input, such as commas.
$element['#attributes']['type'] = 'text';
Element::setAttributes($element, ['id', 'name', 'value', 'size', 'maxlength', 'placeholder']);
static::setAttributes($element, ['form-text']);
return $element;
}
/**
* Gets an instance of the number formatter for the given form element.
*
* @param array $element
* The form element.
*
* @return \CommerceGuys\Intl\Formatter\NumberFormatterInterface
* The number formatter instance.
*/
protected static function getNumberFormatter($element) {
$number_formatter_factory = \Drupal::service('commerce_price.number_formatter_factory');
/** @var \CommerceGuys\Intl\Formatter\NumberFormatterInterface $number_formatter */
$number_formatter = $number_formatter_factory->createInstance(NumberFormatterInterface::DECIMAL);
$number_formatter->setGroupingUsed(FALSE);
if (isset($element['#min_fraction_digits'])) {
$number_formatter->setMinimumFractionDigits($element['#min_fraction_digits']);
}
if (isset($element['#max_fraction_digits'])) {
$number_formatter->setMaximumFractionDigits($element['#max_fraction_digits']);
}
return $number_formatter;
}
}
......@@ -37,9 +37,6 @@ class Price extends FormElement {
'#attached' => [
'library' => ['commerce_price/admin'],
],
'#element_validate' => [
[$class, 'validateElement'],
],
'#process' => [
[$class, 'processElement'],
[$class, 'processAjaxForm'],
......@@ -53,20 +50,6 @@ class Price extends FormElement {
];
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if (is_array($input) && isset($input['number'])) {
// Convert an empty string value to a numeric value.
if ($input['number'] === '') {
$input['number'] = '0';
}
return $input;
}
return NULL;
}
/**
* Builds the commerce_price form element.
*
......@@ -92,11 +75,6 @@ class Price extends FormElement {
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $currency_storage */
$currency_storage = \Drupal::service('entity_type.manager')->getStorage('commerce_currency');
/** @var \CommerceGuys\Intl\Formatter\NumberFormatterInterface $number_formatter */
$number_formatter = \Drupal::service('commerce_price.number_formatter_factory')->createInstance(NumberFormatterInterface::DECIMAL);
$number_formatter->setMaximumFractionDigits(6);
$number_formatter->setGroupingUsed(FALSE);
/** @var \Drupal\commerce_price\Entity\CurrencyInterface[] $currencies */
$currencies = $currency_storage->loadMultiple();
$currency_codes = array_keys($currencies);
......@@ -108,29 +86,19 @@ class Price extends FormElement {
foreach ($currencies as $currency) {
$fraction_digits[] = $currency->getFractionDigits();
}
$number_formatter->setMinimumFractionDigits(min($fraction_digits));
$number = NULL;
if (isset($default_value)) {
// Convert the stored amount to the local format. For example, "9.99"
// becomes "9,99" in many locales. This also strips any extra zeroes,
// as configured via $this->numberFormatter->setMinimumFractionDigits().
$number = $number_formatter->format($default_value['number']);
}
$element['#tree'] = TRUE;
$element['#attributes']['class'][] = 'form-type-commerce-price';
$element['number'] = [
'#type' => 'textfield',
'#type' => 'commerce_number',
'#title' => $element['#title'],
'#default_value' => $number,
'#default_value' => $default_value ? $default_value['number'] : NULL,
'#required' => $element['#required'],
'#size' => $element['#size'],
'#maxlength' => $element['#maxlength'],
// Provide an example to the end user so that they know which decimal
// separator to use. This is the same pattern Drupal core uses.
'#placeholder' => $number_formatter->format('9.99'),
'#min_fraction_digits' => min($fraction_digits),
'#max_fraction_digits' => 6, // Field storage maximum.
];
unset($element['#size']);
unset($element['#maxlength']);
......@@ -182,38 +150,4 @@ class Price extends FormElement {
return TRUE;
}
/**
* Validates the price element.
*
* Converts the number back to the standard format (e.g. "9,99" -> "9.99").
*
* @param array $element
* The commerce_price form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public static function validateElement(array $element, FormStateInterface $form_state) {
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $currency_storage */
$currency_storage = \Drupal::service('entity_type.manager')->getStorage('commerce_currency');
/** @var \CommerceGuys\Intl\Formatter\NumberFormatterInterface $number_formatter */
$number_formatter = \Drupal::service('commerce_price.number_formatter_factory')->createInstance();
$value = $form_state->getValue($element['#parents']);
if (empty($value['number'])) {
return;
}
/** @var \Drupal\commerce_price\Entity\CurrencyInterface $currency */
$currency = $currency_storage->load($value['currency_code']);
$value['number'] = $number_formatter->parseCurrency($value['number'], $currency);
if ($value['number'] === FALSE) {
$form_state->setError($element['number'], t('%title is not numeric.', [
'%title' => $element['#title'],
]));
return;
}
$form_state->setValueForElement($element, $value);
}
}
name: Commerce Test
name: Commerce Price Test
type: module
description: Contains various non-specific things needed in tests.
package: Testing
core: 8.x
dependencies:
......
commerce_price_test.number_test_form:
path: '/commerce_price_test/number_test_form'
defaults:
_form: '\Drupal\commerce_price_test\Form\NumberTestForm'
_title: 'Number test form'
requirements:
_access: 'TRUE'
commerce_price_test.price_test_form:
path: '/commerce_price_test/price_test_form'
defaults:
......
<?php
namespace Drupal\commerce_price_test\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
class NumberTestForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'commerce_number_element_test_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['number'] = [
'#type' => 'commerce_number',
'#title' => $this->t('Amount'),
'#default_value' => 99.99,
'#min' => 2,
'#max' => 100,
'#required' => TRUE,
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Submit'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
drupal_set_message(t('The number is "@number".', [
'@number' => $form_state->getValue('number'),
]));
}
}
......@@ -12,7 +12,7 @@ class PriceTestForm extends FormBase {
* {@inheritdoc}
*/
public function getFormId() {
return 'commerce_price_test_form';
return 'commerce_price_element_test_form';
}
/**
......@@ -23,8 +23,6 @@ class PriceTestForm extends FormBase {
'#type' => 'commerce_price',
'#title' => $this->t('Amount'),
'#default_value' => ['number' => '99.99', 'currency_code' => 'USD'],
'#size' => 60,
'#maxlength' => 128,
'#required' => TRUE,
];
$form['submit'] = [
......
<?php
namespace Drupal\Tests\commerce_price\Functional;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\commerce\Functional\CommerceBrowserTestBase;
/**
* Tests the number element.
*
* @group commerce
*/
class NumberElementTest extends CommerceBrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'commerce_price_test',
'language',
];
/**
* Tests the element with valid and invalid input.
*/
public function testInput() {
$this->drupalGet('/commerce_price_test/number_test_form');
$this->assertSession()->fieldExists('number');
// Default value.
$this->assertSession()->fieldValueEquals('number', '99.99');
// Not a number.
$edit = [
'number' => 'invalid',
];
$this->submitForm($edit, 'Submit');
$this->assertSession()->pageTextContains('Amount must be a number.');
// Number too low.
$edit = [
'number' => '1',
];
$this->submitForm($edit, 'Submit');
$this->assertSession()->pageTextContains('Amount must be higher than or equal to 2.');
// Number too high.
$edit = [
'number' => '101',
];
$this->submitForm($edit, 'Submit');
$this->assertSession()->pageTextContains('Amount must be lower than or equal to 100.');
// Valid submit.
$edit = [
'number' => '10.99',
];
$this->submitForm($edit, 'Submit');
$this->assertSession()->pageTextContains('The number is "10.99".');
}
/**
* Tests the element with a non-English number format.
*/
public function testLocalFormat() {
// French uses a comma as a decimal separator.
ConfigurableLanguage::createFromLangcode('fr')->save();
$this->config('system.site')->set('default_langcode', 'fr')->save();
$this->drupalGet('/commerce_price_test/number_test_form');
$this->assertSession()->fieldExists('number');
// Default value.
$this->assertSession()->fieldValueEquals('number', '99,99');
// Valid submit.
$edit = [
'number' => '10,99',
];
$this->submitForm($edit, 'Submit');
$this->assertSession()->pageTextContains('The number is "10.99".');
}
}
......@@ -2,7 +2,6 @@
namespace Drupal\Tests\commerce_price\Functional;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\commerce\Functional\CommerceBrowserTestBase;
/**
......@@ -36,7 +35,7 @@ class PriceElementTest extends CommerceBrowserTestBase {
'amount[number]' => 'invalid',
];
$this->submitForm($edit, 'Submit');
$this->assertSession()->pageTextContains('Amount is not numeric.');
$this->assertSession()->pageTextContains('Amount must be a number.');
// Valid submit.
$edit = [
......@@ -67,7 +66,7 @@ class PriceElementTest extends CommerceBrowserTestBase {
'amount[currency_code]' => 'USD',
];
$this->submitForm($edit, 'Submit');
$this->assertSession()->pageTextContains('Amount is not numeric.');
$this->assertSession()->pageTextContains('Amount must be a number.');
// Valid submit.
$edit = [
......@@ -78,25 +77,4 @@ class PriceElementTest extends CommerceBrowserTestBase {
$this->assertSession()->pageTextContains('The number is "10.99" and the currency code is "EUR".');
}
/**
* Tests the element with a non-English number format.
*/
public function testLocalFormat() {
// French uses a comma as a decimal separator.
ConfigurableLanguage::createFromLangcode('fr')->save();
$this->config('system.site')->set('default_langcode', 'fr')->save();
$this->drupalGet('/commerce_price_test/price_test_form');
$this->assertSession()->fieldExists('amount[number]');
// Default value.
$this->assertSession()->fieldValueEquals('amount[number]', '99,99');
// Valid submit.
$edit = [
'amount[number]' => '10,99',
];
$this->submitForm($edit, 'Submit');
$this->assertSession()->pageTextContains('The number is "10.99" and the currency code is "USD".');
}
}
......@@ -35,15 +35,14 @@ abstract class PercentageOffBase extends PromotionOfferBase {
$form += parent::buildConfigurationForm($form, $form_state);
$form['amount'] = [
'#type' => 'number',
'#type' => 'commerce_number',
'#title' => $this->t('Percentage'),
'#default_value' => $this->configuration['amount'] * 100,
'#maxlength' => 255,
'#required' => TRUE,
'#step' => 0.1,
'#min' => 0,
'#max' => 100,
'#length' => 4,
'#size' => 4,
'#field_suffix' => t('%'),
];
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment