clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

View File

@@ -0,0 +1,713 @@
langcode: en
status: true
dependencies:
module:
- asset
- farm_location
- geofield
- rest
- user
- views_geojson
enforced:
module:
- farm_location
id: farm_asset_geojson
label: 'Farm Asset GeoJSON'
module: views
description: ''
tag: ''
base_table: asset_field_data
base_field: id
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'view any asset'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous:
next:
pagination_heading_level: h4
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
id:
id: id
table: asset_field_data
field: id
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: number_integer
settings:
thousand_separator: ''
prefix_suffix: true
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: asset
entity_field: id
plugin_id: field
name:
table: asset_field_data
field: name
id: name
entity_type: null
entity_field: name
plugin_id: field
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: string
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
geometry:
id: geometry
table: asset
field: geometry
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: geofield_default
settings:
output_format: wkt
output_escape: true
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: asset
plugin_id: asset_geometry
view_asset:
id: view_asset
table: asset
field: view_asset
relationship: none
group_type: group
admin_label: ''
label: url
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
text: view
output_url_as_text: true
absolute: false
entity_type: asset
plugin_id: entity_link
filters: { }
sorts: { }
header: { }
footer: { }
empty: { }
relationships: { }
arguments:
type:
id: type
table: asset_field_data
field: type
relationship: none
group_type: group
admin_label: ''
default_action: default
exception:
value: all
title_enable: false
title: All
title_enable: false
title: ''
default_argument_type: fixed
default_argument_options:
argument: all
summary_options:
base_path: ''
count: true
items_per_page: 25
override: false
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: true
validate:
type: 'entity:asset_type'
fail: 'not found'
validate_options:
operation: view
multiple: 1
access: false
bundles: { }
glossary: false
limit: 0
case: none
path_case: none
transform_dash: false
break_phrase: true
entity_type: asset
entity_field: type
plugin_id: string
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- user.permissions
tags: { }
centroid:
display_plugin: geojson_export
id: centroid
display_title: 'Centroid (GeoJSON export)'
position: 2
display_options:
display_extenders: { }
display_description: ''
path: assets/geojson/centroid/%
pager:
type: none
options:
offset: 0
style:
type: geojson
options:
data_source:
value: wkt
latitude: geometry
longitude: geometry
geofield: geometry
geolocation: id
wkt: geometry
name_field: name
description_field: ''
jsonp_prefix: ''
fields:
id:
id: id
table: asset_field_data
field: id
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: number_integer
settings:
thousand_separator: ''
prefix_suffix: true
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: asset
entity_field: id
plugin_id: field
name:
table: asset_field_data
field: name
id: name
entity_type: null
entity_field: name
plugin_id: field
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: string
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
geometry:
id: geometry
table: asset
field: geometry
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: geofield_latlon
settings:
output_format: wkt
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: asset
plugin_id: asset_geometry
view_asset:
id: view_asset
table: asset
field: view_asset
relationship: none
group_type: group
admin_label: ''
label: url
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
text: view
output_url_as_text: true
absolute: false
entity_type: asset
plugin_id: entity_link
defaults:
fields: false
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- user.permissions
tags: { }
full:
display_plugin: geojson_export
id: full
display_title: 'Full (GeoJSON export)'
position: 1
display_options:
display_extenders: { }
display_description: ''
path: assets/geojson/full/%
pager:
type: none
options:
offset: 0
style:
type: geojson
options:
data_source:
value: geofield
latitude: id
longitude: id
geofield: geometry
geolocation: id
wkt: id
name_field: name
description_field: ''
jsonp_prefix: ''
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- user.permissions
tags: { }

View File

@@ -0,0 +1,239 @@
langcode: en
status: true
dependencies:
module:
- asset
enforced:
module:
- farm_location
id: farm_location_reference
label: 'Farm Location Reference'
module: views
description: ''
tag: ''
base_table: asset_field_data
base_field: id
display:
default:
id: default
display_title: Master
display_plugin: default
position: 0
display_options:
fields:
name:
id: name
table: asset_field_data
field: name
relationship: none
group_type: group
admin_label: ''
entity_type: null
entity_field: name
plugin_id: field
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: string
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
pager:
type: mini
options:
offset: 0
items_per_page: 10
total_pages: null
id: 0
tags:
next:
previous:
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
pagination_heading_level: h4
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
access:
type: none
options: { }
cache:
type: tag
options: { }
empty: { }
sorts:
name:
id: name
table: asset_field_data
field: name
relationship: none
group_type: group
admin_label: ''
entity_type: asset
entity_field: name
plugin_id: standard
order: ASC
expose:
label: ''
field_identifier: name
exposed: false
arguments: { }
filters:
is_location:
id: is_location
table: asset_field_data
field: is_location
relationship: none
group_type: group
admin_label: ''
entity_type: asset
entity_field: is_location
plugin_id: boolean
operator: '='
value: '1'
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
operator_limit_selection: false
operator_list: { }
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
default_field_elements: true
inline: { }
separator: ''
hide_empty: false
query:
type: views_query
options:
query_comment: ''
disable_sql_rewrite: false
distinct: false
replica: false
query_tags: { }
relationships: { }
header: { }
footer: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
tags: { }
entity_reference:
id: entity_reference
display_title: 'Entity Reference'
display_plugin: entity_reference
position: 1
display_options:
style:
type: entity_reference
options:
search_fields:
name: name
row:
type: entity_reference
options:
default_field_elements: false
inline: { }
separator: '-'
hide_empty: false
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
tags: { }

View File

@@ -0,0 +1,20 @@
# Schema for the asset location third party settings.
asset.type.*.third_party.farm_location:
type: mapping
label: 'Location settings'
mapping:
is_location:
type: boolean
label: 'Is a location'
is_fixed:
type: boolean
label: 'Is fixed'
# Schema for the log location third party settings.
log.type.*.third_party.farm_location:
type: mapping
label: 'Location settings'
mapping:
is_movement:
type: boolean
label: 'Is a movement'

View File

@@ -0,0 +1,7 @@
# Schema for the asset location/geometry views field handlers.
views.field.asset_location:
type: views.field.field
label: 'Asset location field handler'
views.field.asset_geometry:
type: views.field.field
label: 'Asset geometry field handler'

View File

@@ -0,0 +1,184 @@
<?php
/**
* @file
* Code for creating farmOS entity location base field definitions.
*/
use Drupal\farm_location\Field\AssetGeometryItemList;
use Drupal\farm_location\Field\AssetLocationItemList;
/**
* Define asset location base fields.
*/
function farm_location_asset_base_fields() {
$fields = [];
// Current location field.
// This is computed based on an asset's movements.
$options = [
'type' => 'entity_reference',
'label' => t('Current location'),
'target_type' => 'asset',
'multiple' => TRUE,
'computed' => AssetLocationItemList::class,
'hidden' => 'form',
'view_display_options' => [
'label' => 'inline',
'type' => 'asset_current_location',
'settings' => [
'link' => TRUE,
'render_without_location' => TRUE,
],
'weight' => 50,
],
];
$fields['location'] = \Drupal::service('farm_field.factory')->baseFieldDefinition($options);
// Current geometry field.
// This is computed based on an asset's movements or its intrinsic geometry.
$options = [
'type' => 'geofield',
'label' => t('Current geometry'),
'computed' => AssetGeometryItemList::class,
'hidden' => 'form',
'weight' => [
'view' => 40,
],
];
$fields['geometry'] = \Drupal::service('farm_field.factory')->baseFieldDefinition($options);
// Intrinsic geometry field.
// This is added as a bundle field definition to all asset types rather than
// a base field definition so that data is stored in a dedicated database
// table.
$options = [
'type' => 'geofield',
'label' => t('Intrinsic geometry'),
'description' => t('Add geometry data to this asset to describe its intrinsic location. This will only be used if the asset is fixed.'),
'weight' => [
'form' => 50,
],
'hidden' => 'view',
'populate_file_field' => 'file',
];
$fields['intrinsic_geometry'] = \Drupal::service('farm_field.factory')->bundleFieldDefinition($options);
// Location boolean field.
$options = [
'type' => 'boolean',
'label' => t('Is location'),
'description' => t('If this asset is a location, then other assets can be moved to it.'),
'default_value_callback' => 'farm_location_is_location_default_value',
'weight' => [
'form' => 0,
],
'view_display_options' => [
'label' => 'inline',
'type' => 'hideable_boolean',
'settings' => [
'format' => 'default',
'format_custom_false' => '',
'format_custom_true' => '',
'hide_if_false' => TRUE,
],
'weight' => 0,
],
];
$fields['is_location'] = \Drupal::service('farm_field.factory')->baseFieldDefinition($options);
// Fixed boolean field.
$options = [
'type' => 'boolean',
'label' => t('Is fixed'),
'description' => t('If this asset is fixed, then it can have an intrinsic geometry. If the asset will move around, then it is not fixed and geometry will be determined by movement logs.'),
'default_value_callback' => 'farm_location_is_fixed_default_value',
'weight' => [
'form' => 10,
],
'view_display_options' => [
'label' => 'inline',
'type' => 'hideable_boolean',
'settings' => [
'format' => 'default',
'format_custom_false' => '',
'format_custom_true' => '',
'hide_if_false' => TRUE,
],
'weight' => 10,
],
];
$fields['is_fixed'] = \Drupal::service('farm_field.factory')->baseFieldDefinition($options);
return $fields;
}
/**
* Define log location base fields.
*/
function farm_location_log_base_fields() {
$fields = [];
// Location asset reference field.
$options = [
'type' => 'entity_reference',
'label' => t('Location'),
'description' => t('Where does this take place?'),
'target_type' => 'asset',
'multiple' => TRUE,
'weight' => [
'form' => 20,
'view' => 20,
],
];
$field = \Drupal::service('farm_field.factory')->baseFieldDefinition($options);
$field->setSetting('handler', 'views');
$field->setSetting('handler_settings', [
'view' => [
'view_name' => 'farm_location_reference',
'display_name' => 'entity_reference',
],
]);
$fields['location'] = $field;
// Geometry field.
// This is added as a bundle field definition to all log types rather than
// a base field definition so that data is stored in a dedicated database
// table.
$options = [
'type' => 'geofield',
'label' => t('Geometry'),
'description' => t('Add geometry data to this log to describe where it took place.'),
'weight' => [
'form' => 20,
'view' => 20,
],
'populate_file_field' => 'file',
];
$fields['geometry'] = \Drupal::service('farm_field.factory')->bundleFieldDefinition($options);
// Movement boolean field.
$options = [
'type' => 'boolean',
'label' => t('Is movement'),
'description' => t('If this log is a movement, then all assets referenced by it will be located in the referenced locations and/or geometry at the time the log takes place. The log must be complete in order for the movement to take effect.'),
'default_value_callback' => 'farm_location_is_movement_default_value',
'weight' => [
'form' => 20,
],
'view_display_options' => [
'label' => 'inline',
'type' => 'hideable_boolean',
'settings' => [
'format' => 'default',
'format_custom_false' => '',
'format_custom_true' => '',
'hide_if_false' => TRUE,
],
'weight' => 20,
],
];
$fields['is_movement'] = \Drupal::service('farm_field.factory')->baseFieldDefinition($options);
return $fields;
}

