diff --git a/TODO.md b/TODO.md index 27d2c50b..b470c465 100644 --- a/TODO.md +++ b/TODO.md @@ -257,8 +257,6 @@ https://code.djangoproject.com/ticket/24576 + Query Expressions, Conditional Expressions, and Database Functions¶ * forms: You can now pass a callable that returns an iterable of choices when instantiating a ChoiceField. -* migrate to DRF3.x - * move all tests to django-orchestra/tests * *natural keys: those fields that uniquely identify a service, list.name, website.name, webapp.name+account, make sure rest api can not edit thos things @@ -287,3 +285,24 @@ https://code.djangoproject.com/ticket/24576 # Determine the difference between data serializer used for validation and used for the rest API! # Make PluginApiView that fills metadata and other stuff like modeladmin plugin support + +# @classmethods do not need to be called with type(object)! + +# Deprectae widgets.showtext and readonlyField by ReadOnlyFormMixin + +# custom validation for settings +# TODO orchestra related services code reload: celery/uwsgi reloading find aonther way without root and implement reload +# insert settings on dashboard dynamically + +# rename "edit settings" -> change settings + +# View settings file +contrib/orders/models.py: if type(instance) in services: +contrib/orders/models.py: if type(instance) in services: +contrib/orders/helpers.py: if type(node) in services: +contrib/bills/admin.py: return [inline for inline in inlines if type(inline) is not BillLineInline] +contrib/bills/admin.py: return [inline for inline in inlines if type(inline) is not ClosedBillLineInline] +contrib/accounts/actions.py.save: if type(service) in registered_services: +contrib/accounts/actions.py: if type(service) in registered_services: +permissions/options.py: for func in inspect.getmembers(type(self), predicate=inspect.ismethod): + diff --git a/orchestra/admin/__init__.py b/orchestra/admin/__init__.py index 8885990f..75b17e58 100644 --- a/orchestra/admin/__init__.py +++ b/orchestra/admin/__init__.py @@ -1,2 +1,32 @@ -from .options import * +from functools import update_wrapper + +from django.contrib.admin import site + from .dashboard import * +from .options import * + + +# monkey-patch admin.site in order to porvide some extra admin urls + +urls = [] +def register_url(pattern, view, name=""): + global urls + urls.append((pattern, view, name)) +site.register_url = register_url + + +site_get_urls = site.get_urls +def get_urls(): + def wrap(view, cacheable=False): + def wrapper(*args, **kwargs): + return site.admin_view(view, cacheable)(*args, **kwargs) + wrapper.admin_site = site + return update_wrapper(wrapper, view) + global urls + extra_patterns = [] + for pattern, view, name in urls: + extra_patterns.append( + url(pattern, wrap(view), name=name) + ) + return site_get_urls() + extra_patterns +site.get_urls = get_urls diff --git a/orchestra/admin/dashboard.py b/orchestra/admin/dashboard.py index 03586b66..6e79015a 100644 --- a/orchestra/admin/dashboard.py +++ b/orchestra/admin/dashboard.py @@ -1,20 +1,31 @@ -from django.conf import settings +from fluent_dashboard import dashboard +from fluent_dashboard.modules import CmsAppIconList from orchestra.core import services -def generate_services_group(): - models = [] - for model, options in services.get().items(): - if options.get('menu', True): - models.append("%s.%s" % (model.__module__, model._meta.object_name)) - - settings.FLUENT_DASHBOARD_APP_GROUPS += ( - ('Services', { - 'models': models, - 'collapsible': True, - }), - ) - - -generate_services_group() +class OrchestraIndexDashboard(dashboard.FluentIndexDashboard): + def get_application_modules(self): + modules = super(OrchestraIndexDashboard, self).get_application_modules() + models = [] + for model, options in services.get().items(): + if options.get('menu', True): + models.append("%s.%s" % (model.__module__, model._meta.object_name)) + + # TODO make this dynamic + for module in modules: + if module.title == 'Administration': + module.children.append({ + 'models': [{ + 'add_url': '/admin/settings/', + 'app_name': 'settings', + 'change_url': '/admin/settings/setting/', + 'name': 'setting', + 'title': "Settings" }], + 'name': 'settings', + 'title': 'Settings', + 'url': '/admin/settings/' + }) + service_icon_list = CmsAppIconList('Services', models=models, collapsible=True) + modules.append(service_icon_list) + return modules diff --git a/orchestra/admin/forms.py b/orchestra/admin/forms.py index 548bb688..b453c9f0 100644 --- a/orchestra/admin/forms.py +++ b/orchestra/admin/forms.py @@ -47,8 +47,7 @@ class AdminFormSet(BaseModelFormSet): def adminmodelformset_factory(modeladmin, form, formset=AdminFormSet, **kwargs): - formset = modelformset_factory(modeladmin.model, form=form, formset=formset, - **kwargs) + formset = modelformset_factory(modeladmin.model, form=form, formset=formset, **kwargs) formset.modeladmin = modeladmin return formset diff --git a/orchestra/admin/menu.py b/orchestra/admin/menu.py index a776611d..bb1e1c75 100644 --- a/orchestra/admin/menu.py +++ b/orchestra/admin/menu.py @@ -59,6 +59,9 @@ def get_accounts(): def get_administration_items(): childrens = [] + if isinstalled('orchestra.contrib.settings'): + url = reverse('admin:settings_edit_settings') + childrens.append(items.MenuItem(_("Settings"), url)) if isinstalled('orchestra.contrib.services'): url = reverse('admin:services_service_changelist') childrens.append(items.MenuItem(_("Services"), url)) diff --git a/orchestra/conf/base_settings.py b/orchestra/conf/base_settings.py index f965df1d..17ad8178 100644 --- a/orchestra/conf/base_settings.py +++ b/orchestra/conf/base_settings.py @@ -114,6 +114,8 @@ INSTALLED_APPS = ( # Last to load 'orchestra.contrib.resources', + 'orchestra.contrib.settings', + ) @@ -139,7 +141,7 @@ ADMIN_TOOLS_MENU = 'orchestra.admin.menu.OrchestraMenu' # Fluent dashboard # TODO subclass like in admin_tools_menu -ADMIN_TOOLS_INDEX_DASHBOARD = 'fluent_dashboard.dashboard.FluentIndexDashboard' +ADMIN_TOOLS_INDEX_DASHBOARD = 'orchestra.admin.dashboard.OrchestraIndexDashboard' FLUENT_DASHBOARD_ICON_THEME = '../orchestra/icons' FLUENT_DASHBOARD_APP_GROUPS = ( @@ -204,6 +206,7 @@ FLUENT_DASHBOARD_APP_ICONS = { 'issues/ticket': 'Ticket_star.png', 'miscellaneous/miscservice': 'Misc-Misc-Box-icon.png', # Administration + 'settings/setting': 'preferences.png', 'djcelery/taskstate': 'taskstate.png', 'orchestration/server': 'vps.png', 'orchestration/route': 'hal.png', diff --git a/orchestra/contrib/accounts/settings.py b/orchestra/contrib/accounts/settings.py index 67781676..81ab8ac8 100644 --- a/orchestra/contrib/accounts/settings.py +++ b/orchestra/contrib/accounts/settings.py @@ -1,10 +1,10 @@ from django.conf import settings from django.utils.translation import ugettext_lazy as _ -from orchestra.settings import ORCHESTRA_BASE_DOMAIN +from orchestra.settings import ORCHESTRA_BASE_DOMAIN, Setting -ACCOUNTS_TYPES = getattr(settings, 'ACCOUNTS_TYPES', ( +ACCOUNTS_TYPES = Setting('ACCOUNTS_TYPES', ( ('INDIVIDUAL', _("Individual")), ('ASSOCIATION', _("Association")), ('CUSTOMER', _("Customer")), @@ -15,32 +15,26 @@ ACCOUNTS_TYPES = getattr(settings, 'ACCOUNTS_TYPES', ( )) -ACCOUNTS_DEFAULT_TYPE = getattr(settings, 'ACCOUNTS_DEFAULT_TYPE', - 'INDIVIDUAL' -) +ACCOUNTS_DEFAULT_TYPE = Setting('ACCOUNTS_DEFAULT_TYPE', 'INDIVIDUAL', choices=ACCOUNTS_TYPES) -ACCOUNTS_LANGUAGES = getattr(settings, 'ACCOUNTS_LANGUAGES', ( +ACCOUNTS_LANGUAGES = Setting('ACCOUNTS_LANGUAGES', ( ('EN', _('English')), )) -ACCOUNTS_SYSTEMUSER_MODEL = getattr(settings, 'ACCOUNTS_SYSTEMUSER_MODEL', +ACCOUNTS_DEFAULT_LANGUAGE = Setting('ACCOUNTS_DEFAULT_LANGUAGE', 'EN', choices=ACCOUNTS_LANGUAGES) + + +ACCOUNTS_SYSTEMUSER_MODEL = Setting('ACCOUNTS_SYSTEMUSER_MODEL', 'systemusers.SystemUser' ) -ACCOUNTS_DEFAULT_LANGUAGE = getattr(settings, 'ACCOUNTS_DEFAULT_LANGUAGE', - 'EN' -) +ACCOUNTS_MAIN_PK = Setting('ACCOUNTS_MAIN_PK', 1) -ACCOUNTS_MAIN_PK = getattr(settings, 'ACCOUNTS_MAIN_PK', - 1 -) - - -ACCOUNTS_CREATE_RELATED = getattr(settings, 'ACCOUNTS_CREATE_RELATED', ( +ACCOUNTS_CREATE_RELATED = Setting('ACCOUNTS_CREATE_RELATED', ( # , , , ('mailboxes.Mailbox', 'name', @@ -60,6 +54,6 @@ ACCOUNTS_CREATE_RELATED = getattr(settings, 'ACCOUNTS_CREATE_RELATED', ( )) -ACCOUNTS_SERVICE_REPORT_TEMPLATE = getattr(settings, 'ACCOUNTS_SERVICE_REPORT_TEMPLATE', +ACCOUNTS_SERVICE_REPORT_TEMPLATE = Setting('ACCOUNTS_SERVICE_REPORT_TEMPLATE', 'admin/accounts/account/service_report.html' ) diff --git a/orchestra/contrib/bills/actions.py b/orchestra/contrib/bills/actions.py index ce638b5a..ef85f59c 100644 --- a/orchestra/contrib/bills/actions.py +++ b/orchestra/contrib/bills/actions.py @@ -56,7 +56,7 @@ def close_bills(modeladmin, request, queryset): for bill in queryset: if not validate_contact(request, bill): return - SelectSourceFormSet = adminmodelformset_factory(modeladmin, SelectSourceForm, extra=0) + SelectSourceFormSet = adminmodelformset_factory(SelectSourceForm, modeladmin, extra=0) formset = SelectSourceFormSet(queryset=queryset) if request.POST.get('post') == 'generic_confirmation': formset = SelectSourceFormSet(request.POST, request.FILES, queryset=queryset) diff --git a/orchestra/contrib/bills/settings.py b/orchestra/contrib/bills/settings.py index 76dbef13..b80d606e 100644 --- a/orchestra/contrib/bills/settings.py +++ b/orchestra/contrib/bills/settings.py @@ -1,99 +1,97 @@ from django.conf import settings from django_countries import data -from orchestra.settings import ORCHESTRA_BASE_DOMAIN +from orchestra.settings import ORCHESTRA_BASE_DOMAIN, Setting -BILLS_NUMBER_LENGTH = getattr(settings, 'BILLS_NUMBER_LENGTH', - 4 -) +BILLS_NUMBER_LENGTH = Setting('BILLS_NUMBER_LENGTH', 4) -BILLS_INVOICE_NUMBER_PREFIX = getattr(settings, 'BILLS_INVOICE_NUMBER_PREFIX', +BILLS_INVOICE_NUMBER_PREFIX = Setting('BILLS_INVOICE_NUMBER_PREFIX', 'I' ) -BILLS_AMENDMENT_INVOICE_NUMBER_PREFIX = getattr(settings, 'BILLS_AMENDMENT_INVOICE_NUMBER_PREFIX', +BILLS_AMENDMENT_INVOICE_NUMBER_PREFIX = Setting('BILLS_AMENDMENT_INVOICE_NUMBER_PREFIX', 'A' ) -BILLS_FEE_NUMBER_PREFIX = getattr(settings, 'BILLS_FEE_NUMBER_PREFIX', +BILLS_FEE_NUMBER_PREFIX = Setting('BILLS_FEE_NUMBER_PREFIX', 'F' ) -BILLS_AMENDMENT_FEE_NUMBER_PREFIX = getattr(settings, 'BILLS_AMENDMENT_FEE_NUMBER_PREFIX', +BILLS_AMENDMENT_FEE_NUMBER_PREFIX = Setting('BILLS_AMENDMENT_FEE_NUMBER_PREFIX', 'B' ) -BILLS_PROFORMA_NUMBER_PREFIX = getattr(settings, 'BILLS_PROFORMA_NUMBER_PREFIX', +BILLS_PROFORMA_NUMBER_PREFIX = Setting('BILLS_PROFORMA_NUMBER_PREFIX', 'P' ) -BILLS_DEFAULT_TEMPLATE = getattr(settings, 'BILLS_DEFAULT_TEMPLATE', +BILLS_DEFAULT_TEMPLATE = Setting('BILLS_DEFAULT_TEMPLATE', 'bills/microspective.html' ) -BILLS_FEE_TEMPLATE = getattr(settings, 'BILLS_FEE_TEMPLATE', +BILLS_FEE_TEMPLATE = Setting('BILLS_FEE_TEMPLATE', 'bills/microspective-fee.html' ) -BILLS_PROFORMA_TEMPLATE = getattr(settings, 'BILLS_PROFORMA_TEMPLATE', +BILLS_PROFORMA_TEMPLATE = Setting('BILLS_PROFORMA_TEMPLATE', 'bills/microspective-proforma.html' ) -BILLS_CURRENCY = getattr(settings, 'BILLS_CURRENCY', +BILLS_CURRENCY = Setting('BILLS_CURRENCY', 'euro' ) -BILLS_SELLER_PHONE = getattr(settings, 'BILLS_SELLER_PHONE', +BILLS_SELLER_PHONE = Setting('BILLS_SELLER_PHONE', '111-112-11-222' ) -BILLS_SELLER_EMAIL = getattr(settings, 'BILLS_SELLER_EMAIL', +BILLS_SELLER_EMAIL = Setting('BILLS_SELLER_EMAIL', 'sales@{}'.format(ORCHESTRA_BASE_DOMAIN) ) -BILLS_SELLER_WEBSITE = getattr(settings, 'BILLS_SELLER_WEBSITE', +BILLS_SELLER_WEBSITE = Setting('BILLS_SELLER_WEBSITE', 'www.{}'.format(ORCHESTRA_BASE_DOMAIN) ) -BILLS_SELLER_BANK_ACCOUNT = getattr(settings, 'BILLS_SELLER_BANK_ACCOUNT', +BILLS_SELLER_BANK_ACCOUNT = Setting('BILLS_SELLER_BANK_ACCOUNT', '0000 0000 00 00000000 (Orchestra Bank)' ) -BILLS_EMAIL_NOTIFICATION_TEMPLATE = getattr(settings, 'BILLS_EMAIL_NOTIFICATION_TEMPLATE', +BILLS_EMAIL_NOTIFICATION_TEMPLATE = Setting('BILLS_EMAIL_NOTIFICATION_TEMPLATE', 'bills/bill-notification.email' ) -BILLS_ORDER_MODEL = getattr(settings, 'BILLS_ORDER_MODEL', +BILLS_ORDER_MODEL = Setting('BILLS_ORDER_MODEL', 'orders.Order' ) -BILLS_CONTACT_DEFAULT_CITY = getattr(settings, 'BILLS_CONTACT_DEFAULT_CITY', +BILLS_CONTACT_DEFAULT_CITY = Setting('BILLS_CONTACT_DEFAULT_CITY', 'Barcelona' ) -BILLS_CONTACT_COUNTRIES = getattr(settings, 'BILLS_CONTACT_COUNTRIES', - ((k,v) for k,v in data.COUNTRIES.items()) +BILLS_CONTACT_COUNTRIES = Setting('BILLS_CONTACT_COUNTRIES', tuple((k,v) for k,v in data.COUNTRIES.items()), + editable=False ) -BILLS_CONTACT_DEFAULT_COUNTRY = getattr(settings, 'BILLS_CONTACT_DEFAULT_COUNTRY', - 'ES' +BILLS_CONTACT_DEFAULT_COUNTRY = Setting('BILLS_CONTACT_DEFAULT_COUNTRY', 'ES', + choices=BILLS_CONTACT_COUNTRIES ) diff --git a/orchestra/contrib/contacts/settings.py b/orchestra/contrib/contacts/settings.py index adcfe84d..8f6fb6a3 100644 --- a/orchestra/contrib/contacts/settings.py +++ b/orchestra/contrib/contacts/settings.py @@ -1,8 +1,10 @@ from django.conf import settings from django_countries import data +from orchestra.settings import Setting -CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES', ( + +CONTACTS_DEFAULT_EMAIL_USAGES = Setting('CONTACTS_DEFAULT_EMAIL_USAGES', ( 'SUPPORT', 'ADMIN', 'BILLING', @@ -12,16 +14,13 @@ CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES )) -CONTACTS_DEFAULT_CITY = getattr(settings, 'CONTACTS_DEFAULT_CITY', +CONTACTS_DEFAULT_CITY = Setting('CONTACTS_DEFAULT_CITY', 'Barcelona' ) -CONTACTS_COUNTRIES = getattr(settings, 'CONTACTS_COUNTRIES', tuple( - ((k,v) for k,v in data.COUNTRIES.items()) -)) +CONTACTS_COUNTRIES = Setting('CONTACTS_COUNTRIES', tuple((k,v) for k,v in data.COUNTRIES.items()), + editable=False) -CONTACTS_DEFAULT_COUNTRY = getattr(settings, 'CONTACTS_DEFAULT_COUNTRY', - 'ES' -) +CONTACTS_DEFAULT_COUNTRY = Setting('CONTACTS_DEFAULT_COUNTRY', 'ES', choices=CONTACTS_COUNTRIES) diff --git a/orchestra/contrib/databases/settings.py b/orchestra/contrib/databases/settings.py index 21d2a47c..3b4d0d2d 100644 --- a/orchestra/contrib/databases/settings.py +++ b/orchestra/contrib/databases/settings.py @@ -1,17 +1,17 @@ from django.conf import settings +from orchestra.settings import Setting -DATABASES_TYPE_CHOICES = getattr(settings, 'DATABASES_TYPE_CHOICES', ( + +DATABASES_TYPE_CHOICES = Setting('DATABASES_TYPE_CHOICES', ( ('mysql', 'MySQL'), ('postgres', 'PostgreSQL'), )) -DATABASES_DEFAULT_TYPE = getattr(settings, 'DATABASES_DEFAULT_TYPE', - 'mysql' -) +DATABASES_DEFAULT_TYPE = Setting('DATABASES_DEFAULT_TYPE', 'mysql', choices=DATABASES_TYPE_CHOICES) -DATABASES_DEFAULT_HOST = getattr(settings, 'DATABASES_DEFAULT_HOST', +DATABASES_DEFAULT_HOST = Setting('DATABASES_DEFAULT_HOST', 'localhost' ) diff --git a/orchestra/contrib/domains/settings.py b/orchestra/contrib/domains/settings.py index 058d042e..4d112655 100644 --- a/orchestra/contrib/domains/settings.py +++ b/orchestra/contrib/domains/settings.py @@ -1,109 +1,106 @@ from django.conf import settings -from orchestra.settings import ORCHESTRA_BASE_DOMAIN +from orchestra.settings import ORCHESTRA_BASE_DOMAIN, Setting -DOMAINS_DEFAULT_NAME_SERVER = getattr(settings, 'DOMAINS_DEFAULT_NAME_SERVER', +DOMAINS_DEFAULT_NAME_SERVER = Setting('DOMAINS_DEFAULT_NAME_SERVER', 'ns.{}'.format(ORCHESTRA_BASE_DOMAIN) ) -DOMAINS_DEFAULT_HOSTMASTER = getattr(settings, 'DOMAINS_DEFAULT_HOSTMASTER', +DOMAINS_DEFAULT_HOSTMASTER = Setting('DOMAINS_DEFAULT_HOSTMASTER', 'hostmaster@{}'.format(ORCHESTRA_BASE_DOMAIN) ) -DOMAINS_DEFAULT_TTL = getattr(settings, 'DOMAINS_DEFAULT_TTL', +DOMAINS_DEFAULT_TTL = Setting('DOMAINS_DEFAULT_TTL', '1h' ) -DOMAINS_DEFAULT_REFRESH = getattr(settings, 'DOMAINS_DEFAULT_REFRESH', +DOMAINS_DEFAULT_REFRESH = Setting('DOMAINS_DEFAULT_REFRESH', '1d' ) -DOMAINS_DEFAULT_RETRY = getattr(settings, 'DOMAINS_DEFAULT_RETRY', +DOMAINS_DEFAULT_RETRY = Setting('DOMAINS_DEFAULT_RETRY', '2h' ) -DOMAINS_DEFAULT_EXPIRATION = getattr(settings, 'DOMAINS_DEFAULT_EXPIRATION', +DOMAINS_DEFAULT_EXPIRATION = Setting('DOMAINS_DEFAULT_EXPIRATION', '4w' ) -DOMAINS_DEFAULT_MIN_CACHING_TIME = getattr(settings, 'DOMAINS_DEFAULT_MIN_CACHING_TIME', +DOMAINS_DEFAULT_MIN_CACHING_TIME = Setting('DOMAINS_DEFAULT_MIN_CACHING_TIME', '1h' ) -DOMAINS_ZONE_PATH = getattr(settings, 'DOMAINS_ZONE_PATH', +DOMAINS_ZONE_PATH = Setting('DOMAINS_ZONE_PATH', '/etc/bind/master/%(name)s' ) -DOMAINS_MASTERS_PATH = getattr(settings, 'DOMAINS_MASTERS_PATH', +DOMAINS_MASTERS_PATH = Setting('DOMAINS_MASTERS_PATH', '/etc/bind/named.conf.local' ) -DOMAINS_SLAVES_PATH = getattr(settings, 'DOMAINS_SLAVES_PATH', +DOMAINS_SLAVES_PATH = Setting('DOMAINS_SLAVES_PATH', '/etc/bind/named.conf.local' ) -DOMAINS_CHECKZONE_BIN_PATH = getattr(settings, 'DOMAINS_CHECKZONE_BIN_PATH', +DOMAINS_CHECKZONE_BIN_PATH = Setting('DOMAINS_CHECKZONE_BIN_PATH', '/usr/sbin/named-checkzone -i local -k fail -n fail' ) -# Used for creating temporary zone files used for validation -DOMAINS_ZONE_VALIDATION_TMP_DIR = getattr(settings, 'DOMAINS_ZONE_VALIDATION_TMP_DIR', - '/dev/shm' +DOMAINS_ZONE_VALIDATION_TMP_DIR = Setting('DOMAINS_ZONE_VALIDATION_TMP_DIR', '/dev/shm', + help_text="Used for creating temporary zone files used for validation." ) -DOMAINS_DEFAULT_A = getattr(settings, 'DOMAINS_DEFAULT_A', +DOMAINS_DEFAULT_A = Setting('DOMAINS_DEFAULT_A', '10.0.3.13' ) -DOMAINS_DEFAULT_AAAA = getattr(settings, 'DOMAINS_DEFAULT_AAAA', +DOMAINS_DEFAULT_AAAA = Setting('DOMAINS_DEFAULT_AAAA', '' ) -DOMAINS_DEFAULT_MX = getattr(settings, 'DOMAINS_DEFAULT_MX', ( +DOMAINS_DEFAULT_MX = Setting('DOMAINS_DEFAULT_MX', ( '10 mail.{}.'.format(ORCHESTRA_BASE_DOMAIN), '10 mail2.{}.'.format(ORCHESTRA_BASE_DOMAIN), )) -DOMAINS_DEFAULT_NS = getattr(settings, 'DOMAINS_DEFAULT_NS', ( +DOMAINS_DEFAULT_NS = Setting('DOMAINS_DEFAULT_NS', ( 'ns1.{}.'.format(ORCHESTRA_BASE_DOMAIN), 'ns2.{}.'.format(ORCHESTRA_BASE_DOMAIN), )) -DOMAINS_FORBIDDEN = getattr(settings, 'DOMAINS_FORBIDDEN', - # This setting prevents users from providing random domain names, i.e. google.com - # You can generate a 5K forbidden domains list from Alexa's top 1M - # wget http://s3.amazonaws.com/alexa-static/top-1m.csv.zip -O /tmp/top-1m.csv.zip - # unzip -p /tmp/top-1m.csv.zip | head -n 5000 | sed "s/^.*,//" > forbidden_domains.list - - # '%(site_dir)s/forbidden_domains.list') - '' +DOMAINS_FORBIDDEN = Setting('DOMAINS_FORBIDDEN', '', + help_text=( + "This setting prevents users from providing random domain names, i.e. google.com" + "You can generate a 5K forbidden domains list from Alexa's top 1M" + "wget http://s3.amazonaws.com/alexa-static/top-1m.csv.zip -O /tmp/top-1m.csv.zip" + "unzip -p /tmp/top-1m.csv.zip | head -n 5000 | sed 's/^.*,//' > forbidden_domains.list" + "'%(site_dir)s/forbidden_domains.list')" + ) ) -DOMAINS_MASTERS = getattr(settings, 'DOMAINS_MASTERS', - # Additional master server ip addresses other than autodiscovered by router.get_servers() - () +DOMAINS_MASTERS = Setting('DOMAINS_MASTERS', (), + help_text="Additional master server ip addresses other than autodiscovered by router.get_servers()." ) -DOMAINS_SLAVES = getattr(settings, 'DOMAINS_SLAVES', - # Additional slave server ip addresses other than autodiscovered by router.get_servers() - () +DOMAINS_SLAVES = Setting('DOMAINS_SLAVES', (), + help_text="Additional slave server ip addresses other than autodiscovered by router.get_servers()." ) diff --git a/orchestra/contrib/issues/settings.py b/orchestra/contrib/issues/settings.py index 6ba2387b..2c5ea335 100644 --- a/orchestra/contrib/issues/settings.py +++ b/orchestra/contrib/issues/settings.py @@ -1,10 +1,10 @@ -from django.conf import settings +from orchestra.settings import Setting -ISSUES_SUPPORT_EMAILS = getattr(settings, 'ISSUES_SUPPORT_EMAILS', [ +ISSUES_SUPPORT_EMAILS = Setting('ISSUES_SUPPORT_EMAILS', [ ]) -ISSUES_NOTIFY_SUPERUSERS = getattr(settings, 'ISSUES_NOTIFY_SUPERUSERS', +ISSUES_NOTIFY_SUPERUSERS = Setting('ISSUES_NOTIFY_SUPERUSERS', True ) diff --git a/orchestra/contrib/lists/settings.py b/orchestra/contrib/lists/settings.py index 5fb943f2..4e6a015f 100644 --- a/orchestra/contrib/lists/settings.py +++ b/orchestra/contrib/lists/settings.py @@ -1,38 +1,36 @@ -from django.conf import settings - -from orchestra.settings import ORCHESTRA_BASE_DOMAIN +from orchestra.settings import ORCHESTRA_BASE_DOMAIN, Setting -LISTS_DOMAIN_MODEL = getattr(settings, 'LISTS_DOMAIN_MODEL', +LISTS_DOMAIN_MODEL = Setting('LISTS_DOMAIN_MODEL', 'domains.Domain' ) -LISTS_DEFAULT_DOMAIN = getattr(settings, 'LIST_DEFAULT_DOMAIN', +LISTS_DEFAULT_DOMAIN = Setting('LISTS_DEFAULT_DOMAIN', 'lists.{}'.format(ORCHESTRA_BASE_DOMAIN) ) -LISTS_LIST_URL = getattr(settings, 'LISTS_LIST_URL', +LISTS_LIST_URL = Setting('LISTS_LIST_URL', 'https://lists.{}/mailman/listinfo/%(name)s'.format(ORCHESTRA_BASE_DOMAIN) ) -LISTS_MAILMAN_POST_LOG_PATH = getattr(settings, 'LISTS_MAILMAN_POST_LOG_PATH', +LISTS_MAILMAN_POST_LOG_PATH = Setting('LISTS_MAILMAN_POST_LOG_PATH', '/var/log/mailman/post' ) -LISTS_MAILMAN_ROOT_DIR = getattr(settings, 'LISTS_MAILMAN_ROOT_DIR', +LISTS_MAILMAN_ROOT_DIR = Setting('LISTS_MAILMAN_ROOT_DIR', '/var/lib/mailman' ) -LISTS_VIRTUAL_ALIAS_PATH = getattr(settings, 'LISTS_VIRTUAL_ALIAS_PATH', +LISTS_VIRTUAL_ALIAS_PATH = Setting('LISTS_VIRTUAL_ALIAS_PATH', '/etc/postfix/mailman_virtual_aliases' ) -LISTS_VIRTUAL_ALIAS_DOMAINS_PATH = getattr(settings, 'LISTS_VIRTUAL_ALIAS_DOMAINS_PATH', +LISTS_VIRTUAL_ALIAS_DOMAINS_PATH = Setting('LISTS_VIRTUAL_ALIAS_DOMAINS_PATH', '/etc/postfix/mailman_virtual_domains' ) diff --git a/orchestra/contrib/mailboxes/backends.py b/orchestra/contrib/mailboxes/backends.py index fe4963b8..d4ea4986 100644 --- a/orchestra/contrib/mailboxes/backends.py +++ b/orchestra/contrib/mailboxes/backends.py @@ -58,7 +58,7 @@ class UNIXUserMaildirBackend(ServiceController): def delete(self, mailbox): context = self.get_context(mailbox) - self.append('mv %(home)s %(home)s.deleted || exit_code=1' % context) + self.append('mv %(home)s %(home)s.deleted || exit_code=$?' % context) self.append(textwrap.dedent(""" { sleep 2 && killall -u %(user)s -s KILL; } & killall -u %(user)s || true @@ -133,7 +133,7 @@ class DovecotPostfixPasswdVirtualUserBackend(ServiceController): UPDATED_VIRTUAL_MAILBOX_MAPS=1""") % context ) if context['deleted_home']: - self.append("mv %(home)s %(deleted_home)s || exit_code=1" % context) + self.append("mv %(home)s %(deleted_home)s || exit_code=$?" % context) else: self.append("rm -fr %(home)s" % context) diff --git a/orchestra/contrib/mailboxes/settings.py b/orchestra/contrib/mailboxes/settings.py index 8a65e6e6..7c7a8a1c 100644 --- a/orchestra/contrib/mailboxes/settings.py +++ b/orchestra/contrib/mailboxes/settings.py @@ -1,63 +1,62 @@ import os import textwrap -from django.conf import settings from django.utils.translation import ugettext_lazy as _ -from orchestra.settings import ORCHESTRA_BASE_DOMAIN +from orchestra.settings import ORCHESTRA_BASE_DOMAIN, Setting -MAILBOXES_DOMAIN_MODEL = getattr(settings, 'MAILBOXES_DOMAIN_MODEL', +MAILBOXES_DOMAIN_MODEL = Setting('MAILBOXES_DOMAIN_MODEL', 'domains.Domain' ) -MAILBOXES_HOME = getattr(settings, 'MAILBOXES_HOME', +MAILBOXES_HOME = Setting('MAILBOXES_HOME', '/home/%(name)s/' ) -MAILBOXES_SIEVE_PATH = getattr(settings, 'MAILBOXES_SIEVE_PATH', +MAILBOXES_SIEVE_PATH = Setting('MAILBOXES_SIEVE_PATH', os.path.join(MAILBOXES_HOME, 'Maildir/sieve/orchestra.sieve') ) -MAILBOXES_SIEVETEST_PATH = getattr(settings, 'MAILBOXES_SIEVETEST_PATH', +MAILBOXES_SIEVETEST_PATH = Setting('MAILBOXES_SIEVETEST_PATH', '/dev/shm' ) -MAILBOXES_SIEVETEST_BIN_PATH = getattr(settings, 'MAILBOXES_SIEVETEST_BIN_PATH', +MAILBOXES_SIEVETEST_BIN_PATH = Setting('MAILBOXES_SIEVETEST_BIN_PATH', '%(orchestra_root)s/bin/sieve-test' ) -MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH = getattr(settings, 'MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH', +MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH = Setting('MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH', '/etc/postfix/virtual_mailboxes' ) -MAILBOXES_VIRTUAL_ALIAS_MAPS_PATH = getattr(settings, 'MAILBOXES_VIRTUAL_ALIAS_MAPS_PATH', +MAILBOXES_VIRTUAL_ALIAS_MAPS_PATH = Setting('MAILBOXES_VIRTUAL_ALIAS_MAPS_PATH', '/etc/postfix/virtual_aliases' ) -MAILBOXES_VIRTUAL_ALIAS_DOMAINS_PATH = getattr(settings, 'MAILBOXES_VIRTUAL_ALIAS_DOMAINS_PATH', +MAILBOXES_VIRTUAL_ALIAS_DOMAINS_PATH = Setting('MAILBOXES_VIRTUAL_ALIAS_DOMAINS_PATH', '/etc/postfix/virtual_domains' ) -MAILBOXES_LOCAL_DOMAIN = getattr(settings, 'MAILBOXES_LOCAL_DOMAIN', +MAILBOXES_LOCAL_DOMAIN = Setting('MAILBOXES_LOCAL_DOMAIN', ORCHESTRA_BASE_DOMAIN ) -MAILBOXES_PASSWD_PATH = getattr(settings, 'MAILBOXES_PASSWD_PATH', +MAILBOXES_PASSWD_PATH = Setting('MAILBOXES_PASSWD_PATH', '/etc/dovecot/passwd' ) -MAILBOXES_MAILBOX_FILTERINGS = getattr(settings, 'MAILBOXES_MAILBOX_FILTERINGS', { +MAILBOXES_MAILBOX_FILTERINGS = Setting('MAILBOXES_MAILBOX_FILTERINGS', { # value: (verbose_name, filter) 'DISABLE': (_("Disable"), ''), 'REJECT': (_("Reject spam"), textwrap.dedent(""" @@ -76,26 +75,26 @@ MAILBOXES_MAILBOX_FILTERINGS = getattr(settings, 'MAILBOXES_MAILBOX_FILTERINGS', }) -MAILBOXES_MAILBOX_DEFAULT_FILTERING = getattr(settings, 'MAILBOXES_MAILBOX_DEFAULT_FILTERING', - 'REDIRECT' +MAILBOXES_MAILBOX_DEFAULT_FILTERING = Setting('MAILBOXES_MAILBOX_DEFAULT_FILTERING', 'REDIRECT', + choices=tuple((k, v[0]) for k,v in MAILBOXES_MAILBOX_FILTERINGS.items()) ) -MAILBOXES_MAILDIRSIZE_PATH = getattr(settings, 'MAILBOXES_MAILDIRSIZE_PATH', +MAILBOXES_MAILDIRSIZE_PATH = Setting('MAILBOXES_MAILDIRSIZE_PATH', '%(home)s/Maildir/maildirsize' ) -MAILBOXES_LOCAL_ADDRESS_DOMAIN = getattr(settings, 'MAILBOXES_LOCAL_ADDRESS_DOMAIN', +MAILBOXES_LOCAL_ADDRESS_DOMAIN = Setting('MAILBOXES_LOCAL_ADDRESS_DOMAIN', ORCHESTRA_BASE_DOMAIN ) -MAILBOXES_MAIL_LOG_PATH = getattr(settings, 'MAILBOXES_MAIL_LOG_PATH', +MAILBOXES_MAIL_LOG_PATH = Setting('MAILBOXES_MAIL_LOG_PATH', '/var/log/mail.log' ) -MAILBOXES_MOVE_ON_DELETE_PATH = getattr(settings, 'MAILBOXES_MOVE_ON_DELETE_PATH', +MAILBOXES_MOVE_ON_DELETE_PATH = Setting('MAILBOXES_MOVE_ON_DELETE_PATH', '' ) diff --git a/orchestra/contrib/miscellaneous/settings.py b/orchestra/contrib/miscellaneous/settings.py index fe6628a6..7e34c968 100644 --- a/orchestra/contrib/miscellaneous/settings.py +++ b/orchestra/contrib/miscellaneous/settings.py @@ -1,6 +1,6 @@ -from django.conf import settings +from orchestra.settings import Setting -MISCELLANEOUS_IDENTIFIER_VALIDATORS = getattr(settings, 'MISCELLANEOUS_IDENTIFIER_VALIDATORS', { +MISCELLANEOUS_IDENTIFIER_VALIDATORS = Setting('MISCELLANEOUS_IDENTIFIER_VALIDATORS', { # : }) diff --git a/orchestra/contrib/orchestration/settings.py b/orchestra/contrib/orchestration/settings.py index 0c248fa4..9e963889 100644 --- a/orchestra/contrib/orchestration/settings.py +++ b/orchestra/contrib/orchestration/settings.py @@ -1,36 +1,37 @@ from datetime import timedelta from os import path -from django.conf import settings +from orchestra.settings import Setting -ORCHESTRATION_OS_CHOICES = getattr(settings, 'ORCHESTRATION_OS_CHOICES', ( +ORCHESTRATION_OS_CHOICES = Setting('ORCHESTRATION_OS_CHOICES', ( ('LINUX', "Linux"), )) -ORCHESTRATION_DEFAULT_OS = getattr(settings, 'ORCHESTRATION_DEFAULT_OS', 'LINUX') +ORCHESTRATION_DEFAULT_OS = Setting('ORCHESTRATION_DEFAULT_OS', 'LINUX', + choices=ORCHESTRATION_OS_CHOICES) -ORCHESTRATION_SSH_KEY_PATH = getattr(settings, 'ORCHESTRATION_SSH_KEY_PATH', +ORCHESTRATION_SSH_KEY_PATH = Setting('ORCHESTRATION_SSH_KEY_PATH', path.join(path.expanduser('~'), '.ssh/id_rsa')) -ORCHESTRATION_ROUTER = getattr(settings, 'ORCHESTRATION_ROUTER', +ORCHESTRATION_ROUTER = Setting('ORCHESTRATION_ROUTER', 'orchestra.contrib.orchestration.models.Route' ) -ORCHESTRATION_TEMP_SCRIPT_PATH = getattr(settings, 'ORCHESTRATION_TEMP_SCRIPT_PATH', +ORCHESTRATION_TEMP_SCRIPT_PATH = Setting('ORCHESTRATION_TEMP_SCRIPT_PATH', '/dev/shm' ) -ORCHESTRATION_DISABLE_EXECUTION = getattr(settings, 'ORCHESTRATION_DISABLE_EXECUTION', +ORCHESTRATION_DISABLE_EXECUTION = Setting('ORCHESTRATION_DISABLE_EXECUTION', False ) -ORCHESTRATION_BACKEND_CLEANUP_DELTA = getattr(settings, 'ORCHESTRATION_BACKEND_CLEANUP_DELTA', +ORCHESTRATION_BACKEND_CLEANUP_DELTA = Setting('ORCHESTRATION_BACKEND_CLEANUP_DELTA', timedelta(days=15) ) diff --git a/orchestra/contrib/orders/settings.py b/orchestra/contrib/orders/settings.py index e25d7599..ea7f56fe 100644 --- a/orchestra/contrib/orders/settings.py +++ b/orchestra/contrib/orders/settings.py @@ -1,20 +1,20 @@ -from django.conf import settings +from orchestra.settings import Setting # Pluggable backend for bill generation. -ORDERS_BILLING_BACKEND = getattr(settings, 'ORDERS_BILLING_BACKEND', +ORDERS_BILLING_BACKEND = Setting('ORDERS_BILLING_BACKEND', 'orchestra.contrib.orders.billing.BillsBackend' ) # Pluggable service class -ORDERS_SERVICE_MODEL = getattr(settings, 'ORDERS_SERVICE_MODEL', +ORDERS_SERVICE_MODEL = Setting('ORDERS_SERVICE_MODEL', 'services.Service' ) # Prevent inspecting these apps for service accounting -ORDERS_EXCLUDED_APPS = getattr(settings, 'ORDERS_EXCLUDED_APPS', ( +ORDERS_EXCLUDED_APPS = Setting('ORDERS_EXCLUDED_APPS', ( 'orders', 'admin', 'contenttypes', @@ -29,6 +29,6 @@ ORDERS_EXCLUDED_APPS = getattr(settings, 'ORDERS_EXCLUDED_APPS', ( # Only account for significative changes # metric_storage new value: lastvalue*(1+threshold) > currentvalue or lastvalue*threshold < currentvalue -ORDERS_METRIC_ERROR = getattr(settings, 'ORDERS_METRIC_ERROR', +ORDERS_METRIC_ERROR = Setting('ORDERS_METRIC_ERROR', 0.01 ) diff --git a/orchestra/contrib/payments/settings.py b/orchestra/contrib/payments/settings.py index 06fdcaf7..6d71a964 100644 --- a/orchestra/contrib/payments/settings.py +++ b/orchestra/contrib/payments/settings.py @@ -1,28 +1,34 @@ -from django.conf import settings +from orchestra.settings import Setting + +from .. import payments -PAYMENT_CURRENCY = getattr(settings, 'PAYMENT_CURRENCY', +PAYMENT_CURRENCY = Setting('PAYMENT_CURRENCY', 'Eur' ) -PAYMENTS_DD_CREDITOR_NAME = getattr(settings, 'PAYMENTS_DD_CREDITOR_NAME', +PAYMENTS_DD_CREDITOR_NAME = Setting('PAYMENTS_DD_CREDITOR_NAME', 'Orchestra') -PAYMENTS_DD_CREDITOR_IBAN = getattr(settings, 'PAYMENTS_DD_CREDITOR_IBAN', +PAYMENTS_DD_CREDITOR_IBAN = Setting('PAYMENTS_DD_CREDITOR_IBAN', 'IE98BOFI90393912121212') -PAYMENTS_DD_CREDITOR_BIC = getattr(settings, 'PAYMENTS_DD_CREDITOR_BIC', +PAYMENTS_DD_CREDITOR_BIC = Setting('PAYMENTS_DD_CREDITOR_BIC', 'BOFIIE2D') -PAYMENTS_DD_CREDITOR_AT02_ID = getattr(settings, 'PAYMENTS_DD_CREDITOR_AT02_ID', +PAYMENTS_DD_CREDITOR_AT02_ID = Setting('PAYMENTS_DD_CREDITOR_AT02_ID', 'InvalidAT02ID') -PAYMENTS_ENABLED_METHODS = getattr(settings, 'PAYMENTS_ENABLED_METHODS', [ - 'orchestra.contrib.payments.methods.sepadirectdebit.SEPADirectDebit', - 'orchestra.contrib.payments.methods.creditcard.CreditCard', -]) +PAYMENTS_ENABLED_METHODS = Setting('PAYMENTS_ENABLED_METHODS', ( + 'orchestra.contrib.payments.methods.sepadirectdebit.SEPADirectDebit', + 'orchestra.contrib.payments.methods.creditcard.CreditCard', + ), + # lazy loading + choices=lambda : ((m.get_class_path(), m.get_class_path()) for m in payments.methods.PaymentMethod.get_plugins()), + multiple=True, +) diff --git a/orchestra/contrib/resources/forms.py b/orchestra/contrib/resources/forms.py index 37e2e5fa..e6aca62c 100644 --- a/orchestra/contrib/resources/forms.py +++ b/orchestra/contrib/resources/forms.py @@ -6,7 +6,7 @@ from orchestra.forms.widgets import ShowTextWidget, ReadOnlyWidget class ResourceForm(forms.ModelForm): verbose_name = forms.CharField(label=_("Name"), required=False, - widget=ShowTextWidget(bold=True)) + widget=ShowTextWidget(tag='')) allocated = forms.DecimalField(label=_("Allocated")) unit = forms.CharField(label=_("Unit"), widget=ShowTextWidget(), required=False) diff --git a/orchestra/contrib/resources/models.py b/orchestra/contrib/resources/models.py index a11d742c..7e093d5a 100644 --- a/orchestra/contrib/resources/models.py +++ b/orchestra/contrib/resources/models.py @@ -124,7 +124,7 @@ class Resource(models.Model): self.sync_periodic_task() # This only work on tests (multiprocessing used on real deployments) apps.get_app_config('resources').reload_relations() - run('sleep 2 && touch %s/wsgi.py' % get_project_dir(), async=True) + run('{ sleep 2 && touch %s/wsgi.py; } &' % get_project_dir(), async=True) def delete(self, *args, **kwargs): super(Resource, self).delete(*args, **kwargs) diff --git a/orchestra/contrib/saas/settings.py b/orchestra/contrib/saas/settings.py index 132001a2..79221eee 100644 --- a/orchestra/contrib/saas/settings.py +++ b/orchestra/contrib/saas/settings.py @@ -1,85 +1,89 @@ -from django.conf import settings +from orchestra.settings import ORCHESTRA_BASE_DOMAIN, Setting -from orchestra.settings import ORCHESTRA_BASE_DOMAIN +from .. import saas -SAAS_ENABLED_SERVICES = getattr(settings, 'SAAS_ENABLED_SERVICES', ( - 'orchestra.contrib.saas.services.moodle.MoodleService', - 'orchestra.contrib.saas.services.bscw.BSCWService', - 'orchestra.contrib.saas.services.gitlab.GitLabService', - 'orchestra.contrib.saas.services.phplist.PHPListService', - 'orchestra.contrib.saas.services.wordpress.WordPressService', - 'orchestra.contrib.saas.services.dokuwiki.DokuWikiService', - 'orchestra.contrib.saas.services.drupal.DrupalService', - 'orchestra.contrib.saas.services.seafile.SeaFileService', -)) +SAAS_ENABLED_SERVICES = Setting('SAAS_ENABLED_SERVICES', ( + 'orchestra.contrib.saas.services.moodle.MoodleService', + 'orchestra.contrib.saas.services.bscw.BSCWService', + 'orchestra.contrib.saas.services.gitlab.GitLabService', + 'orchestra.contrib.saas.services.phplist.PHPListService', + 'orchestra.contrib.saas.services.wordpress.WordPressService', + 'orchestra.contrib.saas.services.dokuwiki.DokuWikiService', + 'orchestra.contrib.saas.services.drupal.DrupalService', + 'orchestra.contrib.saas.services.seafile.SeaFileService', + ), + # lazy loading + choices=lambda : ((s.get_class_path(), s.get_class_path()) for s in saas.services.SoftwareService.get_plugins()), + multiple=True, +) -SAAS_WORDPRESS_ADMIN_PASSWORD = getattr(settings, 'SAAS_WORDPRESSMU_ADMIN_PASSWORD', +SAAS_WORDPRESS_ADMIN_PASSWORD = Setting('SAAS_WORDPRESSMU_ADMIN_PASSWORD', 'secret' ) -SAAS_WORDPRESS_BASE_URL = getattr(settings, 'SAAS_WORDPRESS_BASE_URL', +SAAS_WORDPRESS_BASE_URL = Setting('SAAS_WORDPRESS_BASE_URL', 'http://blogs.{}/'.format(ORCHESTRA_BASE_DOMAIN) ) -SAAS_DOKUWIKI_TEMPLATE_PATH = getattr(settings, 'SAAS_DOKUWIKI_TEMPLATE_PATH', +SAAS_DOKUWIKI_TEMPLATE_PATH = Setting('SAAS_DOKUWIKI_TEMPLATE_PATH', '/home/httpd/htdocs/wikifarm/template.tar.gz' ) -SAAS_DOKUWIKI_FARM_PATH = getattr(settings, 'WEBSITES_DOKUWIKI_FARM_PATH', +SAAS_DOKUWIKI_FARM_PATH = Setting('WEBSITES_DOKUWIKI_FARM_PATH', '/home/httpd/htdocs/wikifarm/farm' ) -SAAS_DRUPAL_SITES_PATH = getattr(settings, 'WEBSITES_DRUPAL_SITES_PATH', +SAAS_DRUPAL_SITES_PATH = Setting('WEBSITES_DRUPAL_SITES_PATH', '/home/httpd/htdocs/drupal-mu/sites/%(site_name)s' ) -SAAS_PHPLIST_DB_NAME = getattr(settings, 'SAAS_PHPLIST_DB_NAME', +SAAS_PHPLIST_DB_NAME = Setting('SAAS_PHPLIST_DB_NAME', 'phplist_mu' ) -SAAS_PHPLIST_BASE_DOMAIN = getattr(settings, 'SAAS_PHPLIST_BASE_DOMAIN', +SAAS_PHPLIST_BASE_DOMAIN = Setting('SAAS_PHPLIST_BASE_DOMAIN', 'lists.{}'.format(ORCHESTRA_BASE_DOMAIN) ) -SAAS_SEAFILE_DOMAIN = getattr(settings, 'SAAS_SEAFILE_DOMAIN', +SAAS_SEAFILE_DOMAIN = Setting('SAAS_SEAFILE_DOMAIN', 'seafile.{}'.format(ORCHESTRA_BASE_DOMAIN) ) -SAAS_SEAFILE_DEFAULT_QUOTA = getattr(settings, 'SAAS_SEAFILE_DEFAULT_QUOTA', +SAAS_SEAFILE_DEFAULT_QUOTA = Setting('SAAS_SEAFILE_DEFAULT_QUOTA', 50 ) -SAAS_BSCW_DOMAIN = getattr(settings, 'SAAS_BSCW_DOMAIN', +SAAS_BSCW_DOMAIN = Setting('SAAS_BSCW_DOMAIN', 'bscw.{}'.format(ORCHESTRA_BASE_DOMAIN) ) -SAAS_BSCW_DEFAULT_QUOTA = getattr(settings, 'SAAS_BSCW_DEFAULT_QUOTA', +SAAS_BSCW_DEFAULT_QUOTA = Setting('SAAS_BSCW_DEFAULT_QUOTA', 50 ) -SAAS_BSCW_BSADMIN_PATH = getattr(settings, 'SAAS_BSCW_BSADMIN_PATH', +SAAS_BSCW_BSADMIN_PATH = Setting('SAAS_BSCW_BSADMIN_PATH', '/home/httpd/bscw/bin/bsadmin', ) -SAAS_GITLAB_ROOT_PASSWORD = getattr(settings, 'SAAS_GITLAB_ROOT_PASSWORD', +SAAS_GITLAB_ROOT_PASSWORD = Setting('SAAS_GITLAB_ROOT_PASSWORD', 'secret' ) -SAAS_GITLAB_DOMAIN = getattr(settings, 'SAAS_GITLAB_DOMAIN', +SAAS_GITLAB_DOMAIN = Setting('SAAS_GITLAB_DOMAIN', 'gitlab.{}'.format(ORCHESTRA_BASE_DOMAIN) ) diff --git a/orchestra/contrib/services/settings.py b/orchestra/contrib/services/settings.py index 15867ce9..b81ef2a9 100644 --- a/orchestra/contrib/services/settings.py +++ b/orchestra/contrib/services/settings.py @@ -1,39 +1,40 @@ -from django.conf import settings from django.utils.translation import ugettext_lazy as _ +from orchestra.settings import Setting -SERVICES_SERVICE_TAXES = getattr(settings, 'SERVICES_SERVICE_TAXES', ( + +SERVICES_SERVICE_TAXES = Setting('SERVICES_SERVICE_TAXES', ( (0, _("Duty free")), (21, "21%"), )) -SERVICES_SERVICE_DEFAULT_TAX = getattr(settings, 'SERVICES_SERVICE_DEFAULT_TAX', - 0 +SERVICES_SERVICE_DEFAULT_TAX = Setting('SERVICES_SERVICE_DEFAULT_TAX', 0, + choices=SERVICES_SERVICE_TAXES ) -SERVICES_SERVICE_ANUAL_BILLING_MONTH = getattr(settings, 'SERVICES_SERVICE_ANUAL_BILLING_MONTH', - 1 +SERVICES_SERVICE_ANUAL_BILLING_MONTH = Setting('SERVICES_SERVICE_ANUAL_BILLING_MONTH', 1, + choices=tuple((n, n) for n in range(1, 13)) ) -SERVICES_ORDER_MODEL = getattr(settings, 'SERVICES_ORDER_MODEL', +SERVICES_ORDER_MODEL = Setting('SERVICES_ORDER_MODEL', 'orders.Order' ) -SERVICES_RATE_CLASS = getattr(settings, 'SERVICES_RATE_CLASS', +SERVICES_RATE_CLASS = Setting('SERVICES_RATE_CLASS', 'orchestra.contrib.plans.models.Rate' ) -SERVICES_DEFAULT_IGNORE_PERIOD = getattr(settings, 'SERVICES_DEFAULT_IGNORE_PERIOD', +SERVICES_DEFAULT_IGNORE_PERIOD = Setting('SERVICES_DEFAULT_IGNORE_PERIOD', 'TEN_DAYS' ) -SERVICES_IGNORE_ACCOUNT_TYPE = getattr(settings, 'SERVICES_IGNORE_ACCOUNT_TYPE', ( +SERVICES_IGNORE_ACCOUNT_TYPE = Setting('SERVICES_IGNORE_ACCOUNT_TYPE', ( 'superuser', 'STAFF', 'FRIEND', diff --git a/orchestra/contrib/settings/__init__.py b/orchestra/contrib/settings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orchestra/contrib/settings/admin.py b/orchestra/contrib/settings/admin.py new file mode 100644 index 00000000..52785c0d --- /dev/null +++ b/orchestra/contrib/settings/admin.py @@ -0,0 +1,89 @@ +from functools import partial + +from django.contrib import admin, messages +from django.db import models + +from django.views.generic.edit import FormView +from django.utils.translation import ngettext, ugettext_lazy as _ + +from orchestra.settings import Setting +from orchestra.utils import sys, paths + +from . import parser +from .forms import SettingFormSet + + +class SettingView(FormView): + template_name = 'admin/settings/change_form.html' + form_class = SettingFormSet + success_url = '.' + + def get_context_data(self, **kwargs): + context = super(SettingView, self).get_context_data(**kwargs) + context.update({ + 'title': _("Change settings"), + 'settings_file': parser.get_settings_file(), + }) + return context + + def get_initial(self): + initial_data = [] + prev_app = None + account = 0 + for name, setting in Setting.settings.items(): + app = name.split('_')[0] + initial = { + 'name': setting.name, + 'help_text': setting.help_text, + 'default': setting.default, + 'type': type(setting.default), + 'value': setting.value, + 'choices': setting.choices, + 'app': app, + 'editable': setting.editable, + 'multiple': setting.multiple, + } + if app == 'ORCHESTRA': + initial_data.insert(account, initial) + account += 1 + else: + initial_data.append(initial) + return initial_data + + def form_valid(self, form): + settings = Setting.settings + changes = {} + for data in form.cleaned_data: + setting = settings[data['name']] + if not isinstance(data['value'], parser.NotSupported) and setting.editable: + if setting.value != data['value']: + if setting.default == data['value']: + changes[setting.name] = parser.Remove() + else: + changes[setting.name] = parser.serialize(data['value']) + if changes: + # Display confirmation + if not self.request.POST.get('confirmation'): + settings_file = parser.get_settings_file() + new_content = parser.apply(changes) + diff = sys.run("cat < + +{% endblock %} + + +{% block breadcrumbs %} + +{% endblock %} + + +{% block content %} +
+
{% csrf_token %} + {% if diff %} + {% blocktrans %} +

