Merge branch '2.x' of https://github.com/farmOS/farmOS into AssetOwners

This commit is contained in:
Jotham Gates 2022-04-24 22:17:23 +10:00
commit b8246e74bf
34 changed files with 1247 additions and 180 deletions

155
.github/workflows/deliver.yml vendored Normal file
View File

@ -0,0 +1,155 @@
name: Run tests and delivery workflow
on:
schedule:
- cron: '0 8 * * *' # Run at 8AM UTC.
push:
branches:
- '2.x'
- '2.x-**'
tags:
- '2.*'
pull_request:
branches:
- '2.x'
jobs:
build:
name: Build Docker images
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v2
- name: Set default FARMOS_REPO and FARMOS_VERSION.
run: |
echo "FARMOS_REPO=${GITHUB_REPOSITORY}" >> $GITHUB_ENV
echo "FARMOS_VERSION=2.x" >> $GITHUB_ENV
- name: Set FARMOS_VERSION for branch push event.
if: github.event_name == 'push' && github.ref_type == 'branch'
run: echo "FARMOS_VERSION=${GITHUB_REF:11}" >> $GITHUB_ENV
- name: Set FARMOS_VERSION for tag push event.
if: github.event_name == 'push' && github.ref_type == 'tag'
run: echo "FARMOS_VERSION=${GITHUB_REF:10}" >> $GITHUB_ENV
- name: Set FARMOS_VERSION and FARMOS_REPO for pull request event.
if: github.event_name == 'pull_request'
run: |
echo "FARMOS_VERSION=${GITHUB_HEAD_REF}" >> $GITHUB_ENV
echo "FARMOS_REPO=${{ github.event.pull_request.head.repo.full_name }}" >> $GITHUB_ENV
- name: Build and save farmOS 2.x Docker image
run: |
docker build --build-arg FARMOS_REPO=https://github.com/${FARMOS_REPO} --build-arg FARMOS_VERSION=${FARMOS_VERSION} -t farmos/farmos:2.x docker
docker save farmos/farmos:2.x > /tmp/farmos-2x.tar
- name: Cache farmOS 2.x Docker image
uses: actions/cache@v3
with:
path: /tmp/farmos-2x.tar
key: farmos-2x-${{ github.run_id }}
# This builds the dev Docker image using the specified FARMOS_VERSION,
# but notably it does NOT override the default PROJECT_VERSION, so the
# farmOS Composer project 2.x branch is always used.
- name: Build and save farmOS 2.x-dev Docker image
run: |
docker build --build-arg FARMOS_REPO=https://github.com/${FARMOS_REPO} --build-arg FARMOS_VERSION=${FARMOS_VERSION} -t farmos/farmos:2.x-dev docker/dev
docker save farmos/farmos:2.x-dev > /tmp/farmos-2x-dev.tar
- name: Cache farmOS 2.x-dev Docker image
uses: actions/cache@v3
with:
path: /tmp/farmos-2x-dev.tar
key: farmos-2x-dev-${{ github.run_id }}
outputs:
farmos_version: ${{ env.FARMOS_VERSION }}
sniff:
name: Run PHP Codesniffer
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout the repository
uses: actions/checkout@v2
- name: Restore farmOS 2.x-dev Docker image from cache
uses: actions/cache@v3
with:
path: /tmp/farmos-2x-dev.tar
key: farmos-2x-dev-${{ github.run_id }}
- name: Load farmos/farmos:2.x-dev image
run: docker load < /tmp/farmos-2x-dev.tar
- name: Run PHP CodeSniffer
run: docker run farmos/farmos:2.x-dev phpcs /opt/drupal/web/profiles/farm
test:
name: Run PHPUnit tests
runs-on: ubuntu-latest
needs: build
strategy:
matrix:
dbms:
- pgsql
- mariadb
- sqlite
include:
- dbms: pgsql
DB_URL: pgsql://farm:farm@db/farm
processes: auto
- dbms: mariadb
DB_URL: mysql://farm:farm@db/farm
processes: auto
- dbms: sqlite
DB_URL: sqlite://localhost/sites/default/files/db.sqlite
processes: 1
steps:
- name: Print test matrix variables
run: echo "matrix.dbms=${{ matrix.dbms }}, matrix.DB_URL=${{ matrix.DB_URL }}"
- name: Checkout the repository
uses: actions/checkout@v2
- name: Restore farmOS 2.x-dev Docker image from cache
uses: actions/cache@v3
with:
path: /tmp/farmos-2x-dev.tar
key: farmos-2x-dev-${{ github.run_id }}
- name: Load farmos/farmos:2.x-dev image
run: docker load < /tmp/farmos-2x-dev.tar
# Build a new docker-compose.yml file from docker-compose.testing.common + docker-compose.testing.{dbms}.yml.
# Copy to the current directory so that farmOS volume mounts don't change to the docker/www folder.
- name: Create docker-compose.yml
env:
DB_URL: ${{ matrix.DB_URL }}
run: |
cp docker/docker-compose.testing.* .
docker-compose -f docker-compose.testing.common.yml -f docker-compose.testing.${{ matrix.dbms }}.yml config > docker-compose.yml
- name: Start containers
run: docker-compose up -d
- name: Wait until www container is ready
# The www-container-fs-ready file is only created once we expect the containers to be online
# so waiting for that lets us know it is safe to start the tests
run: until [ -f ./www/www-container-fs-ready ]; do sleep 0.1; done
- name: Run PHPUnit tests
run: docker-compose exec -u www-data -T www paratest --verbose=1 --processes=${{ matrix.processes }} /opt/drupal/web/profiles/farm
- name: Test Drush site install with all modules
run: docker-compose exec -u www-data -T www drush site-install --db-url=${{ matrix.DB_URL }} farm farm.modules='all'
release:
name: Create GitHub release
if: github.event_name == 'push' && github.ref_type == 'tag'
runs-on: ubuntu-latest
needs:
- build
- sniff
- test
steps:
- name: Set FARMOS_VERSION from previous output.
run: echo "FARMOS_VERSION=${{ needs.build.outputs.farmos_version }}" >> $GITHUB_ENV
- name: Restore farmOS 2.x Docker image from cache
uses: actions/cache@v3
with:
path: /tmp/farmos-2x.tar
key: farmos-2x-${{ github.run_id }}
- name: Load farmos/farmos:2.x image
run: docker load < /tmp/farmos-2x.tar
- name: Run farmOS Docker container
run: docker run --rm -v /tmp/farmOS:/opt/drupal farmos/farmos:2.x true
- name: Create artifact
run: cd /tmp && tar -czf farmOS-${FARMOS_VERSION}.tar.gz farmOS
- name: Create GitHub release
uses: softprops/action-gh-release@6034af24fba4e5a8e975aaa6056554efe4c794d0 #0.1.13
with:
body: |
For full release notes, see [CHANGELOG.md](https://github.com/farmOS/farmOS/blob/${{ env.FARMOS_VERSION }}/CHANGELOG.md).
files: /tmp/farmOS-${{ env.FARMOS_VERSION }}.tar.gz
draft: false
prerelease: false

View File

@ -1,32 +0,0 @@
name: Create 2.x release
on:
push:
tags:
- '2.*'
jobs:
build:
name: Create Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@master
- name: Set FARMOS_VERSION environment variable
run: echo "FARMOS_VERSION=${GITHUB_REF:10}" >> $GITHUB_ENV
# This builds the Docker image using the specified FARMOS_VERSION,
# but notably it does NOT override the default PROJECT_VERSION, so the
# farmOS Composer project 2.x branch is always used.
- name: Build farmOS Docker image
run: docker build --build-arg FARMOS_REPO=https://github.com/${GITHUB_REPOSITORY} --build-arg FARMOS_VERSION=${FARMOS_VERSION} -t farmos docker
- name: Run farmOS Docker container
run: docker run --rm -v /tmp/farmOS:/opt/drupal farmos true
- name: Create artifact
run: cd /tmp && tar -czf farmOS-${FARMOS_VERSION}.tar.gz farmOS
- name: Create GitHub release
uses: softprops/action-gh-release@6034af24fba4e5a8e975aaa6056554efe4c794d0 #0.1.13
with:
body: |
For full release notes, see [CHANGELOG.md](https://github.com/farmOS/farmOS/blob/${{ env.FARMOS_VERSION }}/CHANGELOG.md).
files: /tmp/farmOS-${{ env.FARMOS_VERSION }}.tar.gz
draft: false
prerelease: false

View File

@ -1,80 +0,0 @@
name: Run 2.x PHPUnit tests
on:
schedule:
- cron: '0 8 * * *' # Run at 8AM UTC.
push:
branches:
- '2.x'
- '2.x-**'
pull_request:
branches:
- '2.x'
jobs:
build:
name: Run PHPUnit tests
runs-on: ubuntu-latest
strategy:
matrix:
dbms:
- pgsql
- mariadb
- sqlite
include:
- dbms: pgsql
DB_URL: pgsql://farm:farm@db/farm
processes: auto
- dbms: mariadb
DB_URL: mysql://farm:farm@db/farm
processes: auto
- dbms: sqlite
DB_URL: sqlite://localhost/sites/default/files/db.sqlite
processes: 1
# Allow all three database tests to run even when one fails.
# This is a temporary workaround for Issue #3241653: Occasional test
# failures with SQLite: SQLSTATE[HY000]: General error: 5 database is
# locked
# @todo https://www.drupal.org/project/farm/issues/3241653
fail-fast: false
steps:
- name: Print test matrix variables
run: echo "matrix.dbms=${{ matrix.dbms }}, matrix.DB_URL=${{ matrix.DB_URL }}"
- name: Checkout the repository
uses: actions/checkout@v2
- name: Set FARMOS_VERSION for push event.
if: ${{ !github.event.pull_request }}
run: |
echo "FARMOS_VERSION=${GITHUB_REF:11}" >> $GITHUB_ENV
echo "FARMOS_REPO=${GITHUB_REPOSITORY}" >> $GITHUB_ENV
- name: Set FARMOS_VERSION for pull request event.
if: ${{ github.event.pull_request }}
run: |
echo "FARMOS_VERSION=${GITHUB_HEAD_REF}" >> $GITHUB_ENV
echo "FARMOS_REPO=${{ github.event.pull_request.head.repo.full_name }}" >> $GITHUB_ENV
- name: Build farmOS 2.x Docker image
run: docker build --build-arg FARMOS_REPO=https://github.com/${FARMOS_REPO} --build-arg FARMOS_VERSION=${FARMOS_VERSION} -t farmos/farmos:2.x docker
# This builds the dev Docker image using the specified FARMOS_VERSION,
# but notably it does NOT override the default PROJECT_VERSION, so the
# farmOS Composer project 2.x branch is always used.
- name: Build farmOS 2.x-dev Docker image
run: docker build --build-arg FARMOS_REPO=https://github.com/${FARMOS_REPO} --build-arg FARMOS_VERSION=${FARMOS_VERSION} -t farmos/farmos:2.x-dev docker/dev
# Build a new docker-compose.yml file from docker-compose.testing.common + docker-compose.testing.{dbms}.yml.
# Copy to the current directory so that farmOS volume mounts don't change to the docker/www folder.
- name: Create docker-compose.yml
env:
DB_URL: ${{ matrix.DB_URL }}
run: |
cp docker/docker-compose.testing.* .
docker-compose -f docker-compose.testing.common.yml -f docker-compose.testing.${{ matrix.dbms }}.yml config > docker-compose.yml
- name: Start containers
run: docker-compose up -d
- name: Wait until www container is ready
# The www-container-fs-ready file is only created once we expect the containers to be online
# so waiting for that lets us know it is safe to start the tests
run: until [ -f ./www/www-container-fs-ready ]; do sleep 0.1; done
- name: Run PHP CodeSniffer
run: docker-compose exec -u www-data -T www phpcs /opt/drupal/web/profiles/farm
- name: Run PHPUnit tests
run: docker-compose exec -u www-data -T www paratest --verbose=1 --processes=${{ matrix.processes }} /opt/drupal/web/profiles/farm
- name: Test Drush site install with all modules
run: docker-compose exec -u www-data -T www drush site-install --db-url=${{ matrix.DB_URL }} farm farm.modules='all'

View File

@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Changed
- [Issue #3275161: Allow IMG tags in default text format](https://www.drupal.org/project/farm/issues/3275161)
- [Update toolbar logo spacing for gin beta #527](https://github.com/farmOS/farmOS/pull/527)
### Fixed
- [Do not check php-geos requirement in the update phase #526](https://github.com/farmOS/farmOS/pull/526)
### Security
- Update Drupal core to 9.3.12 for [SA-CORE-2022-008](https://www.drupal.org/sa-core-2022-008) and
[SA-CORE-2022-009](https://www.drupal.org/sa-core-2022-009).
## [2.0.0-beta4] 2022-04-13
### Added
- [Link from entities to their referenced terms and show entity views on taxonomy terms #458](https://github.com/farmOS/farmOS/pull/458).
- [Encourage GEOS PHP extension use #521](https://github.com/farmOS/farmOS/pull/521)
### Changed
- Update [farmOS-map](https://github.com/farmOS/farmOS-map) to [v2.0.4](https://github.com/farmOS/farmOS-map/releases/tag/v2.0.4).
- [Issue #3270561: Upgrade to gin beta](https://www.drupal.org/project/farm/issues/3270561)
- [Separate Docker image build from testing jobs in run-test.yml workflow #522](https://github.com/farmOS/farmOS/pull/522)
- [Merge test and release workflows into a unified delivery workflow #523](https://github.com/farmOS/farmOS/pull/523)
- [Improve fields documentation #505](https://github.com/farmOS/farmOS/pull/505)
### Fixed
- [Only require a name to build map popups #515](https://github.com/farmOS/farmOS/pull/515)
@ -160,7 +189,8 @@ moving forward.
Drupal 7, which required a complete refactor of the codebase. By comparison,
updating from Drupal 9 to 10 will simply involve updating deprecated code.
[Unreleased]: https://github.com/farmOS/farmOS/compare/2.0.0-beta3...HEAD
[Unreleased]: https://github.com/farmOS/farmOS/compare/2.0.0-beta4...HEAD
[2.0.0-beta4]: https://github.com/farmOS/farmOS/releases/tag/2.0.0-beta4
[2.0.0-beta3]: https://github.com/farmOS/farmOS/releases/tag/2.0.0-beta3
[2.0.0-beta2]: https://github.com/farmOS/farmOS/releases/tag/2.0.0-beta2
[2.0.0-beta1]: https://github.com/farmOS/farmOS/releases/tag/2.0.0-beta1

View File

@ -18,7 +18,7 @@
"require": {
"cweagans/composer-patches": "^1.6",
"drupal/admin_toolbar": "^2.4",
"drupal/core": "9.3.9",
"drupal/core": "9.3.12",
"drupal/config_update": "^1.7",
"drupal/csv_serialization": "^2.0",
"drupal/date_popup": "^1.1",
@ -30,7 +30,7 @@
"drupal/exif_orientation": "^1.1",
"drupal/fraction": "^2.0",
"drupal/geofield": "^1.33",
"drupal/gin": "3.0-alpha37",
"drupal/gin": "^3.0@beta",
"drupal/inline_entity_form": "^1.0@RC",
"drupal/inspire_tree": "^1.0",
"drupal/jsonapi_extras": "^3.15",
@ -60,9 +60,6 @@
"drupal/entity": {
"Issue #3206703: Provide reverse relationships for bundle plugin entity_reference fields.": "https://www.drupal.org/files/issues/2021-05-18/3206703-7.patch"
},
"drupal/gin": {
"Issue #3245203: Sticky region has inconsistent z-index.": "https://www.drupal.org/files/issues/2021-10-21/3245203-alpha37-3.patch"
},
"drupal/jsonapi_schema": {
"Issue #3256795: Float fields have a null schema": "https://www.drupal.org/files/issues/2022-01-03/3256795-4.patch",
"Issue #3246251: Change format utc-millisec to date-time": "https://www.drupal.org/files/issues/2021-10-27/3246251-2.patch"

View File

@ -7,10 +7,10 @@
"type": "package",
"package": {
"name": "farmos/farmos-map",
"version": "2.0.3",
"version": "2.0.4",
"type": "drupal-library",
"dist": {
"url": "https://github.com/farmOS/farmOS-map/releases/download/v2.0.2/v2.0.2-dist.zip",
"url": "https://github.com/farmOS/farmOS-map/releases/download/v2.0.4/v2.0.4-dist.zip",
"type": "zip"
},
"extra": {

View File

@ -1,11 +1,11 @@
{
"require": {
"cweagans/composer-patches": "^1.7",
"drupal/core-composer-scaffold": "9.3.9"
"drupal/core-composer-scaffold": "9.3.12"
},
"require-dev": {
"brianium/paratest": "^4",
"drupal/core-dev": "9.3.9",
"drupal/core-dev": "9.3.12",
"phpspec/prophecy-phpunit": "^2",
"symfony/finder": "^4.0"
},

View File

@ -28,4 +28,7 @@ services:
# Enable this service when executing javascript tests.
# chrome:
# image: selenium/standalone-chrome:latest
# Tests are failing on later versions of this image.
# See https://github.com/farmOS/farmOS/issues/514
# image: selenium/standalone-chrome:4.1.2-20220217

View File

@ -10,4 +10,6 @@ services:
XDEBUG_MODE: 'off'
chrome:
image: selenium/standalone-chrome:latest
# Tests are failing on later versions of this image.
# See https://github.com/farmOS/farmOS/issues/514
image: selenium/standalone-chrome:4.1.2-20220217

View File

@ -26,7 +26,9 @@ by adding the following container:
```yml
chrome:
image: selenium/standalone-chrome:latest
# Tests are failing on later versions of this image.
# See https://github.com/farmOS/farmOS/issues/514
image: selenium/standalone-chrome:4.1.2-20220217
```
## Faster testing without XDebug

View File

@ -15,7 +15,10 @@ If the field should be added to all bundles of a given entity type (eg: all log
types), then they should be added as "base fields" via
`hook_entity_base_field_info()`.
A `farm_field.factory` helper service is provided to make this easier:
A `farm_field.factory` helper service is provided to make this easier. For more
information on how this works, see [Field factory service](/development/module/services/#field-factory-service).
To get started, place the following in the `[modulename].module` file:
```php
<?php
@ -24,12 +27,14 @@ use Drupal\Core\Entity\EntityTypeInterface;
/**
* Implements hook_entity_base_field_info().
* NOTE: Replace 'mymodule' with the module name.
*/
function mymodule_entity_base_field_info(EntityTypeInterface $entity_type) {
$fields = [];
// Add a new string field to Log entities.
// 'log' specifies the entity type to apply to.
if ($entity_type->id() == 'log') {
// Options for the new field. See Field options below.
$options = [
'type' => 'string',
'label' => t('My new field'),
@ -39,6 +44,7 @@ function mymodule_entity_base_field_info(EntityTypeInterface $entity_type) {
'view' => 10,
],
];
// NOTE: Replace 'myfield' with the internal name of the field.
$fields['myfield'] = \Drupal::service('farm_field.factory')->baseFieldDefinition($options);
}
@ -56,10 +62,15 @@ then they should be added as "bundle fields" via
deprecated in favor of a core Drupal hook in the future. See core issue:
[https://www.drupal.org/node/2346347](https://www.drupal.org/node/2346347)
A `farm_field.factory` helper service is provided to make this easier. For more
information on how this works, see [Field factory service](/development/module/services/#field-factory-service).
The format for bundle field definitions is identical to base field definitions
(above), but the `bundleFieldDefinition()` method must be used instead of
`baseFieldDefinition()`.
To get started, place the following in the `[modulename].module` file:
```php
<?php
@ -67,12 +78,15 @@ use Drupal\Core\Entity\EntityTypeInterface;
/**
* Implements hook_farm_entity_bundle_field_info().
* NOTE: Replace 'mymodule' with the module name.
*/
function mymodule_farm_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle) {
$fields = [];
// Add a new string field to Input Logs.
// Add a new string field to Input Logs. 'log' specifies the entity type and
// 'input' specifies the bundle.
if ($entity_type->id() == 'log' && $bundle == 'input') {
// Options for the new field. See Field options below.
$options = [
'type' => 'string',
'label' => t('My new field'),
@ -82,6 +96,7 @@ function mymodule_farm_entity_bundle_field_info(EntityTypeInterface $entity_type
'view' => 10,
],
];
// NOTE: Replace 'myfield' with the internal name of the field.
$fields['myfield'] = \Drupal::service('farm_field.factory')->bundleFieldDefinition($options);
}
@ -126,6 +141,10 @@ Existing options can be overridden or removed by editing/deleting the entities
in the active configuration of the site. (**Warning** changing core types runs
the risk of conflicting with future farmOS updates).
Note that the file name is important and must follow a specific pattern. This
is generally in the form `[select_module_name].[select_field].[id].yml`. See
the examples for more info.
### Examples:
#### Flag
@ -140,11 +159,13 @@ dependencies:
enforced:
module:
- my_module
id: monitor
label: Monitor
id: organic
label: Organic
entity_types: null
```
Note that the file name is in the form `farm_flag.flag.[id].yml`.
The most important parts are the `id`, which is a unique machine name for
the flag, `label`, which is the human readable/translatable label that will be
shown in the select field and other parts of the UI, and `entity_types`, which
@ -194,6 +215,8 @@ id: field
label: Field
```
Note that the file name is in the form `farm_land.land_type.[id].yml`.
#### Structure type
The "Structure" module in farmOS provides a "Building" type like this:
@ -211,6 +234,8 @@ id: building
label: Building
```
Note that the file name is in the form `farm_structure.structure_type.[id].yml`.
#### Lab test type
The "Lab test" module in farmOS provides a "Soil test" type like this:
@ -228,6 +253,8 @@ id: soil
label: Soil test
```
Note that the file name is in the form `farm_lab_test.lab_test_type.[id].yml`.
#### ID tag type
ID tag types are similar to Flags, in that they have an `id` and `label`. They
@ -237,7 +264,7 @@ certain types of assets.
For example, an "Ear tag" type, provided by the "Animal asset" module, only
applies to "Animal" assets:
`animal/config/install/farm_flag.flag.ear_tag.yml`
`animal/config/install/farm_id_tag.id_tag.ear_tag.yml`
```yaml
langcode: en
@ -253,5 +280,7 @@ bundles:
- animal
```
Note that the file name is in the form `farm_flag.flag.ear_tag.[id].yml`.
If you want the tag type to apply to all assets, set `bundles: null`.
(or can it just be omitted?)

View File

@ -74,6 +74,80 @@ $all_inventory = \Drupal::service('asset.inventory')->getInventory($asset);
$gallons_of_fertilizer = \Drupal::service('asset.inventory')->getInventory($asset, 'volume', 'gallons');
```
## Field factory service
**Service name**: `farm_field.factory`
The field factory service provides two methods to make the process of creating
Drupal entity base and bundle field definitions easier and more consistent in
farmOS. This is used by modules that add [fields](/development/module/fields)
to [entity types](/development/module/entities).
Base fields are added to *all* bundles of a given entity type (eg: all logs).
Bundle fields are only added to *specific* bundles (eg: only "Input" logs).
Using this service is optional. It simply generates instances of Drupal core's
`BaseFieldDefinition` class or the Entity API module's `BundleFieldDefinition`
class, with farmOS-specific opinions to help enforce some consistency among
farmOS core and contrib modules. You can create instances of these field
definition classes directly instead of using the farmOS field factory service.
Or you can take the object produced by the service and customize it further
using standard Drupal field definition methods. This service is provided only
as a shortcut.
For more information on Drupal core's field definition API, see
[Drupal FieldTypes, FieldWidgets and FieldFormatters](https://www.drupal.org/docs/drupal-apis/entity-api/fieldtypes-fieldwidgets-and-fieldformatters)
**Methods**:
`baseFieldDefinition($options)` - Generates a base field definition, given an
array of options (see below).
`bundleFieldDefinition($options)` - Generates a bundle field definition, given
an array of options (see below).
**Options**:
Both methods expect an array of field definition options. These include:
- `type` (required) - The field data type. Each type may require additional
options. Supported types include:
- `boolean` - True/false checkbox.
- `entity_reference` - Reference other entities. Additional options:
- `target_type` (required) - The entity type to reference (eg: `asset`,
`log`, `plan`)
- `target_bundle` (optional) - The allowed target bundle. For example,
a `target_type` of `asset` and a `target_bundle` of `animal` would
limit references to animal assets.
- `auto_create` (optional) Only used when `target_type` is set to
`taxonomy_term`. If `auto_create` is set, term references will be
created automatically if the term does not exist.
- `file` - File upload.
- `fraction` - High-precision decimal number storage.
- `geofield` - Geometry on a map.
- `image` - Image upload.
- `list_string` - Select list with allowed values. Additional options:
- `allowed_values` - An associative array of allowed values.
- `allowed_values_function` - The name of a function that returns an
associative array of allowed values.
- `string_long` - Unformatted text field of unlimited length.
- `text_long` - Formatted text field of unlimited length.
- `timestamp` - Date and time.
- `label` - The field label.
- `description` - The field description.
- `required` - Whether the field is required.
- `multiple` - Whether the field should allow multiple values. Defaults to
`FALSE`.
- `cardinality` - How many values are allowed (eg: `1` for single value
fields, `-1` for unlimited values). This is an alternative to `multiple`,
and will take precedence if it is set. Defaults to `1`.
Other options are available for more advanced use-cases. Refer to the
[FarmFieldFactory](https://github.com/farmOS/farmOS/blob/2.x/modules/core/field/src/FarmFieldFactory.php)
class to understand how they work.
For more information and example code, see [Adding fields](/development/module/fields).
## Group membership service
**Service name**: `group.membership`

View File

@ -29,7 +29,7 @@ There are three ways to zoom in/out:
- On touch screens, you can "pinch zoom" using two fingers.
- With a computer mouse, you can use the scroll wheel to zoom in/out (click
on the map first to enable scroll zoom).
- The plus (+) and minus (+) buttons in the top left of the map control zoom.
- The plus (+) and minus (-) buttons in the top left of the map control zoom.
### Geolocating

View File

@ -23,8 +23,9 @@ dependencies. The [farmOS Docker images](#farmos-in-docker) include these.
- `realpath_cache_size=4096K`
- `realpath_cache_ttl=3600`
- **[PHP BCMath extension](https://www.php.net/manual/en/book.bc.php)** and
**[GEOS](https://trac.osgeo.org/geos)** are recommended for more accurate
geometric calculations.
**[GEOS](https://trac.osgeo.org/geos)** are required for accurate geometric
calculations. farmOS can be installed without these, but production usage
without them is strongly discouraged.
### Database server

View File

@ -30,7 +30,6 @@ function farm_modules() {
'farm_plant' => t('Plant assets'),
'farm_animal' => t('Animal assets'),
'farm_equipment' => t('Equipment assets'),
'farm_material' => t('Material assets'),
'farm_structure' => t('Structure assets'),
'farm_water' => t('Water assets'),
'farm_activity' => t('Activity logs'),
@ -46,6 +45,7 @@ function farm_modules() {
],
'optional' => [
'farm_inventory' => t('Inventory management'),
'farm_material' => t('Material assets'),
'farm_seed' => t('Seed assets'),
'farm_sensor' => t('Sensor assets'),
'farm_compost' => t('Compost assets'),

View File

@ -351,7 +351,7 @@ class FarmFieldFactory implements FarmFieldFactoryInterface {
'type' => 'entity_reference_label',
'weight' => $options['weight']['view'] ?? 0,
'settings' => [
'link' => FALSE,
'link' => TRUE,
],
];
break;

View File

@ -11,7 +11,7 @@ filters:
status: true
weight: -10
settings:
allowed_html: '<a href hreflang> <em> <strong> <del> <cite> <blockquote cite> <code> <ul type> <ol start type=''1 A I''> <li> <dl> <dt> <dd> <h2 id=''jump-*''> <h3 id> <h4 id> <h5 id> <h6 id>'
allowed_html: '<a href hreflang> <img src alt height width data-entity-type data-entity-uuid> <em> <strong> <del> <cite> <blockquote cite> <code> <ul type> <ol start type=''1 A I''> <li> <dl> <dt> <dd> <h2 id=''jump-*''> <h3 id> <h4 id> <h5 id> <h6 id>'
filter_html_help: true
filter_html_nofollow: false
filter_autop:

View File

@ -0,0 +1,36 @@
<?php
/**
* @file
* Install, update and uninstall function for the farm_geo module.
*/
/**
* Implements hook_requirements().
*/
function farm_geo_requirements($phase) {
$requirements = [];
// Do not check requirements in the update phase.
// The REQUIREMENT_WARNING severity prevents updates from being run.
if ($phase == 'update') {
return $requirements;
}
// Check for php-geos extension.
if (geoPHP::geosInstalled()) {
$severity = REQUIREMENT_OK;
$message = t('GEOS PHP extension installed');
}
else {
$severity = REQUIREMENT_WARNING;
$message = t('The GEOS PHP extension is not installed. While not required, it is strongly recommended for accurate geometry arithmetic. See %link for more information.', ['%link' => 'https://geophp.net/geos.html']);
}
$requirements['geos'] = [
'title' => t('GEOS PHP extension'),
'severity' => $severity,
'value' => $message,
];
return $requirements;
}

View File

@ -2,7 +2,7 @@ farm_settings.settings_page:
base_route: farm_settings.settings_page
route_name: farm_settings.settings_page
title: 'Farm Info'
weight: -1
weight: -5
farm_settings.modules_form:
base_route: farm_settings.settings_page

View File

@ -2,8 +2,9 @@
Styling for the asset map_popup view mode page.
*/
/* Remove the page background color. */
body {
/* Remove the page background color and body top padding. */
body.path-asset {
--ginHorizontalToolbarOffset: 0;
background: unset !important;
}

View File

@ -3,10 +3,15 @@
* Styling for farmOS toolbar.
*/
/* Add padding to the wrapper around the logo. */
.toolbar .toolbar-bar #toolbar-item-administration-tray .toolbar-logo {
padding: .5em !important;
}
/* Logo size and spacing. */
.toolbar .toolbar-bar #toolbar-item-administration-tray .toolbar-logo img.toolbar-icon-home {
max-width: 100px;
margin: 40px auto;
margin: 20px auto;
}
/* Locations icon. */

View File

@ -27,10 +27,11 @@ function farm_ui_theme_install() {
$gin_settings->set('preset_focus_color', 'orange');
// Use farmOS logo and favicon.
$gin_settings->set('icon_default', FALSE);
$gin_settings->set('icon_path', drupal_get_path('module', 'farm_ui_theme') . '/logo.png');
$path = \Drupal::service('extension.list.module')->getPath('farm_ui_theme');
$gin_settings->set('logo.use_default', FALSE);
$gin_settings->set('logo.path', $path . '/logo.png');
$gin_settings->set('favicon.use_default', FALSE);
$gin_settings->set('favicon.path', drupal_get_path('module', 'farm_ui_theme') . '/favicon.ico');
$gin_settings->set('favicon.path', $path . '/favicon.ico');
// Save Gin settings.
$gin_settings->save();
@ -81,3 +82,15 @@ function farm_ui_theme_uninstall() {
$local_actions_block->set('settings.provider', 'core');
$local_actions_block->save();
}
/**
* Update Gin theme logo settings.
*/
function farm_ui_theme_update_9000(&$sandbox) {
$gin_settings = \Drupal::configFactory()->getEditable('gin.settings');
$gin_settings->set('logo.use_default', $gin_settings->get('icon_default'));
$gin_settings->clear('icon_default');
$gin_settings->set('logo.path', $gin_settings->get('icon_path'));
$gin_settings->clear('icon_path');
$gin_settings->save();
}

View File

@ -9,6 +9,7 @@ dependencies:
- csv_serialization
- entity_browser
- farm_location
- farm_ui_views
- image
- options
- rest
@ -1715,23 +1716,6 @@ display:
- url.query_args
- user.permissions
tags: { }
page_1:
id: page_1
display_title: 'Assigned (page)'
display_plugin: page
position: 7
display_options:
arguments:
owner_target_id:
id: owner_target_id
table: asset__owner
field: owner_target_id
relationship: none
group_type: group
admin_label: ''
entity_type: asset
entity_field: owner
plugin_id: numeric
default_action: 'not found'
exception:
value: all
@ -1746,38 +1730,117 @@ display:
summary_options:
base_path: ''
count: true
override: false
items_per_page: 25
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: true
validate:
type: 'entity:user'
cache_metadata:
max-age: 0
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- user.permissions
tags: { }
page_term:
display_plugin: page
id: page_term
display_title: 'By term (page)'
position: 4
display_options:
display_extenders: { }
display_description: ''
path: taxonomy/term/%taxonomy_term/assets/%entity_bundle
arguments:
asset_taxonomy_term_reference:
id: asset_taxonomy_term_reference
table: asset_field_data
field: asset_taxonomy_term_reference
relationship: none
group_type: group
admin_label: ''
break_phrase: false
default_action: 'not found'
exception:
value: all
title_enable: false
title: All
title_enable: false
title: ''
default_argument_type: fixed
default_argument_options:
argument: ''
default_argument_skip_url: false
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:taxonomy_term'
fail: 'not found'
validate_options:
operation: view
access: false
multiple: 0
bundles: {}
entity_type: asset
plugin_id: entity_taxonomy_term_reference
type:
id: type
table: asset_field_data
field: type
relationship: none
group_type: group
admin_label: ''
default_action: 'not found'
exception:
value: all
title_enable: false
title: All
title_enable: false
title: ''
default_argument_type: fixed
default_argument_options:
argument: all
default_argument_skip_url: false
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: 0
restrict_roles: false
roles: { }
access: false
bundles: { }
glossary: false
limit: 0
case: none
path_case: none
transform_dash: false
break_phrase: false
not: false
entity_type: asset
entity_field: type
plugin_id: string
defaults:
arguments: false
display_description: ''
display_extenders: { }
path: user/%user/assets
menu:
type: tab
title: Assets
description: ''
weight: 0
expanded: false
menu_name: account
parent: ''
context: '0'
cache_metadata:
max-age: 0
contexts:

View File

@ -2471,7 +2471,7 @@ display:
entity_type: log
entity_field: type
plugin_id: string
default_action: ignore
default_action: 'not found'
exception:
value: all
title_enable: false
@ -2521,6 +2521,112 @@ display:
- user
- user.permissions
tags: { }
page_term:
display_plugin: page
id: page_term
display_title: 'By term (page)'
position: 2
display_options:
display_extenders: { }
display_description: ''
path: taxonomy/term/%taxonomy_term/logs/%entity_bundle
arguments:
log_taxonomy_term_reference:
id: log_taxonomy_term_reference
table: log_field_data
field: log_taxonomy_term_reference
relationship: none
group_type: group
admin_label: ''
break_phrase: false
default_action: 'not found'
exception:
value: all
title_enable: false
title: All
title_enable: false
title: ''
default_argument_type: fixed
default_argument_options:
argument: ''
default_argument_skip_url: false
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:taxonomy_term'
fail: 'not found'
validate_options:
operation: view
access: false
multiple: 0
bundles: {}
entity_type: log
plugin_id: entity_taxonomy_term_reference
type:
id: type
table: log_field_data
field: type
relationship: none
group_type: group
admin_label: ''
default_action: 'not found'
exception:
value: all
title_enable: false
title: All
title_enable: false
title: ''
default_argument_type: fixed
default_argument_options:
argument: all
default_argument_skip_url: false
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:log_type'
fail: 'not found'
validate_options:
operation: view
multiple: 0
access: false
bundles: { }
glossary: false
limit: 0
case: none
path_case: none
transform_dash: false
break_phrase: false
entity_type: log
entity_field: type
plugin_id: string
defaults:
arguments: false
cache_metadata:
max-age: 0
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- user
- user.permissions
tags: { }
page_type:
id: page_type
display_title: 'By type (page)'

View File

@ -1,3 +1,4 @@
# Add log view tabs to assets.
farm.asset.logs:
title: 'Logs'
route_name: view.farm_log.page_asset
@ -13,3 +14,21 @@ farm.asset.logs.all:
parent_id: farm.asset.logs
farm.asset.logs.type:
deriver: Drupal\farm_ui_views\Plugin\Derivative\FarmLogViewsTaskLink
# Add asset/log view tabs to taxonomy terms.
farm.taxonomy_term.assets:
title: 'Assets'
route_name: view.farm_asset.page_term
route_parameters:
entity_bundle: 'all'
base_route: entity.taxonomy_term.canonical
weight: 50
farm.taxonomy_term.logs:
title: 'Logs'
route_name: view.farm_log.page_term
route_parameters:
entity_bundle: 'all'
base_route: entity.taxonomy_term.canonical
weight: 50
farm.taxonomy_term.entities.type:
deriver: Drupal\farm_ui_views\Plugin\Derivative\FarmTaxonomyTermViewsTaskLink

View File

@ -278,5 +278,8 @@ function farm_ui_views_get_bundle_argument(ViewExecutable $view, string $display
elseif ($view->id() == 'farm_log' && $display_id == 'page_asset' && !empty($args[1]) && $args[1] != 'all') {
$bundle = $args[1];
}
elseif (in_array($view->id(), ['farm_asset', 'farm_log']) && $display_id == 'page_term' && !empty($args[1]) && $args[1] != 'all') {
$bundle = $args[1];
}
return $bundle;
}

View File

@ -13,6 +13,24 @@ services:
arguments: [ '@entity_type.manager', '@asset.location' ]
tags:
- { name: access_check, applies_to: _asset_children_access }
farm_ui_views.asset_term_access:
class: Drupal\farm_ui_views\Access\FarmTaxonomyTermEntityViewsAccessCheck
arguments:
- 'asset'
- '@entity_type.manager'
- '@entity_type.bundle.info'
- '@entity_field.manager'
tags:
- { name: access_check, applies_to: _asset_term_access }
farm_ui_views.log_term_access:
class: Drupal\farm_ui_views\Access\FarmTaxonomyTermEntityViewsAccessCheck
arguments:
- 'log'
- '@entity_type.manager'
- '@entity_type.bundle.info'
- '@entity_field.manager'
tags:
- { name: access_check, applies_to: _log_term_access }
farm_ui_views.asset_inventory_access:
class: Drupal\farm_ui_views\Access\FarmInventoryAssetViewsAccessCheck
arguments: [ '@entity_type.manager' ]

View File

@ -20,4 +20,26 @@ function farm_ui_views_views_data_alter(array &$data) {
],
];
}
// Provide an asset_taxonomy_term_reference argument for views of assets.
if (isset($data['asset_field_data'])) {
$data['asset_field_data']['asset_taxonomy_term_reference'] = [
'title' => t('Asset Taxonomy Term Reference'),
'help' => t('Taxonomy Terms that are referenced by the asset.'),
'argument' => [
'id' => 'entity_taxonomy_term_reference',
],
];
}
// Provide a log_taxonomy_term_reference argument for views of logs.
if (isset($data['log_field_data'])) {
$data['log_field_data']['log_taxonomy_term_reference'] = [
'title' => t('Log Taxonomy Term Reference'),
'help' => t('Taxonomy Terms that are referenced by the log.'),
'argument' => [
'id' => 'entity_taxonomy_term_reference',
],
];
}
}

View File

@ -129,6 +129,41 @@ function farm_ui_views_views_pre_render(ViewExecutable $view) {
}
}
// If this is the farm_asset/farm_log View and page_term display, include
// the term's name.
if (in_array($view->id(), ['farm_asset', 'farm_log']) && $view->current_display == 'page_term') {
$term_id = $view->args[0];
$entity_bundle = $view->args[1];
$term = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->load($term_id);
if (!empty($term)) {
$vocabulary = \Drupal::entityTypeManager()->getStorage('taxonomy_vocabulary')->load($term->bundle());
$entity_bundle_label = '';
if ($entity_bundle != 'all') {
$bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($view->getBaseEntityType()->id());
if (!empty($bundles[$entity_bundle])) {
$entity_bundle_label = $bundles[$entity_bundle]['label'] . ' ' . $view->getBaseEntityType()->getPluralLabel();
}
}
if (!empty($entity_bundle_label)) {
$title = t('%bundle with %vocab term %term', [
'%bundle' => $entity_bundle_label,
'%vocab' => $vocabulary->label(),
'%term' => $term->label(),
]);
}
else {
$title = t('%base_type with %vocab term %term', [
'%base_type' => $view->getBaseEntityType()->getCollectionLabel(),
'%vocab' => $vocabulary->label(),
'%term' => $term->label(),
]);
}
}
}
// Set the title, if so desired.
if (!empty($title)) {
$view->setTitle($title);

View File

@ -0,0 +1,111 @@
<?php
namespace Drupal\farm_ui_views\Access;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Access\AccessResult;
/**
* Checks access for displaying Views of entities that reference taxonomy terms.
*/
class FarmTaxonomyTermEntityViewsAccessCheck implements AccessInterface {
/**
* The base entity type of the views this access check will be applied to.
*
* @var string
*/
protected $baseEntityType;
/**
* The taxonomy term storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $taxonomyTermStorage;
/**
* The entity type bundle info.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* The entity field manager service.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* FarmTaxonomyTermEntityViewsAccessCheck constructor.
*
* @param string $base_entity_type
* The base entity type of the views this access check will be applied to.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_bundle_info
* The entity type bundle info service.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager service.
*/
public function __construct($base_entity_type,
EntityTypeManagerInterface $entity_type_manager,
EntityTypeBundleInfoInterface $entity_bundle_info,
EntityFieldManagerInterface $entity_field_manager) {
$this->baseEntityType = $base_entity_type;
$this->taxonomyTermStorage = $entity_type_manager->getStorage('taxonomy_term');
$this->entityTypeBundleInfo = $entity_bundle_info;
$this->entityFieldManager = $entity_field_manager;
}
/**
* A custom access check to filter out irrelevant entity bundles.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
*/
public function access(RouteMatchInterface $route_match) {
// If there is no "taxonomy_term" or "asset_type" parameter, bail.
$term_id = $route_match->getParameter('taxonomy_term');
$entity_bundle = $route_match->getParameter('entity_bundle');
if (empty($term_id) || empty($entity_bundle)) {
return AccessResult::forbidden();
}
$term = $this->taxonomyTermStorage->load($term_id);
// Loop through all the entity bundles of the base entity type for the view
// and only return AccessResult::allowed() for those which have a taxonomy
// term entity reference field referencing the taxonomy term bundle of the
// term we loaded above.
$bundles = $this->entityTypeBundleInfo->getBundleInfo($this->baseEntityType);
foreach (array_keys($bundles) as $type) {
// If the route argument is 'all' then we check all the bundles, otherwise
// only check the one that matches.
if ($entity_bundle == 'all' || $type == $entity_bundle) {
$field_definitions = $this->entityFieldManager->getFieldDefinitions($this->baseEntityType, $type);
foreach (array_values($field_definitions) as $field_definition) {
if ($field_definition->getType() == "entity_reference" && $field_definition->getSetting('target_type') == "taxonomy_term") {
$handler_settings = $field_definition->getSetting('handler_settings') ?? [];
if (in_array($term->bundle(), $handler_settings['target_bundles'] ?? [])) {
return AccessResult::allowed();
}
}
}
}
}
return AccessResult::forbidden();
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace Drupal\farm_ui_views\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides task links for farmOS Taxonomy Term Views.
*/
class FarmTaxonomyTermViewsTaskLink extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* The entity type bundle info.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* Constructs a FarmTaxonomyTermViewsTaskLink instance.
*
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_bundle_info
* The entity type bundle info service.
*/
public function __construct(EntityTypeBundleInfoInterface $entity_bundle_info) {
$this->entityTypeBundleInfo = $entity_bundle_info;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.bundle.info')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$links = [];
foreach (['asset', 'log'] as $entity_type) {
$links["farm.taxonomy_term.{$entity_type}s.all"] = [
'id' => "farm.taxonomy_term.{$entity_type}s.all",
'title' => 'All',
'parent_id' => "farm.taxonomy_term.{$entity_type}s",
'route_name' => "view.farm_$entity_type.page_term",
'route_parameters' => [
'entity_bundle' => 'all',
],
] + $base_plugin_definition;
// Add links for each entity bundle.
$entity_bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type);
foreach ($entity_bundles as $entity_bundle => $info) {
$links["farm.taxonomy_term.{$entity_type}s.$entity_bundle"] = [
'id' => "farm.taxonomy_term.{$entity_type}s.$entity_bundle",
'title' => $info['label'],
'parent_id' => "farm.taxonomy_term.{$entity_type}s",
'route_name' => "view.farm_$entity_type.page_term",
'route_parameters' => [
'entity_bundle' => $entity_bundle,
],
] + $base_plugin_definition;
}
}
return $links;
}
}

View File

@ -0,0 +1,194 @@
<?php
namespace Drupal\farm_ui_views\Plugin\views\argument;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\taxonomy\Plugin\views\argument\Taxonomy;
use Drupal\views\Views;
/**
* Argument handler for taxonomy term references from an arbitrary entity field.
*
* @ingroup views_argument_handlers
*
* @ViewsArgument("entity_taxonomy_term_reference")
*/
class EntityTaxonomyTermReferenceArgument extends Taxonomy {
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity type bundle info.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* The entity field manager service.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* EntityTaxonomyTermReferenceArgument constructor.
*
* @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\EntityStorageInterface $term_storage
* The taxonomy term storage service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_bundle_info
* The entity type bundle info service.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager service.
*/
public function __construct(array $configuration,
$plugin_id,
$plugin_definition,
EntityStorageInterface $term_storage,
EntityTypeManagerInterface $entity_type_manager,
EntityTypeBundleInfoInterface $entity_bundle_info,
EntityFieldManagerInterface $entity_field_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $term_storage);
$this->entityTypeManager = $entity_type_manager;
$this->entityTypeBundleInfo = $entity_bundle_info;
$this->entityFieldManager = $entity_field_manager;
}
/**
* {@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')->getStorage('taxonomy_term'),
$container->get('entity_type.manager'),
$container->get('entity_type.bundle.info'),
$container->get('entity_field.manager'));
}
/**
* {@inheritdoc}
*/
public function query($group_by = FALSE) {
// Getting the arguments through views rather than
// from the Drupal route is important since it allows
// the contextual filter previews in the views UI to
// work correctly.
$term_id = $this->argument;
$entity_bundle = $this->view->args[1] ?: 'all';
if (empty($term_id)) {
return;
}
$term = $this->termStorage->load($term_id);
// This is a value like 'asset' or 'log'.
$base_entity_type = $this->view->getBaseEntityType()->id();
$entity_storage = $this->entityTypeManager->getStorage($base_entity_type);
if (!($entity_storage instanceof SqlContentEntityStorage)) {
return;
}
$entity_data_table = $entity_storage->getDataTable();
$entity_table_mapping = $entity_storage->getTableMapping();
$conditions = [];
// Keep track of which field tables we've already joined with since some
// assets share the same field e.g. plant and seed assets.
$already_joined_term_field_tables = [];
// Loop through all the bundles of the base entity type for this view.
$bundles = $this->entityTypeBundleInfo->getBundleInfo($base_entity_type);
foreach (array_keys($bundles) as $type) {
// Consider either all of them or just the one matching the
// bundle argument.
if ($entity_bundle == 'all' || $type == $entity_bundle) {
$field_definitions = $this->entityFieldManager->getFieldDefinitions($base_entity_type, $type);
foreach ($field_definitions as $field_id => $field_definition) {
// Look for taxonomy term entity reference fields which reference the
// target bundle of the term we loaded above.
if ($field_definition->getType() == "entity_reference" && $field_definition->getSetting('target_type') == "taxonomy_term") {
$handler_settings = $field_definition->getSetting('handler_settings') ?? [];
if (in_array($term->bundle(), $handler_settings['target_bundles'] ?? [])) {
// Now that we have found such a field, get the parameters to
// construct a join to allow us to filter only those entities
// which actually reference the term we loaded above.
$field_table_name = $entity_table_mapping->getFieldTableName($field_id);
// Don't add the same join more than once.
if (array_key_exists($field_table_name, $already_joined_term_field_tables)) {
continue;
}
$column_names = $entity_table_mapping->getColumnNames($field_id);
$target_id_column_name = $column_names['target_id'];
// Join the taxonomy reference field table with the entity.
/** @var \Drupal\views\Plugin\views\join\JoinPluginBase $join */
$join = Views::pluginManager('join')->createInstance('standard', [
'table' => $field_table_name,
'field' => 'entity_id',
'left_table' => $entity_data_table,
'left_field' => 'id',
'extra' => [
[
'field' => 'deleted',
'value' => 0,
],
[
'field' => $target_id_column_name,
'value' => $term->id(),
],
],
]);
// Add the relationship.
$relationship_alias = $this->query->addRelationship($field_table_name, $join, $entity_data_table);
// Keep track that we've now joined with that field table.
$already_joined_term_field_tables[$field_table_name] = 1;
// Add a condition to our final WHERE statement that the joined
// taxonomy term reference target id is not NULL.
$conditions[] = "$relationship_alias.$target_id_column_name IS NOT NULL";
}
}
}
}
}
if (!empty($conditions)) {
$combined_conditions = implode(" OR ", $conditions);
$this->query->addWhereExpression(0, "$entity_data_table.id IS NOT NULL AND ($combined_conditions)");
}
}
}

View File

@ -28,6 +28,18 @@ class RouteSubscriber extends RouteSubscriberBase {
$route->setRequirement('_asset_children_access', 'Drupal\farm_ui_views\Access\FarmAssetChildrenViewsAccessCheck::access');
}
// Add our _asset_term_access requirement to
// view.farm_asset.page_term.
if ($route = $collection->get('view.farm_asset.page_term')) {
$route->setRequirement('_asset_term_access', 'Drupal\farm_ui_views\Access\FarmTaxonomyTermEntityViewsAccessCheck::access');
}
// Add our _log_term_access requirement to
// view.farm_log.page_term.
if ($route = $collection->get('view.farm_log.page_term')) {
$route->setRequirement('_log_term_access', 'Drupal\farm_ui_views\Access\FarmTaxonomyTermEntityViewsAccessCheck::access');
}
// Add our _location_assets_access requirement to
// view.farm_asset.page_location.
if ($route = $collection->get('view.farm_asset.page_location')) {

View File

@ -0,0 +1,166 @@
<?php
namespace Drupal\Tests\farm_ui_views\Functional;
use Drupal\asset\Entity\Asset;
use Drupal\Tests\farm_test\Functional\FarmBrowserTestBase;
/**
* Tests the farm_ui_views taxonomy views routes.
*
* @group farm
*/
class TaxonomyTermTasksTest extends FarmBrowserTestBase {
/**
* Test user.
*
* @var \Drupal\user\Entity\User
*/
protected $user;
/**
* Test animal asset.
*
* @var \Drupal\asset\Entity\Asset
*/
protected $favaPlantType;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'classy';
/**
* {@inheritdoc}
*/
protected static $modules = [
'block',
'farm_plant',
'farm_seed',
'farm_ui_views',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('local_tasks_block');
// Create/login a user with permission to access taxonomy pages and assets.
$this->user = $this->createUser(['administer taxonomy', 'view any asset']);
$this->drupalLogin($this->user);
$entity_type_manager = $this->container->get('entity_type.manager');
$term_storage = $entity_type_manager->getStorage('taxonomy_term');
// Create a "Oat" plant type term.
$oat_plant_type = $term_storage->create([
'name' => 'Oat',
'vid' => 'plant_type',
]);
$oat_plant_type->save();
// Create a oat plant.
Asset::create([
'name' => 'Pringle\'s Progress Oat Planting',
'type' => 'plant',
'plant_type' => ['target_id' => $oat_plant_type->id()],
])->save();
// Create a "Fava Bean" plant type term.
$this->favaPlantType = $term_storage->create([
'name' => 'Fava Bean',
'vid' => 'plant_type',
]);
$this->favaPlantType->save();
// Create a fava plant.
Asset::create([
'name' => 'Red Flowering Fava Planting',
'type' => 'plant',
'plant_type' => ['target_id' => $this->favaPlantType->id()],
])->save();
// Create a fava seed.
Asset::create([
'name' => 'Red Flowering Fava Seeds',
'type' => 'seed',
'plant_type' => ['target_id' => $this->favaPlantType->id()],
])->save();
}
/**
* Test that the asset view task links appear on taxonomy term pages.
*/
public function testTaxonomyTermAssetTaskTabsAppear() {
$fava_term_url = 'taxonomy/term/' . $this->favaPlantType->id();
$this->drupalGet($fava_term_url);
$this->assertSession()->statusCodeEquals(200);
$get_array_of_link_text_by_url = function ($elems) {
$result = [];
foreach ($elems as $elem) {
$result[$elem->getAttribute('href')] = $elem->getText();
}
return $result;
};
$primary_tab_links = $get_array_of_link_text_by_url($this->xpath('//*[contains(@class, :class)]//a', [
':class' => 'tabs primary',
]));
$assert_has_link = function ($elements, $url, $label) {
$this->assertArrayHasKey($url, $elements, "No link exists with url '$url' among: " . print_r($elements, TRUE));
$this->assertEquals($label, $elements[$url], "Link label not as expected.");
};
$assert_has_link($primary_tab_links, "/$fava_term_url/assets/all", 'Assets');
$this->drupalGet("$fava_term_url/assets/all");
$this->assertSession()->statusCodeEquals(200);
$secondary_tab_links = $get_array_of_link_text_by_url($this->xpath('//*[contains(@class, :class)]//a', [
':class' => 'tabs secondary',
]));
$this->assertCount(3, $secondary_tab_links, 'Only 3 secondary tabs appear.');
$assert_has_link($secondary_tab_links, "/$fava_term_url/assets/all", 'All(active tab)');
$assert_has_link($secondary_tab_links, "/$fava_term_url/assets/plant", 'Plant');
$assert_has_link($secondary_tab_links, "/$fava_term_url/assets/seed", 'Seed');
}
/**
* Test that the views of assets for terms show the correct assets.
*/
public function testTaxonomyTermAssetViews() {
$fava_term_url = 'taxonomy/term/' . $this->favaPlantType->id();
$this->drupalGet("$fava_term_url/assets/all");
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('Red Flowering Fava Planting');
$this->assertSession()->pageTextContains('Red Flowering Fava Seeds');
$this->assertSession()->pageTextNotContains('Pringle\'s Progress Oat Planting');
$this->drupalGet("$fava_term_url/assets/plant");
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('Red Flowering Fava Planting');
$this->assertSession()->pageTextNotContains('Red Flowering Fava Seeds');
$this->assertSession()->pageTextNotContains('Pringle\'s Progress Oat Planting');
$this->drupalGet("$fava_term_url/assets/seed");
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextNotContains('Red Flowering Fava Planting');
$this->assertSession()->pageTextContains('Red Flowering Fava Seeds');
$this->assertSession()->pageTextNotContains('Pringle\'s Progress Oat Planting');
}
}