Source code for projectroles.app_settings

"""Projectroles app settings API"""

import json
import logging

from django.conf import settings

from projectroles.models import AppSetting, APP_SETTING_TYPES, SODAR_CONSTANTS
from projectroles.plugins import get_app_plugin, get_active_plugins
from projectroles.utils import get_display_name


logger = logging.getLogger(__name__)


# SODAR constants
APP_SETTING_SCOPE_PROJECT = SODAR_CONSTANTS['APP_SETTING_SCOPE_PROJECT']
APP_SETTING_SCOPE_USER = SODAR_CONSTANTS['APP_SETTING_SCOPE_USER']
APP_SETTING_SCOPE_PROJECT_USER = SODAR_CONSTANTS[
    'APP_SETTING_SCOPE_PROJECT_USER'
]
APP_SETTING_SCOPE_SITE = SODAR_CONSTANTS['APP_SETTING_SCOPE_SITE']
PROJECT_TYPE_PROJECT = SODAR_CONSTANTS['PROJECT_TYPE_PROJECT']
PROJECT_TYPE_CATEGORY = SODAR_CONSTANTS['PROJECT_TYPE_CATEGORY']
SITE_MODE_SOURCE = SODAR_CONSTANTS['SITE_MODE_SOURCE']
SITE_MODE_TARGET = SODAR_CONSTANTS['SITE_MODE_TARGET']

# Local constants
APP_SETTING_GLOBAL_DEFAULT = False
APP_SETTING_SCOPES = [
    APP_SETTING_SCOPE_PROJECT,
    APP_SETTING_SCOPE_USER,
    APP_SETTING_SCOPE_PROJECT_USER,
    APP_SETTING_SCOPE_SITE,
]
APP_SETTING_DEFAULT_VALUES = {
    'STRING': '',
    'INTEGER': 0,
    'BOOLEAN': False,
    'JSON': {},
}
APP_SETTING_SCOPE_ARGS = {
    APP_SETTING_SCOPE_PROJECT: {'project': True, 'user': False},
    APP_SETTING_SCOPE_USER: {'project': False, 'user': True},
    APP_SETTING_SCOPE_PROJECT_USER: {'project': True, 'user': True},
    APP_SETTING_SCOPE_SITE: {'project': False, 'user': False},
}
DELETE_SCOPE_ERR_MSG = 'Argument "{arg}" must be set for {scope} scope setting'
GLOBAL_PROJECT_ERR_MSG = (
    'Overriding global settings for remote projects not allowed'
)
GLOBAL_USER_ERR_MSG = (
    'Overriding global user settings on target site not allowed'
)
# TODO: Remove in v1.1 (see #1394)
LOCAL_DEPRECATE_MSG = (
    'The "local" argument for app settings has been deprecated and will be '
    'removed in SODAR Core v1.1: use "global" instead'
)