The following changes will be performed to {{ settings_file }} file.

+ {% endblocktrans %} +
{{ diff }}
+ {{ form.management_form }} + + {% for form in form %} + {{ form }} + {% endfor %} +
+ +
+ {% else %} + {% blocktrans %} +

{{ settings_file }} file will be automatically updated and Orchestra restarted according to your changes. + {% endblocktrans %} + {% if form.errors %} +

+ {% trans "Please correct the errors below." %} +

+ {{ form.non_form_errors.as_ul }} + {% endif %} + {{ form.management_form }} + {% regroup form.forms by app as formlist %} + {% for app in formlist %} +
+

{{ app.grouper|lower|capfirst }}

+ + {% for form in app.list %} + {{ form.non_field_errors }} + {% if forloop.first %} + + {% for field in form.visible_fields %} + + {% endfor %} + + {% endif %} + + {% for field in form.visible_fields %} + + {% endfor %} + + {% endfor %} +
{{ field.label|capfirst }}
+ {# Include the hidden fields in the form #} + {% if forloop.first %} + {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %} + {% endif %} + {{ field.errors.as_ul }} +
{{ field }}{% if forloop.last %}{% if form.changed %}
{% endif %}{% endif %}
+

{{ field.help_text }}

+
+
+ {% endfor %} +
+ {% endif %} +
+{% endblock %} + + diff --git a/orchestra/contrib/systemusers/backends.py b/orchestra/contrib/systemusers/backends.py index 6214c6fc..83caaa1c 100644 --- a/orchestra/contrib/systemusers/backends.py +++ b/orchestra/contrib/systemusers/backends.py @@ -56,12 +56,12 @@ class UNIXUserBackend(ServiceController): self.append(textwrap.dedent("""\ { sleep 2 && killall -u %(user)s -s KILL; } & killall -u %(user)s || true - userdel %(user)s || exit_code=1 - groupdel %(group)s || exit_code=1 + userdel %(user)s || exit_code=$? + groupdel %(group)s || exit_code=$? """) % context ) if context['deleted_home']: - self.append("mv %(base_home)s %(deleted_home)s || exit_code=1" % context) + self.append("mv %(base_home)s %(deleted_home)s || exit_code=$?" % context) else: self.append("rm -fr %(base_home)s" % context) diff --git a/orchestra/contrib/systemusers/settings.py b/orchestra/contrib/systemusers/settings.py index bb658276..8266c83d 100644 --- a/orchestra/contrib/systemusers/settings.py +++ b/orchestra/contrib/systemusers/settings.py @@ -1,9 +1,9 @@ -from django.conf import settings - from django.utils.translation import ugettext_lazy as _ +from orchestra.settings import Setting -SYSTEMUSERS_SHELLS = getattr(settings, 'SYSTEMUSERS_SHELLS', ( + +SYSTEMUSERS_SHELLS = Setting('SYSTEMUSERS_SHELLS', ( ('/dev/null', _("No shell, FTP only")), ('/bin/rssh', _("No shell, SFTP/RSYNC only")), ('/bin/bash', "/bin/bash"), @@ -11,36 +11,36 @@ SYSTEMUSERS_SHELLS = getattr(settings, 'SYSTEMUSERS_SHELLS', ( )) -SYSTEMUSERS_DEFAULT_SHELL = getattr(settings, 'SYSTEMUSERS_DEFAULT_SHELL', - '/dev/null' +SYSTEMUSERS_DEFAULT_SHELL = Setting('SYSTEMUSERS_DEFAULT_SHELL', '/dev/null', + choices=SYSTEMUSERS_SHELLS ) -SYSTEMUSERS_DISABLED_SHELLS = getattr(settings, 'SYSTEMUSERS_DISABLED_SHELLS', ( +SYSTEMUSERS_DISABLED_SHELLS = Setting('SYSTEMUSERS_DISABLED_SHELLS', ( '/dev/null', '/bin/rssh', )) -SYSTEMUSERS_HOME = getattr(settings, 'SYSTEMUSERS_HOME', +SYSTEMUSERS_HOME = Setting('SYSTEMUSERS_HOME', '/home/%(user)s' ) -SYSTEMUSERS_FTP_LOG_PATH = getattr(settings, 'SYSTEMUSERS_FTP_LOG_PATH', +SYSTEMUSERS_FTP_LOG_PATH = Setting('SYSTEMUSERS_FTP_LOG_PATH', '/var/log/vsftpd.log' ) -SYSTEMUSERS_MAIL_LOG_PATH = getattr(settings, 'SYSTEMUSERS_MAIL_LOG_PATH', +SYSTEMUSERS_MAIL_LOG_PATH = Setting('SYSTEMUSERS_MAIL_LOG_PATH', '/var/log/exim4/mainlog' ) -SYSTEMUSERS_DEFAULT_GROUP_MEMBERS = getattr(settings, 'SYSTEMUSERS_DEFAULT_GROUP_MEMBERS', +SYSTEMUSERS_DEFAULT_GROUP_MEMBERS = Setting('SYSTEMUSERS_DEFAULT_GROUP_MEMBERS', ('www-data',) ) -SYSTEMUSERS_MOVE_ON_DELETE_PATH = getattr(settings, 'SYSTEMUSERS_MOVE_ON_DELETE_PATH', +SYSTEMUSERS_MOVE_ON_DELETE_PATH = Setting('SYSTEMUSERS_MOVE_ON_DELETE_PATH', '' ) diff --git a/orchestra/contrib/vps/settings.py b/orchestra/contrib/vps/settings.py index d6022c12..7e1b45e9 100644 --- a/orchestra/contrib/vps/settings.py +++ b/orchestra/contrib/vps/settings.py @@ -1,21 +1,17 @@ -from django.conf import settings +from orchestra.settings import Setting -VPS_TYPES = getattr(settings, 'VPS_TYPES', ( +VPS_TYPES = Setting('VPS_TYPES', ( ('openvz', 'OpenVZ container'), )) -VPS_DEFAULT_TYPE = getattr(settings, 'VPS_DEFAULT_TYPE', - 'openvz' -) +VPS_DEFAULT_TYPE = Setting('VPS_DEFAULT_TYPE', 'openvz', choices=VPS_TYPES) -VPS_TEMPLATES = getattr(settings, 'VPS_TEMPLATES', ( +VPS_TEMPLATES = Setting('VPS_TEMPLATES', ( ('debian7', 'Debian 7 - Wheezy'), )) -VPS_DEFAULT_TEMPLATE = getattr(settings, 'VPS_DEFAULT_TEMPLATE', - 'debian7' -) +VPS_DEFAULT_TEMPLATE = Setting('VPS_DEFAULT_TEMPLATE', 'debian7', choices=VPS_TEMPLATES) diff --git a/orchestra/contrib/webapps/backends/__init__.py b/orchestra/contrib/webapps/backends/__init__.py index 9b9fa1ae..eff5939d 100644 --- a/orchestra/contrib/webapps/backends/__init__.py +++ b/orchestra/contrib/webapps/backends/__init__.py @@ -36,7 +36,7 @@ class WebAppServiceMixin(object): def delete_webapp_dir(self, context): if context['deleted_app_path']: - self.append("mv %(app_path)s %(deleted_app_path)s || exit_code=1" % context) + self.append("mv %(app_path)s %(deleted_app_path)s || exit_code=$?" % context) else: self.append("rm -fr %(app_path)s" % context) diff --git a/orchestra/contrib/webapps/settings.py b/orchestra/contrib/webapps/settings.py index 88a63cee..90cbeca9 100644 --- a/orchestra/contrib/webapps/settings.py +++ b/orchestra/contrib/webapps/settings.py @@ -1,37 +1,37 @@ -from django.conf import settings +from orchestra.settings import ORCHESTRA_BASE_DOMAIN, Setting -from orchestra.settings import ORCHESTRA_BASE_DOMAIN +from .. import webapps -WEBAPPS_BASE_DIR = getattr(settings, 'WEBAPPS_BASE_DIR', +WEBAPPS_BASE_DIR = Setting('WEBAPPS_BASE_DIR', '%(home)s/webapps/%(app_name)s' ) -WEBAPPS_FPM_LISTEN = getattr(settings, 'WEBAPPS_FPM_LISTEN', +WEBAPPS_FPM_LISTEN = Setting('WEBAPPS_FPM_LISTEN', # '127.0.0.1:9%(app_id)03d '/opt/php/5.4/socks/%(user)s-%(app_name)s.sock' ) -WEBAPPS_FPM_DEFAULT_MAX_CHILDREN = getattr(settings, 'WEBAPPS_FPM_DEFAULT_MAX_CHILDREN', +WEBAPPS_FPM_DEFAULT_MAX_CHILDREN = Setting('WEBAPPS_FPM_DEFAULT_MAX_CHILDREN', 3 ) -WEBAPPS_PHPFPM_POOL_PATH = getattr(settings, 'WEBAPPS_PHPFPM_POOL_PATH', +WEBAPPS_PHPFPM_POOL_PATH = Setting('WEBAPPS_PHPFPM_POOL_PATH', '/etc/php5/fpm/pool.d/%(user)s-%(app_name)s.conf' ) -WEBAPPS_FCGID_WRAPPER_PATH = getattr(settings, 'WEBAPPS_FCGID_WRAPPER_PATH', +WEBAPPS_FCGID_WRAPPER_PATH = Setting('WEBAPPS_FCGID_WRAPPER_PATH', # Inside SuExec Document root # Make sure all account wrappers are in the same DIR '/home/httpd/fcgi-bin.d/%(user)s/%(app_name)s-wrapper' ) -WEBAPPS_FCGID_CMD_OPTIONS_PATH = getattr(settings, 'WEBAPPS_FCGID_CMD_OPTIONS_PATH', +WEBAPPS_FCGID_CMD_OPTIONS_PATH = Setting('WEBAPPS_FCGID_CMD_OPTIONS_PATH', # Loaded by Apache '/etc/apache2/fcgid-conf/%(user)s-%(app_name)s.conf' ) @@ -39,33 +39,37 @@ WEBAPPS_FCGID_CMD_OPTIONS_PATH = getattr(settings, 'WEBAPPS_FCGID_CMD_OPTIONS_PA # Greater or equal to your FcgidMaxRequestsPerProcess # http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html#examples -WEBAPPS_PHP_MAX_REQUESTS = getattr(settings, 'WEBAPPS_PHP_MAX_REQUESTS', +WEBAPPS_PHP_MAX_REQUESTS = Setting('WEBAPPS_PHP_MAX_REQUESTS', 400 ) -WEBAPPS_PHP_ERROR_LOG_PATH = getattr(settings, 'WEBAPPS_PHP_ERROR_LOG_PATH', +WEBAPPS_PHP_ERROR_LOG_PATH = Setting('WEBAPPS_PHP_ERROR_LOG_PATH', '' ) -WEBAPPS_MERGE_PHP_WEBAPPS = getattr(settings, 'WEBAPPS_MERGE_PHP_WEBAPPS', +WEBAPPS_MERGE_PHP_WEBAPPS = Setting('WEBAPPS_MERGE_PHP_WEBAPPS', # Combine all fcgid-wrappers/fpm-pools into one per account-php_version # to better control num processes per account and save memory False) -WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', ( - 'orchestra.contrib.webapps.types.php.PHPApp', - 'orchestra.contrib.webapps.types.misc.StaticApp', - 'orchestra.contrib.webapps.types.misc.WebalizerApp', - 'orchestra.contrib.webapps.types.misc.SymbolicLinkApp', - 'orchestra.contrib.webapps.types.wordpress.WordPressApp', - 'orchestra.contrib.webapps.types.python.PythonApp', -)) +WEBAPPS_TYPES = Setting('WEBAPPS_TYPES', ( + 'orchestra.contrib.webapps.types.php.PHPApp', + 'orchestra.contrib.webapps.types.misc.StaticApp', + 'orchestra.contrib.webapps.types.misc.WebalizerApp', + 'orchestra.contrib.webapps.types.misc.SymbolicLinkApp', + 'orchestra.contrib.webapps.types.wordpress.WordPressApp', + 'orchestra.contrib.webapps.types.python.PythonApp', + ), + # lazy loading + choices=lambda : ((t.get_class_path(), t.get_class_path()) for t in webapps.types.AppType.get_plugins()), + multiple=True, +) -WEBAPPS_PHP_VERSIONS = getattr(settings, 'WEBAPPS_PHP_VERSIONS', ( +WEBAPPS_PHP_VERSIONS = Setting('WEBAPPS_PHP_VERSIONS', ( # Execution modle choose by ending -fpm or -cgi ('5.4-fpm', 'PHP 5.4 FPM'), ('5.4-cgi', 'PHP 5.4 FCGID'), @@ -75,69 +79,68 @@ WEBAPPS_PHP_VERSIONS = getattr(settings, 'WEBAPPS_PHP_VERSIONS', ( )) -WEBAPPS_DEFAULT_PHP_VERSION = getattr(settings, 'WEBAPPS_DEFAULT_PHP_VERSION', - '5.4-cgi' +WEBAPPS_DEFAULT_PHP_VERSION = Setting('WEBAPPS_DEFAULT_PHP_VERSION', '5.4-cgi', + choices=WEBAPPS_PHP_VERSIONS ) -WEBAPPS_PHP_CGI_BINARY_PATH = getattr(settings, 'WEBAPPS_PHP_CGI_BINARY_PATH', +WEBAPPS_PHP_CGI_BINARY_PATH = Setting('WEBAPPS_PHP_CGI_BINARY_PATH', # Path of the cgi binary used by fcgid '/usr/bin/php%(php_version_number)s-cgi' ) -WEBAPPS_PHP_CGI_RC_DIR = getattr(settings, 'WEBAPPS_PHP_CGI_RC_DIR', +WEBAPPS_PHP_CGI_RC_DIR = Setting('WEBAPPS_PHP_CGI_RC_DIR', # Path to php.ini '/etc/php%(php_version_number)s/cgi/' ) -WEBAPPS_PHP_CGI_INI_SCAN_DIR = getattr(settings, 'WEBAPPS_PHP_CGI_INI_SCAN_DIR', +WEBAPPS_PHP_CGI_INI_SCAN_DIR = Setting('WEBAPPS_PHP_CGI_INI_SCAN_DIR', # Path to php.ini '/etc/php%(php_version_number)s/cgi/conf.d' ) -WEBAPPS_PYTHON_VERSIONS = getattr(settings, 'WEBAPPS_PYTHON_VERSIONS', ( +WEBAPPS_PYTHON_VERSIONS = Setting('WEBAPPS_PYTHON_VERSIONS', ( ('3.4-uwsgi', 'Python 3.4 uWSGI'), ('2.7-uwsgi', 'Python 2.7 uWSGI'), )) -WEBAPPS_DEFAULT_PYTHON_VERSION = getattr(settings, 'WEBAPPS_DEFAULT_PYTHON_VERSION', - '3.4-uwsgi' +WEBAPPS_DEFAULT_PYTHON_VERSION = Setting('WEBAPPS_DEFAULT_PYTHON_VERSION', '3.4-uwsgi', + choices=WEBAPPS_PYTHON_VERSIONS ) -WEBAPPS_UWSGI_SOCKET = getattr(settings, 'WEBAPPS_UWSGI_SOCKET', +WEBAPPS_UWSGI_SOCKET = Setting('WEBAPPS_UWSGI_SOCKET', '/var/run/uwsgi/app/%(app_name)s/socket' ) -WEBAPPS_UWSGI_BASE_DIR = getattr(settings, 'WEBAPPS_UWSGI_BASE_DIR', +WEBAPPS_UWSGI_BASE_DIR = Setting('WEBAPPS_UWSGI_BASE_DIR', '/etc/uwsgi/' ) -WEBAPPS_PYTHON_MAX_REQUESTS = getattr(settings, 'WEBAPPS_PYTHON_MAX_REQUESTS', +WEBAPPS_PYTHON_MAX_REQUESTS = Setting('WEBAPPS_PYTHON_MAX_REQUESTS', 500 ) -WEBAPPS_PYTHON_DEFAULT_MAX_WORKERS = getattr(settings, 'WEBAPPS_PYTHON_DEFAULT_MAX_WORKERS', +WEBAPPS_PYTHON_DEFAULT_MAX_WORKERS = Setting('WEBAPPS_PYTHON_DEFAULT_MAX_WORKERS', 3 ) -WEBAPPS_PYTHON_DEFAULT_TIMEOUT = getattr(settings, 'WEBAPPS_PYTHON_DEFAULT_TIMEOUT', +WEBAPPS_PYTHON_DEFAULT_TIMEOUT = Setting('WEBAPPS_PYTHON_DEFAULT_TIMEOUT', 30 ) -WEBAPPS_UNDER_CONSTRUCTION_PATH = getattr(settings, 'WEBAPPS_UNDER_CONSTRUCTION_PATH', - # Server-side path where a under construction stock page is - # '/var/www/undercontruction/index.html', - '' +WEBAPPS_UNDER_CONSTRUCTION_PATH = Setting('WEBAPPS_UNDER_CONSTRUCTION_PATH', '', + help_text=("Server-side path where a under construction stock page is " + "'/var/www/undercontruction/index.html'") ) @@ -150,7 +153,7 @@ WEBAPPS_UNDER_CONSTRUCTION_PATH = getattr(settings, 'WEBAPPS_UNDER_CONSTRUCTION_ -WEBAPPS_PHP_DISABLED_FUNCTIONS = getattr(settings, 'WEBAPPS_PHP_DISABLED_FUNCTION', [ +WEBAPPS_PHP_DISABLED_FUNCTIONS = Setting('WEBAPPS_PHP_DISABLED_FUNCTION', [ 'exec', 'passthru', 'shell_exec', @@ -174,49 +177,53 @@ WEBAPPS_PHP_DISABLED_FUNCTIONS = getattr(settings, 'WEBAPPS_PHP_DISABLED_FUNCTIO ]) -WEBAPPS_ENABLED_OPTIONS = getattr(settings, 'WEBAPPS_ENABLED_OPTIONS', ( - 'orchestra.contrib.webapps.options.PublicRoot', - 'orchestra.contrib.webapps.options.Timeout', - 'orchestra.contrib.webapps.options.Processes', - 'orchestra.contrib.webapps.options.PHPEnableFunctions', - 'orchestra.contrib.webapps.options.PHPAllowURLInclude', - 'orchestra.contrib.webapps.options.PHPAllowURLFopen', - 'orchestra.contrib.webapps.options.PHPAutoAppendFile', - 'orchestra.contrib.webapps.options.PHPAutoPrependFile', - 'orchestra.contrib.webapps.options.PHPDateTimeZone', - 'orchestra.contrib.webapps.options.PHPDefaultSocketTimeout', - 'orchestra.contrib.webapps.options.PHPDisplayErrors', - 'orchestra.contrib.webapps.options.PHPExtension', - 'orchestra.contrib.webapps.options.PHPMagicQuotesGPC', - 'orchestra.contrib.webapps.options.PHPMagicQuotesRuntime', - 'orchestra.contrib.webapps.options.PHPMaginQuotesSybase', - 'orchestra.contrib.webapps.options.PHPMaxInputTime', - 'orchestra.contrib.webapps.options.PHPMaxInputVars', - 'orchestra.contrib.webapps.options.PHPMemoryLimit', - 'orchestra.contrib.webapps.options.PHPMySQLConnectTimeout', - 'orchestra.contrib.webapps.options.PHPOutputBuffering', - 'orchestra.contrib.webapps.options.PHPRegisterGlobals', - 'orchestra.contrib.webapps.options.PHPPostMaxSize', - 'orchestra.contrib.webapps.options.PHPSendmailPath', - 'orchestra.contrib.webapps.options.PHPSessionBugCompatWarn', - 'orchestra.contrib.webapps.options.PHPSessionAutoStart', - 'orchestra.contrib.webapps.options.PHPSafeMode', - 'orchestra.contrib.webapps.options.PHPSuhosinPostMaxVars', - 'orchestra.contrib.webapps.options.PHPSuhosinGetMaxVars', - 'orchestra.contrib.webapps.options.PHPSuhosinRequestMaxVars', - 'orchestra.contrib.webapps.options.PHPSuhosinSessionEncrypt', - 'orchestra.contrib.webapps.options.PHPSuhosinSimulation', - 'orchestra.contrib.webapps.options.PHPSuhosinExecutorIncludeWhitelist', - 'orchestra.contrib.webapps.options.PHPUploadMaxFileSize', - 'orchestra.contrib.webapps.options.PHPZendExtension', -)) +WEBAPPS_ENABLED_OPTIONS = Setting('WEBAPPS_ENABLED_OPTIONS', ( + 'orchestra.contrib.webapps.options.PublicRoot', + 'orchestra.contrib.webapps.options.Timeout', + 'orchestra.contrib.webapps.options.Processes', + 'orchestra.contrib.webapps.options.PHPEnableFunctions', + 'orchestra.contrib.webapps.options.PHPAllowURLInclude', + 'orchestra.contrib.webapps.options.PHPAllowURLFopen', + 'orchestra.contrib.webapps.options.PHPAutoAppendFile', + 'orchestra.contrib.webapps.options.PHPAutoPrependFile', + 'orchestra.contrib.webapps.options.PHPDateTimeZone', + 'orchestra.contrib.webapps.options.PHPDefaultSocketTimeout', + 'orchestra.contrib.webapps.options.PHPDisplayErrors', + 'orchestra.contrib.webapps.options.PHPExtension', + 'orchestra.contrib.webapps.options.PHPMagicQuotesGPC', + 'orchestra.contrib.webapps.options.PHPMagicQuotesRuntime', + 'orchestra.contrib.webapps.options.PHPMaginQuotesSybase', + 'orchestra.contrib.webapps.options.PHPMaxInputTime', + 'orchestra.contrib.webapps.options.PHPMaxInputVars', + 'orchestra.contrib.webapps.options.PHPMemoryLimit', + 'orchestra.contrib.webapps.options.PHPMySQLConnectTimeout', + 'orchestra.contrib.webapps.options.PHPOutputBuffering', + 'orchestra.contrib.webapps.options.PHPRegisterGlobals', + 'orchestra.contrib.webapps.options.PHPPostMaxSize', + 'orchestra.contrib.webapps.options.PHPSendmailPath', + 'orchestra.contrib.webapps.options.PHPSessionBugCompatWarn', + 'orchestra.contrib.webapps.options.PHPSessionAutoStart', + 'orchestra.contrib.webapps.options.PHPSafeMode', + 'orchestra.contrib.webapps.options.PHPSuhosinPostMaxVars', + 'orchestra.contrib.webapps.options.PHPSuhosinGetMaxVars', + 'orchestra.contrib.webapps.options.PHPSuhosinRequestMaxVars', + 'orchestra.contrib.webapps.options.PHPSuhosinSessionEncrypt', + 'orchestra.contrib.webapps.options.PHPSuhosinSimulation', + 'orchestra.contrib.webapps.options.PHPSuhosinExecutorIncludeWhitelist', + 'orchestra.contrib.webapps.options.PHPUploadMaxFileSize', + 'orchestra.contrib.webapps.options.PHPZendExtension', + ), + # lazy loading + choices=lambda : ((o.get_class_path(), o.get_class_path()) for o in webapps.options.AppOption.get_plugins()), + multiple=True, +) -WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST = getattr(settings, 'WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST', +WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST = Setting('WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST', 'mysql.{}'.format(ORCHESTRA_BASE_DOMAIN) ) -WEBAPPS_MOVE_ON_DELETE_PATH = getattr(settings, 'WEBAPPS_MOVE_ON_DELETE_PATH', +WEBAPPS_MOVE_ON_DELETE_PATH = Setting('WEBAPPS_MOVE_ON_DELETE_PATH', '' ) diff --git a/orchestra/contrib/websites/backends/apache.py b/orchestra/contrib/websites/backends/apache.py index d3464a5d..3b60f784 100644 --- a/orchestra/contrib/websites/backends/apache.py +++ b/orchestra/contrib/websites/backends/apache.py @@ -14,7 +14,7 @@ from ..utils import normurlpath class Apache2Backend(ServiceController): """ - Apache 2.4 backend with support for the following directives: + Apache ≥2.4 backend with support for the following directives: static, location, fpm, fcgid, uwsgi, \ ssl, security, redirects, proxies, saas """ @@ -24,6 +24,7 @@ class Apache2Backend(ServiceController): model = 'websites.Website' related_models = ( ('websites.Content', 'website'), + ('websites.WebsiteDirective', 'directives'), ('webapps.WebApp', 'website_set'), ) verbose_name = _("Apache 2") diff --git a/orchestra/contrib/websites/settings.py b/orchestra/contrib/websites/settings.py index fd33798d..82a141a0 100644 --- a/orchestra/contrib/websites/settings.py +++ b/orchestra/contrib/websites/settings.py @@ -1,8 +1,11 @@ -from django.conf import settings from django.utils.translation import ugettext_lazy as _ +from orchestra.settings import Setting -WEBSITES_UNIQUE_NAME_FORMAT = getattr(settings, 'WEBSITES_UNIQUE_NAME_FORMAT', +from .. import websites + + +WEBSITES_UNIQUE_NAME_FORMAT = Setting('WEBSITES_UNIQUE_NAME_FORMAT', '%(user)s-%(site_name)s' ) @@ -14,64 +17,68 @@ WEBSITES_UNIQUE_NAME_FORMAT = getattr(settings, 'WEBSITES_UNIQUE_NAME_FORMAT', #)) -WEBSITES_PROTOCOL_CHOICES = getattr(settings, 'WEBSITES_PROTOCOL_CHOICES', ( +WEBSITES_PROTOCOL_CHOICES = Setting('WEBSITES_PROTOCOL_CHOICES', ( ('http', "HTTP"), ('https', "HTTPS"), ('http/https', _("HTTP and HTTPS")), ('https-only', _("HTTPS only")), )) -WEBSITES_DEFAULT_PROTOCOL = getattr(settings, 'WEBSITES_DEFAULT_PROTOCOL', - 'http' +WEBSITES_DEFAULT_PROTOCOL = Setting('WEBSITES_DEFAULT_PROTOCOL', 'http', + choices=WEBSITES_PROTOCOL_CHOICES ) -WEBSITES_DEFAULT_IPS = getattr(settings, 'WEBSITES_DEFAULT_IPS', ( - '*' +WEBSITES_DEFAULT_IPS = Setting('WEBSITES_DEFAULT_IPS', ( + '*', )) -WEBSITES_DOMAIN_MODEL = getattr(settings, 'WEBSITES_DOMAIN_MODEL', +WEBSITES_DOMAIN_MODEL = Setting('WEBSITES_DOMAIN_MODEL', 'domains.Domain' ) -WEBSITES_ENABLED_DIRECTIVES = getattr(settings, 'WEBSITES_ENABLED_DIRECTIVES', ( - 'orchestra.contrib.websites.directives.Redirect', - 'orchestra.contrib.websites.directives.Proxy', - 'orchestra.contrib.websites.directives.ErrorDocument', - 'orchestra.contrib.websites.directives.SSLCA', - 'orchestra.contrib.websites.directives.SSLCert', - 'orchestra.contrib.websites.directives.SSLKey', - 'orchestra.contrib.websites.directives.SecRuleRemove', - 'orchestra.contrib.websites.directives.SecEngine', - 'orchestra.contrib.websites.directives.WordPressSaaS', - 'orchestra.contrib.websites.directives.DokuWikiSaaS', - 'orchestra.contrib.websites.directives.DrupalSaaS', -)) +WEBSITES_ENABLED_DIRECTIVES = Setting('WEBSITES_ENABLED_DIRECTIVES', ( + 'orchestra.contrib.websites.directives.Redirect', + 'orchestra.contrib.websites.directives.Proxy', + 'orchestra.contrib.websites.directives.ErrorDocument', + 'orchestra.contrib.websites.directives.SSLCA', + 'orchestra.contrib.websites.directives.SSLCert', + 'orchestra.contrib.websites.directives.SSLKey', + 'orchestra.contrib.websites.directives.SecRuleRemove', + 'orchestra.contrib.websites.directives.SecEngine', + 'orchestra.contrib.websites.directives.WordPressSaaS', + 'orchestra.contrib.websites.directives.DokuWikiSaaS', + 'orchestra.contrib.websites.directives.DrupalSaaS', + ), + # lazy loading + choices=lambda : ((d.get_class_path(), d.get_class_path()) for d in websites.directives.SiteDirective.get_plugins()), + multiple=True, +) -WEBSITES_BASE_APACHE_CONF = getattr(settings, 'WEBSITES_BASE_APACHE_CONF', +WEBSITES_BASE_APACHE_CONF = Setting('WEBSITES_BASE_APACHE_CONF', '/etc/apache2/' ) -WEBSITES_WEBALIZER_PATH = getattr(settings, 'WEBSITES_WEBALIZER_PATH', +WEBSITES_WEBALIZER_PATH = Setting('WEBSITES_WEBALIZER_PATH', '/home/httpd/webalizer/' ) -WEBSITES_WEBSITE_WWW_ACCESS_LOG_PATH = getattr(settings, 'WEBSITES_WEBSITE_WWW_ACCESS_LOG_PATH', +WEBSITES_WEBSITE_WWW_ACCESS_LOG_PATH = Setting('WEBSITES_WEBSITE_WWW_ACCESS_LOG_PATH', '/var/log/apache2/virtual/%(unique_name)s.log' ) -WEBSITES_WEBSITE_WWW_ERROR_LOG_PATH = getattr(settings, 'WEBSITES_WEBSITE_WWW_ERROR_LOG_PATH', +WEBSITES_WEBSITE_WWW_ERROR_LOG_PATH = Setting('WEBSITES_WEBSITE_WWW_ERROR_LOG_PATH', '' ) -WEBSITES_TRAFFIC_IGNORE_HOSTS = getattr(settings, 'WEBSITES_TRAFFIC_IGNORE_HOSTS', +WEBSITES_TRAFFIC_IGNORE_HOSTS = Setting('WEBSITES_TRAFFIC_IGNORE_HOSTS', ('127.0.0.1',) ) @@ -86,26 +93,28 @@ WEBSITES_TRAFFIC_IGNORE_HOSTS = getattr(settings, 'WEBSITES_TRAFFIC_IGNORE_HOSTS # '') -WEBSITES_SAAS_DIRECTIVES = getattr(settings, 'WEBSITES_SAAS_DIRECTIVES', { +WEBSITES_SAAS_DIRECTIVES = Setting('WEBSITES_SAAS_DIRECTIVES', { 'wordpress-saas': ('fpm', '/opt/php/5.4/socks/pangea.sock', '/home/httpd/wordpress-mu/'), 'drupal-saas': ('fpm', '/opt/php/5.4/socks/pangea.sock','/home/httpd/drupal-mu/'), 'dokuwiki-saas': ('fpm', '/opt/php/5.4/socks/pangea.sock','/home/httpd/moodle-mu/'), }) -WEBSITES_DEFAULT_SSL_CERT = getattr(settings, 'WEBSITES_DEFAULT_SSL_CERT', +WEBSITES_DEFAULT_SSL_CERT = Setting('WEBSITES_DEFAULT_SSL_CERT', '' ) -WEBSITES_DEFAULT_SSL_KEY = getattr(settings, 'WEBSITES_DEFAULT_SSL_KEY', +WEBSITES_DEFAULT_SSL_KEY = Setting('WEBSITES_DEFAULT_SSL_KEY', '' ) -WEBSITES_DEFAULT_SSL_CA = getattr(settings, 'WEBSITES_DEFAULT_SSL_CA', +WEBSITES_DEFAULT_SSL_CA = Setting('WEBSITES_DEFAULT_SSL_CA', '' ) -WEBSITES_VHOST_EXTRA_DIRECTIVES = getattr(settings, 'WEBSITES_VHOST_EXTRA_DIRECTIVES', ( - # (, ), - # ('/cgi-bin/', 'ScriptAlias /cgi-bin/ %(home)s/cgi-bin/'), -)) +WEBSITES_VHOST_EXTRA_DIRECTIVES = Setting('WEBSITES_VHOST_EXTRA_DIRECTIVES', (), + help_text=( + "(, ),
" + "i.e. ('/cgi-bin/', 'ScriptAlias /cgi-bin/ %(home)s/cgi-bin/')" + ) +) diff --git a/orchestra/forms/fields.py b/orchestra/forms/fields.py index af65d2bd..ee383dc6 100644 --- a/orchestra/forms/fields.py +++ b/orchestra/forms/fields.py @@ -1,5 +1,7 @@ from django import forms +from .widgets import SpanWidget + class MultiSelectFormField(forms.MultipleChoiceField): """ http://djangosnippets.org/snippets/1200/ """ @@ -13,3 +15,15 @@ class MultiSelectFormField(forms.MultipleChoiceField): if not value and self.required: raise forms.ValidationError(self.error_messages['required']) return value + + +class SpanField(forms.Field): + """ + A field which renders a value wrapped in a tag. + + Requires use of specific form support. (see ReadonlyForm or ReadonlyModelForm) + """ + + def __init__(self, *args, **kwargs): + kwargs['widget'] = kwargs.get('widget', SpanWidget) + super(SpanField, self).__init__(*args, **kwargs) diff --git a/orchestra/forms/options.py b/orchestra/forms/options.py index d82c13fc..0404275d 100644 --- a/orchestra/forms/options.py +++ b/orchestra/forms/options.py @@ -6,6 +6,9 @@ from orchestra.utils.python import random_ascii from ..core.validators import validate_password +from .fields import SpanField +from .widgets import SpanWidget + class UserCreationForm(forms.ModelForm): """ @@ -65,3 +68,24 @@ class UserChangeForm(forms.ModelForm): # This is done here, rather than on the field, because the # field does not have access to the initial value return self.initial["password"] + + +class ReadOnlyFormMixin(object): + """ + Mixin class for ModelForm or Form that provides support for SpanField on readonly fields + Meta: + readonly_fileds = (ro_field1, ro_field2) + """ + def __init__(self, *args, **kwargs): + super(ReadOnlyFormMixin, self).__init__(*args, **kwargs) + for name in self.Meta.readonly_fields: + field = self.fields[name] + if not isinstance(field, SpanField): + field.widget = SpanWidget() + if hasattr(self, 'instance'): + # Model form + original_value = str(getattr(self.instance, name)) + else: + original_value = str(self.initial.get(name)) + field.widget.original_value = original_value + diff --git a/orchestra/forms/widgets.py b/orchestra/forms/widgets.py index edb70980..63317a27 100644 --- a/orchestra/forms/widgets.py +++ b/orchestra/forms/widgets.py @@ -5,10 +5,40 @@ from django import forms from django.utils.safestring import mark_safe from django.utils.encoding import force_text +from django.contrib.admin.templatetags.admin_static import static + +# TODO rename readonlywidget +class SpanWidget(forms.Widget): + """ + Renders a value wrapped in a tag. + Requires use of specific form support. (see ReadonlyForm or ReadonlyModelForm) + """ + + def __init__(self, *args, **kwargs): + self.tag = kwargs.pop('tag', '') + super(SpanWidget, self).__init__(*args, **kwargs) + + def render(self, name, value, attrs=None): + final_attrs = self.build_attrs(attrs, name=name) + original_value = self.original_value + # Display icon + if original_value in ('True', 'False') or isinstance(original_value, bool): + icon = static('admin/img/icon-%s.gif' % 'yes' if original_value else 'no') + return mark_safe('%s' % (icon, str(original_value))) + tag = self.tag[:-1] + endtag = '/'.join((self.tag[0], self.tag[1:])) + return mark_safe('%s%s >%s%s' % (tag, forms.util.flatatt(final_attrs), original_value, endtag)) + + def value_from_datadict(self, data, files, name): + return self.original_value + + def _has_changed(self, initial, data): + return False + class ShowTextWidget(forms.Widget): def __init__(self, *args, **kwargs): - for kwarg in ['bold', 'warning', 'hidden']: + for kwarg in ['tag', 'warning', 'hidden']: setattr(self, kwarg, kwargs.pop(kwarg, False)) super(ShowTextWidget, self).__init__(*args, **kwargs) @@ -18,8 +48,9 @@ class ShowTextWidget(forms.Widget): return '' if hasattr(self, 'initial'): value = self.initial - if self.bold: - final_value = '%s' % (value) + if self.tag: + endtag = '/'.join((self.tag[0], self.tag[1:])) + final_value = ''.join((self.tag, value, endtag)) else: final_value = '
'.join(value.split('\n')) if self.warning: diff --git a/orchestra/management/commands/startservices.py b/orchestra/management/commands/startservices.py index be97e5b2..7566acf0 100644 --- a/orchestra/management/commands/startservices.py +++ b/orchestra/management/commands/startservices.py @@ -19,7 +19,7 @@ def run_tuple(services, action, options, optional=False): def flatten(nested, depth=0): - if hasattr(nested, '__iter__'): + if isinstance(nested, (list, tuple)): for sublist in nested: for element in flatten(sublist, depth+1): yield element diff --git a/orchestra/plugins/options.py b/orchestra/plugins/options.py index 2ee9c249..7d0c5ca2 100644 --- a/orchestra/plugins/options.py +++ b/orchestra/plugins/options.py @@ -55,6 +55,10 @@ class Plugin(object): def get_change_readonly_fileds(cls): return cls.change_readonly_fileds + @classmethod + def get_class_path(cls): + return '.'.join((cls.__module__, cls.__name__)) + def clean_data(self): """ model clean, uses cls.serizlier by default """ if self.serializer: diff --git a/orchestra/settings.py b/orchestra/settings.py index ad0b32b5..477c26b2 100644 --- a/orchestra/settings.py +++ b/orchestra/settings.py @@ -1,58 +1,100 @@ +from collections import OrderedDict + from django.conf import settings from django.utils.translation import ugettext_lazy as _ -# Domain name used when it will not be possible to infere the domain from a request -# For example in periodic tasks -ORCHESTRA_SITE_URL = getattr(settings, 'ORCHESTRA_SITE_URL', - 'http://localhost' -) +class Setting(object): + """ + Keeps track of the defined settings. + Instances of this class are the native value of the setting. + """ + conf_settings = settings + settings = OrderedDict() + + def __str__(self): + return self.name + + def __repr__(self): + value = str(self.value) + value = ("'%s'" if isinstance(value, str) else '%s') % value + return '<%s: %s>' % (self.name, value) + + def __new__(cls, name, default, help_text="", choices=None, editable=True, multiple=False, call_init=False): + if call_init: + return super(Setting, cls).__new__(cls) + cls.settings[name] = cls(name, default, help_text=help_text, choices=choices, + editable=editable, multiple=multiple, call_init=True) + return cls.get_value(name, default) + + def __init__(self, *args, **kwargs): + self.name, self.default = args + for name, value in kwargs.items(): + setattr(self, name, value) + self.value = self.get_value(self.name, self.default) + self.settings[name] = self + + @classmethod + def get_value(cls, name, default): + return getattr(cls.conf_settings, name, default) -ORCHESTRA_SITE_NAME = getattr(settings, 'ORCHESTRA_SITE_NAME', - 'orchestra' + + # TODO validation, defaults to same type + + +ORCHESTRA_BASE_DOMAIN = Setting('ORCHESTRA_BASE_DOMAIN', + 'orchestra.lan' ) -ORCHESTRA_SITE_VERBOSE_NAME = getattr(settings, 'ORCHESTRA_SITE_VERBOSE_NAME', +ORCHESTRA_SITE_URL = Setting('ORCHESTRA_SITE_URL', 'http://orchestra.%s' % ORCHESTRA_BASE_DOMAIN, + help_text=_("Domain name used when it will not be possible to infere the domain from a request." + "For example in periodic tasks.") +) + + +ORCHESTRA_SITE_NAME = Setting('ORCHESTRA_SITE_NAME', 'orchestra') + + +ORCHESTRA_SITE_VERBOSE_NAME = Setting('ORCHESTRA_SITE_VERBOSE_NAME', _("%s Hosting Management" % ORCHESTRA_SITE_NAME.capitalize()) ) -ORCHESTRA_BASE_DOMAIN = getattr(settings, 'ORCHESTRA_BASE_DOMAIN', - 'orchestra.lan' -) - # Service management commands -ORCHESTRA_START_SERVICES = getattr(settings, 'ORCHESTRA_START_SERVICES', [ +ORCHESTRA_START_SERVICES = Setting('ORCHESTRA_START_SERVICES', ( 'postgresql', 'celeryevcam', 'celeryd', 'celerybeat', ('uwsgi', 'nginx'), -]) +)) -ORCHESTRA_RESTART_SERVICES = getattr(settings, 'ORCHESTRA_RESTART_SERVICES', [ +ORCHESTRA_RESTART_SERVICES = Setting('ORCHESTRA_RESTART_SERVICES', ( 'celeryd', 'celerybeat', 'uwsgi' -]) +)) -ORCHESTRA_STOP_SERVICES = getattr(settings, 'ORCHESTRA_STOP_SERVICES', [ +ORCHESTRA_STOP_SERVICES = Setting('ORCHESTRA_STOP_SERVICES', ( ('uwsgi', 'nginx'), 'celerybeat', 'celeryd', 'celeryevcam', 'postgresql' -]) +)) -ORCHESTRA_API_ROOT_VIEW = getattr(settings, 'ORCHESTRA_API_ROOT_VIEW', +ORCHESTRA_API_ROOT_VIEW = Setting('ORCHESTRA_API_ROOT_VIEW', 'orchestra.api.root.APIRoot' ) -ORCHESTRA_DEFAULT_SUPPORT_FROM_EMAIL = getattr(settings, 'ORCHESTRA_DEFAULT_SUPPORT_FROM_EMAIL', +ORCHESTRA_DEFAULT_SUPPORT_FROM_EMAIL = Setting('ORCHESTRA_DEFAULT_SUPPORT_FROM_EMAIL', 'support@{}'.format(ORCHESTRA_BASE_DOMAIN) ) + + +ORCHESTRA_EDIT_SETTINGS = Setting('ORCHESTRA_EDIT_SETTINGS', True) diff --git a/orchestra/static/orchestra/icons/Preferences.png b/orchestra/static/orchestra/icons/Preferences.png new file mode 100644 index 00000000..c81b6ba6 Binary files /dev/null and b/orchestra/static/orchestra/icons/Preferences.png differ diff --git a/orchestra/static/orchestra/icons/Preferences.svg b/orchestra/static/orchestra/icons/Preferences.svg new file mode 100644 index 00000000..c3df8ea2 --- /dev/null +++ b/orchestra/static/orchestra/icons/Preferences.svg @@ -0,0 +1,783 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + + + + preferences + system + category + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/static/orchestra/icons/preferences.png b/orchestra/static/orchestra/icons/preferences.png new file mode 100644 index 00000000..e323a556 Binary files /dev/null and b/orchestra/static/orchestra/icons/preferences.png differ diff --git a/orchestra/templatetags/utils.py b/orchestra/templatetags/utils.py index 7e1ff468..63f261cc 100644 --- a/orchestra/templatetags/utils.py +++ b/orchestra/templatetags/utils.py @@ -81,6 +81,12 @@ def content_type_id(label): return ContentType.objects.filter(app_label=app_label, model=model).values_list('id', flat=True)[0] +@register.filter +def split(value, sep=' '): + parts = value.split(sep) + return (parts[0], sep.join(parts[1:])) + + @register.filter def admin_url(obj): return change_url(obj) diff --git a/orchestra/utils/sys.py b/orchestra/utils/sys.py index f452f970..f461bca0 100644 --- a/orchestra/utils/sys.py +++ b/orchestra/utils/sys.py @@ -76,9 +76,9 @@ def runiterator(command, display=False, error_codes=[0], silent=False, stdin=b'' #.decode('ascii'), errors='replace') if display and stdout: - sys.stdout.write(stdout) + sys.stdout.write(stdout.decode('utf8')) if display and stderr: - sys.stderr.write(stderr) + sys.stderr.write(stderr.decode('utf8')) state = _Attribute(stdout) state.stderr = stderr