Giter Club home page Giter Club logo

cht-conf's Introduction

CHT App Configurer

CHT Conf is a command-line interface tool to manage and configure your apps built using the Core Framework of the Community Health Toolkit.

Requirements

  • nodejs 8 or later
  • python 2.7
  • or Docker

Installation

Operating System Specific

Ubuntu

npm install -g cht-conf
sudo python -m pip install git+https://github.com/medic/[email protected]#egg=pyxform-medic

OSX

npm install -g cht-conf
pip install git+https://github.com/medic/[email protected]#egg=pyxform-medic

Windows

As Administrator:

npm install -g cht-conf
python -m pip install git+https://github.com/medic/[email protected]#egg=pyxform-medic --upgrade

Docker

CHT Conf can also be run from within a Docker container. This is useful if you are already familiar with Docker and do not wish to configure the various dependencies required for developing CHT apps on your local machine. The necessary dependencies are pre-packaged in the Docker image.

Using the image

The Docker image can be used as a VS Code Development Container (easiest) or as a standalone Docker utility.

Install Docker. If you are using Windows, you also need to enable the Windows Subsystem for Linux (WSL2) to perform the following steps.

VS Code Development Container

If you want to develop CHT apps with VS Code, you can use the Docker image as a Development Container. This will allow you to use the cht-conf utility and its associated tech stack from within VS Code (without needing to install dependencies like NodeJS on your host system).

See the CHT Documentation for more information on building CHT apps with VS Code Development Containers.

Standalone Docker utility

If you are not using VS Code, you can use the Docker image as a standalone utility from the command line. Instead of using the cht ... command, you can run docker run -it --rm -v "$PWD":/workdir medicmobile/cht-app-ide .... This will create an ephemeral container with access to your current directory that will run the given cht command. (Do not include the cht part of the command, just your desired actions/parameters.)

Run the following command inside the project directory to bootstrap your new CHT project:

docker run -it --rm -v "$PWD":/workdir medicmobile/cht-app-ide initialise-project-layout

Note on connecting to a local CHT instance

When using cht-conf within a Docker container to connect to a CHT instance that is running on your local machine (e.g. a development instance), you cannot use the --local flag or localhost in your --url parameter (since these will be interpreted as "local to the container").

It is recommended to run a local CHT instance using the CHT Docker Helper script. You can connect to the resulting ...my.local-ip.co URL from the Docker container (or the VS Code terminal). (Just make sure the port your CHT instance is hosted on is not blocked by your firewall).

Bash completion

To enable tab completion in bash, add the following to your .bashrc/.bash_profile:

eval "$(cht-conf --shell-completion=bash)"

Upgrading

To upgrade to the latest version

npm install -g cht-conf

Usage

cht will upload the configuration from your current directory.

Specifying the server to configure

If you are using the default actionset, or performing any actions that require a CHT instance to function (e.g. upload-xyz or backup-xyz actions) you must specify the server you'd like to function against.

localhost

For developers, this is the instance defined in your COUCH_URL environment variable.

cht --local

A specific Medic-hosted instance

For configuring Medic-hosted instances.

cht --instance=instance-name.dev

Username admin is used. A prompt is shown for entering password.

If a different username is required, add the --user switch:

--user user-name --instance=instance-name.dev

An arbitrary URL

cht --url=https://username:[email protected]:12345

NB - When specifying the URL with --url, be sure not to specify the CouchDB database name in the URL. The CHT API will find the correct database.

Into an archive to be uploaded later

cht --archive

The resulting archive is consumable by CHT API >v3.7 to create default configurations.

Perform specific action(s)

cht <--archive|--local|--instance=instance-name|--url=url> <...action>

The list of available actions can be seen via cht --help.

Perform actions for specific forms

cht <--local|--instance=instance-name|--url=url> <...action> -- <...form>

Protecting against configuration overwriting

Added in v3.2.0