# Define App Settings for projectroles app
PROJECTROLES_APP_SETTINGS = {
    #: App settings definition
    #:
    #: Example ::
    #:
    #:     'example_setting': {
    #:         'scope': 'PROJECT',  # PROJECT/USER/PROJECT-USER/SITE
    #:         'type': 'STRING',  # STRING/INTEGER/BOOLEAN/JSON
    #:         'default': 'example',
    #:         'label': 'Project setting',  # Optional, defaults to name/key
    #:         'placeholder': 'Enter example setting here',  # Optional
    #:         'description': 'Example project setting',  # Optional
    #:         'options': ['example', 'example2'],  # Optional, only for
    #:                    settings of type STRING or INTEGER
    #:         'user_modifiable': True,  # Optional, show/hide in forms
    #:         'global': True,  # Only allow editing on target sites if False
    #:                          # (optional, default True)
    #:         'project_types': [PROJECT_TYPE_PROJECT],  # Optional, list may
    #:          contain PROJECT_TYPE_CATEGORY and/or PROJECT_TYPE_PROJECT
    #:     }
    'ip_restrict': {
        'scope': APP_SETTING_SCOPE_PROJECT,
        'type': 'BOOLEAN',
        'default': False,
        'label': 'IP restrict',
        'description': 'Restrict project access by an allowed IP list',
        'user_modifiable': True,
        'global': True,
    },
    'ip_allowlist': {
        'scope': APP_SETTING_SCOPE_PROJECT,
        'type': 'JSON',
        'default': [],
        'label': 'IP allow list',
        'description': 'List of allowed IPs for project access',
        'user_modifiable': True,
        'global': True,
    },
    'project_star': {
        'scope': APP_SETTING_SCOPE_PROJECT_USER,
        'type': 'BOOLEAN',
        'default': False,
        'global': False,
        'project_types': [PROJECT_TYPE_PROJECT, PROJECT_TYPE_CATEGORY],
    },
    'notify_email_project': {
        'scope': APP_SETTING_SCOPE_USER,
        'type': 'BOOLEAN',
        'default': True,
        'label': 'Receive email for {} updates'.format(
            get_display_name(PROJECT_TYPE_PROJECT)
        ),
        'description': (
            'Receive email notifications for {} or {} creation, updating, '
            'moving and archiving.'.format(
                get_display_name(PROJECT_TYPE_CATEGORY),
                get_display_name(PROJECT_TYPE_PROJECT),
            )
        ),
        'user_modifiable': True,
        'global': True,
    },
    'notify_email_role': {
        'scope': APP_SETTING_SCOPE_USER,
        'type': 'BOOLEAN',
        'default': True,
        'label': 'Receive email for {} membership updates'.format(
            get_display_name(PROJECT_TYPE_PROJECT)
        ),
        'description': (
            'Receive email notifications for {} or {} membership updates and '
            'invitation activity.'.format(
                get_display_name(PROJECT_TYPE_CATEGORY),
                get_display_name(PROJECT_TYPE_PROJECT),
            )
        ),
        'user_modifiable': True,
        'global': True,
    },
}


