diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 55a88503..897673ef 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,6 +27,7 @@ Added - ``SiteReadOnlySettingAjaxView`` Ajax view (#24) - ``siteappsettings`` site app plugin (#1304) - ``SiteAppSettingsFormView`` view (#1304) + - ``SODARAppSettingFormMixin`` form helper mixin (#1545) Changed ------- @@ -41,6 +42,7 @@ Changed - Deprecate ``AppSettingAPI.get_all()`` (#1534) - Allow no role for old owner in ``RoleAssignmentOwnerTransferMixin`` (#836, #1391) - Allow no role for old owner in ``perform_owner_transfer()`` (#836, #1391) + - Move app setting form helpers in ``SODARAppSettingFormMixin`` (#1545) - **Tokens** - Update UI for site read-only mode (#24) - **Userprofile** diff --git a/projectroles/forms.py b/projectroles/forms.py index 65cc02a1..ba4639c6 100644 --- a/projectroles/forms.py +++ b/projectroles/forms.py @@ -47,6 +47,7 @@ SITE_MODE_SOURCE = SODAR_CONSTANTS['SITE_MODE_SOURCE'] SITE_MODE_TARGET = SODAR_CONSTANTS['SITE_MODE_TARGET'] APP_SETTING_SCOPE_PROJECT = SODAR_CONSTANTS['APP_SETTING_SCOPE_PROJECT'] +APP_SETTING_SCOPE_USER = SODAR_CONSTANTS['APP_SETTING_SCOPE_USER'] APP_SETTING_SCOPE_SITE = SODAR_CONSTANTS['APP_SETTING_SCOPE_SITE'] APP_SETTING_TYPE_BOOLEAN = SODAR_CONSTANTS['APP_SETTING_TYPE_BOOLEAN'] APP_SETTING_TYPE_INTEGER = SODAR_CONSTANTS['APP_SETTING_TYPE_INTEGER'] @@ -100,6 +101,100 @@ def add_error(self, field, error): self.logger.error(log_msg) super().add_error(field, error) # Call the error method in Django forms + +class SODARAppSettingsFormMixin: + """Helpers for app settings handling in forms""" + + def set_app_setting_field(self, plugin_name, s_field, s_def): + """ + Helper for setting app setting field, widget and value. + + :param plugin_name: App plugin name + :param s_field: Form field name + :param s_def: PluginAppSettingDef object + """ + scope = s_def.scope + scope_kwargs = {} + if scope == APP_SETTING_SCOPE_PROJECT: + scope_kwargs = { + 'project': self.instance if self.instance.pk else None + } + elif scope == APP_SETTING_SCOPE_USER: + scope_kwargs = {'user': self.user} # NOTE: Requires self.user + s_widget_attrs = s_def.widget_attrs + if scope == APP_SETTING_SCOPE_PROJECT: + s_project_types = s_def.project_types or [PROJECT_TYPE_PROJECT] + s_widget_attrs['data-project-types'] = ','.join( + s_project_types + ).lower() + if s_def.placeholder is not None: + s_widget_attrs['placeholder'] = s_def.placeholder + setting_kwargs = { + 'required': False, + 'label': s_def.label or '{}.{}'.format(plugin_name, s_def.name), + 'help_text': s_def.description, + } + + # Option + if s_def.options and callable(s_def.options): + values = s_def.options(**scope_kwargs) + self.fields[s_field] = forms.ChoiceField( + choices=[ + ( + (str(value[0]), str(value[1])) + if isinstance(value, tuple) + else (str(value), str(value)) + ) + for value in values + ], + **setting_kwargs, + ) + elif s_def.options: + self.fields[s_field] = forms.ChoiceField( + choices=[ + ( + (int(option), int(option)) + if s_def.type == APP_SETTING_TYPE_INTEGER + else (option, option) + ) + for option in s_def.options + ], + **setting_kwargs, + ) + # Other types + elif s_def.type == APP_SETTING_TYPE_STRING: + self.fields[s_field] = forms.CharField( + widget=forms.TextInput(attrs=s_widget_attrs), **setting_kwargs + ) + elif s_def.type == APP_SETTING_TYPE_INTEGER: + self.fields[s_field] = forms.IntegerField( + widget=forms.NumberInput(attrs=s_widget_attrs), **setting_kwargs + ) + elif s_def.type == APP_SETTING_TYPE_BOOLEAN: + self.fields[s_field] = forms.BooleanField(**setting_kwargs) + # JSON + elif s_def.type == APP_SETTING_TYPE_JSON: + # NOTE: Attrs MUST be supplied here (#404) + if 'class' in s_widget_attrs: + s_widget_attrs['class'] += ' sodar-json-input' + else: + s_widget_attrs['class'] = 'sodar-json-input' + self.fields[s_field] = forms.CharField( + widget=forms.Textarea(attrs=s_widget_attrs), **setting_kwargs + ) + + # Add optional attributes from plugin (#404) + self.fields[s_field].widget.attrs.update(s_widget_attrs) + # Set initial value + value = app_settings.get( + plugin_name=plugin_name, + setting_name=s_def.name, + **scope_kwargs, + ) + if s_def.type == APP_SETTING_TYPE_JSON: + value = json.dumps(value) + self.initial[s_field] = value + def get_app_setting_label(self, plugin, label): """Return label for app setting key""" if plugin: @@ -268,7 +363,7 @@ def __init__( # Project form ----------------------------------------------------------------- -class ProjectForm(SODARModelForm): +class ProjectForm(SODARAppSettingsFormMixin, SODARModelForm): """Form for Project creation/updating""" # Set up owner field @@ -389,98 +484,7 @@ def _init_remote_sites(self): required=False, ) - def _set_app_setting_field(self, plugin_name, s_field, s_def): - """ - Internal helper for setting app setting field, widget and value. - - :param plugin_name: App plugin name - :param s_field: Form field name - :param s_def: PluginAppSettingDef object - """ - s_widget_attrs = s_def.widget_attrs - s_project_types = s_def.project_types or [PROJECT_TYPE_PROJECT] - s_widget_attrs['data-project-types'] = ','.join(s_project_types).lower() - if s_def.placeholder is not None: - s_widget_attrs['placeholder'] = s_def.placeholder - setting_kwargs = { - 'required': False, - 'label': s_def.label or '{}.{}'.format(plugin_name, s_def.name), - 'help_text': s_def.description, - } - - # Option - if s_def.options and callable(s_def.options) and self.instance.pk: - values = s_def.options(project=self.instance) - self.fields[s_field] = forms.ChoiceField( - choices=[ - ( - (str(value[0]), str(value[1])) - if isinstance(value, tuple) - else (str(value), str(value)) - ) - for value in values - ], - **setting_kwargs, - ) - elif s_def.options and callable(s_def.options) and not self.instance.pk: - values = s_def.options(project=None) - self.fields[s_field] = forms.ChoiceField( - choices=[ - ( - (str(value[0]), str(value[1])) - if isinstance(value, tuple) - else (str(value), str(value)) - ) - for value in values - ], - **setting_kwargs, - ) - elif s_def.options: - self.fields[s_field] = forms.ChoiceField( - choices=[ - ( - (int(option), int(option)) - if s_def.type == APP_SETTING_TYPE_INTEGER - else (option, option) - ) - for option in s_def.options - ], - **setting_kwargs, - ) - # Other types - elif s_def.type == APP_SETTING_TYPE_STRING: - self.fields[s_field] = forms.CharField( - widget=forms.TextInput(attrs=s_widget_attrs), **setting_kwargs - ) - elif s_def.type == APP_SETTING_TYPE_INTEGER: - self.fields[s_field] = forms.IntegerField( - widget=forms.NumberInput(attrs=s_widget_attrs), **setting_kwargs - ) - elif s_def.type == APP_SETTING_TYPE_BOOLEAN: - self.fields[s_field] = forms.BooleanField(**setting_kwargs) - # JSON - elif s_def.type == APP_SETTING_TYPE_JSON: - # NOTE: Attrs MUST be supplied here (#404) - if 'class' in s_widget_attrs: - s_widget_attrs['class'] += ' sodar-json-input' - else: - s_widget_attrs['class'] = 'sodar-json-input' - self.fields[s_field] = forms.CharField( - widget=forms.Textarea(attrs=s_widget_attrs), **setting_kwargs - ) - - # Add optional attributes from plugin (#404) - self.fields[s_field].widget.attrs.update(s_widget_attrs) - # Set initial value - value = app_settings.get( - plugin_name=plugin_name, - setting_name=s_def.name, - project=self.instance if self.instance.pk else None, - ) - if s_def.type == APP_SETTING_TYPE_JSON: - value = json.dumps(value) - self.initial[s_field] = value - + # TODO: Refactor into generic helper in SODARAppSettingsFormMixin def _set_app_setting_notes(self, s_field, s_def, plugin): """ Internal helper for setting app setting label notes. @@ -505,6 +509,7 @@ def _set_app_setting_notes(self, s_field, s_def, plugin): plugin, self.fields[s_field].label ) + # TODO: Refactor into generic helper in SODARAppSettingsFormMixin def _init_app_settings(self): """ Initialize app settings fields in the form. @@ -531,10 +536,11 @@ def _init_app_settings(self): for s_def in s_defs.values(): s_field = 'settings.{}.{}'.format(plugin_name, s_def.name) # Set field, widget and value - self._set_app_setting_field(plugin_name, s_field, s_def) + self.set_app_setting_field(plugin_name, s_field, s_def) # Set label notes self._set_app_setting_notes(s_field, s_def, plugin) + # TODO: Refactor into generic helper in SODARAppSettingsFormMixin @classmethod def _validate_app_settings( self, @@ -1191,94 +1197,13 @@ def save(self, *args, **kwargs): # Site app settings form ------------------------------------------------------- -class SiteAppSettingsForm(SODARForm): +class SiteAppSettingsForm(SODARAppSettingsFormMixin, SODARForm): """Form for managing site app settings""" - # TODO: Unify repeated code with ProjectForm and UserAppSettingsForm - def _set_app_setting_field(self, plugin_name, s_field, s_def): - """ - Set site app setting field, widget and value. - - :param plugin_name: App plugin name - :param s_field: Form field name - :param s_def: PluginAppSettingDef object - """ - s_widget_attrs = s_def.widget_attrs - if s_def.placeholder is not None: - s_widget_attrs['placeholder'] = s_def.placeholder - setting_kwargs = { - 'required': False, - 'label': s_def.label or '{}.{}'.format(plugin_name, s_def.name), - 'help_text': s_def.description, - } - # Disable global settings if on target site - if ( - s_def.global_edit - and settings.PROJECTROLES_SITE_MODE == SITE_MODE_TARGET - ): - setting_kwargs['label'] += ' ' + SETTING_DISABLE_LABEL - setting_kwargs['help_text'] += ' ' + SETTING_SOURCE_ONLY_MSG - setting_kwargs['disabled'] = True - - if s_def.options and callable(s_def.options): - self.fields[s_field] = forms.ChoiceField( - choices=[ - ( - (str(value[0]), str(value[1])) - if isinstance(value, tuple) - else (str(value), str(value)) - ) - for value in s_def.options() - ], - **setting_kwargs, - ) - elif s_def.options: - self.fields[s_field] = forms.ChoiceField( - choices=[ - ( - (int(option), int(option)) - if s_def.type == APP_SETTING_TYPE_INTEGER - else (option, option) - ) - for option in s_def.options - ], - **setting_kwargs, - ) - elif s_def.type == APP_SETTING_TYPE_STRING: - self.fields[s_field] = forms.CharField( - widget=forms.TextInput(attrs=s_widget_attrs), - **setting_kwargs, - ) - elif s_def.type == APP_SETTING_TYPE_INTEGER: - self.fields[s_field] = forms.IntegerField( - widget=forms.NumberInput(attrs=s_widget_attrs), - **setting_kwargs, - ) - elif s_def.type == APP_SETTING_TYPE_BOOLEAN: - self.fields[s_field] = forms.BooleanField(**setting_kwargs) - elif s_def.type == APP_SETTING_TYPE_JSON: - # NOTE: Attrs MUST be supplied here (#404) - if 'class' in s_widget_attrs: - s_widget_attrs['class'] += ' sodar-json-input' - else: - s_widget_attrs['class'] = 'sodar-json-input' - self.fields[s_field] = forms.CharField( - widget=forms.Textarea(attrs=s_widget_attrs), - **setting_kwargs, - ) - - # Modify initial value and attributes - self.fields[s_field].widget.attrs.update(s_widget_attrs) - value = app_settings.get( - plugin_name=plugin_name, setting_name=s_def.name - ) - if s_def.type == APP_SETTING_TYPE_JSON: - value = json.dumps(value) - self.initial[s_field] = value - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Add settings fields + # TODO: Refactor into generic helper in SODARAppSettingsFormMixin self.project_plugins = get_active_plugins(plugin_type='project_app') self.site_plugins = get_active_plugins(plugin_type='site_app') self.app_plugins = self.project_plugins + self.site_plugins @@ -1298,12 +1223,13 @@ def __init__(self, *args, **kwargs): ) for s_def in s_defs.values(): s_field = 'settings.{}.{}'.format(plugin_name, s_def.name) - self._set_app_setting_field(plugin_name, s_field, s_def) + self.set_app_setting_field(plugin_name, s_field, s_def) self.fields[s_field].label = self.get_app_setting_label( plugin, self.fields[s_field].label ) def clean(self): + # TODO: Refactor into generic helper in SODARAppSettingsFormMixin for plugin in self.app_plugins + [None]: p_kwargs = {'user_modifiable': True} if plugin: diff --git a/userprofile/forms.py b/userprofile/forms.py index e0252a10..d7302c8c 100644 --- a/userprofile/forms.py +++ b/userprofile/forms.py @@ -1,16 +1,14 @@ import json from django import forms -from django.conf import settings # Projectroles dependency from projectroles.app_settings import AppSettingAPI from projectroles.forms import ( SODARForm, SODARModelForm, + SODARAppSettingsFormMixin, SETTING_CUSTOM_VALIDATE_MSG, - SETTING_DISABLE_LABEL, - SETTING_SOURCE_ONLY_MSG, ) from projectroles.models import ( SODARUserAdditionalEmail, @@ -34,94 +32,14 @@ APP_SETTING_TYPE_STRING = SODAR_CONSTANTS['APP_SETTING_TYPE_STRING'] -class UserAppSettingsForm(SODARForm): +class UserAppSettingsForm(SODARAppSettingsFormMixin, SODARForm): """Form for configuring user settings""" - def _set_app_setting_field(self, plugin_name, s_field, s_def): - """ - Set user app setting field, widget and value. - - :param plugin_name: App plugin name - :param s_field: Form field name - :param s_def: PluginAppSettingDef object - """ - s_widget_attrs = s_def.widget_attrs - if s_def.placeholder is not None: - s_widget_attrs['placeholder'] = s_def.placeholder - setting_kwargs = { - 'required': False, - 'label': s_def.label or '{}.{}'.format(plugin_name, s_def.name), - 'help_text': s_def.description, - } - # Disable global user settings if on target site - if ( - s_def.global_edit - and settings.PROJECTROLES_SITE_MODE == SITE_MODE_TARGET - ): - setting_kwargs['label'] += ' ' + SETTING_DISABLE_LABEL - setting_kwargs['help_text'] += ' ' + SETTING_SOURCE_ONLY_MSG - setting_kwargs['disabled'] = True - - if s_def.options and callable(s_def.options): - self.fields[s_field] = forms.ChoiceField( - choices=[ - ( - (str(value[0]), str(value[1])) - if isinstance(value, tuple) - else (str(value), str(value)) - ) - for value in s_def.options(user=self.user) - ], - **setting_kwargs, - ) - elif s_def.options: - self.fields[s_field] = forms.ChoiceField( - choices=[ - ( - (int(option), int(option)) - if s_def.type == APP_SETTING_TYPE_INTEGER - else (option, option) - ) - for option in s_def.options - ], - **setting_kwargs, - ) - elif s_def.type == APP_SETTING_TYPE_STRING: - self.fields[s_field] = forms.CharField( - widget=forms.TextInput(attrs=s_widget_attrs), - **setting_kwargs, - ) - elif s_def.type == APP_SETTING_TYPE_INTEGER: - self.fields[s_field] = forms.IntegerField( - widget=forms.NumberInput(attrs=s_widget_attrs), - **setting_kwargs, - ) - elif s_def.type == APP_SETTING_TYPE_BOOLEAN: - self.fields[s_field] = forms.BooleanField(**setting_kwargs) - elif s_def.type == APP_SETTING_TYPE_JSON: - # NOTE: Attrs MUST be supplied here (#404) - if 'class' in s_widget_attrs: - s_widget_attrs['class'] += ' sodar-json-input' - else: - s_widget_attrs['class'] = 'sodar-json-input' - self.fields[s_field] = forms.CharField( - widget=forms.Textarea(attrs=s_widget_attrs), - **setting_kwargs, - ) - - # Modify initial value and attributes - self.fields[s_field].widget.attrs.update(s_widget_attrs) - value = app_settings.get( - plugin_name=plugin_name, setting_name=s_def.name, user=self.user - ) - if s_def.type == APP_SETTING_TYPE_JSON: - value = json.dumps(value) - self.initial[s_field] = value - def __init__(self, *args, **kwargs): self.user = kwargs.pop('current_user') super().__init__(*args, **kwargs) # Add settings fields + # TODO: Refactor into generic helper in SODARAppSettingsFormMixin self.app_plugins = get_active_plugins(plugin_type='project_app') self.user_plugins = get_active_plugins(plugin_type='site_app') self.app_plugins = self.app_plugins + self.user_plugins @@ -141,13 +59,14 @@ def __init__(self, *args, **kwargs): ) for s_def in s_defs.values(): s_field = 'settings.{}.{}'.format(plugin_name, s_def.name) - self._set_app_setting_field(plugin_name, s_field, s_def) + self.set_app_setting_field(plugin_name, s_field, s_def) self.fields[s_field].label = self.get_app_setting_label( plugin, self.fields[s_field].label ) def clean(self): """Function for custom form validation and cleanup""" + # TODO: Refactor into generic helper in SODARAppSettingsFormMixin for plugin in self.app_plugins + [None]: p_kwargs = {'user_modifiable': True} if plugin: