Add support for YAML configuration files. Add support for editing setting options and saving them using the new YAML configuration file support. Add new revertsettings management command. Add new permission to edit setting via the UI.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
Roberto Rosario
2018-08-16 03:05:26 -04:00
parent ac5f53c538
commit 90cd142e76
17 changed files with 269 additions and 35 deletions

View File

@@ -50,8 +50,11 @@
- Add workflow action to update the label and description of a document.
- Add COMMON_PROJECT_TITLE as a setting option to customize the title
string.
- Add support for YAML configuration files.
- Add support for editing setting options and saving them using the
new YAML configuration file support.
- Add new revertsettings management command.
- Add new permission to edit setting via the UI.
3.0.1 (2018-07-08)
=================

View File

@@ -32,5 +32,5 @@ setting_graphics_backend_config = namespace.add_setting(
DEFAULT_PILLOW_FORMAT
), help_text=_(
'Configuration options for the graphics conversion backend.'
), global_name='CONVERTER_GRAPHICS_BACKEND_CONFIG',
), global_name='CONVERTER_GRAPHICS_BACKEND_CONFIG', quoted=True
)

View File

@@ -16,5 +16,5 @@ setting_storage_backend_arguments = namespace.add_setting(
global_name='SIGNATURES_STORAGE_BACKEND_ARGUMENTS',
default='{{location: {}}}'.format(
os.path.join(settings.MEDIA_ROOT, 'document_signatures')
)
), quoted=True
)

View File