[docs] class AppSettingAPI: @classmethod def _check_project_and_user(cls, scope, project, user): """ Ensure project and user parameters are set according to scope. :param scope: Scope of Setting (USER, PROJECT, PROJECT_USER, SITE) :param project: Project object :param user: User object :raise: ValueError if none or both objects exist """ if not APP_SETTING_SCOPE_ARGS[scope] == { 'project': project is not None, 'user': user is not None, }: raise ValueError( 'Project and/or user are set incorrect for setting ' 'with {} scope'.format(scope) ) @classmethod def _check_scope(cls, scope): """ Ensure the validity of a scope definition. :param scope: String :raise: ValueError if scope is not recognized """ if scope not in APP_SETTING_SCOPES: raise ValueError('Invalid scope "{}"'.format(scope)) @classmethod def _check_type(cls, setting_type): """ Ensure the validity of app setting type. :param setting_type: String :raise: ValueError if type is not recognized """ if setting_type not in APP_SETTING_TYPES: raise ValueError('Invalid setting type "{}"'.format(setting_type)) @classmethod def _check_type_options(cls, setting_type, setting_options): """ Ensure setting_type is allowed to have options. :param setting_type: String :param setting_options: List of options (Strings or Integers) :raise: ValueError if type is not recognized """ if setting_type not in ['INTEGER', 'STRING'] and setting_options: raise ValueError( 'Options are only allowed for settings of type INTEGER and ' 'STRING' ) @classmethod def _check_value_in_options( cls, setting_value, setting_options, project=None, user=None ): """ Ensure setting_value is present in setting_options. :param setting_value: String :param setting_options: List of options (String or Integers) :param project: Project object :param user: User object :raise: ValueError if type is not recognized """ if callable(setting_options): valid_options = [ val[0] if isinstance(val, tuple) else val for val in setting_options(project, user) ] if setting_value not in valid_options: raise ValueError( 'Choice "{}" not found in options ({})'.format( setting_value, ', '.join(map(str, valid_options)), ) ) elif ( setting_options and not callable(setting_options) and setting_value not in setting_options ): raise ValueError( 'Choice "{}" not found in options ({})'.format( setting_value, ', '.join(map(str, setting_options)) ) ) # TODO: Remove in v1.1 (see #1394) @classmethod def _check_local_attr(cls, setting_def): """ Warn if the deprecated "local" attribute is included in a settings definition instead of the new "global" attribute. :param setting_def: Dict """ if 'local' in setting_def: logger.warning(LOCAL_DEPRECATE_MSG) @classmethod def _get_app_plugin(cls, plugin_name): """ Return app plugin by name. :param plugin_name: Name of the app plugin (string) :return: App plugin object :raise: ValueError if plugin is not found with the name """ plugin = get_app_plugin(plugin_name) if not plugin: raise ValueError( 'Plugin not found with name "{}"'.format(plugin_name) ) return plugin @classmethod def _get_defs(cls, plugin=None, plugin_name=None): """ Ensure valid argument values for a settings def query. :param plugin: Plugin object or None :param plugin_name: Name of the app plugin (string or None) :return: Dict :raise: ValueError if args are not valid or plugin is not found """ if not plugin and not plugin_name: raise ValueError('Plugin object and name both unset') if plugin_name == 'projectroles': return cls.get_projectroles_defs() if not plugin: plugin = cls._get_app_plugin(plugin_name) return plugin.app_settings @classmethod def _get_json_value(cls, value): """ Return JSON value as dict regardless of input type :param value: Original value (string or dict) :raise: json.decoder.JSONDecodeError if string value is not valid JSON :raise: ValueError if value type is not recognized or if value is not valid JSON :return: dict """ if not value: return {} try: if isinstance(value, str): return json.loads(value) json.dumps(value) # Ensure this is valid return value except Exception: raise ValueError('Value is not valid JSON: {}'.format(value)) @classmethod def _log_set_debug( cls, action, plugin_name, setting_name, value, project, user ): """ Helper method for logging setting changes in set() method. :param action: Action string (string) :param plugin_name: App plugin name (string) :param setting_name: Setting name (string) :param value: Setting value (string) :param project: Project object :param user: User object """ extra_data = [] if project: extra_data.append('project={}'.format(project.sodar_uuid)) if user: extra_data.append('user={}'.format(user.username)) logger.debug( '{} app setting: {}.{} = "{}"{}'.format( action, plugin_name, setting_name, value, ' ({})'.format('; '.join(extra_data)) if extra_data else '', ) )
[docs] @classmethod def get_default( cls, plugin_name, setting_name, project=None, user=None, post_safe=False ): """ Get default setting value from an app plugin. :param plugin_name: App plugin name (string, equals "name" in plugin) :param setting_name: Setting name (string) :param project: Project object (optional) :param user: User object (optional) :param post_safe: Whether a POST safe value should be returned (bool) :return: Setting value (string, integer or boolean) :raise: ValueError if app plugin is not found :raise: KeyError if nothing is found with setting_name """ if plugin_name == 'projectroles': app_settings = cls.get_projectroles_defs() else: app_plugin = get_app_plugin(plugin_name) if not app_plugin: raise ValueError( 'App plugin not found: "{}"'.format(plugin_name) ) app_settings = app_plugin.app_settings if setting_name not in app_settings: raise KeyError( 'Setting "{}" not found in app plugin "{}"'.format( setting_name, plugin_name ) ) # TODO: Remove _check_local_attr() in v1.1 (see #1394) cls._check_local_attr(app_settings[setting_name]) if callable(app_settings[setting_name].get('default')): try: callable_setting = app_settings[setting_name]['default'] return callable_setting(project, user) except Exception: logger.error( 'Error in callable setting "{}" for plugin "{}"'.format( setting_name, plugin_name ) ) return APP_SETTING_DEFAULT_VALUES[ app_settings[setting_name]['type'] ] elif app_settings[setting_name]['type'] == 'JSON': json_default = app_settings[setting_name].get('default') if not json_default: if isinstance(json_default, dict): return {} elif isinstance(json_default, list): return [] return {} if post_safe: return json.dumps(app_settings[setting_name]['default']) return app_settings[setting_name]['default']
[docs] @classmethod def get( cls, plugin_name, setting_name, project=None, user=None, post_safe=False ): """ Return app setting value for a project or a user. If not set, return default. :param plugin_name: App plugin name (string, equals "name" in plugin) :param setting_name: Setting name (string) :param project: Project object (optional) :param user: User object (optional) :param post_safe: Whether a POST safe value should be returned (bool) :return: String or None :raise: KeyError if nothing is found with setting_name """ if not user or user.is_authenticated: try: val = AppSetting.objects.get_setting_value( plugin_name, setting_name, project=project, user=user ) except AppSetting.DoesNotExist: val = cls.get_default( plugin_name, setting_name, project=project, user=user, post_safe=post_safe, ) else: # Anonymous user val = cls.get_default( plugin_name, setting_name, project=project, user=user, post_safe=post_safe, ) # Handle post_safe for dict values (JSON) if post_safe and isinstance(val, (dict, list)): return json.dumps(val) return val
[docs] @classmethod def get_all(cls, project=None, user=None, post_safe=False): """ Return all setting values. If the value is not found, return the default. :param project: Project object (optional) :param user: User object (optional) :param post_safe: Whether POST safe values should be returned (bool) :return: Dict :raise: ValueError if neither project nor user are set """ if not project and not user: raise ValueError('Project and user are both unset') ret = {} app_plugins = get_active_plugins() for plugin in app_plugins: p_settings = cls.get_definitions( APP_SETTING_SCOPE_PROJECT, plugin=plugin ) for s_key in p_settings: ret['settings.{}.{}'.format(plugin.name, s_key)] = cls.get( plugin.name, s_key, project, user, post_safe ) p_settings = cls.get_definitions( APP_SETTING_SCOPE_PROJECT, plugin_name='projectroles' ) for s_key in p_settings: ret['settings.{}.{}'.format('projectroles', s_key)] = cls.get( 'projectroles', s_key, post_safe ) return ret
[docs] @classmethod def get_defaults(cls, scope, project=None, user=None, post_safe=False): """ Get all default settings for a scope. :param scope: Setting scope (PROJECT, USER or PROJECT_USER) :param project: Project object (optional) :param user: User object (optional) :param post_safe: Whether POST safe values should be returned (bool) :return: Dict """ cls._check_scope(scope) ret = {} app_plugins = get_active_plugins() for plugin in app_plugins: p_settings = cls.get_definitions(scope, plugin=plugin) for s_key in p_settings: ret['settings.{}.{}'.format(plugin.name, s_key)] = ( cls.get_default( plugin.name, s_key, project=project, user=user, post_safe=post_safe, ) ) p_settings = cls.get_definitions(scope, plugin_name='projectroles') for s_key in p_settings: ret['settings.{}.{}'.format('projectroles', s_key)] = ( cls.get_default( 'projectroles', s_key, project=project, user=user, post_safe=post_safe, ) ) return ret
[docs] @classmethod def set( cls, plugin_name, setting_name, value, project=None, user=None, validate=True, ): """ Set value of an existing project or user settings. Creates the object if not found. :param plugin_name: App plugin name (string, equals "name" in plugin) :param setting_name: Setting name (string) :param value: Value to be set :param project: Project object (optional) :param user: User object (optional) :param validate: Validate value (bool, default=True) :return: True if changed, False if not changed :raise: ValueError if validating and value is not accepted for setting type :raise: ValueError if neither project nor user are set :raise: KeyError if setting name is not found in plugin specification """ s_def = cls.get_definition(name=setting_name, plugin_name=plugin_name) cls._check_scope(s_def.get('scope', None)) cls._check_project_and_user(s_def.get('scope', None), project, user) # Check project type if ( project and not s_def.get('project_types', None) and not project.type == PROJECT_TYPE_PROJECT or project and s_def.get('project_types', None) and project.type not in s_def['project_types'] ): raise ValueError( 'Project type {} not allowed for setting {}'.format( project.type, setting_name ) ) # Prevent updating global setting on target site if cls.get_global_value(s_def): if project and project.is_remote(): raise ValueError(GLOBAL_PROJECT_ERR_MSG) if ( user and not project and settings.PROJECTROLES_SITE_MODE == SITE_MODE_TARGET ): raise ValueError(GLOBAL_USER_ERR_MSG) try: # Update existing setting q_kwargs = {'name': setting_name, 'project': project, 'user': user} if not plugin_name == 'projectroles': q_kwargs['app_plugin__name'] = plugin_name else: q_kwargs['app_plugin'] = None setting = AppSetting.objects.get(**q_kwargs) if cls.compare_value(setting, value): return False if validate: cls.validate( setting.type, value, s_def.get('options'), project=project, user=user, ) if setting.type == 'JSON': setting.value_json = cls._get_json_value(value) else: setting.value = value setting.save() cls._log_set_debug( 'Set', plugin_name, setting_name, value, project, user ) return True except AppSetting.DoesNotExist: # Create new s_type = s_def['type'] if plugin_name == 'projectroles': app_plugin_model = None else: app_plugin = get_app_plugin(plugin_name) app_plugin_model = app_plugin.get_model() if validate: v = cls._get_json_value(value) if s_type == 'JSON' else value cls.validate( s_type, v, s_def.get('options'), project=project, user=user, ) s_mod = ( bool(s_def['user_modifiable']) if 'user_modifiable' in s_def else True ) s_vals = { 'app_plugin': app_plugin_model, 'project': project, 'user': user, 'name': setting_name, 'type': s_type, 'user_modifiable': s_mod, } if s_type == 'JSON': s_vals['value_json'] = cls._get_json_value(value) else: s_vals['value'] = value AppSetting.objects.create(**s_vals) cls._log_set_debug( 'Create', plugin_name, setting_name, value, project, user ) return True
[docs] @classmethod def is_set(cls, plugin_name, setting_name, project=None, user=None): """ Return True if the setting has been set, instead of retrieving the default value from the definition. NOTE: Also returns True if the current set value equals the default. :param plugin_name: App plugin name (string, equals "name" in plugin) :param setting_name: Setting name (string) :param project: Project object (optional) :param user: User object (optional) :return: Boolean """ s_def = cls.get_definition(name=setting_name, plugin_name=plugin_name) cls._check_project_and_user(s_def.get('scope', None), project, user) q_kwargs = {'name': setting_name, 'project': project, 'user': user} if not plugin_name == 'projectroles': q_kwargs['app_plugin__name'] = plugin_name else: q_kwargs['app_plugin'] = None return AppSetting.objects.filter(**q_kwargs).exists()
[docs] @classmethod def delete(cls, plugin_name, setting_name, project=None, user=None): """ Delete one or more app setting objects. In case of a PROJECT_USER setting, can be used to delete all settings related to project. :param plugin_name: App plugin name (string, equals "name" in plugin) :param setting_name: Setting name (string) :param project: Project object to delete setting from (optional) :param user: User object to delete setting from (optional) :raise: ValueError with invalid project/user args """ setting_def = cls.get_definition(setting_name, plugin_name=plugin_name) if setting_def['scope'] != APP_SETTING_SCOPE_PROJECT_USER: cls._check_project_and_user( setting_def.get('scope', None), project, user ) elif not project: raise ValueError( 'Project must be set for {} scope settings'.format( APP_SETTING_SCOPE_PROJECT_USER ) ) q_kwargs = {'name': setting_name} if user: q_kwargs['user'] = user if project: q_kwargs['project'] = project logger.debug( 'Delete app setting: {}.{} ({})'.format( plugin_name, setting_name, '; '.join(q_kwargs) ) ) app_settings = AppSetting.objects.filter(**q_kwargs) s_count = app_settings.count() app_settings.delete() logger.debug( 'Deleted {} app setting{}'.format( s_count, 's' if s_count != 1 else '' ) )
[docs] @classmethod def delete_by_scope( cls, scope, project=None, user=None, ): """ Delete all app settings within a given scope for a project and/or user. :param scope: Setting scope (string) :param project: Project object to delete setting from :param user: User object to delete setting from :raise: ValueError if scope, project or user are incorrect """ if not scope: raise ValueError('Scope must be set') cls._check_scope(scope) cls._check_project_and_user(scope, project, user) for plugin_name, app_settings in cls.get_all_defs().items(): for setting_name, setting_def in app_settings.items(): if setting_def['scope'] == scope: cls.delete( plugin_name, setting_name, project=project, user=user, )
[docs] @classmethod def validate( cls, setting_type, setting_value, setting_options, project=None, user=None, ): """ Validate setting value according to its type. :param setting_type: Setting type :param setting_value: Setting value :param setting_options: Setting options (can be None) :param project: Project object (optional) :param user: User object (optional) :raise: ValueError if setting_type or setting_value is invalid """ cls._check_type(setting_type) cls._check_type_options(setting_type, setting_options) cls._check_value_in_options( setting_value, setting_options, project=project, user=user ) # Check callable if callable(setting_value): setting_value(project, user) if setting_type == 'BOOLEAN': if not isinstance(setting_value, bool): raise ValueError( 'Please enter a valid boolean value ({})'.format( setting_value ) ) elif setting_type == 'INTEGER': if ( not isinstance(setting_value, int) and not str(setting_value).isdigit() ): raise ValueError( 'Please enter a valid integer value ({})'.format( setting_value ) ) elif setting_type == 'JSON': try: json.dumps(setting_value) except TypeError: raise ValueError( 'Please enter valid JSON ({})'.format(setting_value) ) return True
[docs] @classmethod def get_definition(cls, name, plugin=None, plugin_name=None): """ Return definition for a single app setting, either based on an app name or the plugin object. :param name: Setting name :param plugin: Plugin object or None :param plugin_name: Name of the app plugin (string or None) :return: Dict :raise: ValueError if neither plugin_name nor plugin are set, or if setting is not found in plugin """ defs = cls._get_defs(plugin, plugin_name) if name not in defs: raise ValueError( 'App setting not found in plugin "{}" with name "{}"'.format( plugin_name or plugin.name, name ) ) ret = defs[name] cls._check_type(ret['type']) cls._check_type_options(ret['type'], ret.get('options')) return ret
[docs] @classmethod def get_definitions( cls, scope, plugin=None, plugin_name=None, user_modifiable=False, ): """ Return app setting definitions of a specific scope from a plugin. :param scope: PROJECT, USER or PROJECT_USER :param plugin: Plugin object or None :param plugin_name: App plugin name (string, equals "name" in plugin) :param user_modifiable: Only return non-superuser modifiable settings if True (boolean) :return: Dict :raise: ValueError if scope is invalid or if neither plugin_name nor plugin are set """ cls._check_scope(scope) defs = cls._get_defs(plugin, plugin_name) ret = { k: v for k, v in defs.items() if ( 'scope' in v and v['scope'] == scope and ( not user_modifiable or ( 'user_modifiable' not in v or v['user_modifiable'] is True ) ) ) } # Ensure type validity for k, v in ret.items(): cls._check_type(v['type']) cls._check_type_options(v['type'], v.get('options')) return ret
[docs] @classmethod def get_projectroles_defs(cls): """ Return projectroles settings definitions. If it exists, get value from settings.PROJECTROLES_APP_SETTINGS_TEST for testing modifications. :return: Dict """ try: app_settings = ( settings.PROJECTROLES_APP_SETTINGS_TEST or PROJECTROLES_APP_SETTINGS ) except AttributeError: app_settings = PROJECTROLES_APP_SETTINGS return app_settings
[docs] @classmethod def get_all_defs(cls): """ Return app setting definitions for projectroles and all active app plugins in a dictionary with the app name as key. :return: Dict """ ret = {'projectroles': cls.get_projectroles_defs()} plugins = ( [] + get_active_plugins('project_app') + get_active_plugins('site_app') ) for p in plugins: ret[p.name] = p.app_settings return ret
[docs] @classmethod def get_global_value(cls, setting_def): """ Get the "global" value of a settings definition. If the deprecated "local" value is still used, return that and log a warning. :param setting_def: Dict :return: Boolean """ if 'local' in setting_def: # TODO: Remove in v1.1 (see #1394) logger.warning(LOCAL_DEPRECATE_MSG) return not setting_def['local'] # Inverse value return setting_def.get('global', APP_SETTING_GLOBAL_DEFAULT)
[docs] @classmethod def compare_value(cls, obj, input_value): """ Compare input value to value in an AppSetting object. Return True if values match, False if there is a mismatch. :param obj: AppSetting object :param input_value: Input value (string, int, bool or dict) :return: Bool """ if obj.type == 'JSON': return ( not obj.value_json and not input_value ) or obj.value_json == cls._get_json_value(input_value) elif obj.type == 'BOOLEAN': if isinstance(input_value, str): input_value = bool(int(input_value)) return bool(int(obj.value)) == input_value return obj.value == str(input_value)
def get_example_setting_default(project=None, user=None): """ Example method for callable default value retrieval for app settings. :param project: Project object :param user: User object :return: String with project and user info or 'No project' """ response = 'N/A' if project and user: response = '{}:{}'.format(project.title, user.username) elif project: response = str(project.sodar_uuid) elif user: response = str(user.sodar_uuid) return response def get_example_setting_options(project=None, user=None): """ Example method for callable option list retrieval for app settings. :param project: Project object :param user: User object :return: List of tuples for ChoiceField """ response = [ ('N/A', 'No project or user for callable'), 'Example string option', ] if project and user: response.append( ( str(project.sodar_uuid), 'Project UUID {} by {}'.format( project.sodar_uuid, user.username ), ) ) elif project: response.append( ( str(project.sodar_uuid), 'Project UUID: {}'.format(project.sodar_uuid), ) ) elif user: response.append( (str(user.sodar_uuid), 'User UUID: {}'.format(user.sodar_uuid)) ) return response