modify.action.inc 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. <?php
  2. /**
  3. * @file VBO action to modify entity values (properties and fields).
  4. */
  5. // Specifies that all available values should be shown to the user for editing.
  6. define('VBO_MODIFY_ACTION_ALL', '_all_');
  7. function views_bulk_operations_modify_action_info() {
  8. return array('views_bulk_operations_modify_action' => array(
  9. 'type' => 'entity',
  10. 'label' => t('Modify entity values'),
  11. 'behavior' => array('changes_property'),
  12. // This action only works when invoked through VBO. That's why it's
  13. // declared as non-configurable to prevent it from being shown in the
  14. // "Create an advanced action" dropdown on admin/config/system/actions.
  15. 'configurable' => FALSE,
  16. 'vbo_configurable' => TRUE,
  17. 'triggers' => array('any'),
  18. ));
  19. }
  20. /**
  21. * Action function.
  22. *
  23. * Goes through new values and uses them to modify the passed entity by either
  24. * replacing the existing values, or appending to them (based on user input).
  25. */
  26. function views_bulk_operations_modify_action($entity, $context) {
  27. list(,,$bundle_name) = entity_extract_ids($context['entity_type'], $entity);
  28. // Handle Field API fields.
  29. if (!empty($context['selected']['bundle_' . $bundle_name])) {
  30. // The pseudo entity is cloned so that changes to it don't get carried
  31. // over to the next execution.
  32. $pseudo_entity = clone $context['entities'][$bundle_name];
  33. foreach ($context['selected']['bundle_' . $bundle_name] as $key) {
  34. // Replace any tokens that might exist in the field columns.
  35. foreach ($pseudo_entity->{$key}[LANGUAGE_NONE] as $delta => &$item) {
  36. foreach ($item as $column => $value) {
  37. if (is_string($value)) {
  38. $item[$column] = token_replace($value, array($context['entity_type'] => $entity), array('sanitize' => FALSE));
  39. }
  40. }
  41. }
  42. if (in_array($key, $context['append']['bundle_' . $bundle_name]) && !empty($entity->$key)) {
  43. $entity->{$key}[LANGUAGE_NONE] = array_merge($entity->{$key}[LANGUAGE_NONE], $pseudo_entity->{$key}[LANGUAGE_NONE]);
  44. // Check if we breached cardinality, and notify the user.
  45. $field_info = field_info_field($key);
  46. $field_count = count($entity->{$key}[LANGUAGE_NONE]);
  47. if ($field_info['cardinality'] != FIELD_CARDINALITY_UNLIMITED && $field_count > $field_info['cardinality']) {
  48. $entity_label = entity_label($context['entity_type'], $entity);
  49. $warning = t('Tried to set !field_count values for field !field_name that supports a maximum of !cardinality.',
  50. array('!field_count' => $field_count,
  51. '!field_name' => $field_info['field_name'],
  52. '!cardinality' => $field_info['cardinality']));
  53. drupal_set_message($warning, 'warning', FALSE);
  54. }
  55. }
  56. else {
  57. $entity->$key = $pseudo_entity->$key;
  58. }
  59. }
  60. }
  61. // Handle properties.
  62. if (!empty($context['selected']['properties'])) {
  63. // Use the wrapper to set property values, since some properties need
  64. // additional massaging by their setter callbacks.
  65. // The wrapper will automatically modify $entity itself.
  66. $wrapper = entity_metadata_wrapper($context['entity_type'], $entity);
  67. foreach ($context['selected']['properties'] as $key) {
  68. if (in_array($key, $context['append']['properties'])) {
  69. $old_values = $wrapper->$key->value();
  70. $wrapper->$key->set($context['properties'][$key]);
  71. $new_values = $wrapper->{$key}->value();
  72. $all_values = array_merge($old_values, $new_values);
  73. $wrapper->$key->set($all_values);
  74. }
  75. else {
  76. $value = $context['properties'][$key];
  77. if (is_string($value)) {
  78. $value = token_replace($value, array($context['entity_type'] => $entity), array('sanitize' => FALSE));
  79. }
  80. $wrapper->$key->set($value);
  81. }
  82. }
  83. }
  84. }
  85. /**
  86. * Action form function.
  87. *
  88. * Displays form elements for properties acquired through Entity Metadata
  89. * (hook_entity_property_info()), as well as field widgets for each
  90. * entity bundle, as provided by field_attach_form().
  91. */
  92. function views_bulk_operations_modify_action_form($context, &$form_state) {
  93. // This action form uses admin-provided settings. If they were not set, pull the defaults now.
  94. if (!isset($context['settings'])) {
  95. $context['settings'] = views_bulk_operations_modify_action_views_bulk_operations_form_options();
  96. }
  97. $form_state['entity_type'] = $entity_type = $context['entity_type'];
  98. // For Field API integration to work, a pseudo-entity is constructed for each
  99. // bundle that has fields available for editing.
  100. // The entities then get passed to Field API functions
  101. // (field_attach_form(), field_attach_form_validate(), field_attach_submit()),
  102. // and filled with form data.
  103. // After submit, the pseudo-entities get passed to the actual action
  104. // (views_bulk_operations_modify_action()) which copies the data from the
  105. // relevant pseudo-entity constructed here to the actual entity being modified.
  106. $form_state['entities'] = array();
  107. $info = entity_get_info($entity_type);
  108. $properties = _views_bulk_operations_modify_action_get_properties($entity_type, $context['settings']['display_values']);
  109. $bundles = _views_bulk_operations_modify_action_get_bundles($entity_type, $context);
  110. $form['#attached']['css'][] = drupal_get_path('module', 'views_bulk_operations') . '/css/modify.action.css';
  111. $form['#tree'] = TRUE;
  112. if (!empty($properties)) {
  113. $form['properties'] = array(
  114. '#type' => 'fieldset',
  115. '#title' => 'Properties',
  116. );
  117. $form['properties']['show_value'] = array(
  118. '#suffix' => '<div class="clearfix"></div>',
  119. );
  120. foreach ($properties as $key => $property) {
  121. $form['properties']['show_value'][$key] = array(
  122. '#type' => 'checkbox',
  123. '#title' => $property['label'],
  124. );
  125. $determined_type = ($property['type'] == 'boolean') ? 'checkbox' : 'textfield';
  126. $form['properties'][$key] = array(
  127. '#type' => $determined_type,
  128. '#title' => $property['label'],
  129. '#description' => $property['description'],
  130. '#states' => array(
  131. 'visible' => array(
  132. '#edit-properties-show-value-' . str_replace('_', '-', $key) => array('checked' => TRUE),
  133. ),
  134. ),
  135. );
  136. if (!empty($property['options list'])) {
  137. $form['properties'][$key]['#type'] = 'select';
  138. $form['properties'][$key]['#options'] = $property['options list']($key, array());
  139. if ($property['type'] == 'list') {
  140. $form['properties'][$key]['#type'] = 'checkboxes';
  141. $form['properties']['_append::' . $key] = array(
  142. '#type' => 'checkbox',
  143. '#title' => t('Add new value(s) to %label, instead of overwriting the existing values.', array('%label' => $property['label'])),
  144. '#states' => array(
  145. 'visible' => array(
  146. '#edit-properties-show-value-' . $key => array('checked' => TRUE),
  147. ),
  148. ),
  149. );
  150. }
  151. }
  152. }
  153. }
  154. foreach ($bundles as $bundle_name => $bundle) {
  155. $bundle_key = $info['entity keys']['bundle'];
  156. $default_values = array();
  157. // If the bundle key exists, it must always be set on an entity.
  158. if (!empty($bundle_key)) {
  159. $default_values[$bundle_key] = $bundle_name;
  160. }
  161. $entity = entity_create($context['entity_type'], $default_values);
  162. $form_state['entities'][$bundle_name] = $entity;
  163. // Show the more detailed label only if the entity type has multiple bundles.
  164. // Otherwise, it would just be confusing.
  165. if (count($info['bundles']) > 1) {
  166. $label = t('Fields for @bundle_key @label', array('@bundle_key' => $bundle_key, '@label' => $bundle['label']));
  167. }
  168. else {
  169. $label = t('Fields');
  170. }
  171. $form_key = 'bundle_' . $bundle_name;
  172. $form[$form_key] = array(
  173. '#type' => 'fieldset',
  174. '#title' => $label,
  175. '#parents' => array($form_key),
  176. );
  177. field_attach_form($context['entity_type'], $entity, $form[$form_key], $form_state, LANGUAGE_NONE);
  178. // Now that all the widgets have been added, sort them by #weight.
  179. // This ensures that they will stay in the correct order when they get
  180. // assigned new weights.
  181. uasort($form[$form_key], 'element_sort');
  182. $display_values = $context['settings']['display_values'];
  183. $instances = field_info_instances($entity_type, $bundle_name);
  184. $weight = 0;
  185. foreach (element_get_visible_children($form[$form_key]) as $field_name) {
  186. // For our use case it makes no sense for any field widget to be required.
  187. $language = $form[$form_key][$field_name]['#language'];
  188. _views_bulk_operations_modify_action_unset_required($form[$form_key][$field_name][$language]);
  189. // The admin has specified which fields to display, but this field didn't
  190. // make the cut. Hide it with #access => FALSE and move on.
  191. if (empty($display_values[VBO_MODIFY_ACTION_ALL]) && empty($display_values[$bundle_name . '::' . $field_name])) {
  192. $form[$form_key][$field_name]['#access'] = FALSE;
  193. continue;
  194. }
  195. $field = $instances[$field_name];
  196. $form[$form_key]['show_value'][$field_name] = array(
  197. '#type' => 'checkbox',
  198. '#title' => $field['label'],
  199. );
  200. $form[$form_key][$field_name]['#states'] = array(
  201. 'visible' => array(
  202. '#edit-bundle-' . str_replace('_', '-', $bundle_name) . '-show-value-' . str_replace('_', '-', $field_name) => array('checked' => TRUE),
  203. ),
  204. );
  205. // All field widgets get reassigned weights so that additional elements
  206. // added between them (such as "_append") can be properly ordered.
  207. $form[$form_key][$field_name]['#weight'] = $weight++;
  208. $field_info = field_info_field($field_name);
  209. if ($field_info['cardinality'] != 1) {
  210. $form[$form_key]['_append::' . $field_name] = array(
  211. '#type' => 'checkbox',
  212. '#title' => t('Add new value(s) to %label, instead of overwriting the existing values.', array('%label' => $field['label'])),
  213. '#states' => array(
  214. 'visible' => array(
  215. '#edit-bundle-' . str_replace('_', '-', $bundle_name) . '-show-value-' . str_replace('_', '-', $field_name) => array('checked' => TRUE),
  216. ),
  217. ),
  218. '#weight' => $weight++,
  219. );
  220. }
  221. }
  222. // Add a clearfix below the checkboxes so that the widgets are not floated.
  223. $form[$form_key]['show_value']['#suffix'] = '<div class="clearfix"></div>';
  224. $form[$form_key]['show_value']['#weight'] = -1;
  225. }
  226. // If the form has only one group (for example, "Properties"), remove the
  227. // title and the fieldset, since there's no need to visually group values.
  228. $form_elements = element_get_visible_children($form);
  229. if (count($form_elements) == 1) {
  230. $element_key = reset($form_elements);
  231. unset($form[$element_key]['#type']);
  232. unset($form[$element_key]['#title']);
  233. // Get a list of all elements in the group, and filter out the non-values.
  234. $values = element_get_visible_children($form[$element_key]);
  235. foreach ($values as $index => $key) {
  236. if ($key == 'show_value' || substr($key, 0, 1) == '_') {
  237. unset($values[$index]);
  238. }
  239. }
  240. // If the group has only one value, no need to hide it through #states.
  241. if (count($values) == 1) {
  242. $value_key = reset($values);
  243. $form[$element_key]['show_value'][$value_key]['#type'] = 'value';
  244. $form[$element_key]['show_value'][$value_key]['#value'] = TRUE;
  245. }
  246. }
  247. if (module_exists('token') && $context['settings']['show_all_tokens']) {
  248. $token_type = str_replace('_', '-', $entity_type);
  249. $form['tokens'] = array(
  250. '#type' => 'fieldset',
  251. '#title' => 'Available tokens',
  252. '#collapsible' => TRUE,
  253. '#collapsed' => TRUE,
  254. '#weight' => 998,
  255. );
  256. $form['tokens']['tree'] = array(
  257. '#theme' => 'token_tree',
  258. '#token_types' => array($token_type, 'site'),
  259. '#global_types' => array(),
  260. '#dialog' => TRUE,
  261. );
  262. }
  263. return $form;
  264. }
  265. /**
  266. * Action form validate function.
  267. *
  268. * Checks that the user selected at least one value to modify, validates
  269. * properties and calls Field API to validate fields for each bundle.
  270. */
  271. function views_bulk_operations_modify_action_validate($form, &$form_state) {
  272. // The form structure for "Show" checkboxes is a bit bumpy.
  273. $search = array('properties');
  274. foreach ($form_state['entities'] as $bundle => $entity) {
  275. $search[] = 'bundle_' . $bundle;
  276. }
  277. $has_selected = FALSE;
  278. foreach ($search as $group) {
  279. // Store names of selected and appended entity values in a nicer format.
  280. $form_state['selected'][$group] = array();
  281. $form_state['append'][$group] = array();
  282. // This group has no values, move on.
  283. if (!isset($form_state['values'][$group])) {
  284. continue;
  285. }
  286. foreach ($form_state['values'][$group]['show_value'] as $key => $value) {
  287. if ($value) {
  288. $has_selected = TRUE;
  289. $form_state['selected'][$group][] = $key;
  290. }
  291. if (!empty($form_state['values'][$group]['_append::' . $key])) {
  292. $form_state['append'][$group][] = $key;
  293. unset($form_state['values'][$group]['_append::' . $key]);
  294. }
  295. }
  296. unset($form_state['values'][$group]['show_value']);
  297. }
  298. if (!$has_selected) {
  299. form_set_error('', t('You must select at least one value to modify.'));
  300. return;
  301. }
  302. // Use the wrapper to validate property values.
  303. if (!empty($form_state['selected']['properties'])) {
  304. // The entity used is irrelevant, and we can't rely on
  305. // $form_state['entities'] being non-empty, so a new one is created.
  306. $info = entity_get_info($form_state['entity_type']);
  307. $bundle_key = $info['entity keys']['bundle'];
  308. $default_values = array();
  309. // If the bundle key exists, it must always be set on an entity.
  310. if (!empty($bundle_key)) {
  311. $bundle_names = array_keys($info['bundles']);
  312. $bundle_name = reset($bundle_names);
  313. $default_values[$bundle_key] = $bundle_name;
  314. }
  315. $entity = entity_create($form_state['entity_type'], $default_values);
  316. $wrapper = entity_metadata_wrapper($form_state['entity_type'], $entity);
  317. $properties = _views_bulk_operations_modify_action_get_properties($form_state['entity_type']);
  318. foreach ($form_state['selected']['properties'] as $key) {
  319. $value = $form_state['values']['properties'][$key];
  320. if (!$wrapper->$key->validate($value)) {
  321. $label = $properties[$key]['label'];
  322. form_set_error('properties][' . $key, t('%label contains an invalid value.', array('%label' => $label)));
  323. }
  324. }
  325. }
  326. foreach ($form_state['entities'] as $bundle_name => $entity) {
  327. field_attach_form_validate($form_state['entity_type'], $entity, $form['bundle_' . $bundle_name], $form_state);
  328. }
  329. }
  330. /**
  331. * Action form submit function.
  332. *
  333. * Fills each constructed entity with property and field values, then
  334. * passes them to views_bulk_operations_modify_action().
  335. */
  336. function views_bulk_operations_modify_action_submit($form, $form_state) {
  337. foreach ($form_state['entities'] as $bundle_name => $entity) {
  338. field_attach_submit($form_state['entity_type'], $entity, $form['bundle_' . $bundle_name], $form_state);
  339. }
  340. return array(
  341. 'append' => $form_state['append'],
  342. 'selected' => $form_state['selected'],
  343. 'entities' => $form_state['entities'],
  344. 'properties' => isset($form_state['values']['properties']) ? $form_state['values']['properties'] : array(),
  345. );
  346. }
  347. /**
  348. * Returns all properties that can be modified.
  349. *
  350. * Properties that can't be changed are entity keys, timestamps, and the ones
  351. * without a setter callback.
  352. *
  353. * @param $entity_type
  354. * The entity type whose properties will be fetched.
  355. * @param $display_values
  356. * An optional, admin-provided list of properties and fields that should be
  357. * displayed for editing, used to filter the returned list of properties.
  358. */
  359. function _views_bulk_operations_modify_action_get_properties($entity_type, $display_values = NULL) {
  360. $properties = array();
  361. $info = entity_get_info($entity_type);
  362. // List of properties that can't be modified.
  363. $disabled_properties = array('created', 'changed');
  364. foreach (array('id', 'bundle', 'revision') as $key) {
  365. if (!empty($info['entity keys'][$key])) {
  366. $disabled_properties[] = $info['entity keys'][$key];
  367. }
  368. }
  369. // List of supported types.
  370. $supported_types = array('text', 'token', 'integer', 'decimal', 'date', 'duration',
  371. 'boolean', 'uri', 'list');
  372. $property_info = entity_get_property_info($entity_type);
  373. foreach ($property_info['properties'] as $key => $property) {
  374. if (in_array($key, $disabled_properties)) {
  375. continue;
  376. }
  377. // Filter out properties that can't be set (they are usually generated by a
  378. // getter callback based on other properties, and not stored in the DB).
  379. if (empty($property['setter callback'])) {
  380. continue;
  381. }
  382. // Determine the property type. If it's empty (permitted), default to text.
  383. // If it's a list type such as list<boolean>, extract the "boolean" part.
  384. $property['type'] = empty($property['type']) ? 'text' : $property['type'];
  385. $type = $property['type'];
  386. if ($list_type = entity_property_list_extract_type($type)) {
  387. $type = $list_type;
  388. $property['type'] = 'list';
  389. }
  390. // Filter out non-supported types (such as the Field API fields that
  391. // Commerce adds to its entities so that they show up in tokens).
  392. if (!in_array($type, $supported_types)) {
  393. continue;
  394. }
  395. $properties[$key] = $property;
  396. }
  397. if (isset($display_values) && empty($display_values[VBO_MODIFY_ACTION_ALL])) {
  398. // Return only the properties that the admin specified.
  399. return array_intersect_key($properties, $display_values);
  400. }
  401. return $properties;
  402. }
  403. /**
  404. * Returns all bundles for which field widgets should be displayed.
  405. *
  406. * If the admin decided to limit the modify form to certain properties / fields
  407. * (through the action settings) then only bundles that have at least one field
  408. * selected are returned.
  409. *
  410. * @param $entity_type
  411. * The entity type whose bundles will be fetched.
  412. * @param $context
  413. * The VBO context variable.
  414. */
  415. function _views_bulk_operations_modify_action_get_bundles($entity_type, $context) {
  416. $bundles = array();
  417. $view = $context['view'];
  418. $display_values = $context['settings']['display_values'];
  419. $info = entity_get_info($entity_type);
  420. $bundle_key = $info['entity keys']['bundle'];
  421. // Check if this View has a filter on the bundle key and assemble a list
  422. // of allowed bundles according to the filter.
  423. $filtered_bundles = array();
  424. if (!empty($bundle_key) && isset($view->filter[$bundle_key]) && !empty($view->filter[$bundle_key]->value)) {
  425. $operator = $view->filter[$bundle_key]->operator;
  426. if ($operator == 'in') {
  427. $filtered_bundles = $view->filter[$bundle_key]->value;
  428. }
  429. elseif ($operator == 'not in') {
  430. $bundle_names = array_keys($info['bundles']);
  431. $filtered_bundles = array_diff($bundle_names, $view->filter[$bundle_key]->value);
  432. }
  433. }
  434. foreach ($info['bundles'] as $bundle_name => $bundle) {
  435. // The view is limited to specific bundles, but this bundle isn't one of
  436. // them. Ignore it.
  437. if (!empty($filtered_bundles) && !in_array($bundle_name, $filtered_bundles)) {
  438. continue;
  439. }
  440. $instances = field_info_instances($entity_type, $bundle_name);
  441. // Ignore bundles that don't have any field instances attached.
  442. if (empty($instances)) {
  443. continue;
  444. }
  445. $has_enabled_fields = FALSE;
  446. foreach ($display_values as $key) {
  447. if (strpos($key, $bundle_name . '::') !== FALSE) {
  448. $has_enabled_fields = TRUE;
  449. }
  450. }
  451. // The admin has either specified that all values should be modifiable, or
  452. // selected at least one field belonging to this bundle.
  453. if (!empty($display_values[VBO_MODIFY_ACTION_ALL]) || $has_enabled_fields) {
  454. $bundles[$bundle_name] = $bundle;
  455. }
  456. }
  457. return $bundles;
  458. }
  459. /**
  460. * Helper function that recursively strips #required from field widgets.
  461. */
  462. function _views_bulk_operations_modify_action_unset_required(&$element) {
  463. unset($element['#required']);
  464. foreach (element_children($element) as $key) {
  465. _views_bulk_operations_modify_action_unset_required($element[$key]);
  466. }
  467. }
  468. /**
  469. * VBO settings form function.
  470. */
  471. function views_bulk_operations_modify_action_views_bulk_operations_form_options() {
  472. $options['show_all_tokens'] = TRUE;
  473. $options['display_values'] = array(VBO_MODIFY_ACTION_ALL);
  474. return $options;
  475. }
  476. /**
  477. * The settings form for this action.
  478. */
  479. function views_bulk_operations_modify_action_views_bulk_operations_form($options, $entity_type, $dom_id) {
  480. // Initialize default values.
  481. if (empty($options)) {
  482. $options = views_bulk_operations_modify_action_views_bulk_operations_form_options();
  483. }
  484. $form['show_all_tokens'] = array(
  485. '#type' => 'checkbox',
  486. '#title' => t('Show available tokens'),
  487. '#description' => t('Check this to show a list of all available tokens in the bottom of the form. Requires the token module.'),
  488. '#default_value' => $options['show_all_tokens'],
  489. );
  490. $info = entity_get_info($entity_type);
  491. $properties = _views_bulk_operations_modify_action_get_properties($entity_type);
  492. $values = array(VBO_MODIFY_ACTION_ALL => t('- All -'));
  493. foreach ($properties as $key => $property) {
  494. $label = t('Properties');
  495. $values[$label][$key] = $property['label'];
  496. }
  497. foreach ($info['bundles'] as $bundle_name => $bundle) {
  498. $bundle_key = $info['entity keys']['bundle'];
  499. // Show the more detailed label only if the entity type has multiple bundles.
  500. // Otherwise, it would just be confusing.
  501. if (count($info['bundles']) > 1) {
  502. $label = t('Fields for @bundle_key @label', array('@bundle_key' => $bundle_key, '@label' => $bundle['label']));
  503. }
  504. else {
  505. $label = t('Fields');
  506. }
  507. $instances = field_info_instances($entity_type, $bundle_name);
  508. foreach ($instances as $field_name => $field) {
  509. $values[$label][$bundle_name . '::' . $field_name] = $field['label'];
  510. }
  511. }
  512. $form['display_values'] = array(
  513. '#type' => 'select',
  514. '#title' => t('Display values'),
  515. '#options' => $values,
  516. '#multiple' => TRUE,
  517. '#description' => t('Select which values the action form should present to the user.'),
  518. '#default_value' => $options['display_values'],
  519. );
  520. return $form;
  521. }