Skip to content

Configurable customizations

At some point you may need to provide a means of configuration for your package, such as api-keys, credentials, solution specific business configuration or run-time configuration for the customer.

Read more about the different types of configuration here

Using Lime Admin and runtime configuration should be the default options, as this is the most user friendly configuration option and come with most tools to validate and migrate configuration

Environment

You can set your package's default enviromental config in my-package/__init__.py.

def default_config():
    """Returns the default values for configuration parameters for this package
    The resulting configuration will be available under
    `lime_config.config['plugins']['{{ cookiecutter.lib_name }}']`
    The values here can be overridden in the service's config file.
    """

    return {}

The default config will be loaded in case there is no configuration entry in the config.yml, and will also serve as "documentation" for your package's configuration requirements.

Code example:

import lime_config

my_config = lime_config.config['packages']['my-package']

Application Configuration

The API used for accessing application configuration lies in the lime_config module. For usage documentation, see the lime-core documentation

Runtime

Configuration specific for your application (database) loaded during runtime using lime_data. Typically this configuration should be more of a "preferences" / non business-critical nature and can be edited by a user through the web client's Admin Pages. With lime-project you can generate the code needed to create a runtime configuration. Runtime configuration will show up in the administrator interface in the Web Client.

💡 Ask yourself the question: 'would the customer be OK with losing this data?'. If the answer is No then you should probably move it to the solution configuration instead.

Information

Note that this type of configuration is stored in the database and thus would be restored from a backup. Having for example an URL to an external system might create problems, restoring a staging environment from a production database will make the staging environment connected to the same external system as the production system

Working with lime-data

Code example:

import lime_data

my_prefs = {
    'background': 'lime',
    'theme': 'mojito'
}

# set data
lime_data.set_data(application, 'prefs', my_prefs, overwrite=True)

# get data
try:
    my_prefs = lime_data.get_data(application, 'prefs')

except NotFoundError:
    logger.error('No prefs found for application')

Warning

Do not store business-critical or sensitive data such as API keys, urls or credentials here.

Editing configuration in Lime Admin

In Lime CRM, configuration is key. It is therefor possible to create a UI to edit your configuration in Lime Admin.

A runtime config can be edited via the Lime Admin page of the web client. To achieve this, you need to create a config module by running the following in the root of your add-on:

$ lime-project generate config-module

This yields the following code in limepkg-cool-package/limepkg_cool_package/config/__init__.py:

import lime_admin.plugins
from .schema import create_schema


class RuntimeConfig(lime_admin.plugins.AdminPlugin):
   """A class to represent the plugin in Lime Administration.

   An instance has access to a `lime_application.application.Application`
   object via `self.application`.

   """

   @property
   def name(self):
       """The name of the plugin

       Note:
           The name is used as the key when persisting the config and
           also as the key for the plugin itself.
       """
       return 'limepkg_cool_package'

   @property
   def title(self):
       """The title of the plugin
       """
       return 'Cool Package'

   @property
   def version(self):
       """The version of the config

       Note:
           Should be incremented if
           the config format has changed. The version (if not None)
           is appended to the name when persisting the config.
       """
       return None

   def get_config(self):
       """Function to retrieve a persisted config

       Note:
           If a config doesn't exist, then a default config should be
           persisted and then returned.

           If version has changed, then any existing config should be
           upgraded, persisted and then returned.

           There needs to be a really, really good reason for not returning
           a config at all.

       Returns:
           `dict`

       Raises:
           `lime_admin.plugins.NotFoundError` if the config doesn't exist.
       """
       try:
           return super().get_config()
       except lime_admin.plugins.NotFoundError:
           return {}

   def get_schema(self):
       """Function to retrieve a schema for the config.

       Returns:
           :class:`marshmallow.Schema`
       """
       return create_schema(self.application)

   def set_config(self, config):
       """Function to persist a config.

       Note:
           The config should be validated before it's persisted.

       Args:
           config (dict): The config to be persisted.

       Raises:
           `lime_admin.plugins.ValueError` if the config is invalid.
       """
       super().set_config(config=config)


def register_config():
"""Function that is called by host when it's registering plugins.

Returns:
    :class:`Plugin`
"""
return RuntimeConfig