@@ -10,6 +10,17 @@ from smart_settings import Namespace
from .literals import DEFAULT_LANGUAGE, DEFAULT_LANGUAGE_CODES
namespace = Namespace(name='documents', label=_('Documents'))
setting_documentimagecache_storage = namespace.add_setting(
global_name='DOCUMENTS_CACHE_STORAGE_BACKEND',
default='django.core.files.storage.FileSystemStorage', quoted=True
)
setting_documentimagecache_storage_arguments = namespace.add_setting(
global_name='DOCUMENTS_CACHE_STORAGE_BACKEND_ARGUMENTS',
default='{{location: {}}}'.format(
os.path.join(settings.MEDIA_ROOT, 'document_cache')
), quoted=True
)
setting_disable_base_image_cache = namespace.add_setting(
global_name='DOCUMENTS_DISABLE_BASE_IMAGE_CACHE', default=False,
help_text=_(
@@ -31,19 +42,6 @@ setting_display_height = namespace.add_setting(
setting_display_width = namespace.add_setting(
global_name='DOCUMENTS_DISPLAY_WIDTH', default='3600'
)
settings_document_page_image_cache_time = namespace.add_setting(
global_name='DOCUMENTS_PAGE_IMAGE_CACHE_TIME', default='31556926'
)
setting_documentimagecache_storage = namespace.add_setting(
global_name='DOCUMENTS_CACHE_STORAGE_BACKEND',
default='django.core.files.storage.FileSystemStorage'
)
setting_documentimagecache_storage_arguments = namespace.add_setting(
global_name='DOCUMENTS_CACHE_STORAGE_BACKEND_ARGUMENTS',
default='{{location: {}}}'.format(
os.path.join(settings.MEDIA_ROOT, 'document_cache')
)
)
setting_fix_orientation = namespace.add_setting(
global_name='DOCUMENTS_FIX_ORIENTATION', default=False,
help_text=_(
@@ -61,6 +59,9 @@ setting_language_codes = namespace.add_setting(
global_name='DOCUMENTS_LANGUAGE_CODES', default=DEFAULT_LANGUAGE_CODES,
help_text=_('List of supported document languages. In ISO639-3 format.')
)
settings_document_page_image_cache_time = namespace.add_setting(
global_name='DOCUMENTS_PAGE_IMAGE_CACHE_TIME', default='31556926'
)
setting_preview_height = namespace.add_setting(
global_name='DOCUMENTS_PREVIEW_HEIGHT', default=''
)

View File

@@ -13,20 +13,20 @@ namespace = Namespace(name='mailer', label=_('Mailing'))
setting_link_subject_template = namespace.add_setting(
default=_('Link for document: {{ document }}'),
help_text=_('Template for the document link email form subject line.'),
global_name='MAILER_LINK_SUBJECT_TEMPLATE',
global_name='MAILER_LINK_SUBJECT_TEMPLATE', quoted=True
)
setting_link_body_template = namespace.add_setting(
default=DEFAULT_LINK_BODY_TEMPLATE,
help_text=_('Template for the document link email form body text. Can include HTML.'),
global_name='MAILER_LINK_BODY_TEMPLATE',
global_name='MAILER_LINK_BODY_TEMPLATE', quoted=True
)
setting_document_subject_template = namespace.add_setting(
default=_('Document: {{ document }}'),
help_text=_('Template for the document email form subject line.'),
global_name='MAILER_DOCUMENT_SUBJECT_TEMPLATE',
global_name='MAILER_DOCUMENT_SUBJECT_TEMPLATE', quoted=True
)
setting_document_body_template = namespace.add_setting(
default=DEFAULT_DOCUMENT_BODY_TEMPLATE,
help_text=_('Template for the document email form body text. Can include HTML.'),
global_name='MAILER_DOCUMENT_BODY_TEMPLATE',
global_name='MAILER_DOCUMENT_BODY_TEMPLATE', quoted=True
)

View File

@@ -2,11 +2,14 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from common import MayanAppConfig, menu_setup, menu_object
from common import MayanAppConfig, menu_secondary, menu_setup, menu_object
from navigation import SourceColumn
from .classes import Namespace, Setting
from .links import link_namespace_detail, link_namespace_list
from .links import (
link_namespace_detail, link_namespace_list, link_namespace_root_list,
link_setting_edit
)
from .widgets import setting_widget
@@ -37,4 +40,10 @@ class SmartSettingsApp(MayanAppConfig):
menu_object.bind_links(
links=(link_namespace_detail,), sources=(Namespace,)
)
menu_object.bind_links(
links=(link_setting_edit,), sources=(Setting,)
)
menu_secondary.bind_links(links=(link_namespace_root_list,))
menu_setup.bind_links(links=(link_namespace_list,))
Setting.save_last_known_good()

View File

@@ -3,6 +3,7 @@ from __future__ import unicode_literals
from importlib import import_module
import logging
import os
import sys
import yaml
@@ -43,9 +44,6 @@ class Namespace(object):
for namespace in cls.get_all():
namespace.invalidate_cache()
def __str__(self):
return force_text(self.label)
def __init__(self, name, label):
if name in self.__class__._registry:
raise Exception(
@@ -56,6 +54,9 @@ class Namespace(object):
self.__class__._registry[name] = self
self._settings = []
def __str__(self):
return force_text(self.label)
def add_setting(self, **kwargs):
return Setting(namespace=self, **kwargs)
@@ -90,14 +91,52 @@ class Setting(object):
return result
@classmethod
def get(cls, global_name):
return cls._registry[global_name].value
def dump_data(cls):
result = []
def __init__(self, namespace, global_name, default, help_text=None, is_path=False):
for setting in cls.get_all():
# Ensure there is at least one newline
line = '{}: {}\n'.format(
setting.global_name, setting.serialized_value
)
# If there are two newlines, remove one
if line.endswith('\n\n'):
line = line[:-1]
result.append(line)
return ''.join(result)
@classmethod
def get(cls, global_name):
return cls._registry[global_name]
@classmethod
def get_all(cls):
return sorted(cls._registry.values(), key=lambda x: x.global_name)
@classmethod
def save_configuration(cls, path=settings.CONFIGURATION_FILEPATH):
with open(path, 'w') as file_object:
file_object.write(cls.dump_data())
@classmethod
def save_last_known_good(cls):
# Don't write over the last good configuration if we are trying
# to restore the last good configuration
if not 'revertsettings' in sys.argv:
cls.save_configuration(
path=settings.CONFIGURATION_LAST_GOOD_FILEPATH
)
def __init__(self, namespace, global_name, default, help_text=None, is_path=False, quoted=False):
self.global_name = global_name
self.default = default
self.help_text = help_text
self.loaded = False
self.namespace = namespace
self.quoted = quoted
namespace._settings.append(self)
self.__class__._registry[global_name] = self
@@ -137,5 +176,9 @@ class Setting(object):
@value.setter
def value(self, value):
# value is in YAML format
self.yaml = value
if self.quoted:
self.yaml = '\'{}\''.format(value)
value = '\'{}\''.format(value)
else:
self.yaml = value
self.raw_value = Setting.deserialize_value(value)

View File

@@ -0,0 +1,36 @@
from __future__ import unicode_literals
import yaml
from django import forms
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.module_loading import import_string
from django.utils.translation import ugettext_lazy as _
class SettingForm(forms.Form):
value = forms.CharField(
help_text=_('Enter the new setting value.'), required=False,
widget=forms.widgets.Textarea()
)
def __init__(self, *args, **kwargs):
super(SettingForm, self).__init__(*args, **kwargs)
self.fields['value'].help_text = self.initial['setting'].help_text
self.fields['value'].initial = self.initial['setting'].value
def clean(self):
try:
yaml.safe_dump(self.cleaned_data['value'])
except yaml.YAMLError as exception:
try:
yaml.safe_load('{}'.format(self.cleaned_data['value']))
except yaml.YAMLError as exception:
raise ValidationError(
_(
'"%s" not a valid entry.'
) % self.cleaned_data['value']
)

View File

@@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _
from navigation import Link
from .icons import icon_namespace_list
from .permissions import permission_settings_view
from .permissions import permission_settings_edit, permission_settings_view
link_namespace_list = Link(
icon_class=icon_namespace_list, permissions=(permission_settings_view,),
@@ -15,3 +15,12 @@ link_namespace_detail = Link(
args='resolved_object.name', permissions=(permission_settings_view,),
text=_('Settings'), view='settings:namespace_detail',
)
# Duplicate the link to use a different name
link_namespace_root_list = Link(
icon_class=icon_namespace_list, permissions=(permission_settings_view,),
text=_('Namespaces'), view='settings:namespace_list'
)
link_setting_edit = Link(
args='resolved_object.global_name', permissions=(permission_settings_edit,),
text=_('Edit'), view='settings:setting_edit_view',
)

View File

@@ -0,0 +1,18 @@
from __future__ import unicode_literals
#from importlib import import_module
#import logging
import os
#import yaml
from django.apps import apps
from django.conf import settings
#from django.utils.functional import Promise
#from django.utils.encoding import force_text, python_2_unicode_compatible
#SETTING_FILE_LAST_KNOWN_GOOD =
CONFIGURATION_FILENAME = '_settings.yml'
CONFIGURATION_FILEPATH = os.path.join(
settings.MEDIA_ROOT, CONFIGURATION_FILENAME
)

View File

@@ -0,0 +1,29 @@
from __future__ import unicode_literals
import errno
import os
from shutil import copyfile
from django.conf import settings
from django.core import management
from django.core.management.utils import get_random_secret_key
class Command(management.BaseCommand):
help = 'Rollback the configuration file to the last valid version.'
def handle(self, *args, **options):
try:
copyfile(
settings.CONFIGURATION_LAST_GOOD_FILEPATH,
settings.CONFIGURATION_FILEPATH
)
except IOError as exception:
if exception.errno == errno.ENOENT:
self.stdout.write(
self.style.NOTICE(
'There is no last valid version to restore.'
)
)
else:
raise

View File

@@ -6,6 +6,9 @@ from permissions import PermissionNamespace
namespace = PermissionNamespace('smart_settings', _('Smart settings'))
permission_settings_edit = namespace.add_permission(
name='permission_settings_edit', label=_('Edit settings')
)
permission_settings_view = namespace.add_permission(
name='permission_settings_view', label=_('View settings')
)

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.conf.urls import url
from .views import NamespaceDetailView, NamespaceListView
from .views import NamespaceDetailView, NamespaceListView, SettingEditView
urlpatterns = [
url(
@@ -13,4 +13,8 @@ urlpatterns = [
r'^namespace/(?P<namespace_name>\w+)/$',
NamespaceDetailView.as_view(), name='namespace_detail'
),
url(
r'^edit/(?P<setting_global_name>\w+)/$',
SettingEditView.as_view(), name='setting_edit_view'
),
]

View File

@@ -1,12 +1,15 @@
from __future__ import unicode_literals
from django.contrib import messages
from django.http import Http404
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from common.views import SingleObjectListView
from common.views import FormView, SingleObjectListView
from .classes import Namespace
from .permissions import permission_settings_view
from .classes import Namespace, Setting
from .forms import SettingForm
from .permissions import permission_settings_edit, permission_settings_view
class NamespaceListView(SingleObjectListView):
@@ -39,3 +42,35 @@ class NamespaceDetailView(SingleObjectListView):
def get_object_list(self):
return self.get_namespace().settings
class SettingEditView(FormView):
form_class = SettingForm
view_permission = permission_settings_edit
def form_valid(self, form):
self.get_object().value = form.cleaned_data['value']
Setting.save_configuration()
messages.success(
self.request, _('Setting updated successfully.')
)
return super(SettingEditView, self).form_valid(form=form)
def get_extra_context(self):
return {
'hide_link': True,
'title': _('Edit setting: %s') % self.get_object(),
}
def get_initial(self):
return {'setting': self.get_object()}
def get_object(self):
return Setting.get(self.kwargs['setting_global_name'])
def get_post_action_redirect(self):
return reverse(
'settings:namespace_detail', args=(
self.get_object().namespace.name,
)
)

View File

@@ -11,9 +11,12 @@ https://docs.djangoproject.com/en/1.10/ref/settings/
"""
from __future__ import unicode_literals
import errno
import os
import sys
import yaml
from django.utils.translation import ugettext_lazy as _
import environ
@@ -347,3 +350,44 @@ else:
# ----- Debug -----
DEBUG = env.bool('MAYAN_DEBUG', default=False)
CONFIGURATION_FILENAME = '_settings.yml'
CONFIGURATION_FILEPATH = os.path.join(MEDIA_ROOT, CONFIGURATION_FILENAME)
CONFIGURATION_USER_FILENAME = 'config.yml'
CONFIGURATION_USER_FILEPATH = os.path.join(
MEDIA_ROOT, CONFIGURATION_USER_FILENAME
)
CONFIGURATION_LAST_GOOD_FILENAME = '_settings_backup.yml'
CONFIGURATION_LAST_GOOD_FILEPATH = os.path.join(
MEDIA_ROOT, CONFIGURATION_LAST_GOOD_FILENAME
)
def read_configuration_file(path):
try:
with open(CONFIGURATION_FILEPATH) as file_object:
file_object.seek(0, os.SEEK_END)
if file_object.tell():
file_object.seek(0)
try:
globals().update(yaml.safe_load(file_object))
except yaml.YAMLError as exception:
exit(
'Error loading configuration file: {}; {}'.format(
CONFIGURATION_FILEPATH, exception
)
)
except IOError as exception:
if exception.errno == errno.ENOENT:
pass
else:
raise
if not 'revertsettings' in sys.argv:
read_configuration_file(CONFIGURATION_FILEPATH)
read_configuration_file(CONFIGURATION_USER_FILEPATH)