Movement quick form (ported from farmOS 1.x) #677
This commit is contained in:
parent
47ac9ae5dd
commit
8c0de9c349
|
@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- [Add QuickStringTrait::entityLabelsSummary() method for summarizing entity labels #675](https://github.com/farmOS/farmOS/pull/675)
|
||||
- [Add asset inventory views field #679](https://github.com/farmOS/farmOS/pull/679)
|
||||
- [Birth quick form (ported from farmOS 1.x) #656](https://github.com/farmOS/farmOS/pull/656)
|
||||
- [Movement quick form (ported from farmOS 1.x) #677](https://github.com/farmOS/farmOS/pull/677)
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
enforced:
|
||||
module:
|
||||
- farm_quick_movement
|
||||
id: quick_movement
|
||||
label: Quick movement
|
||||
description: 'Refreshes map from movement quick form inputs.'
|
||||
library: 'farm_quick_movement/behavior_quick_movement'
|
||||
settings: { }
|
|
@ -0,0 +1,8 @@
|
|||
name: Movement Quick Form
|
||||
description: Provides a quick form for recording asset movements.
|
||||
type: module
|
||||
package: farmOS Quick Forms
|
||||
core_version_requirement: ^9
|
||||
dependencies:
|
||||
- farm:farm_activity
|
||||
- farm:farm_quick
|
|
@ -0,0 +1,8 @@
|
|||
quick_movement:
|
||||
js:
|
||||
js/quick_movement.js: { }
|
||||
behavior_quick_movement:
|
||||
js:
|
||||
js/farmOS.map.behaviors.quick_movement.js: { }
|
||||
dependencies:
|
||||
- farm_map/farm_map
|
|
@ -0,0 +1,29 @@
|
|||
(function () {
|
||||
farmOS.map.behaviors.quick_movement = {
|
||||
attach: function (instance) {
|
||||
|
||||
// Create a layer for the current asset location.
|
||||
var opts = {
|
||||
title: 'Current Location',
|
||||
color: 'blue',
|
||||
};
|
||||
instance.currentLocationLayer = instance.addLayer('vector', opts);
|
||||
},
|
||||
|
||||
// When updating asset geometry, update the current location layer.
|
||||
updateAssetGeometry: function (instance, wkt) {
|
||||
|
||||
// Clear features from the layer.
|
||||
instance.currentLocationLayer.getSource().clear();
|
||||
|
||||
// If WKT is not empty, add features to the layer and zoom.
|
||||
if (wkt) {
|
||||
instance.currentLocationLayer.getSource().addFeatures(instance.readFeatures('wkt', wkt));
|
||||
instance.zoomToLayer(instance.currentLocationLayer);
|
||||
}
|
||||
},
|
||||
|
||||
// Make sure this runs after farmOS.map.behaviors.wkt.
|
||||
weight: 101,
|
||||
};
|
||||
}());
|
|
@ -0,0 +1,32 @@
|
|||
(function (Drupal) {
|
||||
Drupal.behaviors.quick_movement = {
|
||||
attach: function (context, settings) {
|
||||
|
||||
// Only run this when the asset geometry or location geometry wrappers
|
||||
// are loaded/reloaded.
|
||||
if (!context.dataset || !(context.dataset.movementGeometry === 'asset-geometry' || context.dataset.movementGeometry === 'location-geometry')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get WKT from the hidden input field.
|
||||
var wkt = context.querySelector('input').value;
|
||||
|
||||
// Get the farmOS-map element and instance.
|
||||
var element = context.parentElement.querySelector('[data-drupal-selector="edit-geometry-map"]');
|
||||
var instance = farmOS.map.instances[farmOS.map.targetIndex(element)];
|
||||
|
||||
// If this is asset geometry, refresh the map asset geometry.
|
||||
if (context.dataset.movementGeometry === 'asset-geometry') {
|
||||
farmOS.map.behaviors.quick_movement.updateAssetGeometry(instance, wkt);
|
||||
}
|
||||
|
||||
// If this is location geometry, copy WKT into the map's value field and
|
||||
// dispatch the input event so that the input behavior refreshes the map.
|
||||
if (context.dataset.movementGeometry === 'location-geometry') {
|
||||
var input = context.parentElement.querySelector('[data-drupal-selector="edit-geometry-value"]');
|
||||
input.value = wkt;
|
||||
input.dispatchEvent(new Event('input'));
|
||||
}
|
||||
}
|
||||
};
|
||||
}(Drupal));
|
|
@ -0,0 +1,326 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\farm_quick_movement\Plugin\QuickForm;
|
||||
|
||||
use Drupal\Core\Datetime\DrupalDateTime;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\farm_geo\Traits\WktTrait;
|
||||
use Drupal\farm_location\AssetLocationInterface;
|
||||
use Drupal\farm_quick\Plugin\QuickForm\QuickFormBase;
|
||||
use Drupal\farm_quick\Plugin\QuickForm\QuickFormInterface;
|
||||
use Drupal\farm_quick\Traits\QuickFormElementsTrait;
|
||||
use Drupal\farm_quick\Traits\QuickLogTrait;
|
||||
use Drupal\farm_quick\Traits\QuickStringTrait;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Movement quick form.
|
||||
*
|
||||
* @QuickForm(
|
||||
* id = "movement",
|
||||
* label = @Translation("Movement"),
|
||||
* description = @Translation("Record the movement of assets."),
|
||||
* helpText = @Translation("Use this form to record the movement of assets to a new location."),
|
||||
* permissions = {
|
||||
* "create activity log",
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class Movement extends QuickFormBase implements QuickFormInterface {
|
||||
|
||||
use QuickLogTrait;
|
||||
use QuickFormElementsTrait;
|
||||
use QuickStringTrait;
|
||||
use WktTrait;
|
||||
|
||||
/**
|
||||
* The entity type manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Asset location service.
|
||||
*
|
||||
* @var \Drupal\farm_location\AssetLocationInterface
|
||||
*/
|
||||
protected $assetLocation;
|
||||
|
||||
/**
|
||||
* Current user object.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $currentUser;
|
||||
|
||||
/**
|
||||
* Constructs a QuickFormBase object.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin_id for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
|
||||
* The messenger service.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager service.
|
||||
* @param \Drupal\farm_location\AssetLocationInterface $asset_location
|
||||
* Asset location service.
|
||||
* @param \Drupal\Core\Session\AccountInterface $current_user
|
||||
* Current user object.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, MessengerInterface $messenger, EntityTypeManagerInterface $entity_type_manager, AssetLocationInterface $asset_location, AccountInterface $current_user) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition, $messenger);
|
||||
$this->messenger = $messenger;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->assetLocation = $asset_location;
|
||||
$this->currentUser = $current_user;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('messenger'),
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('asset.location'),
|
||||
$container->get('current_user'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state, string $id = NULL) {
|
||||
|
||||
// Date.
|
||||
$form['date'] = [
|
||||
'#type' => 'datetime',
|
||||
'#title' => $this->t('Date'),
|
||||
'#default_value' => new DrupalDateTime('midnight', $this->currentUser->getTimeZone()),
|
||||
'#required' => TRUE,
|
||||
];
|
||||
|
||||
// Assets.
|
||||
$form['asset'] = [
|
||||
'#type' => 'entity_autocomplete',
|
||||
'#title' => $this->t('Assets'),
|
||||
'#description' => $this->t('Which assets are moving?'),
|
||||
'#target_type' => 'asset',
|
||||
'#selection_settings' => [
|
||||
'sort' => [
|
||||
'field' => 'status',
|
||||
'direction' => 'ASC',
|
||||
],
|
||||
],
|
||||
'#maxlength' => 1024,
|
||||
'#tags' => TRUE,
|
||||
'#required' => TRUE,
|
||||
'#ajax' => [
|
||||
'callback' => [$this, 'assetGeometryCallback'],
|
||||
'wrapper' => 'asset-geometry',
|
||||
'event' => 'autocompleteclose change',
|
||||
],
|
||||
];
|
||||
|
||||
// Locations.
|
||||
$form['location'] = [
|
||||
'#type' => 'entity_autocomplete',
|
||||
'#title' => $this->t('Locations'),
|
||||
'#description' => $this->t('Where are the assets moving to?'),
|
||||
'#target_type' => 'asset',
|
||||
'#selection_handler' => 'views',
|
||||
'#selection_settings' => [
|
||||
'view' => [
|
||||
'view_name' => 'farm_location_reference',
|
||||
'display_name' => 'entity_reference',
|
||||
'arguments' => [],
|
||||
],
|
||||
'match_operator' => 'CONTAINS',
|
||||
],
|
||||
'#maxlength' => 1024,
|
||||
'#tags' => TRUE,
|
||||
'#ajax' => [
|
||||
'callback' => [$this, 'locationGeometryCallback'],
|
||||
'wrapper' => 'location-geometry',
|
||||
'event' => 'autocompleteclose change',
|
||||
],
|
||||
];
|
||||
|
||||
// Geometry.
|
||||
$form['geometry'] = [
|
||||
'#type' => 'farm_map_input',
|
||||
'#title' => $this->t('Geometry'),
|
||||
'#description' => $this->t('The current geometry of the assets is blue. The new geometry is orange. It is copied from the locations selected above, and can be modified to give the assets a more specific geometry.'),
|
||||
'#behaviors' => [
|
||||
'quick_movement',
|
||||
],
|
||||
'#display_raw_geometry' => TRUE,
|
||||
];
|
||||
|
||||
// Hidden fields to store asset and location geometry.
|
||||
$form['asset_geometry_wrapper'] = [
|
||||
'#type' => 'container',
|
||||
'#attributes' => [
|
||||
'id' => 'asset-geometry',
|
||||
'data-movement-geometry' => 'asset-geometry',
|
||||
],
|
||||
'asset_geometry' => [
|
||||
'#type' => 'hidden',
|
||||
'#value' => $this->combinedAssetGeometries($this->loadEntityAutocompleteAssets($form_state->getValue('asset'))),
|
||||
],
|
||||
];
|
||||
$form['location_geometry_wrapper'] = [
|
||||
'#type' => 'container',
|
||||
'#attributes' => [
|
||||
'id' => 'location-geometry',
|
||||
'data-movement-geometry' => 'location-geometry',
|
||||
],
|
||||
'location_geometry' => [
|
||||
'#type' => 'hidden',
|
||||
'#value' => $this->combinedAssetGeometries($this->loadEntityAutocompleteAssets($form_state->getValue('location'))),
|
||||
],
|
||||
];
|
||||
|
||||
// Notes.
|
||||
$form['notes'] = [
|
||||
'#type' => 'details',
|
||||
'#title' => $this->t('Notes'),
|
||||
];
|
||||
$form['notes']['notes'] = [
|
||||
'#type' => 'text_format',
|
||||
'#title' => $this->t('Notes'),
|
||||
'#title_display' => 'invisible',
|
||||
'#format' => 'default',
|
||||
];
|
||||
|
||||
// Done.
|
||||
$form['done'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Completed'),
|
||||
'#default_value' => TRUE,
|
||||
];
|
||||
|
||||
// Attach movement quick form JS.
|
||||
$form['#attached']['library'][] = 'farm_quick_movement/quick_movement';
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajax callback for asset geometry field.
|
||||
*/
|
||||
public function assetGeometryCallback(array $form, FormStateInterface $form_state) {
|
||||
return $form['asset_geometry_wrapper'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajax callback for location geometry field.
|
||||
*/
|
||||
public function locationGeometryCallback(array $form, FormStateInterface $form_state) {
|
||||
return $form['location_geometry_wrapper'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load assets from entity_autocomplete values.
|
||||
*
|
||||
* @param array|null $values
|
||||
* The value from $form_state->getValue().
|
||||
*
|
||||
* @return \Drupal\asset\Entity\AssetInterface[]
|
||||
* Returns an array of assets.
|
||||
*/
|
||||
protected function loadEntityAutocompleteAssets($values) {
|
||||
$entities = [];
|
||||
if (empty($values)) {
|
||||
return $entities;
|
||||
}
|
||||
foreach ($values as $value) {
|
||||
if ($value instanceof EntityInterface) {
|
||||
$entities[] = $value;
|
||||
}
|
||||
elseif (!empty($value['target_id'])) {
|
||||
$entities[] = $this->entityTypeManager->getStorage('asset')->load($value['target_id']);
|
||||
}
|
||||
}
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load combined WKT geometry of assets.
|
||||
*
|
||||
* @param array $assets
|
||||
* An array of assets.
|
||||
*
|
||||
* @return string
|
||||
* Returns a WKT geometry string.
|
||||
*/
|
||||
protected function combinedAssetGeometries(array $assets) {
|
||||
if (empty($assets)) {
|
||||
return '';
|
||||
}
|
||||
$geometries = [];
|
||||
foreach ($assets as $asset) {
|
||||
$geometries[] = $this->assetLocation->getGeometry($asset);
|
||||
}
|
||||
return $this->combineWkt($geometries);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateForm(array &$form, FormStateInterface $form_state) {
|
||||
|
||||
// Validate that a geometry is only present if a location is specified.
|
||||
if (empty($form_state->getValue('location')) && !empty($form_state->getValue('geometry'))) {
|
||||
$form_state->setError($form['geometry'], $this->t('A geometry cannot be set if there is no location.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
|
||||
// Draft a movement activity log from the user-submitted data.
|
||||
$timestamp = $form_state->getValue('date')->getTimestamp();
|
||||
$status = $form_state->getValue('done') ? 'done' : 'pending';
|
||||
$log = [
|
||||
'type' => 'activity',
|
||||
'timestamp' => $timestamp,
|
||||
'asset' => $form_state->getValue('asset'),
|
||||
'location' => $form_state->getValue('location'),
|
||||
'geometry' => $form_state->getValue('geometry'),
|
||||
'notes' => $form_state->getValue('notes'),
|
||||
'status' => $status,
|
||||
'is_movement' => TRUE,
|
||||
];
|
||||
|
||||
// Load assets and locations.
|
||||
$assets = $this->loadEntityAutocompleteAssets($form_state->getValue('asset'));
|
||||
$locations = $this->loadEntityAutocompleteAssets($form_state->getValue('location'));
|
||||
|
||||
// Generate a name for the log.
|
||||
$asset_names = $this->entityLabelsSummary($assets);
|
||||
$location_names = $this->entityLabelsSummary($locations);
|
||||
$log['name'] = $this->t('Clear location of @assets', ['@assets' => $asset_names]);
|
||||
if (!empty($location_names)) {
|
||||
$log['name'] = $this->t('Move @assets to @locations', ['@assets' => $asset_names, '@locations' => $location_names]);
|
||||
}
|
||||
|
||||
// Create the log.
|
||||
$this->createLog($log);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\farm_quick_movement\Kernel;
|
||||
|
||||
use Drupal\asset\Entity\Asset;
|
||||
use Drupal\Core\Datetime\DrupalDateTime;
|
||||
use Drupal\Tests\farm_quick\Kernel\QuickFormTestBase;
|
||||
|
||||
/**
|
||||
* Tests for farmOS movement quick form.
|
||||
*
|
||||
* @group farm
|
||||
*/
|
||||
class QuickMovementTest extends QuickFormTestBase {
|
||||
|
||||
/**
|
||||
* Quick form ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $quickFormId = 'movement';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'farm_equipment',
|
||||
'farm_activity',
|
||||
'farm_land',
|
||||
'farm_quick_movement',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->installConfig([
|
||||
'farm_activity',
|
||||
'farm_equipment',
|
||||
'farm_land',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test movement quick form submission.
|
||||
*/
|
||||
public function testQuickMovement() {
|
||||
|
||||
// Get today's date.
|
||||
$today = new DrupalDateTime('midnight');
|
||||
|
||||
// Create two equipment assets and two land assets.
|
||||
$equipment1 = Asset::create([
|
||||
'name' => 'Tractor',
|
||||
'type' => 'equipment',
|
||||
'status' => 'active',
|
||||
]);
|
||||
$equipment1->save();
|
||||
$equipment2 = Asset::create([
|
||||
'name' => 'Combine',
|
||||
'type' => 'equipment',
|
||||
'status' => 'active',
|
||||
]);
|
||||
$equipment2->save();
|
||||
$location1 = Asset::create([
|
||||
'name' => 'Field A',
|
||||
'type' => 'land',
|
||||
'land_type' => 'field',
|
||||
'is_fixed' => TRUE,
|
||||
'is_location' => TRUE,
|
||||
'intrinsic_geometry' => 'POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))',
|
||||
'status' => 'active',
|
||||
]);
|
||||
$location1->save();
|
||||
$location2 = Asset::create([
|
||||
'name' => 'Field B',
|
||||
'type' => 'land',
|
||||
'land_type' => 'field',
|
||||
'is_fixed' => TRUE,
|
||||
'is_location' => TRUE,
|
||||
'intrinsic_geometry' => 'POLYGON ((20 40, 40 80, 60 60, 10 20, 20 40))',
|
||||
'status' => 'active',
|
||||
]);
|
||||
$location2->save();
|
||||
|
||||
// Programmatically submit the movement quick form.
|
||||
$form_values = [
|
||||
'date' => [
|
||||
'date' => $today->format('Y-m-d'),
|
||||
'time' => $today->format('H:i:s'),
|
||||
],
|
||||
'asset' => [
|
||||
['target_id' => $equipment1->id()],
|
||||
['target_id' => $equipment2->id()],
|
||||
],
|
||||
'location' => [
|
||||
['target_id' => $location1->id()],
|
||||
['target_id' => $location2->id()],
|
||||
],
|
||||
'notes' => [
|
||||
'value' => 'Lorem ipsum',
|
||||
'format' => 'default',
|
||||
],
|
||||
'done' => TRUE,
|
||||
];
|
||||
$this->submitQuickForm($form_values);
|
||||
|
||||
// Load logs.
|
||||
$logs = $this->logStorage->loadMultiple();
|
||||
|
||||
// Confirm that one log exists.
|
||||
$this->assertCount(1, $logs);
|
||||
|
||||
// Check that the activity log's fields were populated correctly.
|
||||
$log = $logs[1];
|
||||
$this->assertEquals('activity', $log->bundle());
|
||||
$this->assertEquals($today->getTimestamp(), $log->get('timestamp')->value);
|
||||
$this->assertEquals('Move Tractor, Combine to Field A, Field B', $log->label());
|
||||
$this->assertEquals($equipment1->id(), $log->get('asset')->referencedEntities()[0]->id());
|
||||
$this->assertEquals($equipment2->id(), $log->get('asset')->referencedEntities()[1]->id());
|
||||
$this->assertEquals($location1->id(), $log->get('location')->referencedEntities()[0]->id());
|
||||
$this->assertEquals($location2->id(), $log->get('location')->referencedEntities()[1]->id());
|
||||
$this->assertEquals('Lorem ipsum', $log->get('notes')->value);
|
||||
$this->assertEquals('GEOMETRYCOLLECTION (POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10)),POLYGON ((20 40, 40 80, 60 60, 10 20, 20 40)))', $log->get('geometry')->value);
|
||||
$this->assertEquals('done', $log->get('status')->value);
|
||||
|
||||
// Programmatically submit the movement quick form again, but this time
|
||||
// override the geometry.
|
||||
$form_values['geometry']['value'] = 'POINT (30 10)';
|
||||
$this->submitQuickForm($form_values);
|
||||
|
||||
// Load logs.
|
||||
$logs = $this->logStorage->loadMultiple();
|
||||
|
||||
// Confirm that two logs exist.
|
||||
$this->assertCount(2, $logs);
|
||||
|
||||
// Check that the geometry was overridden.
|
||||
$log = $logs[2];
|
||||
$this->assertEquals($form_values['geometry']['value'], $log->get('geometry')->value);
|
||||
|
||||
// Programmatically submit the movement quick form again, but this time
|
||||
// remove the location without removing geometry. This should fail
|
||||
// validation.
|
||||
$form_values['location'] = NULL;
|
||||
$this->submitQuickForm($form_values);
|
||||
|
||||
// Load logs.
|
||||
$logs = $this->logStorage->loadMultiple();
|
||||
|
||||
// Confirm that only two logs still exist.
|
||||
$this->assertCount(2, $logs);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue