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,172 @@
# Authentication
farmOS includes an OAuth2 Authorization server for providing 1st and 3rd party
clients access to the farmOS API. Rather than using a user's username and
password to both *authorize and authenticate* a request, OAuth2 requires
users to complete an *authorization flow* that generates an `access_token`
to be used for authentication. Access tokens are provided to both 1st and
3rd party clients who wish to access the server's protected resources. Clients
store the `access token` instead of the user's credentials, which makes it a
more secure authentication method.
Read more about the [OAuth 2.0 standards](https://oauth.net/2/).
## Client Libraries
The [farmOS.py](https://github.com/farmOS/farmOS.py) and
[farmOS.js](https://github.com/farmOS/farmOS.js) client libraries use the
OAuth2 protocol to interact with the farmOS API.
## OAuth2 Bearer Tokens
Once you have an OAuth2 token, you can authenticate requests to the farmOS
server by including an `Authentication: Bearer {access_token}` header.
## OAuth2 Details
The OAuth protocol defines a process where users *authorize* 1st and 3rd
party *clients* with *scoped* access to data on the server. The following
describes the details necessary for using OAuth2 authorization with a farmOS
server.
### Scopes
OAuth Scopes define different levels of access. The farmOS server
implements scopes that represent individual roles or permissions. Users will
authorize clients with one or more scopes that determine how much access they
have to data on the server.
The farmOS Default Roles module provides an OAuth scope for each of the default
roles: `farm_manager`, `farm_worker`, and `farm_viewer`.
If you are creating an integration with farmOS, see the
[OAuth](/development/module/oauth) page of the farmOS module development docs
for steps to create additional OAuth Scopes.
### Clients
An OAuth Client represents a 1st or 3rd party integration with the farmOS
server. Clients are uniquely identified by a `client_id` and can have an
optional `client_secret` for private integrations. Clients are configured to
allow only specific OAuth grants and can specify default `scopes` that are
granted when none are requested.
The core `farm_api_default_consumer` module provides a default client with
`client_id = farm` that can use the `password` and `refresh_token` grant. You
can use this client for general usage of the API, like writing a script that
communicates with *your* farmOS server, but it comes with limitations.
If you are creating an integration with farmOS, see the
[OAuth](/development/module/oauth) page of the farmOS module development docs
for steps to create an OAuth Client.
### Authorization Flows
The [OAuth 2.0 standards](https://oauth.net/2/) outline 3
[Oauth2 Grant Types](https://oauth.net/2/grant-types/) to be used in an OAuth2 Authorization Flow - They are
the *Authorization Code, Client Credentials* and *Refresh Token* Grants. The
[Authorization Code](#authorization-code-grant) and
[Refresh Token](#refreshing-tokens) grants are the only Authorization Flows recommended by
farmOS for use with 3rd party clients.
The **Client Credentials Grant** is often used for machine authentication not
associated with a user account. The client credentials grant should only be
used if a `client_secret` can be kept secret. If connecting to multiple
farmOS servers, each server should use a different secret. This is
challenging due to the nature of farmOS being a self-hosted application.
The [Password Credentials Grant](#password-credentials-grant) is a legacy
grant type that is
[no longer recommended](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-2.4).
Only use the Password Credentials Grant if the client can be trusted with a
farmOS username and password (this is considered *1st party*). Even if the
client is trusted, this grant type exposes the username and password and
results in an increased attack surface. In most cases the **Client Credentials
Grant** can be used with an OAuth client that is configured for each separate
integration.
#### Authorization Code Grant
The Authorization Code Grant is most popular for 3rd party client
authorization.
Requesting resources is a four step process:
**First**: the client sends a request to the farmOS server `/oauth/authorize`
endpoint requesting an `Authorization Code`. The user logs in and authorizes
the client to have the OAuth Scopes it is requesting.
Copy this link to browser -
http://localhost/oauth/authorize?response_type=code&client_id=farm&scope=farm_manager&redirect_uri=http://thirdparty/api/authorized&state=p4W8P5f7gJCIDbC1Mv78zHhlpJOidy
**Second**: after the user accepts, the server redirects
to the `redirect_uri` with an authorization `code` and `state` in the query
parameters.
Example redirect url from server:
http://thirdparty/api/authorized?code=9eb9442c7a2b011fd59617635cca5421cd089943&state=p4W8P5f7gJCIDbC1Mv78zHhlpJOidy
**Third**: copy the `code` and `state` from the URL into the body of a POST
request. The `grant_type`, `client_id`, `client_secret` and `redirect_uri` must
also be included in the POST body. The client makes a POST request to the
`/oauth/token` endpoint to retrieve an `access_token` and `refresh_token`.
$ curl -X POST -d "grant_type=authorization_code&code=ae4d1381cc67def1c10dc88a19af6ac30d7b5959&client_id=farm&redirect_uri=http://thirdparty/api/authorized" http://localhost/oauth/token
{"access_token":"3f9212c4a6656f1cd1304e47307927a7c224abb0","expires_in":"10","token_type":"Bearer","scope":"farm_manager","refresh_token":"292810b04d688bfb5c3cee28e45637ec8ef1dd9e"}
**Fourth**: the client sends the access token in the request header to access protected
resources. The header is an Authorization header with a Bearer token:
`Authorization: Bearer access_token`
$ curl --header "Authorization: Bearer b872daf5827a75495c8194c6bfa4f90cf46c143e" http://localhost/api
#### Password Credentials Grant
**NOTE:** The **Password Credentials Grant** is a legacy grant type that is
[no longer recommended](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-2.4).
Only use the **Password Grant** if the client can be trusted with a farmOS
username and password (this is considered *1st party*).
**NOTE:** The [Simple OAuth Password Grant](https://www.drupal.org/project/simple_oauth_password_grant)
module must be enabled to use the password grant.
The Password Credentials Grant uses a farmOS `username` and `password` to
retrieve an `access_token` and `refresh_token` in one step. For the user, this
is the simplest type of *authorization.* Because the client can be trusted with
their farmOS Credentials, a users `username` and `password` can be collected
directly into a login form within the client application. These credentials are
then used (not stored) to request tokens which are used for *authentication*
with the farmOS server and retrieving data.
Requesting protected resources is a two step process:
**First**, the client sends a POST request to the farmOS server `/oauth/token`
endpoint with `grant_type` set to `password` and a `username` and `password`
included in the request body.
$ curl -X POST -d "grant_type=password&username=username&password=test&client_id=farm&scope=farm_manager" http://localhost/oauth/token
{"access_token":"e69c60dea3f5c59c95863928fa6fb860d3506fe9","expires_in":"300","token_type":"Bearer","scope":"farm_manager","refresh_token":"cead7d46d18d74daea83f114bc0b512ec4cc31c3"}
**second**, the client sends the `access_token` in the request header to access protected
resources. The header is an Authorization header with a Bearer token:
`Authorization: Bearer access_token`
$ curl --header "Authorization: Bearer e69c60dea3f5c59c95863928fa6fb860d3506fe9" http://localhost/api
#### Refreshing Tokens
The `refresh_token` can be used to retrieve a new `access_token` if the token
has expired.
It is a one step process:
The client sends an authenticated request to the `/oauth/token`endpoint with
`grant_type` set to `refresh_token` and includes the `refresh_token`,
`client_id` and `client_secret` in the request body.
$ curl -X POST -H 'Authorization: Bearer ad52c04d26c1002084501d28b59196996f0bd93f' -d 'refresh_token=52e7a0e12e8ddd08b155b3b3ee385687fef01664&grant_type=refresh_token&client_id=farm&client_secret=client_secret' http://localhost/oauth/token
{"access_token":"acdbfabb736e42aa301b50fdda95d6b7fd3e7e14","expires_in":"300","token_type":"Bearer","scope":"user_access","refresh_token":"b73f4744840498a26f43447d8cf755238bfd391a"}
The server responds with an `access_token` and `refresh_token` that can be used
in future requests. The previous `access_token` and `refresh_token` will no
longer work.

View File

@@ -0,0 +1,442 @@
# API Changes
## 3.x vs 2.x
The [Simple OAuth](https://www.drupal.org/project/simple_oauth) module has been
updated to version 6. This includes a few breaking changes which may affect API
integrations. farmOS includes code to handle the transition of its own OAuth
clients and scopes, but if you have made any additional clients that used
special roles they will also need to be updated.
The biggest changes are that the "Implicit" grant type has been
removed, and the "Password Credentials" grant type has been moved to an optional
"Simple OAuth Password Grant" module, which must be enabled in order to use that
grant type.
There have also been changes to how scopes are provided. User roles no longer
act as scopes by default. Instead, scopes must be created separately to
reference each role they represent. Scopes can also be associated with
individual permissions and can reference parent scopes to create
hierarchical scope trees. farmOS provides `static` scopes for each of the
default roles: `farm_manager`, `farm_worker` and `farm_viewer`.
The default farmOS client that is included with farmOS has also been
moved to a separate module that is not enabled by default. After the update to
farmOS 3.x, all access tokens will be invalidated, but refresh tokens will still
work to get a new access token.
Other notable changes:
- [Material quantities can reference multiple material types](https://www.drupal.org/node/3395697)
- Log `timestamp` is marked as `required` in JSON Schema
- Allowed values are declared in JSON Schema `oneOf` / `anyOf` enumerations for
more entity attributes.
## 2.x vs 1.x
farmOS 1.x used the [RESTful Web Services](https://drupal.org/project/restws)
module, which provided API endpoints for each entity type (asset, log, taxonomy
term, etc).
farmOS 2.x uses the new [JSON:API](https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module)
module included with Drupal core, which follows the [JSON:API](https://jsonapi.org/)
specification for defining API resources.
The root API endpoint is `/api`.
### JSON Schema
farmOS 2.x also provides [JSON Schema](https://json-schema.org/) information
about all available resources. The root endpoint for schema information is
`/api/schema`.
In farmOS 1.x, the `/farm.json` endpoint provided similar information in the
`resources` property. This has been removed in favor of JSON Schema.
### Authentication
See [API Authentication](/development/api/authentication) for more information
about authorizing and authenticating farmOS 2.x API requests.
Notable changes from 1.x include:
- The new authorization URL is `/oauth/authorize` (was `/oauth2/authorize`).
- The new token URL is `/oauth/token` (was `/oauth2/token`).
- Requests should use `Content-Type: application/vnd.api+json` (was
`Content-Type: application/json`).
### Farm info endpoint
In farmOS 1.x, an informational API endpoint was provided at `/farm.json`. This
included various information describing the farmOS server configuration,
authenticated user, installed languages and available entity types and bundles.
This information was provided as either a simple value or a JSON object:
```json
{
"name": "My Farm",
"url": "https://myfarm.mydomain.com",
"api_version": "1.0",
"system_of_measurement": "metric",
"user": { ... },
"languages": { ... },
"resources": { ... },
"metrics": { ... }
}
```
In farmOS 2.x, a root `/api` endpoint either provides this information, or is a
gateway to this information.
The simple values previously available from
`/farm.json` are now provided in the `meta.farm` object at `/api`:
```json
{
"jsonapi":{ ... },
"data":[],
"meta":{
"links":{
"me":{
"meta":{
"id":"7b2af019-3191-40ca-b221-616f9a365722"
},
"href":"http://localhost/api/user/user/7b2af019-3191-40ca-b221-616f9a365722"
}
},
"farm":{
"name":"My farm name",
"url":"http://localhost",
"version":"2.x",
"system_of_measurement": "metric"
}
},
"links":{ ... }
}
```
The `resources` object has been replaced with the `links` object that
describes all the available resource types and their endpoints. Information
previously provided in the other JSON objects are now available as standalone
resources at their respective endpoints:
- `user` - `/api/user/user`
- The authenticated user's ID is included in the `meta.links.me` object
with a link to the user's resource. The user's attributes, such as name
and language, can be retrieved from that endpoint.
- `languages` - `/api/configurable_language/configurable_language`
### Resource endpoints
In farmOS 1.x, API endpoints for each entity type were available at
`/[entity_type].json`.
For example: `/log.json`
In farmOS 2.x, a root `/api` endpoint is provided, with a `links` object that
describes all the available resource types and their endpoints. These follow
a URL pattern of `/api/[entity-type]/[bundle]`.
For example: `/api/log/activity`
"Bundles" are "sub-types" that can have different sets (bundles) of fields on
them. For example, a "Seeding Log" and a "Harvest Log" will collect different
information, but both are "Logs" (events).
To illustrate the difference between 1.x and 2.x, here are the endpoints for
retrieving all Activity logs.
- farmOS 1.x: `/log.json?type=farm_activity`
- farmOS 2.x: `/api/log/activity`
### IDs
farmOS 2.x assigns
[UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier)
(universally unique identifiers) to all resources, and uses them in the API.
This differs from farmOS 1.x, which used the integer IDs directly from the
auto-incrementing database table that the record was pulled from. The benefit
of UUIDs is they are guaranteed to be unique across multiple farmOS databases,
whereas the old IDs were not.
The internal integer IDs are not exposed via the API, so all code that needs to
integrate should use the new UUIDs instead.
Also note that the migration from farmOS 1.x to 2.x does not preserve the
internal integer IDs, so they may be different after migrating to 2.x.
### Record structure
JSON:API has some rules about how records are structured that differ from
farmOS 1.x. These rules make the API more explicit.
In farmOS 1.x, all the fields/properties of a record were on the same level.
For example, a simple observation log looked like this:
```
{
"id": "5"
"type": "farm_observation",
"name": "Test observation",
"timestamp": "1526584271",
"asset": [
{
"resource": "farm_asset",
"id": "123"
}
]
}
```
In farmOS 2.x, JSON:API dictates that the "attributes" and "relationships" of a
record be explicitly declared under `attributes` and `relationships` properties
in the JSON.
The same record in farmOS 2.x looks like:
```
{
"id": "9bc49ffd-76e8-4f86-b811-b721cb771327"
"type": "log--observation",
"attributes": {
"name": "Test observation",
"timestamp": "1526584271",
},
"relationships": {
"asset": {
"data": [
{
"type": "asset--animal",
"id": "75116e3e-c45e-431d-8b58-1fce6bb315cf",
}
]
}
}
}
```
### Filtering
The URL query parameters for filtering results have a different syntax in 2.x.
Refer to the [Drupal.org JSON:API Filtering documentation](https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module/filtering)
for more information.
To illustrate, this is how to filter activity logs by their completed status:
- farmOS 1.x: `/log.json?type=activity&done=1`
- farmOS 2.x: `/api/log/activity?filter[status]=complete`
### Text format
Long text fields (like `notes`) include `value` and `format` sub-properties,
where `value` is the text value, and `format` is the "Text format" to use when
displaying the text. This is used to filter user-supplied text, to only allow
certain HTML tags (filtering out potential XSS vulnerabilities), convert URLs
to links, etc.
This works the same in farmOS 2.x, but the default `format` has changed from
`farm_format` to `default`.
### Logs
#### Log types
The `farm_` prefix has been dropped from all log type names. For example, in
farmOS 1.x an Activity log was `farm_activity`, and in farmOS 2.x it is simply
`activity`.
Additionally, the "Soil test" and "Water test" log types have been merged into
a single "Lab test" log type.
Also note that "Sale" and "Purchase" logs have been moved out of farmOS core to
a new [farmOS Ledger](https://drupal.org/project/farm_ledger) module.
Below is the full list of log types in farmOS 1.x and their new names in 2.x:
- `farm_activity` -> `activity`
- `farm_harvest` -> `harvest`
- `farm_input` -> `input`
- `farm_maintenance` -> `maintenance`
- `farm_medical` -> `medical`
- `farm_observation` -> `observation`
- `farm_seeding` -> `seeding`
- `farm_soil_test` -> `lab_test`
- `farm_transplanting` -> `transplanting`
- `farm_water_test` -> `lab_test`
#### Log fields
Log field names are largely unchanged, with a few exceptions (note that *new*
fields are not listed here):
- `area` -> `location` (See "Areas" below)
- `date_purchase` -> `purchase_date`
- `done` -> `status` (see "Log status" below)
- `files` -> `file`
- `flags` -> `flag`
- `geofield` -> `geometry`
- `images` -> `image`
- `input_method` -> `method`
- `input_source` -> `source`
- `inventory` (merged into `quantity` entities)
- `log_category` -> `category`
- `log_owner` -> `owner`
- `material` (migrated to "Material" `quantity` entities)
- `seed_source` -> `source`
- `soil_lab` -> `lab` (see "Laboratory" below)
- `water_lab` -> `lab` (see "Laboratory" below)
- `quantity` (see "Quantities" below)
See also "Text format" above for information about the changes to the `format`
parameter of long text fields.
#### Log status
In farmOS 1.x, logs had a boolean property called `done` which was either `1`
(done) or `0` (not done).
In 2.x, the `done` property has changed to `status`, and can be set to either
`done` or `pending`. Additional states may be added in the future.
#### Laboratory
In farmOS 1.x, Soil test and Water test logs had a "Laboratory" field for
storing the name of the lab that performed the test as a string.
In 2.x, a new "Labs" taxonomy has been added, and the "Laboratory" field on
Lab test logs is a term reference field.
### Assets
Asset records in farmOS 1.x had an entity type of `farm_asset`. In farmOS 2.x,
the `farm_` prefix has been dropped. The entity type is now simply `asset`.
#### Asset types
Asset type names are largely unchanged, with one notable exception: the
"Planting" asset type has been renamed to "Plant".
Below is the full list of asset types in farmOS 1.x and their new names in 2.x:
- `animal` (unchanged)
- `compost` (unchanged)
- `equipment` (unchanged)
- `group` (unchanged)
- `planting` -> `plant`
- `sensor` (unchanged)
#### Asset fields
Asset field names are largely unchanged, with a few exceptions (note that *new*
fields are not listed here):
- `animal_castrated` -> `is_castrated`
- `animal_nicknames` -> `nickname`
- `animal_sex` -> `sex`
- `animal_tag` -> `id_tag`
- `archived` -> `status` and `archived` (see "Asset status" below)
- `crop` -> `plant_type`
- `date` -> `birthdate` (on `animal` assets)
- `description` -> `notes` (see also "Text format" above)
- `flags` -> `flag`
- `files` -> `file`
- `images` -> `image`
#### Asset status
In farmOS 1.x, assets had a property called `archived` which was either `0`,
which indicated that the asset was active, or a timestamp that recorded when
the asset was archived.
In farmOS 2.x, these have been split into two separate fields:
- `status` - The status of the asset (either `active` or `archived`).
- `archived` - The timestamp when the asset was archived. This will be empty
if the asset is active.
### Taxonomies
farmOS 2.x continues to use Drupal's core `taxonomy_term` entities to represent
vocabularies of terms. The vocabulary machine names have changed, to drop the
`farm_` prefix, and to standardize plurality.
- `farm_animal_types` -> `animal_type`
- `farm_areas` has been removed (see "Areas" below)
- `farm_log_categories` -> `log_category`
- `farm_materials` -> `material_type`
- `farm_season` -> `season`
- `farm_crops` -> `plant_type`
- `farm_crop_families` -> `crop_family`
- `farm_quantity_units` -> `unit`
### Areas
farmOS 1.x had the concept of "Areas" for representing places/locations. These
were taxonomy terms in the `farm_areas` vocabulary. In farmOS 2.x, these areas
are migrated to new asset types, and any asset can now be designated as a
"location". New asset types are provided, including "Land", "Structure", and
"Water", which have the "location" designation by default. Additional types can
be provided by modules.
Because any asset can be a location, some new fields are available on assets,
including:
- `is_location` - Boolean indicating whether or not other assets can be moved
to this asset.
- `is_fixed` - Boolean indicating that the asset has a fixed geometry and
therefore does not move.
- `intrinsic_geometry` - A geofield representing the intrinsic geometry of
"fixed" assets.
Additionally, two "computed" fields are available on all assets, which provide
quick access to the asset's current location and geometry, regardless of
whether or not it is "fixed":
- `geometry` - The asset's current geometry. This will be the same as the
`intrinsic_geometry` for "fixed" assets. Otherwise, it will mirror the
geometry of the asset's most recent movement log.
- `location` - The asset's current location (an asset reference). This will
always be empty for "fixed" assets. Otherwise, it will mirror the location
reference field of the asset's most recent movement log.
### Quantities
In farmOS 1.x, log quantities were saved within separate Field Collection
entities. farmOS used the [RESTful Web Services Field Collection](https://drupal.org/project/restws_field_collection)
module to hide the fact that these were separate entities, allowing their
field to be accessed and modified in the same request to the host entity.
In farmOS 2.x, quantities are represented as `quantity` entities. These are
referenced under a log's `relationships` in JSON:API, and have a JSON:API
resource name of `quantity--quantity`. In order to add a quantity to a new or
existing log, they must be created in a separate API request before they can be
referenced by the log. Quantities still have `measure`, `value`, `unit` and
`label` fields.
### Files
farmOS 1.x used the [RESTful Web Services File](https://www.drupal.org/project/restws_file)
module to enable file uploads via the API. The API accepted an array of
base64-encoded strings to be included in the JSON body payload of the host
entity.
In farmOS 2.x, file uploads are supported by the core JSON:API module. Instead
of base64-encoded strings, the API requires a separate `POST` of binary data
for each file to upload. This reflects "real" PHP upload semantics, allowing
for faster and larger file uploads via the API. This also means that files
cannot be uploaded in the same request that creates an entity. Instead, a file
can be uploaded to an *existing entity* in a single request, or the file can be
uploaded and assigned to an entity in two separate requests. Refer to the
[Drupal.org JSON:API File Uploads documentation](https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module/file-uploads)
for more information.
For example, to upload an image file to an existing observation log with `curl`:
curl https://example.com/api/log/observation/{UUID}/image \
-H 'Accept: application/vnd.api+json' \
-H 'Content-Type: application/octet-stream' \
-H 'Content-Disposition: attachment; filename="observation.jpg"' \
-H 'Authorization: Bearer …………' \
--data-binary @/path/to/observation.jpg

View File

@@ -0,0 +1,75 @@
# API
farmOS provides an API that other applications and systems can use to read and
write records via HTTP requests.
## Client Libraries
Client libraries are available for interacting with the farmOS API:
- [farmOS.js](https://github.com/farmOS/farmOS.js) - [documentation](https://farmos.org/development/farmos-js/)
- [farmOS.py](https://github.com/farmOS/farmOS.py) - [documentation](https://farmos.org/development/farmos-py/)
## JSON:API
farmOS adheres to the [JSON:API](https://jsonapi.org/) specification for
defining API resources and uses the [JSON:API](https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module)
module included with Drupal core.
Refer to the Drupal JSON:API [documentation](https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module)
for all features including:
- [Core concepts](https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module/core-concepts)
- [Filtering](https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module/filtering)
- [Pagination](https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module/pagination)
- [Sorting](https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module/sorting)
- and many more.
### Endpoints
farmOS uses the `/api` path prefix for all JSON:API endpoints.
A root `/api` endpoint provides information meta information about the
authenticated user and the farmOS server:
```json
"meta": {
"links": {
"me": {
"meta": {
"id": "e437f724-45cd-4c36-852b-e91f7daec5fd"
},
"href": "https://farmos.site/api/user/user/e437f724-45cd-4c36-852b-e91f7daec5fd"
}
},
"farm": {
"name": "Farm Name",
"url": "https://farmos.site",
"version": "3.x",
"system_of_measurement": "metric"
}
}
```
The root `/api` endpoint also provides a `links` object that describes all
the available resource types and their endpoints. These follow a URL pattern of
`/api/[entity-type]/[bundle]`.
For example: `/api/log/activity`
"Bundles" are "sub-types" that can have different sets (bundles) of fields on
them. For example, a "Seeding Log" and a "Harvest Log" will collect different
information, but both are "Logs" (events).
### IDs
farmOS assigns [UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier)
(universally unique identifiers) to all resources, and uses them in the API.
## JSON Schema
[JSON Schema](https://json-schema.org/) is used to describe the available API
resources.
To begin exploring the farmOS API schema, visit `/api/schema`. From there, you
can traverse a graph of interconnected schemas describing the entire API.

View File

@@ -0,0 +1,23 @@
# Coding standards
farmOS follows [Drupal coding standards](https://www.drupal.org/docs/develop/standards).
The farmOS development Docker image comes pre-installed with
[PHP CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) for detecting
code standard violations, and [PHPStan](https://phpstan.org) for static
analysis. All farmOS code must pass both.
The following command will run PHP CodeSniffer on all farmOS code:
docker exec -it -u www-data farmos_www_1 phpcs /opt/drupal/web/profiles/farm
If you see no output, then there are no issues.
In some cases, code standard violations can be fixed automatically with
`phpcbf`:
docker exec -it -u www-data farmos_www_1 phpcbf /opt/drupal/web/profiles/farm
The following command will run PHPStan on all farmOS code:
docker exec -it -u www-data farmos_www_1 phpstan analyze /opt/drupal/web/profiles/farm

View File

@@ -0,0 +1,42 @@
# Composer
The farmOS development Docker image comes pre-installed with
[Composer](https://getcomposer.org), which is used for dependency management.
## Running Composer in Docker
In order to run the `composer` command, you must use `docker exec` to run the
command inside the farmOS container.
docker exec -it -u www-data farmos_www_1 composer
For example, the following will run the `composer help` command:
docker exec -it -u www-data farmos_www_1 composer help'
**Warning**: If `composer update farmos/farmos` is run, it will replace the
Git repository in `web/profiles/farm`, discarding all
changes/branches/remotes/etc.
## Common tasks
Some common Composer tasks are documented here.
### Adding a module
composer require drupal/[module]
This will download the module into the `web/modules/contrib` directory, and add
it to the root `composer.json` file.
If the module is being added to the farmOS installation profile itself, you
need to manually move the `require` line from the root `composer.json` to
`web/profiles/farm/composer.json` and commit it to that repository.
To install the module, use [Drush](/development/environment/drush).
## Notes
- `Could not delete /var/www/html/web/sites/default/default.settings.php`
See https://www.drupal.org/docs/develop/using-composer/starting-a-site-using-drupal-composer-project-templates#s-troubleshooting-permission-issues-prevent-running-composer

View File

@@ -0,0 +1,39 @@
# Debugging
The farmOS development Docker image comes pre-installed with
[XDebug](https://xdebug.org) 3, which allows debugger connections on port 9003.
XDebug can be configured to discover the client host automatically with the
following `extra_hosts` and `environment` configuration in `docker-compose.yml`:
extra_hosts:
- host.docker.internal:host-gateway
environment:
XDEBUG_MODE: debug
XDEBUG_CONFIG: client_host=host.docker.internal
## PHPStorm
If you are using the PHPStorm IDE, some additional environment variables are
necessary:
XDEBUG_SESSION: PHPSTORM
PHP_IDE_CONFIG: serverName=localhost
For example:
extra_hosts:
- host.docker.internal:host-gateway
environment:
XDEBUG_MODE: debug
XDEBUG_CONFIG: client_host=host.docker.internal
XDEBUG_SESSION: PHPSTORM
PHP_IDE_CONFIG: serverName=localhost
With this configuration in place, enable the "Start listening for PHP Debug
Connections" option. Add a breakpoint in your code, load the page in your
browser, and you should see a prompt appear in PHPStorm that will begin the
debugging session and pause execution at your breakpoint.
This also works with command-line scripts like `drush`. You may need to map the
path to Drush (`vendor/drush`) in the PHPStorm debugger config.

View File

@@ -0,0 +1,37 @@
# Docker
## Docker build arguments
The farmOS Docker images allow certain variables to be overridden at
image build time using the `--build-arg` parameter of `docker build`.
Available arguments and their default values are described below:
- `FARMOS_REPO` - The farmOS Git repository URL.
- Default: `https://github.com/farmOS/farmOS.git`
- `FARMOS_VERSION` - The farmOS Git branch/tag/commit to check out.
- Default: `3.x`
- `PROJECT_REPO` - The farmOS Composer project Git repository URL.
- Default: `https://github.com/farmOS/composer-project.git`
- `PROJECT_VERSION` - The farmOS Composer project Git branch/tag/commit to
check out.
- Default: `3.x`
## Development image
The `3.x-dev` image also provides the following build arguments:
- `WWW_DATA_ID` - The ID to use for the `www-data` user and group inside the
image. Setting this to the ID of the developer's user on the host machine
allows Composer to create files owned by www-data inside the container,
while keeping those files editable by the developer outside of the
container. If your user ID is not `1000`, build the image with:
`--build-arg WWW_DATA_ID=$(id -u)`
- Default: `1000`
To build the development image, it is necessary to add the `--target dev` flag
to the `docker build` command.
For example:
`docker build --build-arg WWW_DATA_ID=$(id -u) -t farmos/farmos:3.x-dev --target dev docker`

View File

@@ -0,0 +1,38 @@
# Documentation
In addition to the code for farmOS, this repository includes the source files of the
documentation which is hosted at [http://farmOS.org](http://farmos.org).
It uses [mkdocs](http://www.mkdocs.org) to convert simple markdown files into
static HTML files.
To get started contributing to the farmOS documentation, fork
[farmOS](https://github.com/farmOS/farmOS) on Github. Then install mkdocs and
clone this repo:
$ brew install python # For OSX users
$ sudo apt-get install python-pip # For Debian/Ubuntu users
$ sudo pip install mkdocs mkdocs-material
$ git clone https://github.com/farmOS/farmOS.git farmOS
$ cd farmOS
$ git remote add sandbox git@github.com:<username>/farmOS.git
$ mkdocs serve
Your local farmOS documentation site should now be available for browsing:
http://127.0.0.1:8000/. When you find a typo, an error, unclear or missing
explanations or instructions, hit ctrl-c, to stop the server, and start editing.
Find the page youd like to edit; everything is in the docs/ directory. Make
your changes, commit and push them, and start a pull request:
$ git checkout -b fix_typo # Create a new branch for your changes.
... # Make your changes.
$ mkdocs build --clean; mkdocs serve # Go check your changes.
$ git diff # Make sure there arent any unintended changes.
...
$ git commit -am "Fixed typo." # Useful commit message are a good habit.
$ git push sandbox fix_typo # Push your new branch up to your Github sandbox.
Visit your fork on Github and start a Pull Request.
For more information on writing and managing documentation with mkdocs, read the
official mkdocs documentation: [http://www.mkdocs.org](http://www.mkdocs.org)

View File

@@ -0,0 +1,28 @@
# Drush
The farmOS Docker image comes pre-installed with
[Drush](https://www.drush.org), which provides shell commands for working with
a Drupal installation.
## Running Drush in Docker
In order to run the `drush` command, you must use `docker exec` to run the
command inside the farmOS container.
docker exec -it -u www-data farmos_www_1 drush
For example, the following will run the `drush cr` command to rebuild caches:
docker exec -it -u www-data farmos_www_1 drush cr
## Useful commands
Some useful Drush commands are documented here.
### Rebuild caches
drush cr
### Install a module
drush en log

View File

@@ -0,0 +1,81 @@
# Local HTTPS
Some development testing is easier with farmOS on an `https://` endpoint.
A separate [Nginx](https://nginx.com) reverse proxy provides a simple way to
achieve this without any changes to the Apache configuration that runs in the
farmOS Docker container.
First, generate self-signed SSL certificate files into an `ssl` directory,
from the directory that your `docker-compose.yml` file is in:
```
mkdir ssl
openssl req -newkey rsa:4096 -x509 -sha256 -nodes -out ssl/openssl.crt -keyout ssl/openssl.key
```
Create a file called `nginx.conf` alongside `docker-compose.yml`:
```
events {}
http {
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
server {
server_name localhost;
listen 443 ssl;
ssl_certificate /etc/nginx/ssl/openssl.crt;
ssl_certificate_key /etc/nginx/ssl/openssl.key;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
proxy_pass http://www;
}
}
}
```
Add the following lines to `www/web/sites/default/settings.php`:
```
$settings['reverse_proxy'] = TRUE;
$settings['reverse_proxy_addresses'] = [$_SERVER['REMOTE_ADDR']];
$settings['reverse_proxy_trusted_headers'] = \Symfony\Component\HttpFoundation\Request::HEADER_X_FORWARDED_ALL;
```
Add the following service to your local `docker-compose.yml` file:
```
proxy:
image: nginx
depends_on:
- www
ports:
- '80:80'
- '443:443'
volumes:
- './nginx.conf:/etc/nginx/nginx.conf'
- './ssl:/etc/nginx/ssl'
```
Also remove port 80 from the `www` service:
```
ports:
- '80:80'
```
Finally, start the Docker services:
`docker compose up`
farmOS is now accessible via `https://localhost`.

View File

@@ -0,0 +1,71 @@
# Getting started
Follow these instructions to set up a local farmOS development environment.
The only requirement is [Docker](https://www.docker.com).
## 1. Set up Docker containers
Run the following commands to create a farmOS directory and set up Docker
containers for farmOS and PostgreSQL:
mkdir farmOS && cd farmOS
curl https://raw.githubusercontent.com/farmOS/farmOS/3.x/docker/docker-compose.development.yml -o docker-compose.yml
docker compose up -d
## 2. Install farmOS
Open `http://localhost` in a browser and install farmOS with the following
database credentials:
- Database type: **PostgreSQL**
- Database name: `farm`
- Database user: `farm`
- Database password: `farm`
- Advanced options > Host: `db`
## 3. Develop
After starting the Docker containers, the root `farmOS` directory will contain
two new subdirectories: `www` and `db`.
The `www` directory contains the fully built farmOS codebase, which is
bind-mounted into the `www` container's `/opt/drupal` directory. The `www/web`
directory is used as the Apache webroot. Loading the `www` directory in your
favorite PHP IDE will provide easy code access to the full Symfony + Drupal +
farmOS stack.
The `db` directory contains the PostgreSQL database files, which is
bind-mounted into the `db` container's `/var/lib/postgresql/data` directory.
With the containers stopped, this directory can be backed up (eg: via tarball)
to create snapshots for easy rollback during development.
## Optional
### Configure private filesystem
In order to upload files, a private file path must be configured. The following
line must be added to `www/web/sites/default/settings.php`:
$settings['file_private_path'] = '/opt/drupal/web/sites/default/private/files';
Additionally, create the folder `/opt/drupal/web/sites/default/private/`.
Set the correct user and permissions:
Folder ownership and group should match the web server user. If you are using
the farmOS Docker image (running Apache), this will be `www-data`.
Folder permissions should be set to `770` or `drwxrwx---`.
Finally, make sure to clear the caches by visiting Administration >
Configuration > Development > Performance and clicking the `Clear all caches`
button, or use Drush via the command line: `drush cr`.
### Configure debugger
See [Debugging](/development/environment/debug).
### Enable HTTPS
See [HTTPS](/development/environment/https).

View File

@@ -0,0 +1,11 @@
# PostgreSQL
The farmOS Docker image comes pre-installed with the PostgreSQL client `psql`
command, which can be used to connect to the database and run queries from
the command line.
## Open PostgreSQL prompt
docker exec -it farmos_www_1 psql -h db -d farm -U farm
Enter `farm` as the password.

View File

@@ -0,0 +1,63 @@
# Automated tests
The farmOS development Docker image comes pre-installed with all the
dependencies necessary for running automated tests via
[PHPUnit](https://phpunit.de).
The following command will run all automated tests provided by farmOS:
```sh
docker exec -it -u www-data farmos_www_1 phpunit --verbose --debug /opt/drupal/web/profiles/farm
```
Tests from other projects/dependencies can be run in a similar fashion. For
example, the following command will run all tests in the Log module:
```sh
docker exec -it -u www-data farmos_www_1 phpunit --verbose --debug /opt/drupal/web/modules/log
```
## Chrome/Selenium Container
The PHPUnit tests depend on having Chrome/Selenium available at port 4444 and hostname "chrome".
If using a docker-compose.yml based off [docker-compose.development.yml], this can be easily achieved
by adding the following container:
```yml
chrome:
# 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
The instructions above will run tests with XDebug enabled which may be helpful
for [debugging](/development/environment/debug), but is also slower. XDebug can be disabled
by setting the `XDEBUG_MODE` environment variable to "off".
In a docker-compose.yml based off [docker-compose.development.yml], this might look like:
```yml
www:
...
environment:
...
XDEBUG_MODE: 'off'
```
The tests could then be run via `docker compose exec` as follows:
```sh
docker compose exec -u www-data -T www phpunit --verbose --debug /opt/drupal/web/profiles/farm
```
Alternatively, the `XDEBUG_MODE` environment variable can be specified directly:
```sh
docker compose exec -u www-data -T --env XDEBUG_MODE=off www phpunit --verbose --debug /opt/drupal/web/profiles/farm
```
[run-tests.yml]: https://raw.githubusercontent.com/farmOS/farmOS/3.x/.github/workflows/run-tests.yml
[docker-compose.development.yml]: https://raw.githubusercontent.com/farmOS/farmOS/3.x/docker/docker-compose.development.yml

View File

@@ -0,0 +1,44 @@
# Updating local environment
The following commands will update your local farmOS development environment.
This approach avoids running `composer` commands because that is already done
when the Docker image is built.
**Warning**: This will replace everything except the `profiles` and `sites`
directories. If you are developing farmOS core, this will ensure that your
farmOS Git repository (inside `profiles/farm`) will not be touched. If you
are developing a custom module, make sure that it is in `sites/all/modules`,
otherwise it will be deleted.
```
# Run these commands from the local directory that contains docker-compose.yml.
# The Docker containers should be running.
# Backup www volume, just in case.
sudo tar -czf www.tar.gz www
# Pull latest 3.x-dev Docker image.
docker pull farmos/farmos:3.x-dev
# Move directories.
mv www/web/profiles ./profiles
mv www/web/sites ./sites
# Update codebase.
docker compose down
rm -r www
docker compose up -d
# Restore directories.
sudo rm -rf www/web/profiles www/web/sites
mv ./profiles www/web/profiles
mv ./sites www/web/sites
# Update farmOS profile.
cd www/web/profiles/farm
git checkout 3.x && git pull origin 3.x
# Run Drupal database updates.
docker compose exec -u www-data www drush updb
```

View File

@@ -0,0 +1,300 @@
# CSV importers
[CSV](https://en.wikipedia.org/wiki/Comma-separated_values) files are an easy
way to import data into farmOS.
The farmOS Import CSV module (`farm_import_csv`) provides a framework for
building CSV importers using Drupal's
[Migrate API](https://www.drupal.org/docs/drupal-apis/migrate-api).
The module uses this framework to provide "default" CSV importers for each
asset, log, and taxonomy term type. These are useful if you can fit your data
into them, but in some cases a more customized CSV template and/or import logic
might be necessary.
## Migration YML
Modules can provide their own CSV importers by adding a single YML file to
their `config/install` directory, which will add the importer when the module
is installed.
The YML file defines all the configuration necessary for the importer,
using the Drupal [Migrate Plus](https://drupal.org/project/migrate_plus)
module's `migration` configuration entity type.
The basic template for a CSV importer is as follows (replace all
`{{ VARIABLE }}` sections with your specific configuration):
```yaml
langcode: en
status: true
dependencies:
enforced:
module:
- {{ MODULE_NAME }}
id: {{ UNIQUE_ID }}
label: '{{ LABEL }}'
migration_group: farm_import_csv
migration_tags: []
source:
plugin: csv_file
destination:
plugin: 'entity:{{ ENTITY_TYPE }}'
process:
{{ MAPPING_CONFIG }}
migration_dependencies: { }
third_party_settings:
farm_import_csv:
access:
permissions:
- {{ PERMISSION_STRING }}
columns:
{{ COLUMN_DESCRIPTIONS }}
```
- `{{ MODULE_NAME }}` is the machine-name of the contrib module. Self-dependency
is recommended so that configuration objects are removed upon module
uninstall. If the module is not enforced in `dependencies: { }`, then
`drush config:delete migrate_plus.migration.{{ UNIQUE_ID }}` will need to be
run manually before reinstall.
- `{{ UNIQUE_ID }}` must be a unique machine-name for the importer, consisting
of only alphanumeric characters and underscores.
- `{{ LABEL }}` will be the name of the importer shown in the farmOS UI.
- `{{ ENTITY_TYPE }}` should be `asset`, `log`, or `taxonomy_term`.
- `{{ MAPPING_CONFIG }}` is where all the Drupal Migrate API's `process`
pipeline configuration is defined. This is responsible for mapping CSV column
names to entity fields (or additional processing).
See [Process pipeline](#process-pipeline) below for more information.
- `{{ PERMISSION_STRING }}` should be a Drupal permission that the user must
have in order to use the importer. Multiple permissions can be included on
separate lines.
- `{{ COLUMN_DESCRIPTIONS }}` should be an array of items with `name` and
`description` keys to describe each CSV column.
### Example
Here is an example of an importer for "egg harvests", which will import a CSV
with columns named `Date` and `Eggs`. It will create a harvest log named
"Collected [num] egg(s)" for each row with the number of eggs saved in a
standard `count` quantity:
`egg-harvests.csv`
```csv
Date,Eggs
2023-09-15,12
2023-09-16,14
2023-09-17,9
```
`config/install/migrate_plus.migration.egg_harvest.yml`
```yaml
langcode: en
status: true
dependencies: { }
id: egg_harvest
label: 'Egg harvest importer'
migration_group: farm_import_csv
migration_tags: []
source:
plugin: csv_file
constants:
UNIT: egg(s)
LOG_NAME_PREFIX: Collected
destination:
plugin: 'entity:log'
process:
# Hard-code the bundle.
type:
plugin: default_value
default_value: harvest
# Parse the log timestamp with strtotime() from Date column.
timestamp:
plugin: callback
callable: strtotime
source: Date
# Create or load "egg(s)" unit term.
_unit:
plugin: entity_generate
entity_type: taxonomy_term
value_key: name
bundle_key: vid
bundle: unit
source: constants/UNIT
# Create a quantity from the Eggs column.
quantity:
- plugin: skip_on_empty
source: Eggs
method: process
- plugin: static_map
map: { }
default_value: [ [ ] ]
- plugin: create_quantity
default_values:
type: standard
measure: count
values:
value: Eggs
units: '@_unit'
# Auto-generate the log name.
name:
plugin: concat
source:
- constants/LOG_NAME_PREFIX
- Eggs
- constants/UNIT
delimiter: ' '
# Mark the log as done.
status:
plugin: default_value
default_value: done
migration_dependencies: { }
third_party_settings:
farm_import_csv:
access:
permissions:
- create harvest log
columns:
- name: Date
description: Date of egg harvest.
- name: Eggs
description: Number of eggs harvested.
```
### Process pipeline
The `process` section of the YML is used to define a "process pipeline" for
mapping source data from CSV columns into properties of the destination entity.
Each `process` item declares one or more "process plugins" that can be chained
together to transform data before it is saved to the destination entity.
The simplest example is the `get` process plugin, which copies values from the
source to the destination without any modification. For example, the following
process pipeline will populate the log name from a CSV column called
`Log name`:
```yaml
...
process:
name:
- plugin: get
source: Log name
...
```
There is also a shorthand syntax for the `get` plugin, which is even simpler:
```yaml
...
process:
name: Log name
...
```
Chaining plugins together provides more advanced capabilities. For following
process pipeline will populate the log categories from a CSV column called
`Log categories`, using the `explode` plugin to split a comma-separated list
of categories into separate items, and the `term_lookup` plugin to look up
existing terms from the `log_category` vocabulary to reference:
```yaml
...
process:
category:
- plugin: explode
delimiter: ,
source: Log categories
- plugin: term_lookup
bundle: log_category
...
```
Only the first plugin in a process pipeline needs to define the `source` CSV
column name.
See [Resources](#resources) below for lists of available process plugins.
#### farmOS process plugins
In addition to the process plugins provided by Drupal core and the
[Migrate Plus](https://drupal.org/project/migrate_plus) module, farmOS also
provides some process plugins of its own.
##### asset_lookup
The `asset_lookup` plugin extends the `entity_lookup` plugin to make it easier
to populate asset reference fields on entities. It will attempt to look up an
asset using multiple properties, in the following order of precedence:
- UUID
- ID tag
- Name
- ID (primary key)
```yaml
...
process:
equipment:
- plugin: asset_lookup
bundle: equipment
source: Equipment used
...
```
The `bundle` property is optional, and will limit the allowed asset types. It
can be a single asset type, or an array of multiple types. If omitted, then
all asset types will be allowed.
This plugin assumes a single asset is being looked up. If a source CSV column
may have multiple comma-separate values, use an `explode` plugin before the
`asset_lookup`, and move the `source: Equipment used` to it, as demonstrated in
the example in [Process pipeline](#process-pipeline) above.
If the `source` CSV column contains any values, and any of the asset lookups
fail, the plugin will cause the whole row import to fail and an error will be
shown to the user.
The plugin will ignore case sensitivity, and will automatically trim whitespace
from the start and end of CSV source values.
##### term_lookup
The `term_lookup` plugin extends the `entity_lookup` plugin to make it easier
to populate taxonomy term reference fields on entities.
Example:
```yaml
process:
animal_type:
- plugin: term_lookup
bundle: animal_type
source: Animal type
```
The `bundle` property is required.
If the `source` CSV column contains any values, and any of the term lookups
fail, the plugin will cause the whole row import to fail and an error will be
shown to the user.
The plugin will ignore case sensitivity, and will automatically trim whitespace
from the start and end of CSV source values.
## Resources
A complete overview of all the options available with Drupal's Migrate API is
outside the scope of this documentation, but the following links are a good
place to learn more.
Also note that CSVs are just one type of data source for migrations. These
resources are not specific to CSV imports, but the same principles apply
generally.
- [Drupal Migrate API documentation](https://www.drupal.org/docs/drupal-apis/migrate-api)
- [Migrate API overview](https://www.drupal.org/docs/drupal-apis/migrate-api/migrate-api-overview)
- [Migrate process plugins overview](https://www.drupal.org/docs/8/api/migrate-api/migrate-process-plugins/migrate-process-overview).
- [31 days of Drupal migrations](https://understanddrupal.com/courses/31-days-of-migrations/)
- [List of core Migrate process plugins](https://www.drupal.org/docs/8/api/migrate-api/migrate-process-plugins/list-of-core-migrate-process-plugins)
- [List of process plugins provided by Migrate Plus](https://www.drupal.org/docs/8/api/migrate-api/migrate-process-plugins/list-of-process-plugins-provided-by-migrate-plus)

View File

@@ -0,0 +1,89 @@
# Data Stream
The data stream module provides a custom data stream entity for farmOS. Each
data stream entity represents a time-series stream of data provided by a
farmOS asset, such as Sensor or Equipment assets. Data streams can also specify
any assets which their data is describing, such as a Plant, Animal or
Land assets. Data streams are identified by a `UUID`, and have `private_key` and
`public` fields to limit access to their data.
Further functionality is provided by data stream types. Each type can provide a
custom settings form, methods to retrieve and save data, methods for handling
API requests, and custom display options. This allows custom data stream types
to save data to the farmOS DB or request data from a third party API.
## Using data stream types
The data stream bundle plugin class can be accessed from a data stream entity
with the `getPlugin()` method:
```php
// Load the data stream.
$data_streams = $this->entityTypeManager()->getStorage('data_stream')->loadByProperties([
'uuid' => $uuid,
]);
// Bail if UUID is not found.
if (empty($data_streams)) {
return;
}
/** @var \Drupal\data_stream\Entity\DataStreamInterface $data_stream */
$data_stream = reset($data_streams);
// Get the data stream plugin.
$plugin = $data_stream->getPlugin();
// Access methods on the plugin.
// Available methods will vary depending on type.
$data = $plugin->storageGet();
```
## Core data stream types
### Basic data stream
The data stream module provides a `basic` data stream type. Basic data streams
receive data via the farmOS API and save data to the farmOS SQL database. Each
basic data stream represents a single "value"; a sensor that records
temperature and humidity would provide two data streams. Data can be accessed
via the API with the `private_key`, or by anyone if the data stream is set to
`public`. Basic data streams also provide simple ways to view data in a
table or graph, and export as CSV.
### Listener (Legacy) data stream
The `farm_sensor_listener` module provides a `legacy_listener` data stream type
that is compatible with the Listener sensor type from farmOS 1.x. It is
similar to the `basic` type but has a few differences:
- Each data stream saves multiple values (eg: temperature and humidity are
saved to the same data stream)
- A `public_key` attribute identifies the data stream instead of a `UUID`.
- It responds to the legacy API endpoint at `/farm/sensor/listener/{public_key}`
(to match farmOS 1.x).
## Custom data stream types
Custom data stream types can be created to integrate with data stored outside
of farmOS (such as time-series databases or 3rd party APIs), provide advanced
views of data, and other custom behavior.
Data stream types can be provided by adding two files to a module:
1. An entity type config file (YAML), and:
2. A bundle plugin class (PHP).
For more information see [Entity Types](/development/module/entities).
### Data stream bundle plugin
Data stream bundle plugins must implement the `DataStreamTypeInterface`. The
`DataStreamTypeBase` class can be used as starting point.
Plugins can optionally implement the `DataStreamStorageInterface` and the
`DataStreamApiInterface` to adhere to a common interface other data stream
types might use.
See the "Basic" data stream bundle plugin as an example
(`Drupal\data_stream\Plugin\DataStream\DataStreamType\Basic`).

View File

@@ -0,0 +1,106 @@
# Entity types
Assets, logs, plans, taxonomy terms, users, etc are all types of "entities" in
farmOS/Drupal terminology. Entities can have sub-types called "bundles", which
represent "bundles of fields". Some fields may be common across all bundles of
a given entity type, and some fields may be bundle-specific.
## Adding asset, log, and plan types
Asset types, log types, and plan types can be provided by adding two files to a
module:
1. An entity type config file (YAML), and:
2. A bundle plugin class (PHP).
For example, the "Activity" log type is provided as follows:
`config/install/log.type.activity.yml`:
```yaml
langcode: en
status: true
dependencies:
enforced:
module:
- farm_activity
id: activity
label: Activity
description: ''
name_pattern: 'Activity log [log:id]'
workflow: farm_log_workflow
new_revision: true
```
`src/Plugin/Log/LogType/Activity.php`:
```php
<?php
namespace Drupal\farm_activity\Plugin\Log\LogType;
use Drupal\farm_entity\Plugin\Log\LogType\FarmLogType;
/**
* Provides the activity log type.
*
* @LogType(
* id = "activity",
* label = @Translation("Activity"),
* )
*/
class Activity extends FarmLogType {
}
```
## Bundle fields
Bundles can declare field definitions in their plugin class via the
`buildFieldDefinitions()` method.
A `farm_field.factory` helper service is provided to make this easier.
The Equipment asset type does this to add "Manufacturer", "Model", and
"Serial number" fields:
```php
/**
* {@inheritdoc}
*/
public function buildFieldDefinitions() {
$fields = parent::buildFieldDefinitions();
$field_info = [
'manufacturer' => [
'type' => 'string',
'label' => $this->t('Manufacturer'),
'weight' => [
'form' => -20,
'view' => -50,
],
],
'model' => [
'type' => 'string',
'label' => $this->t('Model'),
'weight' => [
'form' => -15,
'view' => -40,
],
],
'serial_number' => [
'type' => 'string',
'label' => $this->t('Serial number'),
'weight' => [
'form' => -10,
'view' => -30,
],
],
];
foreach ($field_info as $name => $info) {
$fields[$name] = \Drupal::service('farm_field.factory')->bundleFieldDefinition($info);
}
return $fields;
}
```
For more information, see [Adding fields](/development/module/fields).

View File

@@ -0,0 +1,286 @@
# Fields
## Adding fields
A module may add additional fields to assets, logs, and other entity types in
farmOS.
The following documents how to add fields to existing entity types. See
[Entity types](/development/module/entities) to understand how to create new
asset, log, and plan types with custom fields on them.
### Base fields
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. 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
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 = [];
// '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'),
'description' => t('My field description.'),
'weight' => [
'form' => 10,
'view' => 10,
],
];
// NOTE: Replace 'myfield' with the internal name of the field.
$fields['myfield'] = \Drupal::service('farm_field.factory')->baseFieldDefinition($options);
}
return $fields;
}
```
### Bundle fields
If the field should only be added to a single bundle (eg: only "Input" logs),
then they should be added as "bundle fields" via
`hook_farm_entity_bundle_field_info()`&ast;
&ast; Note that this is a custom hook provided by farmOS, which may be
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
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. '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'),
'description' => t('My field description.'),
'weight' => [
'form' => 10,
'view' => 10,
],
];
// NOTE: Replace 'myfield' with the internal name of the field.
$fields['myfield'] = \Drupal::service('farm_field.factory')->bundleFieldDefinition($options);
}
return $fields;
}
```
## Select options
Certain fields on assets and logs include a list of options to select from.
These include:
- **Flags** (on assets, logs, and plans)
- Monitor (`monitor`)
- Needs review (`needs_review`)
- Priority (`priority`)
- **Land types** (on Land assets)
- Property (`property`)
- Field (`field`)
- Bed (`bed`)
- Paddock (`paddock`)
- Landmark (`landmark`)
- Other (`other`)
- **Structure types** (on Structure assets)
- Building (`building`)
- Greenhouse (`greenhouse`)
- **Lab test type** (on Lab test logs)
- Soil test (`soil`)
- Water test (`water`)
- **ID tag type** (on assets)
- Electronic ID (`eid`, on all assets)
- Other (`other`, on all assets)
- Brand (`brand`, on Animal assets)
- Ear tag (`ear_tag`, on Animal assets)
- Leg band (`leg_band`, on Animal assets)
- Tattoo (`tattoo`, on Animal assets)
These options are provided as configuration entities by farmOS modules in the
form of YAML files.
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
An "Organic" flag can be provided by a module named `my_module` by creating a
file called `farm_flag.flag.organic.yml` in `my_module/config/install`:
```yaml
langcode: en
status: true
dependencies:
enforced:
module:
- my_module
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
can optionally specify the entity types and bundles that this flag applies to.
The `langcode` and `status` and `dependencies` are standard configuration
entity properties. By putting the module's name in "enforced modules" it will
ensure that the flag is removed when the module is uninstalled.
Flags can be limited to certain entity types and bundles via an optional
`entity_types` property. This accepts a set of entity types with arrays of
bundles that the flag applies to (or `all` to apply to all bundles). For
example, to create a flag that only applies to Animal assets:
```yaml
entity_types:
asset:
- animal
```
To create a flag that applies to all asset types and log types, but not plans,
specify `all` for the `asset` and `log` bundles, but omit the `plan` entity
type:
```yaml
entity_types:
asset:
- all
log:
- all
```
#### Land type
The "Land" module in farmOS provides a "Field" type like this:
`land/config/install/farm_land.land_type.field.yml`
```yaml
langcode: en
status: true
dependencies:
enforced:
module:
- farm_land
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:
`structure/config/install/farm_structure.structure_type.building.yml`
```yaml
langcode: en
status: true
dependencies:
enforced:
module:
- farm_structure
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:
`lab_test/config/install/farm_lab_test.lab_test_type.soil.yml`
```yaml
langcode: en
status: true
dependencies:
enforced:
module:
- farm_lab_test
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
also have an additional `bundle` property, which allows them to be limited to
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_id_tag.id_tag.ear_tag.yml`
```yaml
langcode: en
status: true
dependencies:
enforced:
module:
- farm_animal
- farm_id_tag
id: ear_tag
label: Ear tag
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

@@ -0,0 +1,91 @@
# farmOS module development
farmOS modules can be written to extend the capabilities of farmOS.
This document describes how to get started with farmOS module development. For
detailed documentation of Drupal development more generally, refer to the
[guide on drupal.org](https://www.drupal.org/docs/creating-custom-modules).
## Modules directory
Modules should be placed in the `sites/all/modules` directory of the server's
document root. If you are using the farmOS Docker image, this will be:
`/opt/drupal/web/sites/all/modules`
A good practice is to download farmOS-specific modules into `modules/farm` to
keep them separate. You may also consider creating a `modules/custom` directory
for custom modules that are specific to your farmOS instance.
## Namespacing
A farmOS (Drupal) module must have a unique name consisting only of
lowercase alphanumeric characters and underscores. This is used as a namespace
throughout the module, and allows Drupal hook functions to be executed on
behalf of your module.
It is best practice to prefix all farmOS-specific module names with `farm_`.
For example, if you were to build a module that adds a new log type called
`irrigation`, you might name it `farm_irrigation`. This serves to specify that
this module is made to work with farmOS, and is not designed to be installed in
other Drupal sites more generally.
## File structure
A farmOS (Drupal) module only requires one file for it to be recognized as a
module: `[modulename].info.yml` (where `[modulename]` is the module name).
This info YML file contains the module's human readable name, description,
dependency declarations, and other meta information about the module. A very
simple example looks like this:
`mylogtype.info.yml`:
```yaml
name: My log type
description: Adds my new custom log type.
type: module
package: farmOS Contrib
core_version_requirement: ^10
dependencies:
- farm:farm_entity
- log:log
```
In this example, we declare dependencies on the `farm_entity` module (provided
by the Drupal `farm` project, aka farmOS) and the `log` module (a separate
Drupal contrib project), because this module adds a log type. Dependencies will
vary depending on the needs of your module. Refer to the modules included with
farmOS for examples.
Other common files and directories in a module include:
- `[modulename].module` - Optional PHP file for Drupal hook implementations.
- `config/install/*.yml` - Configuration entities that will be installed with
the module.
- `config/optional/*.yml` - Optional configuration entities that will only be
installed if certain dependencies are met.
- `src/*` - PHP classes organized using the PSR-4 autoloading specification.
- `tests/*` - Automated tests for the module.
## Publishing
If you want to share your module, consider publishing the repository so that it
can be downloaded and installed by other farmOS users.
It is recommended that "contributed" farmOS modules be made available as a
"project" on [Drupal.org](https://drupal.org). This has two benefits:
- Projects can be included via Composer with: `composer require drupal/mymodule`
- Translations can be automatically managed and downloaded from Drupal's
centralized localization server:
[localize.drupal.org](https://localize.drupal.org)
A list of community modules that have been made available as Drupal projects
can be found in the [farmOS ecosystem](https://www.drupal.org/project/farm/ecosystem).
### License
farmOS modules that are distributed to others must be licensed under the
[GNU General Public License, version 2 or later](https://www.gnu.org/licenses/old-licenses/gpl-2.0.html).
For more information about farmOS and Drupal module licensing requirements,
refer to [Drupal.org Licensing](https://www.drupal.org/about/licensing).

View File

@@ -0,0 +1,358 @@
# Maps
farmOS includes features for rendering and manipulating geometry data in
map-based UIs.
It uses [farmOS-map](https://github.com/farmOS/farmOS-map), which is based on
the open-source [OpenLayers](https://openlayers.org/) project. This includes
tools for drawing and editing geometries, adding imagery and vector layers, and
a framework for writing custom behaviors.
farmOS-map is maintained by the farmOS community as a standalone library for
common agricultural mapping needs. It is designed to be reusable in any
application with similar needs. It is not specific to or dependent on farmOS
itself. Rather, farmOS includes it as a dependency, and provides some helpful
wrappers for using it inside modules. This page describes how to use farmOS-map
in farmOS modules.
For more information about the farmOS-map library itself and what it provides,
refer to the farmOS-map documentation on GitHub:
[github.com/farmOS/farmOS-map](https://github.com/farmOS/farmOS-map)
## Render element
Maps can be embedded in pages as a `farm_map` type render element.
```php
$build['mymap'] = [
'#type' => 'farm_map',
'#map_type' => 'default',
'#map_settings' => [
'mysetting' => 'myvalue',
],
'#behaviors' => [
'mybehavior',
],
];
```
**Properties:**
- `#map_type` (optional) - See [Map types](#map-types). Defaults to `default`.
- `#map_settings` (optional) - An array of map settings, which will be passed
into the map instance's client-side JavaScript object, so they are available
in behavior JavaScript.
- `#behaviors` (optional) - See [Behaviors](#behaviors). Defaults to `[]` (but
behaviors may also be added by map types and render events).
## Form element
Editable maps can be embedded in forms with a `farm_map_input` type element.
These maps will have the drawing/editing controls enabled, allowing geometries
to be added/edited/deleted directly in the map. A default value can be used to
pre-populate the map with a geometry. A text field can be optionally displayed
beneath the map to show the raw geometry data (auto-updates during editing).
**Example:**
```php
$form['mymap'] = [
'#type' => 'farm_map_input',
'#title' => t('My Geometry'),
'#map_type' => 'default',
'#map_settings' => [
'mysetting' => 'myvalue',
],
'#behaviors' => [
'mybehavior',
],
'#display_raw_geometry' => TRUE,
'#default_value' => 'POINT(-45.967095060886315 32.77503850904169)',
];
```
**Properties:**
- `#map_type` (same as render element, above)
- `#map_settings` (same as render element, above)
- `#behaviors` (same as render element, above)
- `#display_raw_geometry` (optional) - Whether to show a text field below the
map with the raw geometry value in Well-Known Text (WKT) format. Defaults to
`FALSE`.
- `#default_value` (optional) - The default geometry value to display in the
map initially, in Well-Known Text (WKT) format. This geometry will be
editable in the map unless `#disabled` is `TRUE`.
## Map types
farmOS modules can optionally define "map types", which are then referenced in
the `#map_type` property of the render and form elements.
**This is optional and in most cases the `default` map type is sufficient.**
Map types are used to define reusable map configurations with common
[behaviors](#behaviors). They can be targeted by [render event](#render-events)
subscribers to add/modify behavior in certain contexts.
Map types are represented as Drupal config entities, installed via modules,
just like asset types, log types, flags, etc.
A very simple example of a custom map type definition looks like this:
`my_module/config/install/farm_map.map_type.mymaptype.yml`
```yaml
langcode: en
status: true
dependencies:
enforced:
module:
- my_module
id: mymaptype
label: My Map Type
description: "My module's custom map type."
behaviors: { }
options: { }
```
**Properties**
- `id` - A unique ID for the map type. This will be referenced in `#map_type`.
- `label` - A human-readable label for the map type.
- `description` - A human-readable description for the map type.
- `behaviors` - A list of [behaviors](#behaviors) to attach to maps of this
type by default.
- `options` - Default options that will be merged with `#map_settings` and
passed into `farmOS.map.create()`. See:
[github.com/farmOS/farmOS-map#creating-a-map](https://github.com/farmOS/farmOS-map#creating-a-map)
## Behaviors
The farmOS-map library uses the concept of "behaviors" to encapsulate common
and reusable sets of map behavior logic into JavaScript objects that can be
"attached" to map instances.
Behaviors can be used to add layers to a map, add new buttons/controls, enable
OpenLayers interactions, connect maps with other elements of a page like forms,
etc.
For general information about farmOS-map behaviors, see:
[github.com/farmOS/farmOS-map#adding-behaviors](https://github.com/farmOS/farmOS-map#adding-behaviors)
Some behaviors that farmOS provides include:
- `wkt` - Adds a vector layer to the map based on a Well-Known Text (WKT)
string. Edit controls can be optionally enabled to allow drawing, modifying,
moving, and deleting geometries within the map. This behavior is enabled
automatically in the `farm_map_input` form element, and when `wkt` is
included in `#map_settings.`
- `input` - Listens for changes to geometries in the map and copies them to a
form input (`textfield` or `hidden`) to be saved/manipulated server-side.
This behavior is enabled automatically in the `farm_map_input` form element.
- `popup` - Adds a popup interaction to the map, which appears when a geometry
feature is clicked.
- `asset_type_layers` - Adds asset geometry vector and cluster layers to a
map. This behavior is responsible for adding the "Locations" layers on the
farmOS dashboard map, the "Assets" and "Asset counts" layers to asset maps,
automatically zooming to visible geometries, and adding asset details to
popups when a geometry is clicked (depends on the `popup` behavior).
### Providing behaviors
Modules can provide their own behaviors with a couple of additional files.
The behavior itself is represented as a Drupal config entity, which gets
installed as a YML config file during module installation.
For example (replace `my_module` with the module name, and `mybehavior` with
the behavior name):
`my_module/config/install/farm_map.behavior.mybehavior.yml`
```yaml
langcode: en
status: true
dependencies:
enforced:
module:
- my_module
id: mybehavior
label: My Behavior
description: 'Adds my custom behavior logic.'
library: 'my_module/behavior_mybehavior'
settings: { }
```
The module must declare the behavior JavaScript file as a "library" so that
it can be included in the page(s) that need it.
For example (replace `my_module` with the module name, and `mybehavior` with
the behavior name):
`my_module/my_module.libraries.yml`
```yaml
behavior_mybehavior:
js:
js/farmOS.map.behaviors.mybehavior.js: { }
dependencies:
- farm_map/farm_map
```
Finally, the behavior JavaScript file should have a path and filename that
matches the library definition.
For example (replace `my_module` with the module name, and `mybehavior` with
the behavior name):
`my_module/js/farmOS.map.behaviors.mybehavior.js`
```js
(function () {
farmOS.map.behaviors.mybehavior = {
attach: function (instance) {
// My custom behavior logic.
}
};
}());
```
The `instance` object represents the farmOS-map instance, and includes helper
methods for common needs (eg: `instance.addLayer()`), as well as direct access
to the OpenLayers map object at `instance.map`.
For more information see:
[github.com/farmOS/farmOS-map](https://github.com/farmOS/farmOS-map)
### Attaching behaviors
Behaviors can be "attached" (enabled) in a map in a few different ways:
- [Map types](#map-types) can include a list of default `behaviors`.
- The `#behaviors` property of the `farm_map` [render element](#render-element)
and `farm_map_input` [form element](#form-element) can add specific behaviors
to individual elements.
- A [render event](#render-events) subscriber can use the
`$event->addBehavior()` method.
In all cases the behavior's `id` (as defined it its config entity YML) is used.
### Behavior settings
Some behaviors may require additional settings based on their context. Best
practice is to include these in the map settings so that they are available in
the behavior JavaScript in the following way:
`const settings = instance.farmMapSettings.behaviors.mybehavior;`
This can be accomplished in different ways, depending on how the behavior is
being attached to the map.
[Map types](#map-types) can add behavior settings to their `options` property.
For example:
```yaml
langcode: en
status: true
dependencies:
enforced:
module:
- my_module
id: mymaptype
label: My Map Type
description: "My module's custom map type."
behaviors:
- mybehavior
options:
behaviors:
mybehavior:
mysetting: True
```
Maps added as [render](#render-element) or [form](#form-element) elements can
add behavior settings in their `#map_settings` property. For example:
```php
$build['mymap'] = [
'#type' => 'farm_map',
'#map_settings' => [
'behaviors' => [
'mybehavior' => [
'mysetting' => TRUE,
],
],
],
'#behaviors' => [
'mybehavior',
],
];
```
Behaviors that are added via [render event](#render-events) subscribers can add
settings at the same time:
```php
$event->addBehavior('mybehavior', ['mysetting' => TRUE]);
```
All of the above approaches will make the settings available in the behavior
JavaScript in the same place.
## Render events
farmOS will trigger an event when a map is rendered. Modules can set up an
event subscriber to perform additional logic at that time, such as adding
behaviors.
For example, to add a behavior to all maps in farmOS, add the following two
files (replace `my_module` with the module name, and `mybehavior` with the
behavior name):
`my_module/my_module.services.yml`
```yaml
services:
my_module_map_render_event_subscriber:
class: Drupal\my_module\EventSubscriber\MapRenderEventSubscriber
tags:
- { name: 'event_subscriber' }
```
`my_module/src/EventSubscriber/MapRenderEventSubscriber`
```php
<?php
namespace Drupal\my_module\EventSubscriber;
use Drupal\farm_map\Event\MapRenderEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* An event subscriber for the MapRenderEvent.
*/
class MapRenderEventSubscriber implements EventSubscriberInterface {
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
MapRenderEvent::EVENT_NAME => 'onMapRender',
];
}
/**
* React to the MapRenderEvent.
*
* @param \Drupal\farm_map\Event\MapRenderEvent $event
* The MapRenderEvent.
*/
public function onMapRender(MapRenderEvent $event) {
$event->addBehavior('mybehavior');
}
}
```

View File

@@ -0,0 +1,136 @@
# OAuth
The [Simple OAuth](https://www.drupal.org/project/simple_oauth) module is used
to provide an [OAuth2 standard](https://oauth.net/2/) authorization server.
For documentation on using and authenticating with the farmOS API see [API](/api).
## Providing OAuth Scopes
OAuth Scopes define different levels of access. The farmOS server
implements scopes that represent individual roles or permissions. Users will
authorize clients with one or more scopes that determine how much access they
have to data on the server.
OAuth scopes are provided to the server using scope provider plugins. Each
scope provider determines how scopes are implemented and created. The OAuth
server must choose a single scope provider to provide all the scopes
necessary for the server's authorization needs. All scopes use the same
configuration and provide the same features regardless of the scope provider.
The [Simple OAuth](https://www.drupal.org/project/simple_oauth) module
provides two scope providers: `static` and `dynamic`. The `static` scope
provider implements scopes as a `yaml` plugin that must be provided by modules
and prevents scopes from being modified. The `dynamic` scope provider
implements scopes as a config entity and allows scopes to be created and
modified via the UI. Modules can provide `dynamic` scopes as well but there are
no guarantees that these scopes will remain unmodified.
farmOS defaults to using the `static` scope provider. This allows modules
providing OAuth scopes to guarantee that their scopes exist unmodified within
the server. The farmOS administrator can change to using the `dynamic` scope
provider if necessary, but may need to re-create any `static` scopes that
are needed for integrations provided by other modules.
The farmOS Default Roles module provides a `static` OAuth scope for each of the
default roles: `farm_manager`, `farm_worker`, and `farm_viewer`.
### Scope Configuration
All scopes use the same configuration and provide the same features
regardless of the scope provider:
- Scopes must provide a `name` to uniquely identify the scope
- Scopes must provide a `description`
- Scopes must specify if they are an `umbrella` scope. Umbrella scopes are
only used as parent for child scopes to reference and do not specify a
`granularity`.
- Scopes must configure which `grant_types` they allow. Each grant type can
include an optional `description` to describe how the scope is used in the
context of each grant type.
- Scopes can optionally specify a `parent` scope that the scope is a part of.
When the parent scope is requested, all of its child scopes are granted as
well.
- Scopes must specify a `granularity` if they are not an `umbrella` scope.
This value must be equal to `permission` or `role`. The scope must
provide a single value for the `permission` or `role` it is associated with.
This configuration is most easily demonstrated with
`static` scopes that are provided in a `module.oauth2_scopes.yml` plugin file.
```yaml
"scope:name":
description: string (required)
umbrella: boolean (required)
grant_types: (required)
GRANT_TYPE_PLUGIN_ID: (required: only known grant types)
status: boolean (required)
description: string
parent: string
granularity: string (required: if umbrella is FALSE, values: permission or role)
permission: string (required: if umbrella is FALSE and granularity set to permission)
role: string (required: if umbrella is FALSE and granularity set to role)
```
An example of the static `farm_manager` scope provided by the farmOS Role
Roles mdoule:
```yaml
farm_manager:
description: 'Grants access to the Farm Manager role.'
umbrella: false
grant_types:
authorization_code:
status: true
refresh_token:
status: true
password:
status: true
granularity: 'role'
role: 'farm_manager'
```
## Providing OAuth Clients
OAuth clients are modeled as "Consumer" entities (provided by the
[Consumers](https://www.drupal.org/project/consumers) module. To create
integrations with farmOS a `consumer` entity must be created that
identifies the integration and configures the OAuth Client for the desired
authorization behavior.
The core `farm_api_default_consumer` module provides a default client with
`client_id = farm` that can use the `password` and `refresh_token` grant. You
can use this client for general usage of the API, like writing a script that
communicates with *your* farmOS server, but it comes with limitations.
## Client Configuration
Standard Consumer configuration:
- `consumer.label` - A label used to identify the third party integration.
- `consumer.client_id` - An optional `client_id` machine name to identify the
consumer. The `simple_oauth` module uses a UUID by default, but a machine
name makes it easier to identify clients across multiple farmOS servers.
- `consumer.secret` - A `client_secret` used to secure the OAuth client.
- `consumer.confidential` - A boolean indicating whether the client secret
needs to be validated.
- Most farmOS third party integrations will disable this. Otherwise the
same `client_secret` must be configured on all farmOS servers, or the
third party must keep track of a different secret for each server. This
challenge is due to the nature of farmOS being a self-hosted application.
- `consumer.user_id` - When no specific user is authenticated Drupal will use
this user as the author of all the actions made by this consumer.
- This is only the case during the `Client Credentials` authorization flow.
- `consumer.grant_types` - A list of the grant types that the client allows.
- `consumer.scopes` - A list of default scopes that will be granted for this
client if no scopes are requested during the authorization flow. No scopes
will be granted that the user does not have access to.
- `consumer.access_token_expiration` - The lifetime of access tokens in seconds.
- `consumer.refresh_token_expiration` - The lifetime of refresh tokens in
seconds.
- `consumer.redirect_uri` - The URI this client will redirect to when needed.
- This is used with the Authorization Code authorization flow.
- `consumer.allowed_origins` - Define any allowed origins the farmOS server
should allow CORS requests from. This is required for any API integrations
that will run in the browser.
- `consumer.third_party` - Enable if the Consumer represents a third party.
- Users will skip the "grant" step of the authorization flow for first
party consumers only.

View File

@@ -0,0 +1,639 @@
# Quick forms
Quick forms provide a simplified user interface for common data entry tasks.
## Building quick forms
To add a quick form to a module, create a quick form plugin class in
`src/Plugin/QuickForm` that extends the `QuickFormBase` class, and add a
dependency on `farm:farm_quick` to the module's `*.info.yml` file.
Quick forms are essentially just specialized forms created using Drupal's
[Form API](https://www.drupal.org/docs/drupal-apis/form-api/introduction-to-form-api),
with some special wrappers and helper methods to simplify and standardize
common requirements in the context of farmOS. They are defined as plugins via
a single PHP class. farmOS handles all the rest, including adding them to the
main navigation menu.
For example, a simple "Harvest" quick form can be provided in a module
comprised of two files (the `*.info.yml` file and the quick form plugin class),
as follows:
`/farm_quick_harvest.info.yml`
```yaml
name: Harvest Quick Form
description: Provides a quick form for recording a harvest.
type: module
package: farmOS Quick Forms
core_version_requirement: ^10
dependencies:
- farm:farm_harvest
- farm:farm_quantity_standard
- farm:farm_quick
```
This file defines the module itself, along with the dependencies required by
this quick form. In this example, the "Harvest" (`farm:farm_harvest`) and
"Standard quantity" (`farm:farm_quick_standard`) modules are dependencies, in
addition to the "Quick form" module (`farm:farm_quick`).
`/src/Plugin/QuickForm/Harvest.php`:
```php
<?php
namespace Drupal\farm_quick_harvest\Plugin\QuickForm;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Form\FormStateInterface;
use Drupal\farm_quick\Plugin\QuickForm\QuickFormBase;
use Drupal\farm_quick\Traits\QuickLogTrait;
/**
* Harvest quick form.
*
* @QuickForm(
* id = "harvest",
* label = @Translation("Harvest"),
* description = @Translation("Record when a harvest takes place."),
* helpText = @Translation("Use this form to record a harvest."),
* permissions = {
* "create harvest log",
* }
* )
*/
class Harvest extends QuickFormBase {
use QuickLogTrait;
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// Date+time selection field (defaults to now).
$form['timestamp'] = [
'#type' => 'datetime',
'#title' => $this->t('Date'),
'#default_value' => new DrupalDateTime('now', \Drupal::currentUser()->getTimeZone()),
'#required' => TRUE,
];
// Asset reference field (allow multiple).
$form['asset'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Assets'),
'#target_type' => 'asset',
'#tags' => TRUE,
];
// Harvest quantity field.
$form['quantity'] = [
'#type' => 'number',
'#title' => $this->t('Quantity'),
'#required' => TRUE,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Draft a harvest log from the user-submitted data.
$timestamp = $form_state->getValue('timestamp')->getTimestamp();
$asset = $form_state->getValue('asset');
$quantity = $form_state->getValue('quantity');
$log = [
'type' => 'harvest',
'timestamp' => $timestamp,
'asset' => $asset,
'quantity' => [
[
'type' => 'standard',
'value' => $quantity,
],
],
'status' => 'done',
];
// Create the log.
$this->createLog($log);
}
}
```
This file declares a new `Harvest` class, that extends from the `QuickFormBase`
class.
The `buildForm()` method builds the quick form, by adding "Date", "Asset", and
"Quantity" fields. A "Submit" button will be automatically added by the base
class, but can be overridden if customization is required.
The `submitForm()` is responsible for gathering the input and saving it to a
harvest log. It uses the `createLog()` helper method that is provided by the
`QuickLogTrait` trait, and the `entityLabelsSummary()` method provided by the
`QuickStringTrait` trait to build a log name.
See [Methods](#methods) and [Traits](#traits) below for more information about
the available methods, or examine the `QuickFormBase` class to understand the
internal workings.
### Annotation
The `@QuickForm` annotation comment above the class declaration is required,
and provides import metadata about the quick form.
- `id` - The quick form's unique ID.
- `label` - The translated label of the quick form displayed at the top of the
quick form and in the quick form index page.
- `description` - The translated description of the quick form displayed in the
quick form index page.
- `helpText` - The translated help text of the quick form displayed above the
quick form when the Help module is enabled.
- `permissions` - An array of permissions that are required to access the quick
form.
### Methods
The `QuickFormBase` class implements all the necessary methods defined in the
`QuickFormInterface` interface, and child classes can choose to override only
the ones they need to. At minimum, this will usually include the `buildForm()`
and `submitForm()` methods.
Available methods include:
- `access()` - Checks to see if the current use has access to the quick form.
If omitted, then the `QuickFormBase::access()` parent method will check to
see if the user has all of the permissions specified in the list of
`permissions` in the `@QuickForm` annotation. Overriding this method allows
a quick form to implement more customized access control logic.
- `buildForm()` - Build the quick form as an array using the
[Drupal Form API](https://www.drupal.org/docs/drupal-apis/form-api/introduction-to-form-api).
- `validateForm()` - Perform validation on the user input.
- `submitForm()` - Perform logic when the form is submitted. This will not run
if validation fails.
### Traits
farmOS provides some helpers for common quick form operations. These are
available in the form of traits that can be added to the quick form class.
Available traits and the methods that they provide include:
- `QuickAssetTrait`
- `createAsset($values)` - Creates and returns a new asset entity from
an array of values. This also creates a link in the database between the
entity and the quick form that created it, and displays a message to the
user upon submission with a link to the entity.
- `QuickLogTrait`
- `createLog($values)` - Creates and returns a new log entity from an
array of values. This also creates a link in the database between the
entity and the quick form that created it, and displays a message to the
user upon submission with a link to the entity.
- `QuickPrepopulateTrait`
- `getPrepopulatedEntities($entity_type)` - Returns entities of the specified
entity type that have been prepopulated for the quick form. Entities may
be prepopulated by either a query param or a user specific tempstore that
is populated by the quick form action.
- `QuickQuantityTrait`
- `createQuantity($values)` - Creates and returns a new quantity entity from
an array of values.
- `QuickStringTrait`
- `trimString($value, $max_length, $suffix)` - Trims a string down to the
specified length, respecting word boundaries.
- `prioritizedString($strings, $priority_keys, $max_length, $suffix)` -
Concatenates strings together with some intelligence for prioritizing
certain parts when the full string will not fit within a maximum length.
Expects a keyed array of strings to concatenate together, along with an
optional array of keys that should be prioritized in case the full string
won't fit.
- `entityLabelsSummary($entities, $cutoff)` - Generate a summary of entity
labels. Example: "Asset 1, Asset 2, Asset 3 (+ 15 more)". Note that this
does NOT sanitize the entity labels. It is the responsibility of downstream
code to do so, if it is printing text to the page.
- `QuickTermTrait`
- `createTerm($values)` - Creates and returns a new term entity from an array
of values.
- `createOrLoadTerm($name, $vocabulary)` - Attempts to load an existing term,
given a name and vocabulary. If the term does not exist, a new term will be
created.
## Configurable quick forms
A "configurable" quick form is one that allows users to change how the quick
form behaves, by providing settings and a configuration form for customizing
them.
To make an existing quick form configurable:
1. Add `implements ConfigurableQuickFormInterface` to the quick form's class
definition. This indicates to farmOS that the quick form is configurable,
builds a router item for the configuration form, adds it to the UI, etc.
2. Add `use ConfigurableQuickFormTrait` to the quick form's class definition.
This adds default methods required by the `ConfigurableQuickFormInterface`.
3. Add a `defaultConfiguration()` method that returns an array of default
configuration values.
4. Add a `buildConfigurationForm()` method that builds a configuration form
with form items for each of the properties defined in
`defaultConfiguration()`.
5. Add a `submitConfigurationForm()` method that processes submitted values and
assigns configuration to `$this->configuration`.
6. Add a `config/schema/[mymodule].schema.yml` file that describes the
[configuration schema/metatdata](https://www.drupal.org/docs/drupal-apis/configuration-api/configuration-schemametadata).
7. Add `'#default_value' => $this->configuration['...']` lines to the form
elements that are configurable in the `buildForm()` method.
The following is the same "Harvest" example as above, with the changes described
above, followed by the schema file that describes the settings.
```php
<?php
namespace Drupal\farm_quick_harvest\Plugin\QuickForm;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Form\FormStateInterface;
use Drupal\farm_quick\Plugin\QuickForm\ConfigurableQuickFormInterface;
use Drupal\farm_quick\Plugin\QuickForm\QuickFormBase;
use Drupal\farm_quick\Traits\ConfigurableQuickFormTrait;
use Drupal\farm_quick\Traits\QuickLogTrait;
/**
* Harvest quick form.
*
* @QuickForm(
* id = "harvest",
* label = @Translation("Harvest"),
* description = @Translation("Record when a harvest takes place."),
* helpText = @Translation("Use this form to record a harvest."),
* permissions = {
* "create harvest log",
* }
* )
*/
class Harvest extends QuickFormBase implements ConfigurableQuickFormInterface {
use ConfigurableQuickFormTrait;
use QuickLogTrait;
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'default_quantity' => 100,
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// Date+time selection field (defaults to now).
$form['timestamp'] = [
'#type' => 'datetime',
'#title' => $this->t('Date'),
'#default_value' => new DrupalDateTime('now', \Drupal::currentUser()->getTimeZone()),
'#required' => TRUE,
];
// Asset reference field (allow multiple).
$form['asset'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Assets'),
'#target_type' => 'asset',
'#tags' => TRUE,
];
// Harvest quantity field.
$form['quantity'] = [
'#type' => 'number',
'#title' => $this->t('Quantity'),
'#required' => TRUE,
'#default_value' => $this->configuration['default_quantity'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Draft a harvest log from the user-submitted data.
$timestamp = $form_state->getValue('timestamp')->getTimestamp();
$asset = $form_state->getValue('asset');
$quantity = $form_state->getValue('quantity');
$log = [
'type' => 'harvest',
'timestamp' => $timestamp,
'asset' => $asset,
'quantity' => [
[
'type' => 'standard',
'value' => $quantity,
],
],
'status' => 'done',
];
// Create the log.
$this->createLog($log);
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
// Default quantity configuration.
$form['default_quantity'] = [
'#type' => 'number',
'#title' => $this->t('Default quantity'),
'#default_value' => $this->configuration['default_quantity'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['default_quantity'] = $form_state->getValue('default_quantity');
}
}
```
`config/schema/farm_quick_harvest.schema.yml`:
```yaml
farm_quick.settings.harvest:
type: quick_form_settings
label: 'Harvest quick form settings'
mapping:
default_quantity:
type: integer
label: 'Default quantity'
```
### Methods
The `ConfigurableQuickFormTrait` class add all the necessary methods required
by `ConfigurableQuickFormInterface` (which is used to designate a quick form as
"configurable"). Child classes can override these methods to customize their
behavior. At a minimum, most configurable quick form classes should override
`defaultConfiguration()`, `buildConfigurationForm()`, and
`submitConfigurationForm()`.
Available methods include:
- `defaultConfiguration()` - Provide an array of default configuration values.
- `buildConfigurationForm()` - Build the configuration form as an array using
[Drupal Form API](https://www.drupal.org/docs/drupal-apis/form-api/introduction-to-form-api).
- `validateConfigurationForm()` - Perform validation on the user input.
- `submitConfigurationForm()` - Perform logic when the form is submitted to
prepare the quick form configuration entity. This will not run if validation
fails.
## Quick form configuration entities
Each quick form that is displayed to the user in farmOS is represented as a
[configuration entity](https://www.drupal.org/docs/drupal-apis/entity-api/configuration-entity).
Each configuration entity specifies which quick form plugin it uses (aka which
PHP class that extends from `QuickFormBase`), along with other information like
label, description, help text, and configuration settings (used by configurable
quick forms).
However, if a configuration entity is not saved, farmOS will try to provide a
"default instance" of the quick form plugin. From a module developer's
perspective, this means that the module does not need to provide any config
entity YML files in `config/install`. It can rely on farmOS's default quick
form instance logic to show the quick form.
In the case of configurable quick forms, a config entity will be automatically
created when the user modifies the quick form's configuration and submits the
configuration form.
Quick form configuration entities can also be used to override defaults,
including the label, description, and help text. They can also be used to
disable a quick form entirely by setting the config entity's `status` to
false.
If multiple configuration entities are provided for the same plugin, multiple
quick forms will be displayed in the UI. This is useful if you want to create
a set of similar quick forms with pre-set configuration options.
### Disable default instance
In some cases, a plugin may not want a "default instance" to be created.
Instead, they may want to require that a quick form configuration entity be
explicitly created. For example, if a plugin requires configuration settings,
but there isn't a sensible default for that configuration and user input is
required, a "default instance" may not be possible.
In that case, the plugin can add `requiresEntity = True` to its annotation,
which will tell farmOS not to create a default instance of the quick form.
The quick form will only be made available if a configuration entity is saved.
## Quick form actions
farmOS provides lists of logs and assets throughout its interface. Many of
these lists allow the user to select one or more entities and perform a
"bulk action" (eg: "Archive asset", "Assign owners", etc).
Quick form actions provide a shortcut to completing a quick form that performs
actions on or references existing entities.
This allows a user to select one or more entities from a list in farmOS, and be
redirected to the quick form with the selected entities passed in. These
selected entities can then be used in the quick form code in various ways.
### Providing a quick form action
To add a quick form action, three additional files are added to the module:
1. a PHP class in `src/Plugin/Action` that extends from `QuickFormActionBase`
2. an action config entity in `config/install/system.action.*.yml`
3. a `config/schema/[mymodule].schema.yml` file that describes action schema
(see example below).
For example, an action that redirects to the "Harvest" quick form defined above
for prepopulating the "Asset" field would be provided as follows:
`/src/Plugin/Action/Harvest.php`:
```php
<?php
namespace Drupal\farm_quick_harvest\Plugin\Action;
use Drupal\farm_quick\Plugin\Action\QuickFormActionBase;
/**
* Action for recording harvests.
*
* @Action(
* id = "harvest",
* label = @Translation("Record harvest"),
* type = "asset",
* confirm_form_route_name = "farm.quick.harvest"
* )
*/
class Harvest extends QuickFormActionBase {
/**
* {@inheritdoc}
*/
public function getQuickFormId(): string {
return 'harvest';
}
}
```
`/config/install/system.action.harvest.yml`:
```yml
langcode: en
status: true
dependencies:
module:
- asset
- farm_quick_harvest
id: harvest
label: 'Record harvest'
type: asset
plugin: harvest
configuration: { }
```
`/config/schema/farm_quick_harvest.schema.yml`:
```yml
# Schema for actions.
action.configuration.harvest:
type: action_configuration_default
label: 'Configuration for the harvest action'
```
Note that config entities are only created when the module is installed. In
order to add a config entity to a module that is already installed, an update
hook must be used to manually create the config entity.
### Using the selected entities
To get a list of the selected entities within the quick form class, add the
`QuickPrepopulateTrait` trait and use the `getPrepopulatedEntities()` helper
method that it provides. Specify the entity type and pass in the `$form_state`
object, as follows:
`$entities = $this->getPrepopulatedEntities('asset', $form_state);`
This will return a list of fully-loaded entity objects that can be used in the
quick form code.
The following is the same "Harvest" example as above, with two additions:
1. The `use QuickPrepopulateTrait;` line is added at the top of the class (as
well as a corresponding `use` statement at the top of the file defining
the full trait namespace).
2. The `getPrepopulatedEntities()` method is used to populate the `asset`
field's default value.
```php
<?php
namespace Drupal\farm_quick_harvest\Plugin\QuickForm;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Form\FormStateInterface;
use Drupal\farm_quick\Plugin\QuickForm\QuickFormBase;
use Drupal\farm_quick\Traits\QuickLogTrait;
use Drupal\farm_quick\Traits\QuickPrepopulateTrait;
/**
* Harvest quick form.
*
* @QuickForm(
* id = "harvest",
* label = @Translation("Harvest"),
* description = @Translation("Record when a harvest takes place."),
* helpText = @Translation("Use this form to record a harvest."),
* permissions = {
* "create harvest log",
* }
* )
*/
class Harvest extends QuickFormBase {
use QuickLogTrait;
use QuickPrepopulateTrait;
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// Date+time selection field (defaults to now).
$form['timestamp'] = [
'#type' => 'datetime',
'#title' => $this->t('Date'),
'#default_value' => new DrupalDateTime('now', \Drupal::currentUser()->getTimeZone()),
'#required' => TRUE,
];
// Asset reference field (allow multiple).
$form['asset'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Assets'),
'#target_type' => 'asset',
'#tags' => TRUE,
'#default_value' => $this->getPrepopulatedEntities('asset', $form_state),
];
// Harvest quantity field.
$form['quantity'] = [
'#type' => 'number',
'#title' => $this->t('Quantity'),
'#required' => TRUE,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Draft a harvest log from the user-submitted data.
$timestamp = $form_state->getValue('timestamp')->getTimestamp();
$asset = $form_state->getValue('asset');
$quantity = $form_state->getValue('quantity');
$log = [
'type' => 'harvest',
'timestamp' => $timestamp,
'asset' => $asset,
'quantity' => [
[
'type' => 'standard',
'value' => $quantity,
],
],
'status' => 'done',
];
// Create the log.
$this->createLog($log);
}
}
```

View File

@@ -0,0 +1,192 @@
# Roles
Roles are groups of permissions that can be assigned to users to grant them
granular access to data and features in farmOS.
Module developers can define new roles, and specify which permissions they
should include. farmOS also builds on top of Drupal's role and permission
system to provide a concept of "Managed Roles".
## Managed Roles
The farmOS Access module provides methods to create user roles with permissions
that are managed for the purposes of farmOS. These roles cannot be modified
from the Admin Permissions UI. Instead, these roles allow permissions to be
provided by other modules that want to provide sensible defaults for common
farmOS roles.
### Creating a managed role
User roles are provided as Drupal Configuration Entities. Managed roles are
provided in the same way the only difference being that they include
additional third party settings the farmOS Access module uses to build
managed permissions. The `user.role.*.third_party.farm_acccess` schema
defines the structure of these settings.
- `access`: An optional array of default access permissions.
- `config`: Boolean that specifies whether the role should have access to
configuration. Only grant this to trusted roles.
- `entity`: Access permissions relating to entities.
- `view all`: Boolean that specifies the role should have access to view
all bundles of all entity types.
- `create all`: Boolean that specifies the role should have access to
create all bundles of all entity types.
- `update all`: Boolean that specifies the role should have access to
update all bundles of all entity types.
- `delete all`: Boolean that specifies the role should have access to
delete all bundles of all entity types.
- `type`: Access permissions for specific entity types.
- `{entity_type}`: The id of the entity type. eg: `log`,`asset`,
`taxonomy_term`, etc.
- `{operation}`: The operation to grant bundles of this entity
type. Eg: `create`, `view any`, `view own`, `delete any`,
`delete own`, etc.
- `{bundle}`: The id of the entity type bundle or `all` to
grant the operation permission to all bundles of the entity
type.
Settings used for the Manager role (full access to all entities + access to
configuration):
`user.role.farm_manager.yml`
```yaml
# (standard role config goes here)
third_party_settings:
farm_role:
access:
config: true
entity:
view all: true
create all: true
update all: true
delete all: true
```
Example settings to define a "Harvester" role with these limitations:
* View all log entities.
* Only create harvest logs, update harvest logs, and delete own harvest logs.
* View all asset entities.
* Only update planting assets.
* View, edit and delete any taxonomy_term entity.
`user.role.farm_harvester.yml`
```yaml
# (standard role config goes here)
third_party_settings:
farm_role:
access:
entity:
view all: true
type:
log:
create:
- harvest
update any:
- harvest
delete own:
- harvest
asset:
update any:
- planting
taxonomy_term:
edit:
- all
delete:
- all
```
### Providing permissions for managed roles
Modules can define sensible permissions to any managed roles. These permissions
are provided by creating a `ManagedRolePermissions` plugin in the
`module.managed_role_permissions.yml` file. The following keys can be provided:
- `default_permissions`: A list of permissions that will be added to *all*
managed roles.
- `config_permissions`: A list of permissions that will be added to managed
roles that have access to configuration (`config: true`).
- `permission_callbacks`: A list of callbacks in controller notation that
return an array of permissions to add to managed roles. Callbacks are
provided a `Role` object so that permissions can be applied conditionally
based on the managed role's settings.
As an example, the `farm_role` module provides the following permissions:
`farm_role.managed_role_permissions.yml`
```yaml
farm_role:
default_permissions:
- access content
- access user profiles
- change own username
config_permissions:
- access taxonomy overview
```
#### Permission callbacks
Example that adds permissions conditionally based on the role name and settings:
Plugin definition:
`my_module.managed_role_permissions.yml`
```yaml
my_module:
permission_callbacks:
- Drupal\my_module\CustomPermissions::permissions
```
Example implementation of a `permission_callback`:
`my_module/src/CustomPermissions.php`
```php
<?php
namespace Drupal\my_module;
use Drupal\user\RoleInterface;
/**
* Example custom permission callback.
*/
class CustomPermissions {
/**
* Return an array of permission strings that will be added to the role.
*
* @param \Drupal\user\RoleInterface $role
* The role to add permissions to.
*
* @return array
* An array of permission strings.
*/
public function permissions(RoleInterface $role) {
// Array of permissions to return.
$perms = [];
// Add permissions based on role name.
if ($role->id() == 'farm_manager') {
$perms = 'my manager permission';
}
// Get the farm_role third party settings from the Role entity.
$access_settings = $role->getThirdPartySetting('farm_role', 'access');
$entity_settings = $access_settings['entity'] ?: [];
// Only add permissions if `update all` and `delete all` are true.
if (!empty($entity_settings['update all'] && $entity_settings['delete all'])) {
$perms[] = 'recover all permission';
}
// Return array of permissions.
return $perms;
}
}
```

View File

@@ -0,0 +1,237 @@
# Services
farmOS provides some [services](https://symfony.com/doc/current/service_container.html)
that encapsulate common logic like querying logs and getting an asset's current
location. Some of these services are documented here.
## Asset logs service
**Service name**: `asset.logs`
The asset logs service provides methods for retrieving logs that reference
assets.
**Methods**:
`getLogs($asset, $log_type = NULL, $access_check = TRUE)` - Load a list of logs
that reference an asset, optionally filtered by log type. Access checking is
performed by default but can be optionally disabled. Returns a list of log
entities.
`getFirstLog($asset, $log_type = NULL, $access_check = TRUE)` - Load the first
log that references an asset, optionally filtered by log type. Access checking
is performed by default but can be optionally disabled. Returns a log entity, or
`NULL` if no logs were found.
**Example usage**:
```php
// Get all observation logs that reference an asset.
$observation_logs = \Drupal::service('asset.logs')->getLogs($asset, 'observation');
```
## Asset location service
**Service name**: `asset.location`
The asset location service provides methods that encapsulate the logic for
determining an asset's location and geometry.
Note that these methods do not perform access checking on any of the assets or
logs used to determine location. It is up to downstream code to ensure access
controls are respected.
**Methods**:
`isLocation($asset)` - Check if an asset is a location. Returns a boolean.
`isFixed($asset)` - Check if an asset is fixed. Returns a boolean.
`hasLocation($asset, $timestamp = NULL)` - Check if an asset is located within
other location assets, optionally at a given timestamp (defaults to current
time). Returns a boolean.
`hasGeometry($asset, $timestamp = NULL)` - Check if an asset has geometry,
optionally at a given timestamp (defaults to current time). Returns a boolean.
`getLocation($asset, $timestamp = NULL)` - Get location assets that an asset is
located within, optionally at a given timestamp (defaults to current time).
Returns an array of asset entities.
`getGeometry($asset, $timestamp = NULL)` - Get an asset's geometry, optionally
at a given timestamp (defaults to current time). Returns a Well-Known Text
string.
`getMovementLog($asset, $timestamp = NULL)` - Find the latest movement log that
references an asset, optionally at a given timestamp (defaults to current
time). Returns a log entity, or `NULL` if no logs were found.
`setIntrinsicGeometry($asset, $wkt)` - Set an asset's intrinsic geometry, given
a string in Well-Known Text format.
`getAssetsByLocation($locations, $timestamp = NULL)` - Get assets that are in
locations, optionally at a given timestamp (defaults to current time).
**Example usage**:
```php
// Get an asset's current geometry.
$geometry = \Drupal::service('asset.location')->getGeometry($asset);
```
## Asset inventory service
**Service name**: `asset.inventory`
The asset inventory service provides methods that encapsulate the logic for
determining an asset's inventory.
Note that these methods do not perform access checking on any of the assets or
logs used to determine inventory. It is up to downstream code to ensure access
controls are respected.
**Methods**:
`getInventory($asset, $measure = '', $units = 0, $timestamp = NULL)` - Get
inventory summaries for an asset, optionally at a given timestamp (defaults
to current time). Returns an array of arrays with the following keys:
`measure`, `value`, `units`. This can be optionally filtered by `$measure`
(string) and `$units` (term ID).
**Example usage**:
```php
// Get summaries of all inventories for an asset.
$all_inventory = \Drupal::service('asset.inventory')->getInventory($asset);
// Get the current inventory for a given measure (string) and units (term id).
$gallons_of_fertilizer = \Drupal::service('asset.inventory')->getInventory($asset, 'volume', 123);
```
## 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.
- `decimal` - Decimal number with fixed precision. Additional options:
- `precision` (optional) - Total number of digits (including after the
decimal point). Defaults to 10.
- `scale` (optional) - Number digits to the right of the decimal point.
Defaults to 2.
- `min` (optional) - The minimum value.
- `max` (optional) - The maximum value.
- `email` - Email field.
- `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.
- `integer` - Integer number. Additional options:
- `size` (optional) - The integer database column size (`tiny`,
`small`, `medium`, `normal`, or `big`). Defaults to `normal`.
- `min` (optional) - The minimum value.
- `max` (optional) - The maximum value.
- `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` - Unformatted text field of fixed length. Additional options:
- `max_length` - Maximum length. Defaults to 255.
- `string_long` - Unformatted text field of unlimited length.
- `text_long` - Formatted text field of unlimited length.
- `timestamp` - Date and time.
- `uri` - Uniform Resource Identifier.
- `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/3.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`
The group membership service provides methods that encapsulate the logic for
determining an asset's group membership. This is provided by the optional Group
Asset module, and will only be available if that module is installed.
Note that these methods do not perform access checking on any of the assets or
logs used to determine group membership. It is up to downstream code to ensure
access controls are respected.
**Methods**:
`hasGroup($asset, $timestamp = NULL)` - Check if an asset is a member of a
group, optionally at a given timestamp (defaults to current time). Returns a
boolean.
`getGroup($asset, $timestamp = NULL)` - Get group assets that an asset is a
member of, optionally at a given timestamp (defaults to current time). Returns
an array of asset entities.
`getGroupAssignmentLog($asset, $timestamp = NULL)` - Find the latest group
assignment log that references an asset, optionally at a given timestamp
(defaults to current time). Returns a log entity, or `NULL` if no logs were
found.
`getGroupMembers($groups, $recurse = TRUE, $timestamp = NULL)` - Get assets that
are members of groups, optionally recursing into child groups, and optionally
at a given timestamp (defaults to current time).
**Example usage:**
```php
// Get the groups that an asset is a member of.
$groups = \Drupal::service('group.membership')->getGroup($asset);
```

View File

@@ -0,0 +1,94 @@
# Automated updates
## Update hooks
farmOS modules may change and evolve over time. If these changes require
updates to a farmOS database or configuration, then update logic should be
provided so that users of the module can perform the necessary changes
automatically when they update to the new version.
This logic can be supplied via implementations of `hook_update_N()` and
`hook_post_update_NAME()`.
For more information, see the documentation for Drupal's
[Update API](https://www.drupal.org/docs/drupal-apis/update-api/).
## Configuration updates
If the farmOS Update module is enabled, changes to configuration entities will
be automatically reverted when caches are rebuilt. The purpose of this is to
make it easier for farmOS module developers to make minor changes to the
configuration included with their module without writing update hooks.
Note that this only handles overridden configuration. It does not handle
missing, inactive, or added configuration. It also does not touch "simple"
configuration (eg: module settings) - only configuration entities (eg: Views).
If your module is adding or deleting configuration, the recommended approach is
to implement `hook_post_update_NAME()` to perform the necessary operations.
In most cases this is desirable, but if you are intentionally overriding
configuration in your farmOS instance then you have a few options for
disabling this behavior.
### Disable farmOS Update module
The easiest way to disable automatic configuration updates is to turn off the
`farm_update` module. This can be done via Drush:
drush pm-uninstall farm_update
This will completely disable automatic reverts of configuration. You can then
manage all configuration changes and deployment manually. One way to do this
is with the `config_update_ui` module, which provides a report of all missing,
inactive, added, and changed configuration. This can be enabled via Drush:
drush en config_update_ui
Then go to `/admin/config/development/configuration/report/type/system.all` to
see the full report. Individual configuration items can be reverted, imported,
and deleted.
### Exclude specific configuration
Alternatively, if you want to keep automatic updates enabled, but want control
over certain items, the farmOS Update module provides two mechanisms for
excluding specific configuration from automatic updates.
#### `hook_farm_update_exclude_config()`
If a module overrides certain configuration items, either in
`hook_install()` or via something like the `config_rewrite` module, the
module can list these configuration items in an array returned by an
implementation of `hook_farm_update_exclude_config()`.
For example, in `mymodule.module`:
```php
/**
* Implements hook_farm_update_exclude_config().
*/
function mymodule_farm_update_exclude_config() {
// Exclude mymodule_custom view from automatic configuration updates.
return [
'views.view.mymodule_custom',
];
}
```
#### `farm_update.settings`
The farmOS Update module will also check the `exclude_config` setting in
its own `farm_update.settings` configuration for a list of configuration
items to exclude from automatic updates. This can be provided by a custom
module in `config/install/farm_update.settings.yml`, or synced/imported into
active configuration by other means.
For example, in `farm_update.settings.yml`:
```yaml
exclude_config:
# Exclude mymodule_custom view from automatic configuration updates.
- views.view.mymodule_custom
```