Commit 8d206bf5 authored by Dries's avatar Dries

Issue #1901100 by Gábor Hojtsy, jessebeach, Wim Leers: Make Edit module work...

Issue #1901100 by Gábor Hojtsy, jessebeach, Wim Leers: Make Edit module work with TempStore, so revisions are not saved on all atomic field edits.
parent 59f478ff
......@@ -15,6 +15,7 @@
use Drupal\edit\Form\EditFieldForm;
use Drupal\Component\Utility\NestedArray;
use Drupal\entity\Plugin\Core\Entity\EntityDisplay;
use Drupal\user\TempStoreFactory;
/**
* Implements hook_menu().
......@@ -178,7 +179,7 @@ function edit_entity_view_alter(&$build, EntityInterface $entity, EntityDisplay
*
* @ingroup forms
*/
function edit_field_form(array $form, array &$form_state, EntityInterface $entity, $field_name) {
function edit_field_form(array $form, array &$form_state, EntityInterface $entity, $field_name, TempStoreFactory $temp_store_factory) {
$form_handler = new EditFieldForm();
return $form_handler->build($form, $form_state, $entity, $field_name);
return $form_handler->build($form, $form_state, $entity, $field_name, $temp_store_factory);
}
......@@ -19,3 +19,12 @@ edit_field_form:
requirements:
_permission: 'access in-place editing'
_access_edit_entity_field: 'TRUE'
edit_entity_save:
pattern: '/edit/entity/{entity_type}/{entity}'
defaults:
_controller: '\Drupal\edit\EditController::entitySave'
requirements:
_permission: 'access in-place editing'
_access_edit_entity: 'TRUE'
......@@ -6,6 +6,10 @@ services:
class: Drupal\edit\Access\EditEntityFieldAccessCheck
tags:
- { name: access_check }
access_check.edit.entity:
class: Drupal\edit\Access\EditEntityAccessCheck
tags:
- { name: access_check }
edit.editor.selector:
class: Drupal\edit\EditorSelector
arguments: ['@plugin.manager.edit.editor']
......
<?php
/**
* @file
* Contains \Drupal\edit\Access\EditEntityAccessCheck.
*/
namespace Drupal\edit\Access;
use Drupal\Core\Access\AccessCheckInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Drupal\Core\Entity\EntityInterface;
/**
* Access check for editing entities.
*/
class EditEntityAccessCheck implements AccessCheckInterface, EditEntityAccessCheckInterface {
/**
* {@inheritdoc}
*/
public function applies(Route $route) {
// @see edit.routing.yml
return array_key_exists('_access_edit_entity', $route->getRequirements());
}
/**
* {@inheritdoc}
*/
public function access(Route $route, Request $request) {
// @todo Request argument validation and object loading should happen
// elsewhere in the request processing pipeline:
// http://drupal.org/node/1798214.
$this->validateAndUpcastRequestAttributes($request);
return $this->accessEditEntity($request->attributes->get('entity'));
}
/**
* {@inheritdoc}
*/
public function accessEditEntity(EntityInterface $entity) {
return $entity->access('update');
}
/**
* Validates and upcasts request attributes.
*/
protected function validateAndUpcastRequestAttributes(Request $request) {
// Load the entity.
if (!is_object($entity = $request->attributes->get('entity'))) {
$entity_id = $entity;
$entity_type = $request->attributes->get('entity_type');
if (!$entity_type || !entity_get_info($entity_type)) {
throw new NotFoundHttpException();
}
$entity = entity_load($entity_type, $entity_id);
if (!$entity) {
throw new NotFoundHttpException();
}
$request->attributes->set('entity', $entity);
}
}
}
<?php
/**
* @file
* Contains \Drupal\edit\Access\EditEntityAccessCheckInterface.
*/
namespace Drupal\edit\Access;
use Drupal\Core\Entity\EntityInterface;
/**
* Access check for editing entities.
*/
interface EditEntityAccessCheckInterface {
/**
* Checks access to edit the requested entity.
*/
public function accessEditEntity(EntityInterface $entity);
}
......@@ -22,6 +22,7 @@ class EditEntityFieldAccessCheck implements AccessCheckInterface, EditEntityFiel
* Implements AccessCheckInterface::applies().
*/
public function applies(Route $route) {
// @see edit.routing.yml
return array_key_exists('_access_edit_entity_field', $route->getRequirements());
}
......
<?php
/**
* @file
* Contains \Drupal\edit\Ajax\EntitySavedCommand.
*/
namespace Drupal\edit\Ajax;
use Drupal\Core\Ajax\CommandInterface;
/**
* AJAX command to indicate the entity was loaded from TempStore and saved into
* the database.
*/
class EntitySavedCommand extends BaseCommand {
/**
* Constructs a EntitySaveCommand object.
*
* @param string $data
* The data to pass on to the client side.
*/
public function __construct($data) {
parent::__construct('editEntitySaved', $data);
}
}
......@@ -10,8 +10,8 @@
use Drupal\Core\Ajax\CommandInterface;
/**
* AJAX command to indicate a field form was saved without validation errors and
* pass the rerendered field to Edit's JavaScript app.
* AJAX command to indicate a field was saved into TempStore without validation
* errors and pass the rerendered field to Edit's JavaScript app.
*/
class FieldFormSavedCommand extends BaseCommand {
......
......@@ -7,6 +7,7 @@
namespace Drupal\edit;
use Drupal;
use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
......@@ -16,13 +17,32 @@
use Drupal\edit\Ajax\FieldFormCommand;
use Drupal\edit\Ajax\FieldFormSavedCommand;
use Drupal\edit\Ajax\FieldFormValidationErrorsCommand;
use Drupal\edit\Ajax\EntitySavedCommand;
use Drupal\edit\Ajax\MetadataCommand;
use Drupal\user\TempStoreFactory;
/**
* Returns responses for Edit module routes.
*/
class EditController extends ContainerAware {
/**
* Stores the tempstore factory.
*
* @var \Drupal\user\TempStoreFactory
*/
protected $tempStoreFactory;
/**
* Constructs a new EditController.
*
* @param \Drupal\user\TempStoreFactory $temp_store_factory
* The factory for the temp store object.
*/
public function __construct(TempStoreFactory $temp_store_factory = NULL) {
$this->tempStoreFactory = $temp_store_factory ?: Drupal::service('user.tempstore');
}
/**
* Returns the metadata for a set of fields.
*
......@@ -118,17 +138,27 @@ public function attachments(Request $request) {
public function fieldForm(EntityInterface $entity, $field_name, $langcode, $view_mode_id, Request $request) {
$response = new AjaxResponse();
// Replace entity with tempstore copy if available and not resetting, init
// tempstore copy otherwise.
$tempstore_entity = $this->tempStoreFactory->get('edit')->get($entity->uuid());
if ($tempstore_entity && !(isset($_POST['reset']) && $_POST['reset'] === 'true')) {
$entity = $tempstore_entity;
}
else {
$this->tempStoreFactory->get('edit')->set($entity->uuid(), $entity);
}
$form_state = array(
'langcode' => $langcode,
'no_redirect' => TRUE,
'build_info' => array('args' => array($entity, $field_name)),
'build_info' => array('args' => array($entity, $field_name, $this->tempStoreFactory)),
);
$form = drupal_build_form('edit_field_form', $form_state);
if (!empty($form_state['executed'])) {
// The form submission took care of saving the updated entity. Return the
// updated view of the field.
$entity = entity_load($form_state['entity']->entityType(), $form_state['entity']->id(), TRUE);
// The form submission saved the entity in tempstore. Return the
// updated view of the field from the tempstore copy.
$entity = $this->tempStoreFactory->get('edit')->get($entity->uuid());
// @todo Remove when http://drupal.org/node/1346214 is complete.
$entity = $entity->getBCEntity();
$output = field_view_field($entity, $field_name, $view_mode_id, $langcode);
......@@ -156,4 +186,30 @@ public function fieldForm(EntityInterface $entity, $field_name, $langcode, $view
return $response;
}
/**
* Saves an entity into the database, from TempStore.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity being edited.
*/
public function entitySave(EntityInterface $entity) {
// Take the entity from tempstore and save in entity storage. fieldForm()
// ensures that the tempstore copy exists ahead.
$tempstore = $this->tempStoreFactory->get('edit');
$tempstore->get($entity->uuid())->save();
$tempstore->delete($entity->uuid());
// Return information about the entity that allows a front end application
// to identify it.
$output = array(
'entity_type' => $entity->entityType(),
'entity_id' => $entity->id()
);
// Respond to client that the entity was saved properly.
$response = new AjaxResponse();
$response->addCommand(new EntitySavedCommand($output));
return $response;
}
}
......@@ -7,20 +7,30 @@
namespace Drupal\edit\Form;
use Drupal;
use Drupal\Core\Entity\EntityInterface;
use Drupal\user\TempStoreFactory;
/**
* Builds and process a form for editing a single entity field.
*/
class EditFieldForm {
/**
* Stores the tempstore factory.
*
* @var \Drupal\user\TempStoreFactory
*/
protected $tempStoreFactory;
/**
* Builds a form for a single entity field.
*/
public function build(array $form, array &$form_state, EntityInterface $entity, $field_name) {
public function build(array $form, array &$form_state, EntityInterface $entity, $field_name, TempStoreFactory $temp_store_factory) {
if (!isset($form_state['entity'])) {
$this->init($form_state, $entity, $field_name);
}
$this->tempStoreFactory = $temp_store_factory;
// Add the field form.
field_attach_form($form_state['entity'], $form, $form_state, $form_state['langcode'], array('field_name' => $form_state['field_name']));
......@@ -88,7 +98,9 @@ public function validate(array $form, array &$form_state) {
*/
public function submit(array $form, array &$form_state) {
$form_state['entity'] = $this->buildEntity($form, $form_state);
$form_state['entity']->save();
// Store entity in tempstore with its UUID as tempstore key.
$this->tempStoreFactory->get('edit')->set($form_state['entity']->uuid(), $form_state['entity']);
}
/**
......
......@@ -145,9 +145,52 @@ function testUserWithPermission() {
$response = $this->retrieveFieldForm('node/1/body/und/full');
$this->assertResponse(200);
$ajax_commands = drupal_json_decode($response);
$this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in three AJAX commands.');
$this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
$this->assertIdentical('editFieldForm', $ajax_commands[0]['command'], 'The first AJAX command is an editFieldForm command.');
$this->assertIdentical('<form ', Unicode::substr($ajax_commands[0]['data'], 0, 6), 'The editFieldForm command contains a form.');
// Prepare form values for submission. drupalPostAJAX() is not suitable
// for handling pages with JSON responses, so we need our own solution
// here.
$form_tokens_found = preg_match('/\sname="form_token" value="([^"]+)"/', $ajax_commands[0]['data'], $token_match) && preg_match('/\sname="form_build_id" value="([^"]+)"/', $ajax_commands[0]['data'], $build_id_match);
$this->assertTrue($form_tokens_found, 'Form tokens found in output.');
if ($form_tokens_found) {
$edit = array();
$edit['form_id'] = 'edit_field_form';
$edit['form_token'] = $token_match[1];
$edit['form_build_id'] = $build_id_match[1];
$edit['body[und][0][summary]'] = '';
$edit['body[und][0][value]'] = '<p>Fine thanks.</p>';
$edit['body[und][0][format]'] = 'filtered_html';
$edit['op'] = t('Save');
// Submit field form and check response. This should store the
// updated entity in TempStore on the server.
$response = $this->submitFieldForm('node/1/body/und/full', $edit);
$this->assertResponse(200);
$ajax_commands = drupal_json_decode($response);
$this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
$this->assertIdentical('editFieldFormSaved', $ajax_commands[0]['command'], 'The first AJAX command is an editFieldFormSaved command.');
$this->assertTrue(strpos($ajax_commands[0]['data'], 'Fine thanks.'), 'Form value saved and printed back.');
// Ensure the text on the original node did not change yet.
$this->drupalGet('node/1');
$this->assertText('How are you?');
// Save the entity by moving the TempStore values to entity storage.
$response = $this->saveEntity('node/1');
$this->assertResponse(200);
$ajax_commands = drupal_json_decode($response);
$this->assertIdentical(1, count($ajax_commands), 'The entity submission HTTP request results in one AJAX command.');
$this->assertIdentical('editEntitySaved', $ajax_commands[0]['command'], 'The first AJAX command is an editEntitySaved command.');
$this->assertIdentical($ajax_commands[0]['data']['entity_type'], 'node', 'Saved entity is of type node.');
$this->assertIdentical($ajax_commands[0]['data']['entity_id'], '1', 'Entity id is 1.');
// Ensure the text on the original node did change.
$this->drupalGet('node/1');
$this->assertText('Fine thanks.');
}
}
/**
......@@ -225,6 +268,39 @@ protected function retrieveAttachments($editors) {
));
}
/**
* Submit field form data to the server.
*
* @param string $field_id
* An Edit field ID.
* @param array $post
* An array of post data to send.
*
* @return string
* The response body.
*/
protected function submitFieldForm($field_id, $post) {
// Serialize POST values.
foreach ($post as $key => $value) {
// Encode according to application/x-www-form-urlencoded
// Both names and values needs to be urlencoded, according to
// http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
$post[$key] = urlencode($key) . '=' . urlencode($value);
}
$post = implode('&', $post);
// Perform HTTP request.
return $this->curlExec(array(
CURLOPT_URL => url('edit/form/' . $field_id, array('absolute' => TRUE)),
CURLOPT_POST => TRUE,
CURLOPT_POSTFIELDS => $post . $this->getAjaxPageStatePostData(),
CURLOPT_HTTPHEADER => array(
'Accept: application/json',
'Content-Type: application/x-www-form-urlencoded',
),
));
}
/**
* Retrieve field form from the server. May also result in additional
* JavaScript settings and CSS/JS being loaded.
......@@ -251,6 +327,31 @@ protected function retrieveFieldForm($field_id) {
));
}
/**
* Save entity edits on the server.
*
* @param string $entity_type_id
* The edit type and ID URI portion.
*
* @return string
* The response body.
*/
protected function saveEntity($entity_type_id) {
// Build & serialize POST value.
$post = urlencode('nocssjs') . '=' . urlencode('true');
// Perform HTTP request.
return $this->curlExec(array(
CURLOPT_URL => url('edit/entity/' . $entity_type_id, array('absolute' => TRUE)),
CURLOPT_POST => TRUE,
CURLOPT_POSTFIELDS => $post . $this->getAjaxPageStatePostData(),
CURLOPT_HTTPHEADER => array(
'Accept: application/json',
'Content-Type: application/x-www-form-urlencoded',
),
));
}
/**
* Get extra information to the POST data as ajax.js does.
*
......
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