This enables the admin pages to pick up your config and add it to the Addons Configuration category. Let's go through the methods that need some more adaptation.

get_config

get_config will search (via lime_data) for your config with the key limepkg_cool_package or limepkg_cool_package.<VERSION> if version isn't None. If the key doesn't exist in the database (because e.g. this config gets edited for the first time), it raises a NotFoundError, so you can provide a default config in the corresponding except block.

If get_config can't find the latest version but an outdated version instead, it will run all necessary migrations and then return the migrated version.

get_schema

In order to get a UI for your config, you need to provide a schema. You can add the logic for that in the generated schema.py file. We use the marshmallow framework, so please check their documentation for how to declare your own schema.

In addition to the generic marshmallow fields you can also use Lime specific fields:

  • lime-core:lime_type.fields.LimeTypeField
  • lime-core:lime_type.fields.LimePropertyField
  • lime-core:lime_filter.fields.StoredFilterField
  • lime-core:lime_authentication.fields.GroupField
  • lime-core:lime_authentication.fields.UserField

Here's an example for how to use those fields. You can go to <DOMAIN>/<APP-NAME>/webadmin/#/showcase-config to see the resulting form.

from lime_type.fields import LimeTypeField, LimePropertyField
from lime_filter.fields import StoredFilterField
from lime_authentication.fields import GroupField, UserField
from marshmallow import Schema, fields

class LimetypeFieldsSchema(Schema):
    class Meta:
        ordered = True

    limetype = LimeTypeField(application=self.application,
                             title='LimeTypeField',
                             description='Choose a limetype based '
                                         'on your application')
    lime_property = LimePropertyField(
        application=self.application,
        limetype_field='limetype',
        title='LimePropertyField',
        description='Choose properties from the limetype you '
                    'previously selected')

class LimeAuthenticationSchema(Schema):
    class Meta:
        ordered = True

    groups = GroupField(application=self.application,
                        title='GroupField',
                        description='Choose from all groups of your '
                                    'application')
    users = UserField(application=self.application,
                      title='UserField',
                      description='Choose from all users of your '
                                  'application')

class LimeFilterSchema(Schema):
    filter = StoredFilterField(
        application=self.application,
        title='StoredFilterField',
        description='Choose from all stored filters. You could also '
                    'depend the selection on a previously selected '
                    'limetype')

class ShowcaseSchema(Schema):
    class Meta:
        ordered = True

    lime_type = fields.Nested(title='Limetype fields',
                              nested=LimetypeFieldsSchema)
    lime_filter = fields.Nested(title='Filter fields',
                                nested=LimeFilterSchema)
    lime_authentication = fields.Nested(
        title='Authentication fields',
        nested=LimeAuthenticationSchema)

Migrations

If you want to change the schema of an already released add-on, you will have to provide a migration, so that any saved and outdated config can be adapted to the new structure.

Let's assume your schema has been released like this:

from marshmallow import Schema, fields
class CoolPackageSchema(Schema):
   some_field = fields.Str(title='Cool field')

For some reason you want to update it to this:

from marshmallow import Schema, fields
class CoolPackageSubSchema(Schema):
   some_field = fields.Str(title='Cool field')

class CoolPackageSchema(Schema):
   group_field = fields.Nested(title='Cool category',
   nested=CoolPackageSubSchema)

That means for any saved config you want to migrate {'cool_field': 'some value'} to {'group_field': {'cool_field': 'some value'}}. Therefore, add a module called migrations.py to your config package:

import lime_admin.plugins


class Migration1(lime_admin.plugins.ConfigMigration):
    """ describe the schema changes
    """

    @property
    def version(self):
        return 1  # the new version, after running the upgrade

    def upgrade(self, config):
        return {'group_field': config}

    def downgrade(self, config):
        return config.get('group_field', {})

Afterwards, add the following method to your RuntimeConfig class and change the version from None to 1:

@property
def version(self):
    return 1
def get_migrations(self):
    """Get a list of migrations for the config.
      Returns: :class:`list`(:class:`lime_admin.plugins.Migration`)
    """
   return [Migration1()]

Now whenever get_config will find an older version than 1, it will run the migration and return the upgraded structure.

The next time you need a migration, you should write another implementation of lime_admin.plugins.ConfigMigration, add it to the array of migrations and increase the version to 2.