In order to avoid overwriting someone else's configuration cht-conf records the last uploaded configuration snapshot in the .snapshots directory. The remote.json file should be committed to your repository along with the associated configuration change. When uploading future configuration if cht-conf detects the snapshot doesn't match the configuration on the server you will be prompted to overwrite or cancel.

Currently supported

Settings

  • compile app settings from:
    • tasks
    • rules
    • schedules
    • contact-summary
    • purge
  • app settings can also be defined in a more modular way by having the following files in app_settings folder:
    • base_settings.json
    • forms.json
    • schedules.json
  • backup app settings from server
  • upload app settings to server
  • upload resources to server
  • upload custom translations to the server
  • upload privacy policies to server
  • upload branding to server
  • upload partners to server

Forms

  • fetch from Google Drive and save locally as .xlsx
  • backup from server
  • delete all forms from server
  • delete specific form from server
  • upload all app or contact forms to server
  • upload specified app or contact forms to server

Managing data and images

  • convert CSV files with contacts and reports to JSON docs
  • move contacts by downloading and making the changes locally first
  • upload JSON files as docs on instance
  • compress PNGs and SVGs in the current directory and its subdirectories

Editing contacts across the hierarchy.

To edit existing couchdb documents, create a CSV file that contains the id's of the document you wish to update, and the columns of the document attribute(s) you wish to add/edit. By default, values are parsed as strings. To parse a CSV column as a JSON type, refer to the Property Types section to see how you can parse the values to different types. Also refer to the Excluded Columns section to see how to exclude column(s) from being added to the docs.

Parameter Description Required
column(s) Comma delimited list of columns you wish to add/edit. If this is not specified all columns will be added. No
docDirectoryPath This action outputs files to local disk at this destination No. Default json-docs
file(s) Comma delimited list of files you wish to process using edit-contacts. By default, contact.csv is searched for in the current directory and processed. No.
updateOfflineDocs If passed, this updates the docs already in the docDirectoryPath instead of downloading from the server. No.

Example

  1. Create a contact.csv file with your columns in the csv folder in your current path. The documentID column is a requirement. (The documentID column contains the document IDs to be fetched from couchdb.)

    documentID is_in_emnch:bool
    documentID1 false
    documentID2 false
    documentID3 true
  2. Use the following command to download and edit the documents:

    cht --instance=*instance* edit-contacts -- --column=*is_in_emnch* --docDirectoryPath=*my_folder*
    

    or this one to update already downloaded docs

    cht --instance=*instance* edit-contacts -- --column=*is_in_emnch* --docDirectoryPath=*my_folder* --updateOfflineDocs
    
  3. Then upload the edited documents using the upload-docs command.

Project layout

This tool expects a project to be structured as follows:

example-project/
	.eslintrc
	app_settings.json
	contact-summary.js
	privacy-policies.json
	privacy-policies/
	    language1.html
	    …
	purge.js
	resources.json
	resources/
		icon-one.png
		…
	targets.js
	tasks.js
	task-schedules.json
	forms/
		app/
			my_project_form.xlsx
			my_project_form.xml
			my_project_form.properties.json
			my_project_form-media/
				[extra files]
				…
		contact/
			person-create.xlsx
			person-create.xml
			person-create-media/
				[extra files]
				…
		…
		…
	translations/
		messages-xx.properties
		…

If you are starting from scratch you can initialise the file layout using the initialise-project-layout action:

cht initialise-project-layout

Derived configs

Configuration can be inherited from another project, and then modified. This allows the app_settings.json and contained files (task-schedules.json, targets.json etc.) to be imported, and then modified.

To achieve this, create a file called settings.inherit.json in your project's root directory with the following format:

{
	"inherit": "../path/to/other/project",
	"replace": {
		"keys.to.replace": "value-to-replace-it-with"
	},
	"merge": {
		"complex.objects": {
			"will_be_merged": true
		}
	},
	"delete": [
		"all.keys.listed.here",
		"will.be.deleted"
	],
	"filter": {
		"object.at.this.key": [
			"will",
			"keep",
			"only",
			"these",
			"properties"
		]
	}
}