View File

@@ -0,0 +1,14 @@
name: farmOS Location
description: Provides movement/location tracking for assets via logs.
type: module
package: farmOS
core_version_requirement: ^10
dependencies:
- drupal:views
- farm:asset
- farm:farm_field
- farm:farm_geo
- farm:farm_log
- geofield:geofield
- log:log
- views_geojson:views_geojson

View File

@@ -0,0 +1,147 @@
<?php
/**
* @file
* Contains farm_location.module.
*/
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Implements hook_form_BASE_FORM_ID_alter().
*/
function farm_location_form_asset_form_alter(&$form, FormStateInterface $form_state, $form_id) {
// Check if the form has the required location fields.
if (isset($form['intrinsic_geometry']) && isset($form['is_fixed'])) {
// Set the visible state of the asset.intrinsic_geometry field.
// Only display if is_fixed is checked.
$form['intrinsic_geometry']['#states']['visible'] = [':input[name="is_fixed[value]"]' => ['checked' => TRUE]];
}
}
/**
* Implements hook_entity_base_field_info().
*/
function farm_location_entity_base_field_info(EntityTypeInterface $entity_type) {
\Drupal::moduleHandler()->loadInclude('farm_location', 'inc', 'farm_location.base_fields');
switch ($entity_type->id()) {
// Build asset base fields.
case 'asset':
return farm_location_asset_base_fields();
// Build log base fields.
case 'log':
return farm_location_log_base_fields();
default:
return [];
}
}
/**
* Implements hook_entity_base_field_info_alter().
*/
function farm_location_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) {
/** @var \Drupal\field\Entity\FieldConfig[] $fields */
// Prevent creating circular asset location.
if ($entity_type->id() == 'log' && !empty($fields['asset'])) {
$fields['asset']->addConstraint('CircularAssetLocation');
}
}
/**
* Sets the default value for asset is_location boolean field.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity being created.
* @param \Drupal\Core\Field\FieldDefinitionInterface $definition
* The field definition.
*
* @return array
* An array of default value keys with each entry keyed with the “value” key.
*
* @see \Drupal\Core\Field\FieldConfigBase::getDefaultValue()
*/
function farm_location_is_location_default_value(ContentEntityInterface $entity, FieldDefinitionInterface $definition): array {
$default = FALSE;
// Load the entity bundle.
$bundle = \Drupal::service('entity_type.manager')->getStorage('asset_type')->load($entity->bundle());
// Use the bundle's is_location third-party setting as a default.
$is_location = $bundle->getThirdPartySetting('farm_location', 'is_location');
if (!empty($is_location)) {
$default = TRUE;
}
return [
['value' => $default],
];
}
/**
* Sets the default value for asset is_fixed boolean field.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity being created.
* @param \Drupal\Core\Field\FieldDefinitionInterface $definition
* The field definition.
*
* @return array
* An array of default value keys with each entry keyed with the “value” key.
*
* @see \Drupal\Core\Field\FieldConfigBase::getDefaultValue()
*/
function farm_location_is_fixed_default_value(ContentEntityInterface $entity, FieldDefinitionInterface $definition): array {
$default = FALSE;
// Load the entity bundle.
$bundle = \Drupal::service('entity_type.manager')->getStorage('asset_type')->load($entity->bundle());
// Use the bundle's is_fixed third-party setting as a default.
$is_fixed = $bundle->getThirdPartySetting('farm_location', 'is_fixed');
if (!empty($is_fixed)) {
$default = TRUE;
}
return [
['value' => $default],
];
}
/**
* Sets the default value for log movement boolean field.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity being created.
* @param \Drupal\Core\Field\FieldDefinitionInterface $definition
* The field definition.
*
* @return array
* An array of default value keys with each entry keyed with the “value” key.
*
* @see \Drupal\Core\Field\FieldConfigBase::getDefaultValue()
*/
function farm_location_is_movement_default_value(ContentEntityInterface $entity, FieldDefinitionInterface $definition): array {
$default = FALSE;
// Load the entity bundle.
$bundle = \Drupal::service('entity_type.manager')->getStorage('log_type')->load($entity->bundle());
// Use the bundle's is_movement third-party setting as a default.
$is_movement = $bundle->getThirdPartySetting('farm_location', 'is_movement');
if (!empty($is_movement)) {
$default = TRUE;
}
return [
['value' => $default],
];
}

View File

@@ -0,0 +1,18 @@
<?php
/**
* @file
* Post update hooks for the farm_location module.
*/
use Drupal\system\Entity\Action;
/**
* Uninstall system.action.asset_move_action.
*/
function farm_location_post_update_uninstall_asset_move_action(&$sandbox) {
$config = Action::load('asset_move_action');
if (!empty($config)) {
$config->delete();
}
}

View File

@@ -0,0 +1,13 @@
services:
asset.location:
class: Drupal\farm_location\AssetLocation
arguments:
[ '@log.location', '@farm.log_query', '@entity_type.manager', '@datetime.time' , '@database']
log.location:
class: Drupal\farm_location\LogLocation
farm_location.log_event_subscriber:
class: Drupal\farm_location\EventSubscriber\LogEventSubscriber
arguments:
[ '@log.location', '@asset.location', '@cache_tags.invalidator' ]
tags:
- { name: 'event_subscriber' }

View File

@@ -0,0 +1,37 @@
<?php
/**
* @file
* Provides Views data for farm_location.module.
*/
/**
* Implements hook_views_data_alter().
*/
function farm_location_views_data_alter(array &$data) {
// Add computed fields to assets.
if (isset($data['asset'])) {
// Computed geometry.
$data['asset']['geometry'] = [
'title' => t('Geometry'),
'field' => [
'id' => 'asset_geometry',
'field_name' => 'geometry',
],
];
// Computed location.
$data['asset']['location'] = [
'title' => t('Current location'),
'field' => [
'id' => 'asset_location',
'field_name' => 'location',
],
'argument' => [
'id' => 'asset_location',
],
];
}
}

View File

