diff --git a/poll.install b/poll.install index 4adea1b..fcc37d2 100644 --- a/poll.install +++ b/poll.install @@ -5,6 +5,8 @@ * Install, update, and uninstall functions for the Poll module. */ +use Drupal\poll\Entity\PollChoice; + /** * Implements hook_schema(). */ @@ -65,3 +67,57 @@ function poll_schema() { return $schema; } + +/** + * Convert choices to a separate entity type. + */ +function poll_update_8001() { + // Create the entity type. + \Drupal::entityTypeManager()->clearCachedDefinitions(); + $poll_choice = \Drupal::entityTypeManager()->getDefinition('poll_choice'); + \Drupal::entityDefinitionUpdateManager()->installEntityType($poll_choice); + + // Migrate the data to the new entity type. + $result = \Drupal::database()->query('SELECT * FROM {poll__choice}'); + foreach ($result as $row) { + $choice = PollChoice::create([ + 'langcode' => $row->langcode, + 'id' => $row->choice_chid, + 'choice' => $row->choice_choice, + ]); + $choice->enforceIsNew(TRUE); + + $choice->setChoice($row->choice_choice); + $choice->save(); + } + + $target_id_schema = [ + 'description' => 'The ID of the target entity.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ]; + + // Convert the choice reference table. + $schema = \Drupal::database()->schema(); + $schema->dropField('poll__choice', 'choice_choice'); + $schema->dropField('poll__choice', 'choice_vote'); + $schema->changeField('poll__choice', 'choice_chid', 'choice_target_id', $target_id_schema); + $schema->addIndex('poll__choice', 'choice_target_id', ['choice_target_id'], ['fields' => ['choice_target_id' => $target_id_schema]]); + + // Update the field storage repository. + \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); + $storage_definition = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('poll')['choice']; + \Drupal::service('entity.last_installed_schema.repository')->setLastInstalledFieldStorageDefinition($storage_definition); + + // Update the stored field schema. + // @todo: There has to be a better way to do this. + $field_schema = \Drupal::keyValue('entity.storage_schema.sql')->get('poll.field_schema_data.choice'); + unset($field_schema['poll__choice']['fields']['choice_chid']); + unset($field_schema['poll__choice']['fields']['choice_choice']); + unset($field_schema['poll__choice']['fields']['choice_vote']); + unset($field_schema['poll__choice']['indexes']['choice_chid']); + $field_schema['poll__choice']['fields']['choice_target_id'] = $target_id_schema; + $field_schema['poll__choice']['indexes']['choice_target_id'] = ['choice_target_id']; + \Drupal::keyValue('entity.storage_schema.sql')->set('poll.field_schema_data.choice', $field_schema); +} diff --git a/src/Entity/Poll.php b/src/Entity/Poll.php index d5edb50..f4cec99 100644 --- a/src/Entity/Poll.php +++ b/src/Entity/Poll.php @@ -7,7 +7,6 @@ namespace Drupal\poll\Entity; -use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; @@ -246,9 +245,13 @@ class Poll extends ContentEntityBase implements PollInterface { ->setLabel(t('Language code')) ->setDescription(t('The poll language code.')); - $fields['choice'] = BaseFieldDefinition::create('poll_choice') + $fields['choice'] = BaseFieldDefinition::create('entity_reference') ->setLabel(t('Choice')) - ->setDescription(t('Enter a poll choice and default vote.')) + ->setSetting('target_type', 'poll_choice') + ->setDescription(t('Enter the poll choices.')) + // The number and order of choices may not be translated, only the + // referenced choices. + ->setTranslatable(FALSE) ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) ->setSetting('max_length', 255) ->setDisplayOptions('form', [ @@ -388,8 +391,8 @@ class Poll extends ContentEntityBase implements PollInterface { public function getOptions() { $options = array(); if (count($this->choice)) { - foreach ($this->choice as $option) { - $options[$option->chid] = SafeMarkup::checkPlain($option->choice); + foreach ($this->choice as $choice_item) { + $options[$choice_item->target_id] = $choice_item->entity->label(); } } return $options; @@ -403,8 +406,8 @@ class Poll extends ContentEntityBase implements PollInterface { public function getOptionValues() { $options = array(); if (count($this->choice)) { - foreach ($this->choice as $option) { - $options[$option->chid] = $option->vote; + foreach ($this->choice as $choice_item) { + $options[$choice_item->target_id] = 1; } } return $options; @@ -413,12 +416,54 @@ class Poll extends ContentEntityBase implements PollInterface { /** * {@inheritdoc} */ + public function preSave(EntityStorageInterface $storage) { + parent::preSave($storage); + foreach ($this->choice as $choice_item) { + if ($choice_item->entity && $choice_item->entity->needsSaving()) { + $choice_item->entity->save(); + $choice_item->target_id = $choice_item->entity->id(); + } + } + + // Delete no longer referenced choices. + if (!$this->isNew()) { + $original_choices = []; + foreach ($this->original->choice as $choice_item) { + $original_choices[] = $choice_item->target_id; + } + + $current_choices = []; + foreach ($this->choice as $choice_item) { + $current_choices[] = $choice_item->target_id; + } + + $removed_choices = array_diff($original_choices, $current_choices); + if ($removed_choices) { + $storage = \Drupal::entityTypeManager()->getStorage('poll_choice'); + $storage->delete($storage->loadMultiple($removed_choices)); + } + } + } + + /** + * {@inheritdoc} + */ public static function postDelete(EntityStorageInterface $storage, array $entities) { parent::postDelete($storage, $entities); + // Delete votes. foreach ($entities as $entity) { $storage->deleteVotes($entity); } + + // Delete referenced choices. + $choices = []; + foreach ($entities as $entity) { + $choices = array_merge($choices, $entity->choice->referencedEntities()); + } + if ($choices) { + \Drupal::entityTypeManager()->getStorage('poll_choice')->delete($choices); + } } /** diff --git a/src/Entity/PollChoice.php b/src/Entity/PollChoice.php new file mode 100644 index 0000000..cb8a61d --- /dev/null +++ b/src/Entity/PollChoice.php @@ -0,0 +1,93 @@ +set('choice', $question); + return $this; + } + + /** + * {@inheritdoc} + */ + public function needsSaving($new_value = NULL) { + // If explicitly set, return that value. otherwise fall back to isNew(), + // saving is always required for new entities. + $return = $this->needsSave !== NULL ? $this->needsSave : $this->isNew(); + + if ($new_value !== NULL) { + $this->needsSave = $new_value; + } + + return $return; + } + + /** + * {@inheritdoc} + */ + public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { + $fields['id'] = BaseFieldDefinition::create('integer') + ->setLabel(t('Choice ID')) + ->setReadOnly(TRUE) + ->setSetting('unsigned', TRUE); + + $fields['uuid'] = BaseFieldDefinition::create('uuid') + ->setLabel(t('UUID')) + ->setReadOnly(TRUE); + + $fields['choice'] = BaseFieldDefinition::create('string') + ->setLabel(t('Choice')) + ->setRequired(TRUE) + ->setTranslatable(TRUE) + ->setSetting('max_length', 255) + ->setDisplayOptions('form', array( + 'type' => 'string_textfield', + 'weight' => -100, + )); + + $fields['langcode'] = BaseFieldDefinition::create('language') + ->setLabel(t('Language code')) + ->setDescription(t('The poll language code.')); + + return $fields; + } + +} diff --git a/src/Plugin/Field/FieldFormatter/PollChoiceDefaultFormatter.php b/src/Plugin/Field/FieldFormatter/PollChoiceDefaultFormatter.php deleted file mode 100644 index 05f14f3..0000000 --- a/src/Plugin/Field/FieldFormatter/PollChoiceDefaultFormatter.php +++ /dev/null @@ -1,38 +0,0 @@ - $item) { - $elements[$delta] = array('#markup' => $item->choice); - } - return $elements; - } - -} diff --git a/src/Plugin/Field/FieldType/PollChoiceItem.php b/src/Plugin/Field/FieldType/PollChoiceItem.php deleted file mode 100644 index 4e72c17..0000000 --- a/src/Plugin/Field/FieldType/PollChoiceItem.php +++ /dev/null @@ -1,102 +0,0 @@ -setLabel(t('Choice ID')); - $properties['choice'] = DataDefinition::create('string') - ->setLabel(t('Choice')); - $properties['vote'] = DataDefinition::create('integer') - ->setLabel(t('Vote')); - - return $properties; - } - - /** - * {@inheritdoc} - */ - public static function schema(FieldStorageDefinitionInterface $field_definition) { - return array( - 'columns' => array( - 'chid' => array( - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - ), - 'choice' => array( - 'type' => 'varchar', - 'length' => 512, - 'not null' => TRUE, - ), - 'vote' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => FALSE, - ), - ), - 'indexes' => array( - 'chid' => array('chid'), - ), - 'primary key' => array('chid'), - ); - } - - /** - * {@inheritdoc} - */ - public function isEmpty() { - $value = $this->get('choice')->getValue(); - return $value === NULL || $value === ''; - } - - /** - * {@inheritdoc} - */ - public function getConstraints() { - $constraint_manager = \Drupal::typedDataManager() - ->getValidationConstraintManager(); - $constraints = parent::getConstraints(); - $constraints[] = $constraint_manager->create('ComplexData', array( - 'choice' => array( - 'Length' => array( - 'max' => static::POLL_CHOICE_MAX_LENGTH, - 'maxMessage' => t('%name: the choice field may not be longer than @max characters.', array( - '%name' => $this->getFieldDefinition()->getLabel(), - '@max' => static::POLL_CHOICE_MAX_LENGTH - )), - ) - ), - )); - return $constraints; - } - -} diff --git a/src/Plugin/Field/FieldWidget/PollChoiceDefaultWidget.php b/src/Plugin/Field/FieldWidget/PollChoiceDefaultWidget.php index 6784ff7..53e7856 100644 --- a/src/Plugin/Field/FieldWidget/PollChoiceDefaultWidget.php +++ b/src/Plugin/Field/FieldWidget/PollChoiceDefaultWidget.php @@ -7,6 +7,7 @@ namespace Drupal\poll\Plugin\Field\FieldWidget; +use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; @@ -20,7 +21,7 @@ use Drupal\Core\Form\FormStateInterface; * module = "poll", * label = @Translation("Poll choice"), * field_types = { - * "poll_choice" + * "entity_reference" * } * ) */ @@ -35,25 +36,102 @@ class PollChoiceDefaultWidget extends WidgetBase { * {@inheritdoc} */ public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { - $element['chid'] = array( + $langcode = $this->getCurrentLangcode($form_state, $items); + + /* @var \Drupal\poll\PollChoiceInterface $choice */ + $choice = $items[$delta]->entity; + if ($choice) { + // If target translation is not yet available, populate it with data from + // the original choice. + if ($choice->language()->getId() != $langcode && !$choice->hasTranslation($langcode)) { + $choice->addTranslation($langcode, $choice->toArray()); + } + + // Initiate the choice with the correct translation. + $choice = $choice->getTranslation($langcode); + } + + $element['target_id'] = array( '#type' => 'value', - '#value' => $items[$delta]->chid, + '#value' => $choice ? $choice->id() : NULL, ); + $element['langcode'] = array( + '#type' => 'value', + '#value' => $langcode, + ); + $element['choice'] = array( '#type' => 'textfield', '#placeholder' => t('Choice'), '#empty_value' => '', - '#default_value' => isset($items[$delta]->choice) ? $items[$delta]->choice : '', + '#default_value' => $choice ? $choice->choice->value : NULL, '#prefix' => '
', ); - $element['vote'] = array( - '#type' => 'number', - '#placeholder' => t('Vote'), - '#empty_value' => '', - '#default_value' => isset($items[$delta]->vote) ? $items[$delta]->vote : static::VOTE_DEFAULT_VALUE, - '#min' => 0, - '#suffix' => '
', - ); return $element; } + + /** + * Gets current language code from the form state or item. + * + * Since the choice field is not set as translatable, the item language + * code is set to the source language. The intended translation language + * is only accessibly through the form state. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * @param \Drupal\Core\Field\FieldItemListInterface $items + * The field items object. + * + * @return string + * The language code to be used. + */ + protected function getCurrentLangcode(FormStateInterface $form_state, FieldItemListInterface $items) { + return $form_state->get('langcode') ?: $items->getEntity()->language()->getId(); + } + + /** + * {@inheritdoc} + */ + public static function isApplicable(FieldDefinitionInterface $field_definition) { + return $field_definition->getTargetEntityTypeId() == 'poll' && $field_definition->getName() == 'choice'; + } + + /** + * {@inheritdoc} + */ + public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { + foreach ($values as $delta => &$item_values) { + $entity_type_manager = \Drupal::entityTypeManager(); + $storage = $entity_type_manager->getStorage('poll_choice'); + $langcode = $item_values['langcode']; + + // Remove empty values. Removed choices will be deleted automatically. + if (empty($item_values['choice'])) { + unset($values[$delta]); + continue; + } + + /** @var \Drupal\poll\PollChoiceInterface $choice */ + $choice = !empty($item_values['target_id']) ? $storage->load($item_values['target_id']) : $storage->create(['langcode' => $langcode]); + + // If target translation is not yet available, populate it with data from the original paragraph. + if ($choice->language()->getId() != $langcode && !$choice->hasTranslation($langcode)) { + $choice->addTranslation($langcode, $choice->toArray()); + } + + // Initiate the paragraph with the correct translation. + $choice = $choice->getTranslation($langcode); + + // If the choice is new or changed, resave it. + if ($choice->isNew() || $item_values['choice'] != $choice->choice->value) { + $choice->choice->value = $item_values['choice']; + + } + unset($item_values['target_id'], $item_values['choice'], $item_values['langcode']); + + $item_values['entity'] = $choice; + } + return $values; + } + } diff --git a/src/PollChoiceInterface.php b/src/PollChoiceInterface.php new file mode 100644 index 0000000..4419ee0 --- /dev/null +++ b/src/PollChoiceInterface.php @@ -0,0 +1,39 @@ +database->query("SELECT chid, COUNT(chid) AS votes FROM {poll_vote} WHERE pid = :pid GROUP BY chid", array(':pid' => $poll->id())); - $results = $query->fetchAll(); + $result = $this->database->query("SELECT chid, COUNT(chid) AS votes FROM {poll_vote} WHERE pid = :pid GROUP BY chid", array(':pid' => $poll->id())); // Replace the count for options that have recorded votes in the database. - // Multiply by the vote value for each option. - $optionValues = $poll->getOptionValues(); - foreach ($results as $result) { - $votes[$result->chid] = $result->votes * $optionValues[$result->chid]; + foreach ($result as $row) { + $votes[$row->chid] = $row->votes; } return $votes; diff --git a/src/Tests/PollBlockTest.php b/src/Tests/PollBlockTest.php index 7a5a939..5a7a1e0 100644 --- a/src/Tests/PollBlockTest.php +++ b/src/Tests/PollBlockTest.php @@ -46,6 +46,7 @@ class PollBlockTest extends PollTestBase { // the choices might. $this->assertText($poll->label(), SafeMarkup::format('@title Poll appears in block.', array('@title' => $this->poll->label()))); $options = $poll->getOptions(); + debug($options); foreach ($options as $option) { $this->assertText($option, 'Poll option appears in block.'); } diff --git a/src/Tests/PollCreateTest.php b/src/Tests/PollCreateTest.php index 1b9984a..e033f62 100644 --- a/src/Tests/PollCreateTest.php +++ b/src/Tests/PollCreateTest.php @@ -46,15 +46,12 @@ class PollCreateTest extends PollTestBase { // Now add a new option to make sure that when we update the poll, the // option is displayed. $vote_choice = $this->randomMachineName(); - $vote_count = '2000'; - $poll->choice[0]->choice = $vote_choice; - $poll->choice[0]->vote = $vote_count; - $poll->save(); + $poll->choice[0]->entity->setChoice($vote_choice); + $poll->choice[0]->entity->save(); // Check the new choice has taken effect. $this->drupalGet('poll/' . $poll->id() . '/edit'); $this->assertFieldByXPath("//input[@name='choice[0][choice]']", $vote_choice, 'Choice successfully changed.'); - $this->assertFieldByXPath("//input[@name='choice[0][vote]']", $vote_count, 'Vote successfully changed.'); } diff --git a/src/Tests/PollDeleteChoiceTest.php b/src/Tests/PollDeleteChoiceTest.php index 927b455..4121b03 100644 --- a/src/Tests/PollDeleteChoiceTest.php +++ b/src/Tests/PollDeleteChoiceTest.php @@ -19,13 +19,26 @@ class PollDeleteChoiceTest extends PollTestBase { */ function testChoiceRemoval() { // Set up a poll with three choices. - $choices = array('First choice', 'Second choice', 'Third choice'); $this->assertTrue($this->poll->id(), 'Poll for choice deletion logic test created.'); - // @TODO need to fix the poll nid and not hard code poll/1 + + $ids = \Drupal::entityQuery('poll_choice') + ->condition('choice', $this->poll->choice[0]->entity->label()) + ->execute(); + $this->assertEqual(count($ids), 1, 'Choice 1 exists in the database'); + + // Record a vote for the second choice. + $edit = array( + 'choice' => $this->poll->choice[1]->target_id, + ); + $this->drupalPostForm('poll/' . $this->poll->id(), $edit, t('Vote')); + + // Assert the selected option. + $xml = $this->xpath('//dt[text()=:choice]/following-sibling::dd[1]/div', [':choice' => $this->poll->choice[1]->entity->label()]); + $this->assertEqual(1, $xml[0]['data-value']); // Edit the poll, and try to delete first poll choice. $this->drupalGet("poll/" . $this->poll->id() . "/edit"); - $edit['choice[0][choice]'] = ''; + $edit = ['choice[0][choice]' => '']; $this->drupalPostForm(NULL, $edit, t('Save')); // Click on the poll title to go to poll page. @@ -33,8 +46,20 @@ class PollDeleteChoiceTest extends PollTestBase { $this->clickLink($this->poll->label()); // Check the first poll choice is deleted, while the others remain. - $this->assertNoText($this->poll->choice[0]->choice, 'First choice removed.'); - $this->assertText($this->poll->choice[1]->choice, 'Second choice remains.'); - $this->assertText($this->poll->choice[2]->choice, 'Third choice remains.'); + $this->assertNoText($this->poll->choice[0]->entity->label(), 'First choice removed.'); + $this->assertText($this->poll->choice[1]->entity->label(), 'Second choice remains.'); + $this->assertText($this->poll->choice[2]->entity->label(), 'Third choice remains.'); + + $ids = \Drupal::entityQuery('poll_choice') + ->condition('choice', $this->poll->choice[0]->entity->label()) + ->execute(); + $this->assertEqual(count($ids), 0, 'Choice 1 has been deleted in the database'); + + // Ensure that the existing vote still shows. + $this->drupalGet('poll/' . $this->poll->id()); + + // Assert the selected option. + $xml = $this->xpath('//dt[text()=:choice]/following-sibling::dd[1]/div', [':choice' => $this->poll->choice[1]->entity->label()]); + $this->assertEqual(1, $xml[0]['data-value']); } }