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:
@@ -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)
|
||||
=================
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=''
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
36
mayan/apps/smart_settings/forms.py
Normal file
36
mayan/apps/smart_settings/forms.py
Normal 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']
|
||||
)
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
18
mayan/apps/smart_settings/literals.py
Normal file
18
mayan/apps/smart_settings/literals.py
Normal 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
|
||||
)
|
||||
0
mayan/apps/smart_settings/management/__init__.py
Normal file
0
mayan/apps/smart_settings/management/__init__.py
Normal 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
|
||||
@@ -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')
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user