@@ -0,0 +1,309 @@
<?php
namespace Drupal\farm_location;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\asset\Entity\AssetInterface;
use Drupal\farm_log\LogQueryFactoryInterface;
use Drupal\log\Entity\LogInterface;
/**
* Asset location logic.
*/
class AssetLocation implements AssetLocationInterface {
/**
* The name of the asset intrinsic geometry field.
*
* @var string
*/
const ASSET_FIELD_GEOMETRY = 'intrinsic_geometry';
/**
* The name of the asset boolean location field.
*
* @var string
*/
const ASSET_FIELD_LOCATION = 'is_location';
/**
* The name of the asset boolean fixed field.
*
* @var string
*/
const ASSET_FIELD_FIXED = 'is_fixed';
/**
* Log location service.
*
* @var \Drupal\farm_location\LogLocationInterface
*/
protected LogLocationInterface $logLocation;
/**
* Log query factory.
*
* @var \Drupal\farm_log\LogQueryFactoryInterface
*/
protected LogQueryFactoryInterface $logQueryFactory;
/**
* Entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The time service.
*
* @var \Drupal\Component\Datetime\TimeInterface
*/
protected $time;
/**
* The database service.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Class constructor.
*
* @param \Drupal\farm_location\LogLocationInterface $log_location
* Log location service.
* @param \Drupal\farm_log\LogQueryFactoryInterface $log_query_factory
* Log query factory.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* Entity type manager.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param \Drupal\Core\Database\Connection $database
* The database service.
*/
public function __construct(LogLocationInterface $log_location, LogQueryFactoryInterface $log_query_factory, EntityTypeManagerInterface $entity_type_manager, TimeInterface $time, Connection $database) {
$this->logLocation = $log_location;
$this->logQueryFactory = $log_query_factory;
$this->entityTypeManager = $entity_type_manager;
$this->time = $time;
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public function isLocation(AssetInterface $asset): bool {
return !empty($asset->{static::ASSET_FIELD_LOCATION}->value);
}
/**
* {@inheritdoc}
*/
public function isFixed(AssetInterface $asset): bool {
return !empty($asset->{static::ASSET_FIELD_FIXED}->value);
}
/**
* {@inheritdoc}
*/
public function hasLocation(AssetInterface $asset, $timestamp = NULL): bool {
// If the asset is fixed, always return FALSE.
if ($this->isFixed($asset)) {
return FALSE;
}
// Load the movement log. Bail if empty.
$log = $this->getMovementLog($asset, $timestamp);
if (empty($log)) {
return FALSE;
}
// Return whether the log has a location.
return $this->logLocation->hasLocation($log);
}
/**
* {@inheritdoc}
*/
public function hasGeometry(AssetInterface $asset, $timestamp = NULL): bool {
// If the asset is fixed, return emptiness of its intrinsic geometry.
if ($this->isFixed($asset)) {
return !$asset->get(static::ASSET_FIELD_GEOMETRY)->isEmpty();
}
// Load the movement log. Bail if empty.
$log = $this->getMovementLog($asset, $timestamp);
if (empty($log)) {
return FALSE;
}
// Return emptiness of the log's location reference.
return $this->logLocation->hasGeometry($log);
}
/**
* {@inheritdoc}
*/
public function getLocation(AssetInterface $asset, $timestamp = NULL): array {
// If the asset is fixed, return empty array.
if ($this->isFixed($asset)) {
return [];
}
// Load the movement log. Bail if empty.
$log = $this->getMovementLog($asset, $timestamp);
if (empty($log)) {
return [];
}
// Return locations referenced by the log.
return $this->logLocation->getLocation($log);
}
/**
* {@inheritdoc}
*/
public function getGeometry(AssetInterface $asset, $timestamp = NULL): string {
// If the asset is fixed, return its intrinsic geometry.
if ($this->isFixed($asset)) {
return $asset->get(static::ASSET_FIELD_GEOMETRY)->value ?? '';
}
// Load the movement log. Bail if empty.
$log = $this->getMovementLog($asset, $timestamp);
if (empty($log)) {
return '';
}
// Return the log's geometry.
return $this->logLocation->getGeometry($log);
}
/**
* {@inheritdoc}
*/
public function getMovementLog(AssetInterface $asset, $timestamp = NULL): ?LogInterface {
// If the asset is new, no movement logs will reference it.
if ($asset->isNew()) {
return NULL;
}
// If $timestamp is NULL, use the current time.
if (is_null($timestamp)) {
$timestamp = $this->time->getRequestTime();
}
// Query for movement logs that reference the asset.
// We do not check access on the logs to ensure that none are filtered out.
$options = [
'asset' => $asset,
'timestamp' => $timestamp,
'status' => 'done',
'limit' => 1,
];
$query = $this->logQueryFactory->getQuery($options);
$query->condition('is_movement', TRUE);
$query->accessCheck(FALSE);
$log_ids = $query->execute();
// Bail if no logs are found.
if (empty($log_ids)) {
return NULL;
}
// Load the first log.
/** @var \Drupal\log\Entity\LogInterface $log */
$log = $this->entityTypeManager->getStorage('log')->load(reset($log_ids));
// Return the log, if available.
if (!is_null($log)) {
return $log;
}
// Otherwise, return NULL.
return NULL;
}
/**
* {@inheritdoc}
*/
public function setIntrinsicGeometry(AssetInterface $asset, string $wkt): void {
$asset->{static::ASSET_FIELD_GEOMETRY} = $wkt;
}
/**
* {@inheritdoc}
*/
public function getAssetsByLocation(array $locations, $timestamp = NULL): array {
// This should never limit itself to assets with `is_location` property
// set to TRUE, so that we can always prevent circular asset location.
// @see Drupal\farm_location\Plugin\Validation\Constraint\CircularAssetLocationConstraintValidator
// Get location ids.
$location_ids = array_map(function (AssetInterface $location) {
return $location->id();
}, $locations);
// Bail if there are no location ids.
if (empty($location_ids)) {
return [];
}
// If $timestamp is NULL, use the current time.
if (is_null($timestamp)) {
$timestamp = $this->time->getRequestTime();
}
// Build query for assets in locations.
$query = "
-- Select asset IDs from the asset base table.
SELECT DISTINCT a.id
FROM {asset} a
-- Inner join logs that reference the assets.
INNER JOIN {asset_field_data} afd ON afd.id = a.id
INNER JOIN {log__asset} la ON a.id = la.asset_target_id AND la.deleted = 0
INNER JOIN {log_field_data} lfd ON lfd.id = la.entity_id
-- Inner join location assets referenced by the logs.
INNER JOIN {log__location} ll ON ll.entity_id = lfd.id AND ll.deleted = 0
-- Left join ANY future movement logs for the same asset.
-- In the WHERE clause we'll exclude all records that have future logs,
-- leaving only the 'current' log entry.
LEFT JOIN (
{log_field_data} lfd2
INNER JOIN {log__asset} la2 ON la2.entity_id = lfd2.id AND la2.deleted = 0
) ON lfd2.is_movement = 1 AND la2.asset_target_id = a.id
-- Future log entries have either a higher timestamp, or an equal timestamp and higher log ID.
AND (lfd2.timestamp > lfd.timestamp OR (lfd2.timestamp = lfd.timestamp AND lfd2.id > lfd.id))
-- Don't include future logs beyond the given timestamp.
-- These conditions should match the values in the WHERE clause.
AND (lfd2.status = 'done') AND (lfd2.timestamp <= :timestamp)
-- Limit results to completed movement logs to the desired location that
-- took place before the given timestamp.
WHERE (lfd.is_movement = 1) AND (lfd.status = 'done') AND (lfd.timestamp <= :timestamp) AND (ll.location_target_id IN (:location_ids[]))
-- Exclude records with future log entries.
AND lfd2.id IS NULL";
$args = [
':timestamp' => $timestamp,
':location_ids[]' => $location_ids,
];
$asset_ids = $this->database->query($query, $args)->fetchCol();
return $this->entityTypeManager->getStorage('asset')->loadMultiple($asset_ids);
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace Drupal\farm_location;
use Drupal\asset\Entity\AssetInterface;
use Drupal\log\Entity\LogInterface;
/**
* Asset location logic.
*/
interface AssetLocationInterface {
/**
* Check if an asset is a location.
*
* @param \Drupal\asset\Entity\AssetInterface $asset
* The Asset entity.
*
* @return bool
* Returns TRUE if it is a location, FALSE otherwise.
*/
public function isLocation(AssetInterface $asset): bool;
/**
* Check if an asset is fixed.
*
* @param \Drupal\asset\Entity\AssetInterface $asset
* The Asset entity.
*
* @return bool
* Returns TRUE if it is fixed, FALSE otherwise.
*/
public function isFixed(AssetInterface $asset): bool;
/**
* Check if an asset is located within other location assets.
*
* @param \Drupal\asset\Entity\AssetInterface $asset
* The Asset entity.
* @param int|null $timestamp
* Include logs with a timestamp less than or equal to this.
* If this is NULL (default), the current time will be used.
*
* @return bool
* Returns TRUE if the asset has a location, FALSE otherwise.
*/
public function hasLocation(AssetInterface $asset, $timestamp = NULL): bool;
/**
* Check if an asset has geometry.
*
* @param \Drupal\asset\Entity\AssetInterface $asset
* The Asset entity.
* @param int|null $timestamp
* Include logs with a timestamp less than or equal to this.
* If this is NULL (default), the current time will be used.
*
* @return bool
* Returns TRUE if the asset has geometry, FALSE otherwise.
*/
public function hasGeometry(AssetInterface $asset, $timestamp = NULL): bool;
/**
* Get location assets that an asset is located within.
*
* @param \Drupal\asset\Entity\AssetInterface $asset
* The Asset entity.
* @param int|null $timestamp
* Include logs with a timestamp less than or equal to this.
* If this is NULL (default), the current time will be used.
*
* @return array
* Returns an array of assets.
*/
public function getLocation(AssetInterface $asset, $timestamp = NULL): array;
/**
* Get an asset's geometry.
*
* @param \Drupal\asset\Entity\AssetInterface $asset
* The Asset entity.
* @param int|null $timestamp
* Include logs with a timestamp less than or equal to this.
* If this is NULL (default), the current time will be used.
*
* @return string
* Returns a WKT string.
*/
public function getGeometry(AssetInterface $asset, $timestamp = NULL): string;
/**
* Find the latest movement log that references an asset.
*
* @param \Drupal\asset\Entity\AssetInterface $asset
* The Asset entity.
* @param int $timestamp
* Include logs with a timestamp less than or equal to this.
* If this is 0 (default), the current time will be used.
*
* @return \Drupal\log\Entity\LogInterface|null
* A log entity, or NULL if no logs were found.
*/
public function getMovementLog(AssetInterface $asset, int $timestamp = 0): ?LogInterface;
/**
* Set an asset's intrinsic geometry.
*
* @param \Drupal\asset\Entity\AssetInterface $asset
* The Asset entity.
* @param string $wkt
* Geometry as a WKT string.
*/
public function setIntrinsicGeometry(AssetInterface $asset, string $wkt): void;
/**
* Get assets that are in locations.
*
* @param \Drupal\asset\Entity\AssetInterface[] $locations
* An array of location assets to lookup.
* @param int|null $timestamp
* Include logs with a timestamp less than or equal to this.
* If this is NULL (default), the current time will be used.
*
* @return \Drupal\asset\Entity\AssetInterface[]
* An array of asset objects indexed by their IDs.
*/
public function getAssetsByLocation(array $locations, $timestamp = NULL): array;
}

View File

@@ -0,0 +1,308 @@
<?php
namespace Drupal\farm_location\EventSubscriber;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\farm_geo\Traits\WktTrait;
use Drupal\farm_location\AssetLocationInterface;
use Drupal\farm_location\LogLocationInterface;
use Drupal\log\Entity\LogInterface;
use Drupal\log\Event\LogEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Populate log geometry and invalidate asset cache on movement logs.
*/
class LogEventSubscriber implements EventSubscriberInterface {
use WktTrait;
/**
* The name of the log asset field.
*
* @var string
*/
const LOG_FIELD_ASSET = 'asset';
/**
* Log location service.
*
* @var \Drupal\farm_location\LogLocationInterface
*/
protected LogLocationInterface $logLocation;
/**
* Asset location service.
*
* @var \Drupal\farm_location\AssetLocationInterface
*/
protected AssetLocationInterface $assetLocation;
/**
* Cache tag invalidator service.
*
* @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface
*/
protected CacheTagsInvalidatorInterface $cacheTagsInvalidator;
/**
* LogEventSubscriber Constructor.
*
* @param \Drupal\farm_location\LogLocationInterface $log_location
* Log location service.
* @param \Drupal\farm_location\AssetLocationInterface $asset_locaiton
* Asset location service.
* @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator
* Cache tag invalidator service.
*/
public function __construct(LogLocationInterface $log_location, AssetLocationInterface $asset_locaiton, CacheTagsInvalidatorInterface $cache_tags_invalidator) {
$this->logLocation = $log_location;
$this->assetLocation = $asset_locaiton;
$this->cacheTagsInvalidator = $cache_tags_invalidator;
}
/**
* {@inheritdoc}
*
* @return array
* The event names to listen for, and the methods that should be executed.
*/
public static function getSubscribedEvents() {
return [
LogEvent::DELETE => 'logDelete',
LogEvent::PRESAVE => 'logPresave',
];
}
/**
* Perform actions on log delete.
*
* @param \Drupal\log\Event\LogEvent $event
* The log event.
*/
public function logDelete(LogEvent $event) {
$this->invalidateAssetCacheOnMovement($event->log);
}
/**
* Perform actions on log presave.
*
* @param \Drupal\log\Event\LogEvent $event
* The log event.
*/
public function logPresave(LogEvent $event) {
// Get the log entity from the event.
$log = $event->log;
// Populate the log geometry from the location geometry.
$this->populateGeometryFromLocation($log);
// Invalidate asset caches when assets are moved.
$this->invalidateAssetCacheOnMovement($log);
}
/**
* Populate a log's geometry based on its location.
*
* @param \Drupal\log\Entity\LogInterface $log
* The Log entity.
*/
protected function populateGeometryFromLocation(LogInterface $log): void {
// Load location assets referenced by the log.
$assets = $this->getLocationAssets($log);
// If the log does not reference any location assets, we will have nothing
// to copy from, so do nothing.
if (empty($assets)) {
return;
}
// If this is a new log and it has a geometry, do nothing.
if (empty($log->original) && $this->logLocation->hasGeometry($log)) {
return;
}
// If this is an update to an existing log, and the new geometry is not
// empty, perform some checks to see if we should proceed or not. We always
// want to proceed if the updated log's geometry is empty because this is
// an indication that it was cleared manually by the user in order to
// re-populate it.
if (!empty($log->original) && $this->logLocation->hasGeometry($log)) {
// If the original log has a custom geometry, do nothing.
if ($this->hasCustomGeometry($log->original)) {
return;
}
// If the geometry has changed, do nothing.
$old_geometry = $this->logLocation->getGeometry($log->original);
$new_geometry = $this->logLocation->getGeometry($log);
if ($old_geometry != $new_geometry) {
return;
}
}
// Get the combined location asset geometry.
$wkt = $this->getCombinedAssetGeometry($assets);
// If the WKT is not empty, set the log geometry.
if (!empty($wkt)) {
$this->logLocation->setGeometry($log, $wkt);
}
}
/**
* Check if a log has a custom geometry.
*
* This is determined by checking to see if the log's geometry matches that
* of the location assets it references. If it does not, and it is not empty,
* them we assume it has a custom geometry.
*
* @param \Drupal\log\Entity\LogInterface $log
* The Log entity.
*
* @return bool
* Returns TRUE if it matches, FALSE otherwise.
*/
protected function hasCustomGeometry(LogInterface $log): bool {
// If the log's geometry is empty, then it does not have a custom geometry.
if (!$this->logLocation->hasGeometry($log)) {
return FALSE;
}
// Load location assets referenced by the log.
$assets = $this->getLocationAssets($log);
// Get the combined location asset geometry.
$location_geometry = $this->getCombinedAssetGeometry($assets);
// Get the log geometry.
$log_geometry = $this->logLocation->getGeometry($log);
// Compare the log and location geometries.
return $log_geometry != $location_geometry;
}
/**
* Get location assets referenced by the log.
*
* This will first check for assets in the location reference field. If none
* are found, it will also look for location assets in the asset reference
* field.
*
* @param \Drupal\log\Entity\LogInterface $log
* The Log entity.
*
* @return \Drupal\asset\Entity\AssetInterface[]
* An array of location assets.
*/
protected function getLocationAssets(LogInterface $log) {
// Load location assets referenced by the log.
$assets = $this->logLocation->getLocation($log);
// If there are no assets in the location reference field, look for location
// assets in the asset reference field. Only do this if the log is not a
// movement, otherwise it would be impossible to clear the geometry of a
// non-fixed location asset via movement Logs.
if (empty($assets) && !$log->get('is_movement')->value) {
foreach ($log->{static::LOG_FIELD_ASSET}->referencedEntities() as $asset) {
if ($this->assetLocation->isLocation($asset)) {
$assets[] = $asset;
}
}
}
return $assets;
}
/**
* Load a combined set of location asset geometries.
*
* @param \Drupal\asset\Entity\AssetInterface[] $assets
* An array of location assets.
*
* @return string
* Returns a WKT string of the combined asset geometries.
*/
protected function getCombinedAssetGeometry(array $assets): string {
// Collect all the location geometries.
$geoms = [];
foreach ($assets as $asset) {
if ($this->assetLocation->hasGeometry($asset)) {
$geoms[] = $this->assetLocation->getGeometry($asset);
}
}
// Combine the geometries into a single WKT string.
$wkt = $this->combineWkt($geoms);
return $wkt;
}
/**
* Invalidate asset caches when assets are moved.
*
* @param \Drupal\log\Entity\LogInterface $log
* The Log entity.
*/
protected function invalidateAssetCacheOnMovement(LogInterface $log): void {
// Keep track if we need to invalidate the cache of referenced assets so
// the computed 'location' and 'geometry' fields are updated.
$update_asset_cache = FALSE;
// If the log is a 'done' movement log, invalidate the cache.
if ($this->isActiveMovementLog($log)) {
$update_asset_cache = TRUE;
}
// If updating an existing 'done' movement log, invalidate the cache.
// This catches any movement logs changing from done to pending.
if (!empty($log->original) && $this->isActiveMovementLog($log->original)) {
$update_asset_cache = TRUE;
}
// If an update is not necessary, bail.
if (!$update_asset_cache) {
return;
}
// Build a list of cache tags.
// @todo Only invalidate cache if the movement log changes the asset's current location. This might be different for each asset.
$tags = [];
// Include assets that were previously referenced.
if (!empty($log->original)) {
foreach ($log->original->get('asset')->referencedEntities() as $asset) {
array_push($tags, ...$asset->getCacheTags());
}
}
// Include assets currently referenced by the log.
foreach ($log->get('asset')->referencedEntities() as $asset) {
array_push($tags, ...$asset->getCacheTags());
}
// Invalidate the cache tags.
$this->cacheTagsInvalidator->invalidateTags($tags);
}
/**
* Helper method to check if a log is an active movement log.
*
* @param \Drupal\log\Entity\LogInterface $log
* The Log entity.
*
* @return bool
* Boolean indicating if the log is an active movement log.
*/
public static function isActiveMovementLog(LogInterface $log): bool {
return $log->get('status')->value == 'done' && $log->get('is_movement')->value && $log->get('timestamp')->value <= \Drupal::time()->getCurrentTime();
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Drupal\farm_location\Field;
use Drupal\Core\Field\FieldItemList;
use Drupal\Core\TypedData\ComputedItemListTrait;
/**
* Computes the current geometry value for assets.
*/
class AssetGeometryItemList extends FieldItemList {
use ComputedItemListTrait;
/**
* Computes the current geometry value for the asset.
*/
protected function computeValue() {
// Get the asset entity.
$entity = $this->getEntity();
// Get the asset geometry.
$geometry = \Drupal::service('asset.location')->getGeometry($entity);
// Update the assets current geometry value to match.
// @todo Cache this field computation.
$this->list[0] = $this->createItem(0, $geometry);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Drupal\farm_location\Field;
use Drupal\Core\Field\EntityReferenceFieldItemList;
use Drupal\Core\TypedData\ComputedItemListTrait;
/**
* Computes the current location value for assets.
*/
class AssetLocationItemList extends EntityReferenceFieldItemList {
use ComputedItemListTrait;
/**
* Computes the current location value for the asset.
*/
protected function computeValue() {
// Get the asset entity.
$entity = $this->getEntity();
// Get the asset's current locations.
$locations = \Drupal::service('asset.location')->getLocation($entity);
// Update the assets current location values to match.
// @todo Cache this field computation.
foreach ($locations as $delta => $location) {
if (!empty($location->id())) {
$value = ['target_id' => $location->id()];
$this->list[$delta] = $this->createItem($delta, $value);
}
}
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Drupal\farm_location;
use Drupal\log\Entity\LogInterface;
/**
* Log location logic.
*/
class LogLocation implements LogLocationInterface {
/**
* The name of the log location field.
*
* @var string
*/
const LOG_FIELD_LOCATION = 'location';
/**
* The name of the log geometry field.
*
* @var string
*/
const LOG_FIELD_GEOMETRY = 'geometry';
/**
* {@inheritdoc}
*/
public function hasLocation(LogInterface $log): bool {
return !$log->get(static::LOG_FIELD_LOCATION)->isEmpty();
}
/**
* {@inheritdoc}
*/
public function hasGeometry(LogInterface $log): bool {
return !$log->get(static::LOG_FIELD_GEOMETRY)->isEmpty();
}
/**
* {@inheritdoc}
*/
public function getLocation(LogInterface $log): array {
return $log->{static::LOG_FIELD_LOCATION}->referencedEntities() ?? [];
}
/**
* {@inheritdoc}
*/
public function getGeometry(LogInterface $log): string {
return $log->get(static::LOG_FIELD_GEOMETRY)->value ?? '';
}
/**
* {@inheritdoc}
*/
public function setLocation(LogInterface $log, array $assets): void {
foreach ($assets as $asset) {
$log->{static::LOG_FIELD_LOCATION}[] = ['target_id' => $asset->id()];
}
}
/**
* {@inheritdoc}
*/
public function setGeometry(LogInterface $log, string $wkt): void {
$log->{static::LOG_FIELD_GEOMETRY}->value = $wkt;
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Drupal\farm_location;
use Drupal\log\Entity\LogInterface;
/**
* The interface for log location logic.
*/
interface LogLocationInterface {
/**
* Check if a log references location assets.
*
* @param \Drupal\log\Entity\LogInterface $log
* The Log entity.
*
* @return bool
* Returns TRUE if the log references location assets, FALSE otherwise.
*/
public function hasLocation(LogInterface $log): bool;
/**
* Check if a log has geometry.
*
* @param \Drupal\log\Entity\LogInterface $log
* The Log entity.
*
* @return bool
* Returns TRUE if the log has geometry, FALSE otherwise.
*/
public function hasGeometry(LogInterface $log): bool;
/**
* Get location assets referenced by a log.
*
* @param \Drupal\log\Entity\LogInterface $log
* The Log entity.
*
* @return array
* Returns an array of assets.
*/
public function getLocation(LogInterface $log): array;
/**
* Get a log's geometry.
*
* @param \Drupal\log\Entity\LogInterface $log
* The Log entity.
*
* @return string
* Returns a WKT string.
*/
public function getGeometry(LogInterface $log): string;
/**
* Set a log's location.
*
* @param \Drupal\log\Entity\LogInterface $log
* The Log entity.
* @param \Drupal\asset\Entity\AssetInterface[] $assets
* An array of location asset entities.
*/
public function setLocation(LogInterface $log, array $assets): void;
/**
* Set a log's geometry.
*
* @param \Drupal\log\Entity\LogInterface $log
* The Log entity.
* @param string $wkt
* The geometry as a WKT string.
*/
public function setGeometry(LogInterface $log, string $wkt): void;
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Drupal\farm_location\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceLabelFormatter;
use Drupal\Core\Form\FormStateInterface;
/**
* Field formatter for the current location asset field.
*
* @FieldFormatter(
* id = "asset_current_location",
* label = @Translation("Asset current location"),
* description = @Translation("Display the label of the referenced entities."),
* field_types = {
* "entity_reference"
* }
* )
*/
class AssetCurrentLocationFormatter extends EntityReferenceLabelFormatter {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'render_without_location' => FALSE,
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$elements = parent::settingsForm($form, $form_state);
$elements['render_without_location'] = [
'#title' => $this->t('Render without location'),
'#description' => $this->t('Include this field when the asset has no current location.'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('render_without_location'),
];
return $elements;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = parent::settingsSummary();
$summary[] = $this->getSetting('render_without_location') ? $this->t('Render without current location') : $this->t('Do not render without current location');
return $summary;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
// Build labels in parent.
$elements = parent::viewElements($items, $langcode);
// Get the asset.
$asset = $items->getEntity();
// If the asset is fixed don't render additional information.
if ($asset->get('is_fixed')->value) {
return $elements;
}
// If there are no current locations only render if configured to.
if (empty($elements) && !$this->getSetting('render_without_location')) {
return $elements;
}
// Add N/A if there are no current locations.
if (empty($elements)) {
// Render N/A if configured.
$elements[] = ['#markup' => 'N/A'];
}
return $elements;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Drupal\farm_location\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Checks that a log is not creating a circular asset location.
*
* @Constraint(
* id = "CircularAssetLocation",
* label = @Translation("Circular asset location", context = "Validation"),
* )
*/
class CircularAssetLocationConstraint extends Constraint {
/**
* The default violation message.
*
* @var string
*/
public $message = '%asset cannot be located within itself.';
}

View File

@@ -0,0 +1,141 @@
<?php
namespace Drupal\farm_location\Plugin\Validation\Constraint;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\asset\Entity\AssetInterface;
use Drupal\farm_location\AssetLocationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the CircularAssetLocation constraint.
*/
class CircularAssetLocationConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* Asset location service.
*
* @var \Drupal\farm_location\AssetLocationInterface
*/
protected $assetLocation;
/**
* CircularAssetLocationConstraintValidator constructor.
*
* @param \Drupal\farm_location\AssetLocationInterface $asset_location
* Asset location service.
*/
public function __construct(AssetLocationInterface $asset_location) {
$this->assetLocation = $asset_location;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('asset.location'),
);
}
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint) {
/** @var \Drupal\Core\Field\EntityReferenceFieldItemList $value */
/** @var \Drupal\farm_location\Plugin\Validation\Constraint\CircularAssetLocationConstraint $constraint */
// Get the log that this field is on.
$log = $value->getParent()->getValue();
// If the log is not a movement, we have nothing to validate.
if (empty($log->get('is_movement')->value)) {
return;
}
// Get the locations(s) that asset(s) are being moved to.
$locations = $log->get('location')->referencedEntities();
// If there are no locations, we have nothing to validate.
if (empty($locations)) {
return;
}
// Get the log's timestamp.
$timestamp = $log->get('timestamp')->value;
// Iterate through referenced entities.
foreach ($value->referencedEntities() as $delta => $asset) {
// Load assets that are located in the asset being referenced.
// Use our own method to recurse into sub-location assets as well.
$assets_in_location = $this->getAssetsByLocationRecursively($asset, $timestamp);
// Make sure that none of the assets are located in this asset.
// Iterate through the locations and check for violations.
$violation = FALSE;
foreach ($locations as $location) {
// Make sure that the asset and location are not the same.
if ($location->id() == $asset->id()) {
$violation = TRUE;
}
// Make sure that none of the assets are located in this asset.
foreach ($assets_in_location as $asset_in_location) {
if ($location->id() == $asset_in_location->id()) {
$violation = TRUE;
break;
}
}
}
// If a violation was found, flag it.
if ($violation) {
$this->context->buildViolation($constraint->message, ['%asset' => $asset->label()])
->atPath((string) $delta . '.target_id')
->setInvalidValue($asset->id())
->addViolation();
}
}
}
/**
* Recursively get all assets in a location based on movement logs.
*
* @param \Drupal\asset\Entity\AssetInterface $location
* The location asset.
* @param int $timestamp
* Include logs with a timestamp less than or equal to this.
*
* @return \Drupal\asset\Entity\AssetInterface[]
* An array of assets in the location.
*/
private function getAssetsByLocationRecursively(AssetInterface $location, int $timestamp) {
// This should never limit itself to assets with `is_location` property
// set to TRUE, because that property can change. And changes to it are on
// the asset itself, not on the log that we're validating here. This means
// that if we only checked `is_location` assets here, they could potentially
// be changed later, making it possible for a "valid" log to become
// "invalid" without editing it. In order to thoroughly prevent circular
// asset location we need to be able to be able to find assets that are
// "located" in both location and non-location assets.
// @see Drupal\farm_location\AssetLocation::getAssetsByLocation()
// Get assets in this location.
$assets = $this->assetLocation->getAssetsByLocation([$location], $timestamp);
// Recurse into each asset to get any other assets located in each.
foreach ($assets as $asset) {
$assets += $this->getAssetsByLocationRecursively($asset, $timestamp);
}
// Return assets.
return $assets;
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Drupal\farm_location\Plugin\views\argument;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\farm_location\AssetLocationInterface;
use Drupal\views\Plugin\views\argument\ArgumentPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* An argument for filtering assets by their current location.
*
* @ingroup views_argument_handlers
*
* @ViewsArgument("asset_location")
*/
class AssetLocation extends ArgumentPluginBase {
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The asset location service.
*
* @var \Drupal\farm_location\AssetLocationInterface
*/
protected $assetLocation;
/**
* Constructs an AssetLocation 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\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\farm_location\AssetLocationInterface $asset_location
* The asset location service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, AssetLocationInterface $asset_location) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
$this->assetLocation = $asset_location;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('asset.location'),
);
}
/**
* {@inheritdoc}
*/
public function query($group_by = FALSE) {
// First query for a list of asset IDs in the location, then use this list
// to filter the current View.
// We do this in two separate queries for a few reasons:
// 1. The Drupal and Views query APIs do not support the kind of compound
// JOIN that we use in the asset.location service's getAssetsByLocation().
// 2. We need to allow other modules to override the asset.location service
// to provide their own location logic (eg: the Group asset module). They
// shouldn't have to override this Views argument handler as well.
// 3. It keeps this Views argument handler's query modifications very
// simple. It only needs the condition: "WHERE asset.id IN (:asset_ids)".
// See https://www.drupal.org/project/farm/issues/3217168 for more info.
/** @var \Drupal\asset\Entity\AssetInterface $location */
$location = $this->entityTypeManager->getStorage('asset')->load($this->argument);
$assets = $this->assetLocation->getAssetsByLocation([$location]);
$asset_ids = [];
foreach ($assets as $asset) {
$asset_ids[] = $asset->id();
}
// If there are no asset IDs, add 0 to ensure the array is not empty.
if (empty($asset_ids)) {
$asset_ids[] = 0;
}
// Filter to only include assets with those IDs.
$this->ensureMyTable();
$this->query->addWhere(0, "$this->tableAlias.id", $asset_ids, 'IN');
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Drupal\farm_location\Plugin\views\field;
use Drupal\views\Plugin\views\field\EntityField;
/**
* A field that displays asset geometry.
*
* @ingroup views_field_handlers
*
* @ViewsField("asset_geometry")
*/
class AssetGeometry extends EntityField {
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Drupal\farm_location\Plugin\views\field;
use Drupal\views\Plugin\views\field\EntityField;
/**
* A field that displays asset location.
*
* @ingroup views_field_handlers
*
* @ViewsField("asset_location")
*/
class AssetLocation extends EntityField {
}

View File

@@ -0,0 +1,7 @@
langcode: en
status: true
id: location
label: Location
description: ''
workflow: asset_default
new_revision: true

View File

@@ -0,0 +1,7 @@
langcode: en
status: true
id: object
label: Object
description: ''
workflow: asset_default
new_revision: true

View File

@@ -0,0 +1,8 @@
langcode: en
status: true
id: activity
label: Activity
description: ''
name_pattern: 'Activity log [log:id]'
workflow: log_default
new_revision: true

View File

@@ -0,0 +1,8 @@
langcode: en
status: true
id: movement
label: Movement
description: ''
name_pattern: 'Movement log [log:id]'
workflow: log_default
new_revision: true

View File

@@ -0,0 +1,10 @@
name: farmOS Location Tests
description: 'Support module for log testing.'
type: module
package: Testing
core_version_requirement: ^10
dependencies:
- farm:asset
- farm:farm_location
- farm:farm_log_asset
- log:log

View File

@@ -0,0 +1,17 @@
<?php
namespace Drupal\farm_location_test\Plugin\Asset\AssetType;
use Drupal\farm_entity\Plugin\Asset\AssetType\FarmAssetType;
/**
* Provides the test location asset type.
*
* @AssetType(
* id = "location",
* label = @Translation("Location"),
* )
*/
class Location extends FarmAssetType {
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Drupal\farm_location_test\Plugin\Asset\AssetType;
use Drupal\farm_entity\Plugin\Asset\AssetType\FarmAssetType;
/**
* Provides the test object asset type.
*
* @AssetType(
* id = "object",
* label = @Translation("Object"),
* )
*/
class ObjectAssetType extends FarmAssetType {
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Drupal\farm_location_test\Plugin\Log\LogType;
use Drupal\farm_entity\Plugin\Log\LogType\FarmLogType;
/**
* Provides the test activity log type.
*
* @LogType(
* id = "activity",
* label = @Translation("Activity"),
* )
*/
class Activity extends FarmLogType {
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Drupal\farm_location_test\Plugin\Log\LogType;
use Drupal\farm_entity\Plugin\Log\LogType\FarmLogType;
/**
* Provides the test movement log type.
*
* @LogType(
* id = "movement",
* label = @Translation("Movement"),
* )
*/
class Movement extends FarmLogType {
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Drupal\Tests\farm_location\Functional;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Url;
use Drupal\Tests\farm_test\Functional\FarmBrowserTestBase;
use Drupal\Tests\jsonapi\Functional\JsonApiRequestTestTrait;
use Drupal\farm_geo\Traits\WktTrait;
use GuzzleHttp\RequestOptions;
/**
* Tests for the location api.
*
* @group farm
*/
class LocationAPITest extends FarmBrowserTestBase {
use WktTrait;
use JsonApiRequestTestTrait;
use LocationFunctionalTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'basic_auth',
'farm_location',
'farm_location_test',
'farm_api',
];
/**
* Test creating an asset via the API.
*
* Ensure that the geometry and location fields are not populated.
*/
public function testApiAssetPostResponse() {
// Logout the user so we don't need a CSRF token. Use basic auth.
$this->drupalLogout();
// Setup the request.
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
$request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
$request_options[RequestOptions::HEADERS]['Authorization'] = 'Basic ' . base64_encode($this->user->name->value . ':' . $this->user->passRaw);
$asset_uri = "base://api/asset/object";
// Create an asset with no intrinsic geometry.
$post_data = [
'data' => [
'type' => 'asset--object',
'attributes' => [
'name' => 'Test object',
],
],
];
$request_options[RequestOptions::BODY] = Json::encode($post_data);
$response = $this->request('POST', Url::fromUri($asset_uri), $request_options);
// Assert a valid response.
$this->assertEquals(201, $response->getStatusCode());
$response_data = Json::decode((string) $response->getBody());
$this->assertArrayhasKey('data', $response_data);
// Assert that there is no geometry.
$this->assertArrayHasKey('attributes', $response_data['data']);
$this->assertArrayHasKey('geometry', $response_data['data']['attributes']);
$this->assertEmpty($response_data['data']['attributes']['geometry']);
// Assert that there is no location.
$this->assertArrayHasKey('relationships', $response_data['data']);
$this->assertArrayHasKey('location', $response_data['data']['relationships']);
$this->assertEmpty($response_data['data']['relationships']['location']['data']);
// Create a fixed asset with intrinsic geometry.
$post_data['data']['attributes']['is_fixed'] = TRUE;
$post_data['data']['attributes']['intrinsic_geometry'] = $this->reduceWkt(\Drupal::service('geofield.wkt_generator')->wktGeneratePolygon(NULL, 5));
$request_options[RequestOptions::BODY] = Json::encode($post_data);
$response = $this->request('POST', Url::fromUri($asset_uri), $request_options);
// Assert a valid response.
$this->assertEquals(201, $response->getStatusCode());
$response_data = Json::decode((string) $response->getBody());
$this->assertArrayhasKey('data', $response_data);
// Assert that the intrinsic_geometry was used.
$this->assertArrayHasKey('attributes', $response_data['data']);
$this->assertArrayHasKey('geometry', $response_data['data']['attributes']);
$this->assertArrayHasKey('value', $response_data['data']['attributes']['geometry']);
$this->assertEquals($response_data['data']['attributes']['geometry']['value'], $post_data['data']['attributes']['intrinsic_geometry']);
// Assert that there is no location.
$this->assertArrayHasKey('relationships', $response_data['data']);
$this->assertArrayHasKey('location', $response_data['data']['relationships']);
$this->assertEmpty($response_data['data']['relationships']['location']['data']);
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Drupal\Tests\farm_location\Functional;
/**
* Trait for setting up functional location tests.
*/
trait LocationFunctionalTestTrait {
/**
* Test user.
*
* @var \Drupal\user\Entity\User
*/
protected $user;
/**
* Test location asset.
*
* @var \Drupal\asset\Entity\AssetInterface
*/
protected $location;
/**
* Test asset.
*
* @var \Drupal\asset\Entity\AssetInterface
*/
protected $asset;
/**
* Test movement log.
*
* @var \Drupal\log\Entity\LogInterface
*/
protected $log;
/**
* {@inheritdoc}
*/
public function setUp(): void {
parent::setUp();
// Load asset and log storage.
$entity_type_manager = $this->container->get('entity_type.manager');
$asset_storage = $entity_type_manager->getStorage('asset');
$log_storage = $entity_type_manager->getStorage('log');
// Create and login a user with permission to administer assets and logs.
$this->user = $this->createUser(['administer assets', 'administer log']);
$this->drupalLogin($this->user);
// Generate a location with random WKT polygon.
$this->location = $asset_storage->create([
'type' => 'location',
'name' => $this->randomMachineName(),
'intrinsic_geometry' => $this->reduceWkt(\Drupal::service('geofield.wkt_generator')->wktGeneratePolygon(NULL, 5)),
'is_fixed' => TRUE,
]);
$this->location->save();
// Create a new asset.
$this->asset = $asset_storage->create([
'type' => 'object',
'name' => $this->randomMachineName(),
]);
$this->asset->save();
// Create a "done" movement log that references the asset.
$this->log = $log_storage->create([
'type' => 'movement',
'status' => 'done',
'asset' => ['target_id' => $this->asset->id()],
'location' => ['target_id' => $this->location->id()],
'is_movement' => TRUE,
]);
$this->log->save();
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace Drupal\Tests\farm_location\Functional;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\jsonapi\Functional\JsonApiRequestTestTrait;
use Drupal\farm_geo\Traits\WktTrait;
/**
* Tests for farmOS location logic.
*
* @group farm
*/
class LocationTest extends WebDriverTestBase {
use StringTranslationTrait;
use WktTrait;
use JsonApiRequestTestTrait;
use LocationFunctionalTestTrait {
setUp as locationSetup;
}
/**
* {@inheritdoc}
*/
protected $profile = 'farm';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = [
'basic_auth',
'farm_location',
'farm_location_test',
'farm_api',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$GLOBALS['farm_test'] = TRUE;
$this->locationSetup();
}
/**
* Test computed asset location.
*/
public function testComputedAssetLocation() {
// The computed location of the asset is the location asset.
$location = $this->asset->get('location')->referencedEntities();
$this->assertEquals($this->location->id(), $location[0]->id(), 'Computed asset location is the location asset.');
// The computed geometry of the asset is the location asset geometry.
$this->assertEquals($this->location->get('intrinsic_geometry')->value, $this->asset->get('geometry')->value, 'Computed asset geometry is the location asset geometry.');
}
/**
* Test geometry and location field visibility.
*/
public function testLocationFieldVisibility() {
// Go to the asset edit form.
$this->drupalGet('asset/' . $this->asset->id() . '/edit');
// Test that current geometry and current location fields are all hidden.
$this->assertSession()->fieldNotExists('geometry[0][value]');
$this->assertSession()->fieldNotExists('location[0][target_id]');
// Test that intrinsic_geometry field is hidden.
$page = $this->getSession()->getPage();
$intrinsic_geometry_field = $page->findById('edit-intrinsic-geometry-wrapper');
$this->assertNotEmpty($intrinsic_geometry_field);
$this->assertFalse($intrinsic_geometry_field->isVisible());
// Go to the asset view page.
$this->drupalGet('asset/' . $this->asset->id());
// Test that current geometry and location fields are visible.
$this->assertSession()->pageTextContains("Current geometry");
$this->assertSession()->pageTextContains("Current location");
// Test that the intrinsic geometry field is hidden.
$this->assertSession()->pageTextNotContains("Intrinsic geometry");
// Make the asset fixed.
$this->asset->is_fixed = TRUE;
$this->asset->save();
// Go back to the edit form.
$this->drupalGet('asset/' . $this->asset->id() . '/edit');
// Test that the intrinsic geometry field is visible.
$page = $this->getSession()->getPage();
$intrinsic_geometry_field = $page->findById('edit-intrinsic-geometry-wrapper');
$this->assertNotEmpty($intrinsic_geometry_field);
$this->assertTrue($intrinsic_geometry_field->isVisible());
}
}

View File

@@ -0,0 +1,700 @@
<?php
namespace Drupal\Tests\farm_location\Kernel;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\farm_test\Kernel\FarmAssetTestTrait;
use Drupal\Tests\farm_test\Kernel\FarmEntityCacheTestTrait;
use Drupal\asset\Entity\Asset;
use Drupal\farm_geo\Traits\WktTrait;
use Drupal\log\Entity\Log;
/**
* Tests for farmOS location logic.
*
* @group farm
*/
class LocationTest extends KernelTestBase {
use StringTranslationTrait;
use FarmEntityCacheTestTrait;
use FarmAssetTestTrait;
use WktTrait;
/**
* WKT Generator service.
*
* @var \Drupal\geofield\WktGeneratorInterface
*/
protected $wktGenerator;
/**
* Asset location service.
*
* @var \Drupal\farm_location\AssetLocationInterface
*/
protected $assetLocation;
/**
* Log location service.
*
* @var \Drupal\farm_location\LogLocationInterface
*/
protected $logLocation;
/**
* Array of polygon WKT strings.
*
* @var string[]
*/
protected $polygons = [];
/**
* Array of location assets.
*
* @var \Drupal\asset\Entity\AssetInterface[]
*/
protected $locations = [];
/**
* {@inheritdoc}
*/
protected static $modules = [
'asset',
'geofield',
'log',
'farm_entity_views',
'farm_field',
'farm_location',
'farm_location_test',
'farm_log',
'farm_log_asset',
'rest',
'serialization',
'state_machine',
'system',
'user',
'views',
'views_geojson',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->wktGenerator = \Drupal::service('geofield.wkt_generator');
$this->assetLocation = \Drupal::service('asset.location');
$this->logLocation = \Drupal::service('log.location');
$this->installEntitySchema('asset');
$this->installEntitySchema('log');
$this->installEntitySchema('user');
$this->installConfig([
'farm_entity_views',
'farm_location',
'farm_location_test',
]);
// Generate random WKT polygons.
for ($i = 0; $i < 3; $i++) {
$segments = rand(3, 7);
$this->polygons[] = $this->reduceWkt($this->wktGenerator->wktGeneratePolygon(NULL, $segments));
}
// Generate location assets.
for ($i = 0; $i < 3; $i++) {
$location = Asset::create([
'type' => 'location',
'name' => $this->randomMachineName(),
'status' => 'active',
'intrinsic_geometry' => $this->polygons[$i],
'is_fixed' => TRUE,
'is_location' => TRUE,
]);
$location->save();
$this->locations[] = $location;
}
}
/**
* Test auto population of log geometry field.
*/
public function testPopulateLogGeometry() {
// When a log is saved with a location and without a geometry, the geometry
// is copied from the location.
/** @var \Drupal\log\Entity\LogInterface $log */
$log = Log::create([
'type' => 'activity',
'status' => 'pending',
'location' => ['target_id' => $this->locations[0]->id()],
]);
$log->save();
$this->assertEquals($this->locations[0]->get('intrinsic_geometry')->value, $log->get('geometry')->value, 'Empty geometry is populated from location.');
// When multiple locations are added, all of their geometries are combined.
$log->location = [
['target_id' => $this->locations[0]->id()],
['target_id' => $this->locations[1]->id()],
];
$log->geometry->value = '';
$log->save();
$combined = $this->combineWkt([$this->polygons[0], $this->polygons[1]]);
$this->assertEquals($combined, $log->get('geometry')->value, 'Geometries from multiple locations are combined.');
// When a log's locations change, and the geometry is not customized, the
// geometry is updated.
$log->location = ['target_id' => $this->locations[1]->id()];
$log->save();
$this->assertEquals($this->locations[1]->get('intrinsic_geometry')->value, $log->get('geometry')->value, 'Geometry is updated when locations are changed.');
// When a log's geometry is cleared, it is re-copied from locations.
$log->geometry->value = '';
$log->save();
$this->assertEquals($this->locations[1]->get('intrinsic_geometry')->value, $log->get('geometry')->value, 'Geometry is re-copied from locations when it is cleared.');
// When a log's geometry is set, it is saved.
$log->geometry->value = $this->polygons[2];
$log->save();
$this->assertEquals($this->polygons[2], $log->get('geometry')->value, 'Custom geometry can be saved.');
// When a log's locations change, and the geometry is customized, the
// geometry is not updated.
$log->location = ['target_id' => $this->locations[0]->id()];
$log->save();
$this->assertEquals($this->polygons[2], $log->get('geometry')->value, 'Custom geometry is not overwritten when locations change.');
// When a log's custom geometry is cleared, it is re-copied from locations.
$log->geometry->value = '';
$log->save();
$this->assertEquals($this->locations[0]->get('intrinsic_geometry')->value, $log->get('geometry')->value, 'Geometry is re-copied from locations when custom geometry is cleared.');
// When a log references a location asset in the asset field, but nothing in
// the location or geometry fields, the geometry is copied from the asset.
/** @var \Drupal\log\Entity\LogInterface $log */
$log = Log::create([
'type' => 'activity',
'status' => 'pending',
'asset' => ['target_id' => $this->locations[0]->id()],
]);
$log->save();
$this->assertEquals($this->locations[0]->get('intrinsic_geometry')->value, $log->get('geometry')->value, 'Geometry is copied from asset reference field if no location assets or custom geometry provided.');
// If a non-location asset is referenced, its geometry is not copied.
$object_geom = $this->reduceWkt($this->wktGenerator->wktGeneratePolygon(NULL, 5));
$object = Asset::create([
'type' => 'object',
'name' => $this->randomMachineName(),
'status' => 'active',
'intrinsic_geometry' => $object_geom,
'is_fixed' => TRUE,
'is_location' => FALSE,
]);
$object->save();
$log->asset = ['target_id' => $object->id()];
$log->location = [];
$log->geometry->value = '';
$log->save();
$this->assertEmpty($log->get('geometry')->value, 'Non-location asset geometry is not copied.');
// If both location and non-location assets are referenced, only the
// location asset geometry is copied.
$log->asset = [
['target_id' => $this->locations[0]->id()],
['target_id' => $object->id()],
];
$log->location = [];
$log->geometry->value = '';
$log->save();
$this->assertEquals($this->locations[0]->get('intrinsic_geometry')->value, $log->get('geometry')->value, 'Only location asset geometry is copied.');
// If location assets are referenced in both the asset and location field,
// only the geometry of the asset referenced in the location field is
// copied.
$log->asset = ['target_id' => $this->locations[0]->id()];
$log->location = ['target_id' => $this->locations[1]->id()];
$log->geometry->value = '';
$log->save();
$this->assertEquals($this->locations[1]->get('intrinsic_geometry')->value, $log->get('geometry')->value, 'Only location asset geometry is copied when both assets and locations are referenced.');
// If the log is a movement, and only references a location asset in the
// asset reference field, the geometry is not copied.
/** @var \Drupal\log\Entity\LogInterface $movement_log */
$movement_log = Log::create([
'type' => 'movement',
'status' => 'pending',
'asset' => ['target_id' => $this->locations[0]->id()],
'location' => [],
'is_movement' => TRUE,
]);
$movement_log->save();
$this->assertEmpty($movement_log->get('geometry')->value, 'Movement logs do not copy geometry from locations referenced in the asset field.');
}
/**
* Test asset location.
*/
public function testAssetLocation() {
// Create a new asset.
/** @var \Drupal\asset\Entity\AssetInterface $asset */
$asset = Asset::create([
'type' => 'object',
'name' => $this->randomMachineName(),
'status' => 'active',
]);
$asset->save();
// Populate a cache value dependent on the asset's cache tags.
$this->populateEntityTestCache($asset);
// When an asset has no movement logs, it has no location or geometry.
$this->assertFalse($this->assetLocation->hasLocation($asset), 'New assets do not have location.');
$this->assertFalse($this->assetLocation->hasGeometry($asset), 'New assets do not have geometry.');
// Assert that the asset's cache tags were not invalidated.
$this->assertEntityTestCache($asset, TRUE);
// Create a "done" movement log that references the asset.
/** @var \Drupal\log\Entity\LogInterface $first_log */
$first_log = Log::create([
'type' => 'movement',
'status' => 'done',
'asset' => ['target_id' => $asset->id()],
'location' => ['target_id' => $this->locations[0]->id()],
'is_movement' => TRUE,
]);
$first_log->save();
// When a movement log is created and marked as "done", the asset has
// the same location and geometry as the log.
$this->assertTrue($this->assetLocation->hasLocation($asset), 'Asset with movement log has location.');
$this->assertTrue($this->assetLocation->hasGeometry($asset), 'Asset with movement log has geometry.');
$this->assertEquals($this->logLocation->getLocation($first_log), $this->assetLocation->getLocation($asset), 'Asset with movement log has same location as log.');
$this->assertEquals($this->logLocation->getGeometry($first_log), $this->assetLocation->getGeometry($asset), 'Asset with movement log has same geometry as log.');
// Assert that the asset's cache tags were invalidated.
$this->assertEntityTestCache($asset, FALSE);
// Re-populate a cache value dependent on the asset's cache tags.
$this->populateEntityTestCache($asset);
// When a movement log's locations are changed, the asset location changes.
$first_log->location = ['target_id' => $this->locations[1]->id()];
$first_log->save();
$this->assertEquals($this->logLocation->getLocation($first_log), $this->assetLocation->getLocation($asset), 'Asset with changed movement log has same location as log.');
$this->assertEquals($this->logLocation->getGeometry($first_log), $this->assetLocation->getGeometry($asset), 'Asset with changed movement log has same geometry as log.');
// Assert that the asset's cache tags were invalidated.
$this->assertEntityTestCache($asset, FALSE);
// Re-populate a cache value dependent on the asset's cache tags.
$this->populateEntityTestCache($asset);
// Create a "pending" movement log that references the asset.
/** @var \Drupal\log\Entity\LogInterface $second_log */
$second_log = Log::create([
'type' => 'movement',
'status' => 'pending',
'asset' => ['target_id' => $asset->id()],
'location' => ['target_id' => $this->locations[2]->id()],
'is_movement' => TRUE,
]);
$second_log->save();
// When an asset has a "pending" movement log, the asset location and
// geometry remain the same as the previous "done" movement log.
$this->assertEquals($this->logLocation->getLocation($first_log), $this->assetLocation->getLocation($asset), 'Asset with pending movement log has original location');
$this->assertEquals($this->logLocation->getGeometry($first_log), $this->assetLocation->getGeometry($asset), 'Asset with pending movement log has original geometry.');
// Assert that the asset's cache tags were not invalidated.
$this->assertEntityTestCache($asset, TRUE);
// When the log is marked as "done", the asset location is updated.
$second_log->status = 'done';
$second_log->save();
$this->assertEquals($this->logLocation->getLocation($second_log), $this->assetLocation->getLocation($asset), 'Asset with second movement log has new location');
$this->assertEquals($this->logLocation->getGeometry($second_log), $this->assetLocation->getGeometry($asset), 'Asset with second movement log has new geometry.');
// Assert that the asset's cache tags were invalidated.
$this->assertEntityTestCache($asset, FALSE);
// Re-populate a cache value dependent on the asset's cache tags.
$this->populateEntityTestCache($asset);
// Create a third "done" movement log in the future.
/** @var \Drupal\log\Entity\LogInterface $third_log */
$third_log = Log::create([
'type' => 'movement',
'timestamp' => \Drupal::time()->getRequestTime() + 86400,
'status' => 'done',
'asset' => ['target_id' => $asset->id()],
'location' => ['target_id' => $this->locations[0]->id()],
'is_movement' => TRUE,
]);
$third_log->save();
// When an asset has a "done" movement log in the future, the asset
// location and geometry remain the same as the previous "done" movement
// log.
$this->assertEquals($this->logLocation->getLocation($second_log), $this->assetLocation->getLocation($asset), 'Asset with future movement log has current location');
$this->assertEquals($this->logLocation->getGeometry($second_log), $this->assetLocation->getGeometry($asset), 'Asset with future movement log has current geometry.');
// Assert that the asset's cache tags were not invalidated.
$this->assertEntityTestCache($asset, TRUE);
// Create a fourth "done" movement log without location.
/** @var \Drupal\log\Entity\LogInterface $fourth_log */
$fourth_log = Log::create([
'type' => 'movement',
'status' => 'done',
'asset' => ['target_id' => $asset->id()],
'is_movement' => TRUE,
]);
$fourth_log->save();
// When a movement log is created with no location/geometry, it effectively
// "unsets" the asset's location/geometry.
$this->assertFalse($this->assetLocation->hasLocation($asset), 'Asset location can be unset.');
$this->assertFalse($this->assetLocation->hasGeometry($asset), 'Asset geometry can be unset.');
// Assert that the asset's cache tags were invalidated.
$this->assertEntityTestCache($asset, FALSE);
// Re-populate a cache value dependent on the asset's cache tags.
$this->populateEntityTestCache($asset);
// Delete the fourth log.
$fourth_log->delete();
// When a movement log is deleted, the previous location is used.
$this->assertEquals($this->logLocation->getLocation($second_log), $this->assetLocation->getLocation($asset), 'When a movement log is deleted, the previous location is used.');
$this->assertEquals($this->logLocation->getGeometry($second_log), $this->assetLocation->getGeometry($asset), 'When a movement log is deleted, the previous locations geometry is used. .');
// Assert that the asset's cache tags were invalidated.
$this->assertEntityTestCache($asset, FALSE);
}
/**
* Test fixed asset location.
*/
public function testFixedAssetLocation() {
// Create a new "fixed" asset.
/** @var \Drupal\asset\Entity\AssetInterface $asset */
$asset = Asset::create([
'type' => 'object',
'name' => $this->randomMachineName(),
'status' => 'active',
'is_fixed' => TRUE,
]);
$asset->save();
// When a new asset is saved, it does not have a geometry.
$this->assertFalse($this->assetLocation->hasGeometry($asset), 'New assets do not have geometry.');
// When an asset is fixed, and has intrinsic geometry, it is the asset's
// geometry.
$this->assetLocation->setIntrinsicGeometry($asset, $this->polygons[0]);
$asset->save();
$this->assertTrue($this->assetLocation->hasGeometry($asset), 'Assets with intrinsic geometry have geometry.');
$this->assertEquals($this->polygons[0], $this->assetLocation->getGeometry($asset), 'Asset intrinsic geometry is asset geometry.');
// Create a "done" movement log that references the asset.
/** @var \Drupal\log\Entity\LogInterface $log */
$log = Log::create([
'type' => 'movement',
'status' => 'done',
'asset' => ['target_id' => $asset->id()],
'location' => ['target_id' => $this->locations[0]->id()],
'is_movement' => TRUE,
]);
$log->save();
// Movement logs of a fixed asset do not affect that asset's location or
// geometry.
$this->assertEquals([], $this->assetLocation->getLocation($asset), 'Movement logs of a fixed asset do not affect location.');
$this->assertEquals($this->polygons[0], $this->assetLocation->getGeometry($asset), 'Movement logs of a fixed asset do not affect geometry.');
// Set is_fixed to FALSE on the asset.
$asset->is_fixed = FALSE;
$asset->save();
// If an asset has a movement log and is no longer fixed, it's location and
// geometry equal location and geometry of the log.
$this->assertEquals($this->logLocation->getLocation($log), $this->assetLocation->getLocation($asset), 'Movement logs of a not fixed asset do affect location.');
$this->assertEquals($this->logLocation->getGeometry($log), $this->assetLocation->getGeometry($asset), 'Movement logs of a not fixed asset do affect geometry.');
}
/**
* Test assets in location.
*/
public function testLocationAssets() {
// Locations have no assets.
$this->assertEmpty($this->assetLocation->getAssetsByLocation([$this->locations[0], $this->locations[1]]));
// Create an asset and move it to the first location.
/** @var \Drupal\asset\Entity\AssetInterface $first_asset */
$first_asset = Asset::create([
'type' => 'object',
'name' => $this->randomMachineName(),
'status' => 'active',
]);
$first_asset->save();
/** @var \Drupal\log\Entity\LogInterface $first_log */
$first_log = Log::create([
'type' => 'movement',
'status' => 'done',
'asset' => ['target_id' => $first_asset->id()],
'location' => ['target_id' => $this->locations[0]->id()],
'is_movement' => TRUE,
]);
$first_log->save();
// First location has one asset, second has none.
$this->assertCorrectAssets([$first_asset], $this->assetLocation->getAssetsByLocation([$this->locations[0]]), TRUE);
$this->assertCorrectAssets([], $this->assetLocation->getAssetsByLocation([$this->locations[1]]), TRUE);
$this->assertCorrectAssets([$first_asset], $this->assetLocation->getAssetsByLocation([$this->locations[0], $this->locations[1]]), TRUE);
// Create a second asset and move it to the second location.
/** @var \Drupal\asset\Entity\AssetInterface $second_asset */
$second_asset = Asset::create([
'type' => 'object',
'name' => $this->randomMachineName(),
'status' => 'active',
]);
$second_asset->save();
/** @var \Drupal\log\Entity\LogInterface $first_log */
$second_log = Log::create([
'type' => 'movement',
'status' => 'done',
'asset' => ['target_id' => $second_asset->id()],
'location' => ['target_id' => $this->locations[1]->id()],
'is_movement' => TRUE,
]);
$second_log->save();
// Both locations have one asset.
$this->assertCorrectAssets([$first_asset], $this->assetLocation->getAssetsByLocation([$this->locations[0]]), TRUE);
$this->assertCorrectAssets([$second_asset], $this->assetLocation->getAssetsByLocation([$this->locations[1]]), TRUE);
$this->assertCorrectAssets([$first_asset, $second_asset], $this->assetLocation->getAssetsByLocation([$this->locations[0], $this->locations[1]]), TRUE);
// Create a third log that moves both assets to the first location.
/** @var \Drupal\log\Entity\LogInterface $third_log */
$third_log = Log::create([
'type' => 'movement',
'status' => 'done',
'asset' => [
['target_id' => $first_asset->id()],
['target_id' => $second_asset->id()],
],
'location' => ['target_id' => $this->locations[0]->id()],
'is_movement' => TRUE,
]);
$third_log->save();
// First location has two assets, second has none.
$this->assertCorrectAssets([$first_asset, $second_asset], $this->assetLocation->getAssetsByLocation([$this->locations[0]]), TRUE);
$this->assertCorrectAssets([], $this->assetLocation->getAssetsByLocation([$this->locations[1]]), TRUE);
$this->assertCorrectAssets([$first_asset, $second_asset], $this->assetLocation->getAssetsByLocation([$this->locations[0], $this->locations[1]]), TRUE);
// Create a fourth log that moves first asset to the second location.
/** @var \Drupal\log\Entity\LogInterface $fourth_log */
$fourth_log = Log::create([
'type' => 'movement',
'status' => 'done',
'asset' => [
['target_id' => $first_asset->id()],
],
'location' => ['target_id' => $this->locations[1]->id()],
'is_movement' => TRUE,
]);
$fourth_log->save();
// Both locations have one asset.
$this->assertCorrectAssets([$second_asset], $this->assetLocation->getAssetsByLocation([$this->locations[0]]), TRUE);
$this->assertCorrectAssets([$first_asset], $this->assetLocation->getAssetsByLocation([$this->locations[1]]), TRUE);
$this->assertCorrectAssets([$first_asset, $second_asset], $this->assetLocation->getAssetsByLocation([$this->locations[0], $this->locations[1]]), TRUE);
// Create a fifth log that moves first asset to the both locations.
/** @var \Drupal\log\Entity\LogInterface $fifth_log */
$fifth_log = Log::create([
'type' => 'movement',
'status' => 'done',
'asset' => [
['target_id' => $first_asset->id()],
],
'location' => [
['target_id' => $this->locations[0]->id()],
['target_id' => $this->locations[1]->id()],
],
'is_movement' => TRUE,
]);
$fifth_log->save();
// First location has two asset, second location has one asset.
$this->assertCorrectAssets([$first_asset, $second_asset], $this->assetLocation->getAssetsByLocation([$this->locations[0]]), TRUE);
$this->assertCorrectAssets([$first_asset], $this->assetLocation->getAssetsByLocation([$this->locations[1]]), TRUE);
$this->assertCorrectAssets([$first_asset, $second_asset], $this->assetLocation->getAssetsByLocation([$this->locations[0], $this->locations[1]]), TRUE);
}
/**
* Test past/future asset location.
*/
public function testAssetLocationTimestamp() {
// Create a new asset.
/** @var \Drupal\asset\Entity\AssetInterface $asset */
$asset = Asset::create([
'type' => 'object',
'name' => $this->randomMachineName(),
'status' => 'active',
]);
$asset->save();
// Create a series of timestamps, each 24 hours apart.
$now = \Drupal::time()->getRequestTime();
$timestamps = [];
for ($i = 0; $i < 4; $i++) {
$timestamps[$i] = $now + (86400 * $i);
}
// Create a series of movement logs, moving the asset into each of the
// locations, and then one that removes location at the end.
/** @var \Drupal\log\Entity\LogInterface[] $logs */
$logs = [];
$locations = [];
for ($i = 0; $i < 3; $i++) {
$locations[] = ['target_id' => $this->locations[$i]->id()];
}
$locations[] = [];
for ($i = 0; $i < 4; $i++) {
$log = Log::create([
'type' => 'movement',
'timestamp' => $timestamps[$i],
'status' => 'done',
'asset' => ['target_id' => $asset->id()],
'location' => $locations[$i],
'is_movement' => TRUE,
]);
$log->save();
$logs[] = $log;
}
// Confirm that the asset has no location before all logs.
$timestamp = $now - 86400;
$this->assertEquals(FALSE, $this->assetLocation->hasLocation($asset, $timestamp));
$this->assertEquals(FALSE, $this->assetLocation->hasGeometry($asset, $timestamp));
$this->assertEquals([], $this->assetLocation->getLocation($asset, $timestamp));
$this->assertEquals('', $this->assetLocation->getGeometry($asset, $timestamp));
$this->assertNull($this->assetLocation->getMovementLog($asset, $timestamp));
$this->assertEquals([], $this->assetLocation->getAssetsByLocation($this->locations, $timestamp));
// Confirm that the asset is where it should be after each log.
for ($i = 0; $i < 3; $i++) {
$this->assertEquals(TRUE, $this->assetLocation->hasLocation($asset, $timestamps[$i]));
$this->assertEquals(TRUE, $this->assetLocation->hasGeometry($asset, $timestamps[$i]));
$this->assertEquals($this->logLocation->getLocation($logs[$i]), $this->assetLocation->getLocation($asset, $timestamps[$i]));
$this->assertEquals($this->logLocation->getGeometry($logs[$i]), $this->assetLocation->getGeometry($asset, $timestamps[$i]));
$this->assertEquals($logs[$i]->id(), $this->assetLocation->getMovementLog($asset, $timestamps[$i])->id());
$this->assertCorrectAssets([$asset], $this->assetLocation->getAssetsByLocation([$this->locations[$i]], $timestamps[$i]), TRUE);
}
// Confirm that the asset is still in the same location 1 second later.
// This tests the <= operator.
$this->assertEquals(TRUE, $this->assetLocation->hasLocation($asset, $timestamps[2] + 1));
$this->assertEquals(TRUE, $this->assetLocation->hasGeometry($asset, $timestamps[2] + 1));
$this->assertEquals($this->logLocation->getLocation($logs[2]), $this->assetLocation->getLocation($asset, $timestamps[2] + 1));
$this->assertEquals($this->logLocation->getGeometry($logs[2]), $this->assetLocation->getGeometry($asset, $timestamps[2] + 1));
$this->assertEquals($logs[2]->id(), $this->assetLocation->getMovementLog($asset, $timestamps[2] + 1)->id());
$this->assertCorrectAssets([$asset], $this->assetLocation->getAssetsByLocation([$this->locations[2]], $timestamps[2] + 1), TRUE);
// Confirm that the asset has no location after the last log.
$this->assertEquals(FALSE, $this->assetLocation->hasLocation($asset, $timestamps[3]));
$this->assertEquals(FALSE, $this->assetLocation->hasGeometry($asset, $timestamps[3]));
$this->assertEquals([], $this->assetLocation->getLocation($asset, $timestamps[3]));
$this->assertEquals('', $this->assetLocation->getGeometry($asset, $timestamps[3]));
$this->assertEquals($logs[3]->id(), $this->assetLocation->getMovementLog($asset, $timestamps[3])->id());
$this->assertEquals([], $this->assetLocation->getAssetsByLocation($this->locations, $timestamps[3]));
}
/**
* Test that circular asset location is prevented.
*/
public function testCircularAssetLocation() {
// First, make sure we can't create a movement log that references the same
// asset in both the asset and location fields.
/** @var \Drupal\log\Entity\LogInterface $first_log */
$first_log = Log::create([
'type' => 'movement',
'asset' => ['target_id' => $this->locations[0]->id()],
'location' => ['target_id' => $this->locations[0]->id()],
'is_movement' => TRUE,
'status' => 'done',
]);
// Validate the log and confirm that a circular location constraint
// violation was set.
$violations = $first_log->validate();
$this->assertCount(1, $violations);
$this->assertEquals($this->t('%asset cannot be located within itself.', ['%asset' => $this->locations[0]->label()]), $violations[0]->getMessage());
// Tweak the log so that it moves the first location to the second location.
$first_log->set('location', ['target_id' => $this->locations[1]->id()]);
// Confirm that validation passes.
$violations = $first_log->validate();
$this->assertCount(0, $violations);
// Save the first log.
$first_log->save();
// Start a log that moves the second location to the first location (which
// would cause a circular asset location).
/** @var \Drupal\log\Entity\LogInterface $second_log */
$second_log = Log::create([
'type' => 'movement',
'asset' => ['target_id' => $this->locations[1]->id()],
'location' => ['target_id' => $this->locations[0]->id()],
'is_movement' => TRUE,
'status' => 'done',
]);
// Validate the second log and confirm that a circular location constraint
// violation was set.
$violations = $second_log->validate();
$this->assertCount(1, $violations);
$this->assertEquals($this->t('%asset cannot be located within itself.', ['%asset' => $this->locations[1]->label()]), $violations[0]->getMessage());
// Update the second log to reference the third location instead.
$second_log->set('location', ['target_id' => $this->locations[2]->id()]);
// Confirm that validation passes.
$violations = $second_log->validate();
$this->assertCount(0, $violations);
// Save the second log.
$second_log->save();
// Now, start a third log that moves the third location to the first
// location (which would create a recursive circular asset location).
/** @var \Drupal\log\Entity\LogInterface $first_log */
$third_log = Log::create([
'type' => 'movement',
'asset' => ['target_id' => $this->locations[2]->id()],
'location' => ['target_id' => $this->locations[0]->id()],
'is_movement' => TRUE,
'status' => 'done',
]);
// Validate the third log and confirm that a circular location constraint
// violation was set.
$violations = $third_log->validate();
$this->assertCount(1, $violations);
$this->assertEquals($this->t('%asset cannot be located within itself.', ['%asset' => $this->locations[2]->label()]), $violations[0]->getMessage());
}
}