Fetching logs

Fetch logs from a CHT v2.x production server.

This is a standalone command installed alongside cht-conf. For usage information, run cht-logs --help.

Usage

cht-logs <instance-name> <log-types...>

Accepted log types:

api
couchdb
gardener
nginx
sentinel

Development

To develop a new action or improve an existing one, check the "Actions" doc.

Testing

Execute npm test to run static analysis checks and the test suite. Requires Docker to run integration tests against a CouchDB instance.

Executing your local branch

  1. Clone the project locally
  2. Make changes to cht-conf or checkout a branch for testing
  3. Test changes
    1. To test CLI changes locally you can run node <project_dir>/src/bin/index.js. This will run as if you installed via npm.
    2. To test changes that are imported in code run npm install <project_dir> to use the local version of cht-conf.

Releasing

  1. Create a pull request with prep for the new release.
  2. Get the pull request reviewed and approved.
  3. When doing the squash and merge, make sure that your commit message is clear and readable and follows the strict format described in the commit format section below. If the commit message does not comply, automatic release will fail.
  4. In case you are planning to merge the pull request with a merge commit, make sure that every commit in your branch respects the format.

Commit format

The commit format should follow this conventional-changelog angular preset. Examples are provided below.

Type Example commit message Release type
Bug fixes fix(#123): infinite spinner when clicking contacts tab twice patch
Performance perf(#789): lazily loaded angular modules patch
Features feat(#456): add home tab minor
Non-code chore(#123): update README none
Breaking perf(#2): remove reporting rates feature
BREAKING CHANGE: reporting rates no longer supported
major

Releasing betas

  1. Checkout the default branch, for example main
  2. Run npm version --no-git-tag-version <major>.<minor>.<patch>-beta.1. This will only update the versions in package.json and package-lock.json. It will not create a git tag and not create an associated commit.
  3. Run npm publish --tag beta. This will publish your beta tag to npm's beta channel.

To install from the beta channel, run npm install cht-conf@beta.

Build status

Builds brought to you courtesy of GitHub actions.

Copyright

Copyright 2013-2023 Medic Mobile, Inc. [email protected]

License

The software is provided under AGPL-3.0. Contributions to this project are accepted under the same license.

cht-conf's People

Contributors

1yuv avatar abbyad avatar alxndrsn avatar billwambua avatar binokaryg avatar dependabot[bot] avatar derickl avatar dianabarsan avatar eljhkrr avatar elvisdorkenoo avatar freddieptf avatar garethbowen avatar hareet avatar jkuester avatar jonathanbataire avatar jonathanvq avatar kennsippell avatar latin-panda avatar m5r avatar mrjones-plip avatar mrsarm avatar newtewt avatar njogz avatar nomulex avatar rosteve avatar scdf avatar semantic-release-bot avatar sugat009 avatar tookam avatar vimemo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

cht-conf's Issues

Handle templated contact/place forms

The forms PLACE_TYPE-create.xlsx, PLACE_TYPE-edit.xlsx need to be converted to:

  • clinic-create.xml
  • clinic-edit.xml
  • district_hospital-create.xml
  • district_hospital-edit.xml
  • health_center-create.xml
  • health_center-edit.xml

The only difference between them is that PLACE_TYPE and PLACE_NAME are replaced differently.

The XForm also has some custom edits to work as a place form and create the contact properly:

  • Move place to above person (in data model, due to XML ordering mattering, even though it shouldn't: medic/cht-core#1494)
  • Move custom place field from init to within place form - so that it isn't saved in place doc
  • Move new contact person to first page - move the group inside previous group
  • Add space after output field medic/cht-core#3324

The forms at https://github.com/medic/medic-projects/tree/master/standard/forms are fully generated by this convention, so tests could be created based around the xlsx and forms in that project.

Media files not being displayed due to broken links

The media files listed in an XLSform are not found in the app because the links are incorrect. All images and videos are given a path within an image or video folder by pyxform eg jr://images/positive.png, but on the form doc these media files are in _attachments with no folder.

In the previous convert.sh script these folders were stripped, but we are not (yet) doing this with convert-forms.js. The workaround for now is to edit the XML to strip the folders before running medic-conf with upload-app-forms. For instance, <value form="image">jr://images/positive.png</value> becomes <value form="image">jr://positive.png</value>

Handle derivative configs

We have 10+ Standard projects using the same base configuration that gets updated whenever fixes or new features are added to the Standard config. Typically the different Standard projects have slight modifications to the Standard config, such as locale_outgoing, default_country_code, and gateway_number, and we do not want to override these when pushing an updated Standard config..

Manually maintaining the modifications for Standard projects is very error prone and important changes can get lost with updates. To avoid this situation we have been using a child settings.json which refers to a parent app_settings.json. We have a script that then generates the app_settings for the child project accordingly. We should include a reliable way to do this within medic-config to get rid of one of the last separate config scripts still in use. This is a good time to review and improve the process if possible.


Excerpt of usage in the settings.js script related to the child settings.json:

Structured like app_settings.json, and copied into the parent settings.
Additionally, special fields can be used:
  _parent  path to parent app_settings into which these settings will be merged
  _forms   array of forms to include. All others will be removed. Because the
                merge into the parent settings uses a deep copy it is not possible
                to remove fields. Setting which forms to use allows a future proof
                way to specify the forms to include in the final app_settings.

Here is an child setting file which specifies the forms in use, so that forms for other use cases are not shown in the reports filter*:

{
  "_parent": "../standard/app_settings.json",
  "_forms": [
    "N",
    "P",
    "V",
    "D",
    "F",
    "OFF",
    "ON"
  ],
  "locale_outgoing": "id",
  "default_country_code": "62",
  "gateway_number": "+62812_______",
  "district_admins_access_unallocated_messages": true,
  "outgoing_deny_list": "3636, 1166, 98888, TELKOMSEL"
}

* it could be better to add context to JSON forms so that they can be hidden from the Reports tab filter accordingly.

Bulk user import

Creating users has become quite complex, as you need...

  • a doc in the _users db
  • a user-settings doc in the medic db
  • a contact to associate with the user (optional but wanted most often)
  • a facility to assign the user to (optional but wanted most often)

This is really annoying when creating a bunch of users.

Create a bulk import command so it's easy for an admin to upload a file and have all required documents created on the server.

While there it may make sense to implement a user export, but the export wouldn't be able to have the password, so make sure you handle this case without overwriting existing user's passwords.

Update the header format for CSV

The current header naming structure is functional but confusing, especially that there are 3 different formats that work differently. We should update the header format to make it easy to use for tech leads and partners. Below is a proposed format that has a common format for plain headers, those with external references matching a field, and those matching to a row number.

CURRENT STRUCTURES:

  1. No external reference, with optional conversion to Type:
    [ {{TYPE}}: ]{{TARGET}}

  2. Reference to a CSV row:
    csv:{{doc_type}}: [ id | doc | .source_field ] >target_field_name
    Note: unclear if type conversion is currently supported with this format

  3. Match to another doc:
    match={{field_in_other_doc}}:field1=value1&...&fieldN=fieldN: [ id | doc | .source_field ] >target_field_name
    Note: unclear if type conversion is currently supported with this format

PROPOSED STRUCTURE:

[ {{TYPE}}: ] [ {{SOURCE}}: ] {{TARGET}}

TYPE and SOURCE are optional. Without them we have the following:

{{TARGET}}
No type conversion or external reference

{{TYPE}}:{{TARGET}}
No external reference, set the type for conversion of data in the column.

{{SOURCE}}:{{TARGET}}
No conversion done, and the target gets the result of the reference as a string or object

It should be easy enough to differentiate the two 2-part formats since TYPE is one of 6 possible types that don't overlap with the SOURCE types.

TYPE

The type of data for this field, saved as corresponding JSON type:

  • string default
  • number
  • boolean
  • object
  • date date in ISO8601 format
  • timestamp date in MS since epoch. Useful for reported_date

SOURCE

The docs in which to look for the reference:
{{doc_type}}[?match={{field_name}}&{{field_X}}={{value_x}}&{{field_Y}}={{value_y}}]

  • The doc_type is the type of the doc you are referencing: [data_record, person, clinic, health_center, district_hospital].
  • With match the row's value is matched to the value of the specified column in the source docs. If match is missing the column's value (a number) will be used to find the corresponding row number in the source csv. See Questions section about whether we need to support row number matching.
  • Further narrowing can be done with field/value pairs.
  • If the source is empty that is ok, it is just not an external reference!

TARGET

Assigning the data to a field name in the resulting JSON:

[{{source_field}}>]{{target_field}} where source_field could be one of the following:

  • .field_name for any field on the object
  • doc for the whole doc (might not be needed at all, see Questions below)
  • id for the _id value (might not be needed at all, see Questions below)

Some examples:

  • target_property_name
  • doc>target_property_name
  • id>target_property_name
  • ._id>target_property_name
  • ._id>target_property_name._id
  • ._name>target_property_name

EXAMPLES

Here are some column headers in 1.6.12 vs proposed structure

Without reference

sex --> ::sex
or sex (no change!)

timestamp:reported_date --> timestamp::reported_date
or timestamp:reported_date (no change!)

Reference by row number

csv:F:id>target_property_name --> string:F:._id>target_property_name._id
or F:._id>target_property_name._id

csv:F:doc>target_property_name --> object:F:target_property_name
or F:target_property_name

csv:F:.some_field>target_property
-->string:F:.some_field>target_property
or F:.some_field>target_property

Reference with match

match=P:X=a&Y=y:id>target_property_name
--> string:F?match=P&X=a&Y=y:._id>target_property_name._id
or F?match=P&X=a&Y=y:._id>target_property_name._id

match=P:X=a&Y=y:doc>target_property_name
--> object:F?match=P&X=a&Y=y:target_property_name
or F?match=P&X=a&Y=y:target_property_name

match=P:X=a&Y=y:.some_field>target_property_name
--> string:F?match=P&X=a&Y=y:.some_field>target_property_name
or F?match=P&X=a&Y=y:.some_field>target_property_name

match=external_id:type=clinic:id>parent
--> string:clinic?match=external_id:._id>parent._id
or clinic?match=external_id:._id>parent._id

match=external_id:type=clinic:doc>parent
--> object:clinic?match=external_id:parent
or clinic?match=external_id:parent

match=external_id:type=person:._id>fields.patient_id
--> string:person?match=external_id:._id>parent_id
or person?match=external_id:._id>parent_id

match=external_id:type=person:doc>contact
--> object:person?match=external_id:contact
or person?match=external_id:contact

QUESTIONS

  • Is it necessary to have the doc and id keywords? Seems like we can eliminate them and still get the same result by assigning whatever the value is, either an object or string. Without it it is pretty clear what is going on and avoids needing to learn/understand the keywords.
  • Do we need reference by row number? If there is no use case for this perhaps we can remove that to reduce complexity.
  • Should we consider allowing type=place for looking at any place type? If so, differentiation between place types can still be done with the field matching eg type=clinic

Fix file structure for tasks

The file structure that we currently have does not represent well how the content of the files are associated to each other.

example-project/
    ...
    targets.json
        tasks/
            rules.nools.js
            schedules.json

The rules.nools.js is in the tasks folder, but it also generates the targets, so it should be moved out of tasks/. Also, the json files are the definition files for possible tasks and targets, so we should rename them as such. At that point the tasks folder isn't really needed anymore:

example-project/
    ...
    rules.nools.js
    targets.json
    tasks.json

Allow Collect forms to have the default language attribute

We should find a way to allow setting the default language for Collect forms. Having the default language attribute in our Enketo form breaks them in our app, so we remove it here. Setting a default language is useful however in Collect. This could be resolved in medic-configurer, otherwise we could figure out why the default language attribute is not working in our Enketo forms... and fix that.

Move to medic

Thanks @alxndrsn for making this awesome tool!

We should bring in it as a supported tool at medic at your earliest convenience.

Move custom place field from init to within place form - so that it isn't saved in place doc

Rationale: we need to show this field in the place group only when relevant, so cannot fully replace the place's name field. Having it there full time would mean having a non-relevant field saved with the place, which is a duplicate field and possibly junk.

Currently done crudely at https://github.com/medic/medic-projects/blob/3f6671f79b1763d45e5faab87cea8e4b7ce5aae8/standard/forms/process_place_forms.sh#L82-L86

Invalid JSON appears to be uploaded

If you upload invalid JSON for the app_settings the destination is thankfully not updated, however there is no notice of such in the command line:

medic-conf . https://admin:[password]@[instance].app.medicmobile.org upload-app-settings
INFO Processing config in . for https://admin:****@standard.app.medicmobile.org.
INFO Actions:
     - upload-app-settings
INFO Extra args: undefined
INFO Starting action: upload-app-settings…
INFO upload-app-settings complete.

The actual response when uploading invalid JSON is: {"error":"bad_request","reason":"invalid_json"}
We should report this back to the user.

Typing medic-conf just does stuff!

Most tools with options either display help or wait for input if you just type their name. This one starts doing stuff, probably not correctly (since I haven't specified where I want it to deploy to):

$ medic-conf
INFO Processing config in standard for undefined.

It should probably just display help instead.

[upload-to-docs] save final report to file

The final report can get very big. It would be great if this was instead saved as e.g. upload-to-docs.2017-11-02_${timestamp}.log.json, and a more concise report displayed on the console (e.g. just totals).

Allow uploading of .svg resource files

We are working to transition from png to svg for all icons in our app. Currently, it's only possible to upload PNGs using medic-conf. We should add support for uploading of SVGs as well. cc @alxndrsn

Resolve circular references

In legacy mode (up to 2.12.x) docs can have somewhat circular references, but limited by the app. For instance the primary contact for a place has doc.parent.contact == doc. In our app it is dealt with by not including the fields that would create infinite nesting eg doc.parent.contact does not further nest parent.

Here is a minimal example that we need to handle.

person.csv:

external_id,match=external_id:type=clinic:doc>parent,name,sex,timestamp:reported_date
place_714_contact,place_714,Person A,female,2012-01-23
place_715_contact,place_715,Person B,female,2012-01-25

place.clinic.csv:

external_id,name,match=external_id:type=person:doc>contact,timestamp:reported_date
place_714,Person A Family,place_714_contact,2012-01-23
place_715,Person B Family,place_715_contact,2012-01-25

Right now running csv-to-docs on these we get

ERROR TypeError: Converting circular structure to JSON
    at Object.stringify (native)
    at pretty ([...]\npm\node_modules\medic-configurer-beta\src\fn\csv-to-docs.js:9:26)
    at fs.recurseFiles.filter.reduce.then.then ([...]\npm\node_modules\medic-configurer-beta\src\fn\csv-to-docs.js:65:64)
    at process._tickCallback (internal/process/next_tick.js:103:7)

And the line in question: const pretty = o => JSON.stringify(o, null, 2);

Ability to delete individual forms

When creating forms to test an issue or feature I upload the form via medic-conf and then removing it via futon. Now that we have to do a SSH tunnel to access futon it's more of a hassle to remove a single form. Via medic-conf the only option right now is to delete-all-forms and then re-upload them all previous forms. It would be nice if we could delete a single form from an instance instead. For example, using delete-*-forms -- form_name, similar to how we can use upload-*-forms -- form_name.

Add `"collect": "true"` to the properties files for all collect forms

In order for the Collect app to download forms from an instance, the collect form properties file must include context.collect: true. To avoid manual changes to the collect properties files, this should be done automatically by medic-configurer when converting collect forms.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.