From 12910bf072e4d6e7a8c70748a783de6416804a85 Mon Sep 17 00:00:00 2001 From: Marc Aymerich Date: Wed, 4 Mar 2015 21:06:16 +0000 Subject: [PATCH] Webapps massive refactoring --- TODO.md | 8 +- orchestra/apps/accounts/admin.py | 16 + orchestra/apps/databases/backends.py | 2 + orchestra/apps/databases/models.py | 4 +- orchestra/apps/domains/backends.py | 8 +- orchestra/apps/domains/models.py | 7 + orchestra/apps/orchestration/admin.py | 11 +- orchestra/apps/orchestration/backends.py | 1 + orchestra/apps/orchestration/manager.py | 9 +- orchestra/apps/orchestration/middlewares.py | 42 +- orchestra/apps/orchestration/models.py | 37 +- orchestra/apps/orchestration/widgets.py | 24 + orchestra/apps/payments/models.py | 15 +- orchestra/apps/saas/admin.py | 12 +- orchestra/apps/saas/models.py | 9 +- orchestra/apps/saas/services/bscw.py | 2 +- orchestra/apps/saas/services/gitlab.py | 2 +- orchestra/apps/saas/services/moodle.py | 2 +- orchestra/apps/saas/services/options.py | 12 +- orchestra/apps/saas/services/phplist.py | 2 +- orchestra/apps/systemusers/backends.py | 4 +- orchestra/apps/systemusers/forms.py | 1 + orchestra/apps/webapps/admin.py | 47 +- orchestra/apps/webapps/backends/__init__.py | 14 +- orchestra/apps/webapps/backends/phpfcgid.py | 1 + orchestra/apps/webapps/backends/phpfpm.py | 4 +- orchestra/apps/webapps/backends/static.py | 1 + .../apps/webapps/backends/symboliclink.py | 27 + orchestra/apps/webapps/backends/wordpress.py | 48 + .../apps/webapps/backends/wordpressmu.py | 3 +- .../apps/webapps/migrations/0001_initial.py | 52 + .../webapps/migrations/0002_webapp_data.py | 21 + orchestra/apps/webapps/migrations/__init__.py | 0 orchestra/apps/webapps/models.py | 66 +- orchestra/apps/webapps/options.py | 300 ++++ orchestra/apps/webapps/settings.py | 316 +--- orchestra/apps/webapps/types.py | 309 ++++ orchestra/apps/websites/admin.py | 5 +- orchestra/apps/websites/models.py | 20 +- orchestra/apps/websites/options.py | 127 ++ orchestra/apps/websites/settings.py | 79 +- orchestra/forms/widgets.py | 11 +- orchestra/plugins/admin.py | 28 +- orchestra/plugins/forms.py | 19 +- orchestra/plugins/options.py | 7 +- .../orchestra/icons/apps}/BSCW.png | Bin .../orchestra/icons/apps/DokuWikiMu.png | Bin 0 -> 3844 bytes .../orchestra/icons/apps/DokuWikiMu.svg | 630 ++++++++ .../orchestra/icons/apps}/Dokuwiki.png | Bin .../orchestra/icons/apps}/Dokuwiki.svg | 0 .../orchestra/icons/apps}/Drupal.png | Bin .../orchestra/icons/apps}/Drupal.svg | 0 .../static/orchestra/icons/apps/DrupalMu.png | Bin 0 -> 2313 bytes .../static/orchestra/icons/apps/DrupalMu.svg | 160 ++ .../orchestra/icons/apps}/Moodle.png | Bin .../orchestra/icons/apps}/Moodle.svg | 0 .../static/orchestra/icons/apps/MoodleMu.png | Bin 0 -> 3265 bytes .../static/orchestra/icons/apps/MoodleMu.svg | 538 +++++++ orchestra/static/orchestra/icons/apps/PHP.svg | 63 + .../static/orchestra/icons/apps/PHPFCGI.png | Bin 0 -> 4353 bytes .../static/orchestra/icons/apps/PHPFCGI.svg | 182 +++ .../static/orchestra/icons/apps/PHPFPM.png | Bin 0 -> 4282 bytes .../static/orchestra/icons/apps/PHPFPM.svg | 182 +++ .../orchestra/icons/apps}/Phplist.png | Bin .../orchestra/icons/apps}/Phplist.svg | 0 .../static/orchestra/icons/apps/Python.svg | 36 + .../static/orchestra/icons/apps/Static.png | Bin 0 -> 3252 bytes .../static/orchestra/icons/apps/Static.svg | 1365 +++++++++++++++++ .../static/orchestra/icons/apps/Stats.png | Bin 0 -> 2436 bytes .../static/orchestra/icons/apps/Stats.svg | 394 +++++ .../orchestra/icons/apps/SymbolicLink.png | Bin 0 -> 1928 bytes .../orchestra/icons/apps/SymbolicLink.svg | 177 +++ .../orchestra/icons/apps}/WordPress.png | Bin .../orchestra/icons/apps}/WordPress.svg | 0 .../orchestra/icons/apps/WordPressMu.png | Bin 0 -> 2874 bytes .../orchestra/icons/apps/WordPressMu.svg | 127 ++ .../orchestra/icons/apps}/gitlab.png | Bin orchestra/static/orchestra/icons/saas.svg | 2 +- .../admin/plugins/select_plugin.html | 2 +- orchestra/utils/humanize.py | 1 - orchestra/utils/python.py | 6 + orchestra/utils/tests.py | 5 +- 82 files changed, 5118 insertions(+), 487 deletions(-) create mode 100644 orchestra/apps/orchestration/widgets.py create mode 100644 orchestra/apps/webapps/backends/symboliclink.py create mode 100644 orchestra/apps/webapps/backends/wordpress.py create mode 100644 orchestra/apps/webapps/migrations/0001_initial.py create mode 100644 orchestra/apps/webapps/migrations/0002_webapp_data.py create mode 100644 orchestra/apps/webapps/migrations/__init__.py create mode 100644 orchestra/apps/webapps/options.py create mode 100644 orchestra/apps/webapps/types.py create mode 100644 orchestra/apps/websites/options.py rename orchestra/{apps/saas/static/saas/icons => static/orchestra/icons/apps}/BSCW.png (100%) create mode 100644 orchestra/static/orchestra/icons/apps/DokuWikiMu.png create mode 100644 orchestra/static/orchestra/icons/apps/DokuWikiMu.svg rename orchestra/{apps/saas/static/saas/icons => static/orchestra/icons/apps}/Dokuwiki.png (100%) rename orchestra/{apps/saas/static/saas/icons => static/orchestra/icons/apps}/Dokuwiki.svg (100%) rename orchestra/{apps/saas/static/saas/icons => static/orchestra/icons/apps}/Drupal.png (100%) rename orchestra/{apps/saas/static/saas/icons => static/orchestra/icons/apps}/Drupal.svg (100%) create mode 100644 orchestra/static/orchestra/icons/apps/DrupalMu.png create mode 100644 orchestra/static/orchestra/icons/apps/DrupalMu.svg rename orchestra/{apps/saas/static/saas/icons => static/orchestra/icons/apps}/Moodle.png (100%) rename orchestra/{apps/saas/static/saas/icons => static/orchestra/icons/apps}/Moodle.svg (100%) create mode 100644 orchestra/static/orchestra/icons/apps/MoodleMu.png create mode 100644 orchestra/static/orchestra/icons/apps/MoodleMu.svg create mode 100644 orchestra/static/orchestra/icons/apps/PHP.svg create mode 100644 orchestra/static/orchestra/icons/apps/PHPFCGI.png create mode 100644 orchestra/static/orchestra/icons/apps/PHPFCGI.svg create mode 100644 orchestra/static/orchestra/icons/apps/PHPFPM.png create mode 100644 orchestra/static/orchestra/icons/apps/PHPFPM.svg rename orchestra/{apps/saas/static/saas/icons => static/orchestra/icons/apps}/Phplist.png (100%) rename orchestra/{apps/saas/static/saas/icons => static/orchestra/icons/apps}/Phplist.svg (100%) create mode 100644 orchestra/static/orchestra/icons/apps/Python.svg create mode 100644 orchestra/static/orchestra/icons/apps/Static.png create mode 100644 orchestra/static/orchestra/icons/apps/Static.svg create mode 100644 orchestra/static/orchestra/icons/apps/Stats.png create mode 100644 orchestra/static/orchestra/icons/apps/Stats.svg create mode 100644 orchestra/static/orchestra/icons/apps/SymbolicLink.png create mode 100644 orchestra/static/orchestra/icons/apps/SymbolicLink.svg rename orchestra/{apps/saas/static/saas/icons => static/orchestra/icons/apps}/WordPress.png (100%) rename orchestra/{apps/saas/static/saas/icons => static/orchestra/icons/apps}/WordPress.svg (100%) create mode 100644 orchestra/static/orchestra/icons/apps/WordPressMu.png create mode 100644 orchestra/static/orchestra/icons/apps/WordPressMu.svg rename orchestra/{apps/saas/static/saas/icons => static/orchestra/icons/apps}/gitlab.png (100%) diff --git a/TODO.md b/TODO.md index c7d5a89e..54d7a587 100644 --- a/TODO.md +++ b/TODO.md @@ -30,8 +30,8 @@ * LAST version of this shit http://wkhtmltopdf.org/downloads.html * translations - from django.utils import translation - with translation.override('en'): + from django.utils import translation + with translation.override('en'): * help_text on readonly_fields specialy Bill.state. (eg. A bill is in OPEN state when bla bla ) @@ -190,7 +190,6 @@ Multi-tenant WebApps * forms autocomplete="off", doesn't work in chrome - ln -s /proc/self/fd /dev/fd @@ -204,3 +203,6 @@ POST INSTALL ssh-keygen ssh-copy-id root@ + +* symbolicLink webapp (link stuff from other places) + diff --git a/orchestra/apps/accounts/admin.py b/orchestra/apps/accounts/admin.py index 9ceda689..d67d9406 100644 --- a/orchestra/apps/accounts/admin.py +++ b/orchestra/apps/accounts/admin.py @@ -161,6 +161,22 @@ class AccountAdminMixin(object): account_link.allow_tags = True account_link.admin_order_field = 'account__username' + def get_fields(self, request, obj=None): + """ remove account or account_link depending on the case """ + fields = super(AccountAdminMixin, self).get_fields(request, obj) + fields = list(fields) + if obj is not None or getattr(self, 'account_id', None): + try: + fields.remove('account') + except ValueError: + pass + else: + try: + fields.remove('account_link') + except ValueError: + pass + return fields + def get_readonly_fields(self, request, obj=None): """ provide account for filter_by_account_fields """ if obj: diff --git a/orchestra/apps/databases/backends.py b/orchestra/apps/databases/backends.py index 967a6654..df1454c8 100644 --- a/orchestra/apps/databases/backends.py +++ b/orchestra/apps/databases/backends.py @@ -11,6 +11,7 @@ from . import settings class MySQLBackend(ServiceController): verbose_name = "MySQL database" model = 'databases.Database' + default_route_match = "database.type == 'mysql'" def save(self, database): if database.type != database.MYSQL: @@ -53,6 +54,7 @@ class MySQLBackend(ServiceController): class MySQLUserBackend(ServiceController): verbose_name = "MySQL user" model = 'databases.DatabaseUser' + default_route_match = "databaseuser.type == 'mysql'" def save(self, user): if user.type != user.MYSQL: diff --git a/orchestra/apps/databases/models.py b/orchestra/apps/databases/models.py index c907a8ee..76972437 100644 --- a/orchestra/apps/databases/models.py +++ b/orchestra/apps/databases/models.py @@ -37,7 +37,9 @@ class Database(models.Model): return users.order_by('id').first().databaseuser -Database.users.through._meta.unique_together = (('database', 'databaseuser'),) +Database.users.through._meta.unique_together = ( + ('database', 'databaseuser'), +) class DatabaseUser(models.Model): diff --git a/orchestra/apps/domains/backends.py b/orchestra/apps/domains/backends.py index 137c5f9a..396a167f 100644 --- a/orchestra/apps/domains/backends.py +++ b/orchestra/apps/domains/backends.py @@ -4,6 +4,7 @@ import textwrap from django.utils.translation import ugettext_lazy as _ from orchestra.apps.orchestration import ServiceController +from orchestra.apps.orchestration.models import BackendOperation as Operation from orchestra.utils.python import AttrDict from . import settings @@ -75,10 +76,11 @@ class Bind9MasterDomainBackend(ServiceController): self.append('[[ $UPDATED == 1 ]] && service bind9 reload') def get_servers(self, domain, backend): - from orchestra.apps.orchestration.models import Route - operation = AttrDict(backend=backend, action='save', instance=domain) + """ Get related server IPs from registered backend routes """ + from orchestra.apps.orchestration.manager import router + operation = Operation.create(backend_cls=backend, action=Operation.SAVE, instance=domain) servers = [] - for server in Route.get_servers(operation): + for server in router.get_servers(operation): servers.append(server.get_ip()) return servers diff --git a/orchestra/apps/domains/models.py b/orchestra/apps/domains/models.py index 6d71592b..58e41f2e 100644 --- a/orchestra/apps/domains/models.py +++ b/orchestra/apps/domains/models.py @@ -96,7 +96,14 @@ class Domain(models.Model): def render_zone(self): origin = self.origin zone = origin.render_records() + tail = [] for subdomain in origin.get_subdomains(): + if subdomain.name.startswith('*'): + # This subdomains needs to be rendered last in order to avoid undesired matches + tail.append(subdomain) + else: + zone += subdomain.render_records() + for subdomain in sorted(tail, key=lambda x: len(x.name), reverse=True): zone += subdomain.render_records() return zone diff --git a/orchestra/apps/orchestration/admin.py b/orchestra/apps/orchestration/admin.py index 160507f5..94b4a14e 100644 --- a/orchestra/apps/orchestration/admin.py +++ b/orchestra/apps/orchestration/admin.py @@ -3,13 +3,12 @@ from django.contrib import admin from django.utils.html import escape from django.utils.translation import ugettext_lazy as _ -from orchestra.forms.widgets import DynamicHelpTextSelect from orchestra.admin.html import monospace_format from orchestra.admin.utils import admin_link, admin_date, admin_colored from .backends import ServiceBackend from .models import Server, Route, BackendLog, BackendOperation - +from .widgets import RouteBackendSelect STATE_COLORS = { BackendLog.RECEIVED: 'darkorange', @@ -22,18 +21,22 @@ STATE_COLORS = { } + class RouteAdmin(admin.ModelAdmin): list_display = [ 'id', 'backend', 'host', 'match', 'display_model', 'display_actions', 'is_active' ] - list_editable = ['backend', 'host', 'match', 'is_active'] + list_editable = ['host', 'match', 'is_active'] list_filter = ['host', 'is_active', 'backend'] BACKEND_HELP_TEXT = { backend: "This backend operates over '%s'" % ServiceBackend.get_backend(backend).model for backend, __ in ServiceBackend.get_plugin_choices() } + DEFAULT_MATCH = { + backend.get_name(): backend.default_route_match for backend in ServiceBackend.get_backends(active=False) + } def display_model(self, route): try: @@ -54,7 +57,7 @@ class RouteAdmin(admin.ModelAdmin): def formfield_for_dbfield(self, db_field, **kwargs): """ Provides dynamic help text on backend form field """ if db_field.name == 'backend': - kwargs['widget'] = DynamicHelpTextSelect('this.id', self.BACKEND_HELP_TEXT) + kwargs['widget'] = RouteBackendSelect('this.id', self.BACKEND_HELP_TEXT, self.DEFAULT_MATCH) return super(RouteAdmin, self).formfield_for_dbfield(db_field, **kwargs) def get_form(self, request, obj=None, **kwargs): diff --git a/orchestra/apps/orchestration/backends.py b/orchestra/apps/orchestration/backends.py index 0d2c81ad..904cc846 100644 --- a/orchestra/apps/orchestration/backends.py +++ b/orchestra/apps/orchestration/backends.py @@ -34,6 +34,7 @@ class ServiceBackend(plugins.Plugin): type = 'task' # 'sync' ignore_fields = [] actions = [] + default_route_match = 'True' __metaclass__ = ServiceMount diff --git a/orchestra/apps/orchestration/manager.py b/orchestra/apps/orchestration/manager.py index 0ec144cb..c1605c83 100644 --- a/orchestra/apps/orchestration/manager.py +++ b/orchestra/apps/orchestration/manager.py @@ -10,6 +10,7 @@ from .helpers import send_report logger = logging.getLogger(__name__) +router = import_class(settings.ORCHESTRATION_ROUTER) def as_task(execute): @@ -41,17 +42,17 @@ def close_connection(execute): def execute(operations, async=False): """ generates and executes the operations on the servers """ - router = import_class(settings.ORCHESTRATION_ROUTER) scripts = {} cache = {} # Generate scripts per server+backend for operation in operations: logger.debug("Queued %s" % str(operation)) - servers = router.get_servers(operation, cache=cache) - for server in servers: + if operation.servers is None: + operation.servers = router.get_servers(operation, cache=cache) + for server in operation.servers: key = (server, operation.backend) if key not in scripts: - scripts[key] = (operation.backend(), [operation]) + scripts[key] = (operation.backend, [operation]) scripts[key][0].prepare() else: scripts[key][1].append(operation) diff --git a/orchestra/apps/orchestration/middlewares.py b/orchestra/apps/orchestration/middlewares.py index e8074546..16ee19ca 100644 --- a/orchestra/apps/orchestration/middlewares.py +++ b/orchestra/apps/orchestration/middlewares.py @@ -7,6 +7,7 @@ from django.http.response import HttpResponseServerError from orchestra.utils.python import OrderedSet +from .manager import router from .backends import ServiceBackend from .helpers import message_user from .models import BackendLog @@ -51,6 +52,16 @@ class OperationsMiddleware(object): return request.pending_operations return set() + @classmethod + def get_route_cache(cls): + """ chache the routes to save sql queries """ + if hasattr(cls.thread_locals, 'request'): + request = cls.thread_locals.request + if not hasattr(request, 'route_cache'): + request.route_cache = {} + return request.route_cache + return {} + @classmethod def collect(cls, action, **kwargs): """ Collects all pending operations derived from model signals """ @@ -58,13 +69,14 @@ class OperationsMiddleware(object): if request is None: return pending_operations = cls.get_pending_operations() - for backend in ServiceBackend.get_backends(): + route_cache = cls.get_route_cache() + for backend_cls in ServiceBackend.get_backends(): # Check if there exists a related instance to be executed for this backend instances = [] - if backend.is_main(kwargs['instance']): + if backend_cls.is_main(kwargs['instance']): instances = [(kwargs['instance'], action)] else: - candidate = backend.get_related(kwargs['instance']) + candidate = backend_cls.get_related(kwargs['instance']) if candidate: if candidate.__class__.__name__ == 'ManyRelatedManager': if 'pk_set' in kwargs: @@ -76,7 +88,7 @@ class OperationsMiddleware(object): candidates = [candidate] for candidate in candidates: # Check if a delete for candidate is in pending_operations - delete_mock = Operation.create(backend, candidate, Operation.DELETE) + delete_mock = Operation.create(backend_cls, candidate, Operation.DELETE) if delete_mock not in pending_operations: # related objects with backend.model trigger save() instances.append((candidate, Operation.SAVE)) @@ -84,7 +96,7 @@ class OperationsMiddleware(object): # Maintain consistent state of pending_operations based on save/delete behaviour # Prevent creating a deleted instance by deleting existing saves if iaction == Operation.DELETE: - save_mock = Operation.create(backend, instance, Operation.SAVE) + save_mock = Operation.create(backend_cls, instance, Operation.SAVE) try: pending_operations.remove(save_mock) except KeyError: @@ -97,17 +109,23 @@ class OperationsMiddleware(object): if update_fields != []: execute = False for field in update_fields: - if field not in backend.ignore_fields: + if field not in backend_cls.ignore_fields: execute = True break if not execute: continue - operation = Operation.create(backend, instance, iaction) - if iaction != Operation.DELETE: - # usually we expect to be using last object state, - # except when we are deleting it - pending_operations.discard(operation) - pending_operations.add(operation) + operation = Operation.create(backend_cls, instance, iaction) + # Only schedule operations if the router gives servers to execute into + servers = router.get_servers(operation, cache=route_cache) + if servers: + operation.servers = servers + if iaction != Operation.DELETE: + # usually we expect to be using last object state, + # except when we are deleting it + pending_operations.discard(operation) + elif iaction == Operation.DELETE: + operation.preload_context() + pending_operations.add(operation) def process_request(self, request): """ Store request on a thread local variable """ diff --git a/orchestra/apps/orchestration/models.py b/orchestra/apps/orchestration/models.py index 797a10b4..788cb3cc 100644 --- a/orchestra/apps/orchestration/models.py +++ b/orchestra/apps/orchestration/models.py @@ -11,7 +11,7 @@ from orchestra.core.validators import validate_ip_address, ValidationError from orchestra.models.fields import NullableCharField #from orchestra.utils.apps import autodiscover -from . import settings, manager +from . import settings from .backends import ServiceBackend @@ -119,36 +119,43 @@ class BackendOperation(models.Model): def __hash__(self): """ set() """ - backend = getattr(self, 'backend', self.backend) - return hash(backend) + hash(self.instance) + hash(self.action) + backend_cls = type(self.backend) + return hash(backend_cls) + hash(self.instance) + hash(self.action) def __eq__(self, operation): """ set() """ return hash(self) == hash(operation) @classmethod - def create(cls, backend, instance, action): - op = cls(backend=backend.get_name(), instance=instance, action=action) - op.backend = backend + def create(cls, backend_cls, instance, action, servers=None): + op = cls(backend=backend_cls.get_name(), instance=instance, action=action) + op.backend = backend_cls() # instance should maintain any dynamic attribute until backend execution # deep copy is prefered over copy otherwise objects will share same atributes (queryset cache) op.instance = copy.deepcopy(instance) - if action == cls.DELETE: - # Heuristic, running get_context will prevent most of related objects do not exist errors - if hasattr(backend, 'get_context'): - backend().get_context(op.instance) + op.servers = servers return op @classmethod def execute(cls, operations, async=False): + from . import manager return manager.execute(operations, async=async) @classmethod def execute_action(cls, instance, action): backends = ServiceBackend.get_backends(instance=instance, action=action) - operations = [cls.create(backend, instance, action) for backend in backends] + operations = [cls.create(backend_cls, instance, action) for backend_cls in backends] return cls.execute(operations) + def preload_context(self): + """ + Heuristic + Running get_context will prevent most of related objects do not exist errors + """ + if self.action == self.DELETE: + if hasattr(self.backend, 'get_context'): + self.backend.get_context(op.instance) + def backend_class(self): return ServiceBackend.get_backend(self.backend) @@ -187,14 +194,14 @@ class Route(models.Model): def get_servers(cls, operation, **kwargs): cache = kwargs.get('cache', {}) servers = [] - backend = operation.backend - key = (backend.get_name(), operation.action) + backend_cls = type(operation.backend) + key = (backend_cls.get_name(), operation.action) try: routes = cache[key] except KeyError: cache[key] = [] - for route in cls.objects.filter(is_active=True, backend=backend.get_name()): - for action in backend.get_actions(): + for route in cls.objects.filter(is_active=True, backend=backend_cls.get_name()): + for action in backend_cls.get_actions(): _key = (route.backend, action) try: cache[_key].append(route) diff --git a/orchestra/apps/orchestration/widgets.py b/orchestra/apps/orchestration/widgets.py new file mode 100644 index 00000000..576de4e8 --- /dev/null +++ b/orchestra/apps/orchestration/widgets.py @@ -0,0 +1,24 @@ +import textwrap + +from orchestra.forms.widgets import DynamicHelpTextSelect + + +class RouteBackendSelect(DynamicHelpTextSelect): + """ Updates matches input field based on selected backend """ + def __init__(self, target, help_text, route_matches, *args, **kwargs): + kwargs['attrs'] = { + 'onfocus': "this.oldvalue = this.value;", + } + self.route_matches = route_matches + super(RouteBackendSelect, self).__init__(target, help_text, *args, **kwargs) + + def get_dynamic_help_text(self, target, help_text): + help_text = super(RouteBackendSelect, self).get_dynamic_help_text(target, help_text) + return help_text + textwrap.dedent("""\ + routematches = {route_matches}; + match = $("#id_match"); + if ( this.oldvalue == "" || match.value == routematches[this.oldvalue]) + match.value = routematches[this.options[this.selectedIndex].value]; + this.oldvalue = this.value; + """.format(route_matches=self.route_matches) + ) diff --git a/orchestra/apps/payments/models.py b/orchestra/apps/payments/models.py index 86111dad..834c34ee 100644 --- a/orchestra/apps/payments/models.py +++ b/orchestra/apps/payments/models.py @@ -33,25 +33,30 @@ class PaymentSource(models.Model): def method_class(self): return PaymentMethod.get_plugin(self.method) + @cached_property + def service_instance(self): + """ Per request lived method_instance """ + return self.method_class() + @cached_property def label(self): - return self.method_class().get_label(self.data) + return self.method_instance.get_label(self.data) @cached_property def number(self): - return self.method_class().get_number(self.data) + return self.method_instance.get_number(self.data) def get_bill_context(self): - method = self.method_class() + method = self.method_instance return { 'message': method.get_bill_message(self), } def get_due_delta(self): - return self.method_class().due_delta + return self.method_instance.due_delta def clean(self): - self.data = self.method_class().clean_data(self.data) + self.data = self.method_instance.clean_data(self.data) class TransactionQuerySet(models.QuerySet): diff --git a/orchestra/apps/saas/admin.py b/orchestra/apps/saas/admin.py index ed125b4d..4d1f33eb 100644 --- a/orchestra/apps/saas/admin.py +++ b/orchestra/apps/saas/admin.py @@ -13,6 +13,7 @@ class SaaSAdmin(SelectPluginAdminMixin, AccountAdminMixin, admin.ModelAdmin): list_filter = ('service',) plugin = SoftwareService plugin_field = 'service' + plugin_title = 'Software as a Service' def display_site_name(self, saas): site_name = saas.get_site_name() @@ -20,15 +21,6 @@ class SaaSAdmin(SelectPluginAdminMixin, AccountAdminMixin, admin.ModelAdmin): display_site_name.short_description = _("Site name") display_site_name.allow_tags = True display_site_name.admin_order_field = 'site_name' - - def get_fields(self, request, obj=None): - fields = super(SaaSAdmin, self).get_fields(request, obj) - fields = list(fields) - # TODO do it in AccountAdminMixin? - if obj is not None: - fields.remove('account') - else: - fields.remove('account_link') - return fields + admin.site.register(SaaS, SaaSAdmin) diff --git a/orchestra/apps/saas/models.py b/orchestra/apps/saas/models.py index 00c5fa8f..9f51aeb1 100644 --- a/orchestra/apps/saas/models.py +++ b/orchestra/apps/saas/models.py @@ -35,11 +35,16 @@ class SaaS(models.Model): def service_class(self): return SoftwareService.get_plugin(self.service) + @cached_property + def service_instance(self): + """ Per request lived service_instance """ + return self.service_class() + def get_site_name(self): - return self.service_class().get_site_name(self) + return self.service_instance.get_site_name(self) def clean(self): - self.data = self.service_class().clean_data(self) + self.data = self.service_instance.clean_data(self) def set_password(self, password): self.password = password diff --git a/orchestra/apps/saas/services/bscw.py b/orchestra/apps/saas/services/bscw.py index 722c22bf..877c61c4 100644 --- a/orchestra/apps/saas/services/bscw.py +++ b/orchestra/apps/saas/services/bscw.py @@ -22,7 +22,7 @@ class BSCWService(SoftwareService): verbose_name = "BSCW" form = BSCWForm serializer = BSCWDataSerializer - icon = 'saas/icons/BSCW.png' + icon = 'orchestra/icons/apps/BSCW.png' # TODO override from settings site_name = 'bascw.orchestra.lan' change_readonly_fileds = ('email',) diff --git a/orchestra/apps/saas/services/gitlab.py b/orchestra/apps/saas/services/gitlab.py index d9d2f581..250b4cff 100644 --- a/orchestra/apps/saas/services/gitlab.py +++ b/orchestra/apps/saas/services/gitlab.py @@ -3,4 +3,4 @@ from .options import SoftwareService class GitLabService(SoftwareService): verbose_name = "GitLab" - icon = 'saas/icons/gitlab.png' + icon = 'orchestra/icons/apps/gitlab.png' diff --git a/orchestra/apps/saas/services/moodle.py b/orchestra/apps/saas/services/moodle.py index 97277108..d2c69995 100644 --- a/orchestra/apps/saas/services/moodle.py +++ b/orchestra/apps/saas/services/moodle.py @@ -17,4 +17,4 @@ class MoodleService(SoftwareService): verbose_name = "Moodle" form = MoodleForm description_field = 'site_name' - icon = 'saas/icons/Moodle.png' + icon = 'orchestra/icons/apps/Moodle.png' diff --git a/orchestra/apps/saas/services/options.py b/orchestra/apps/saas/services/options.py index 9439c13b..bf6380e4 100644 --- a/orchestra/apps/saas/services/options.py +++ b/orchestra/apps/saas/services/options.py @@ -29,12 +29,6 @@ class SoftwareServiceForm(PluginDataForm): super(SoftwareServiceForm, self).__init__(*args, **kwargs) self.is_change = bool(self.instance and self.instance.pk) if self.is_change: - for field in self.plugin.change_readonly_fileds + ('username',): - value = getattr(self.instance, field, None) or self.instance.data[field] - self.fields[field].required = False - self.fields[field].widget = widgets.ReadOnlyWidget(value) - self.fields[field].help_text = None - site_name = self.instance.get_site_name() self.fields['password1'].required = False self.fields['password1'].widget = forms.HiddenInput() @@ -79,7 +73,7 @@ class SoftwareService(plugins.Plugin): site_name = None site_name_base_domain = 'orchestra.lan' icon = 'orchestra/icons/apps.png' - change_readonly_fileds = () + change_readonly_fileds = ('username',) class_verbose_name = _("Software as a Service") @classmethod @@ -98,6 +92,10 @@ class SoftwareService(plugins.Plugin): raise ValidationError(serializer.errors) return serializer.data + @classmethod + def get_change_readonly_fileds(cls): + return cls.change_readonly_fileds + ('username',) + def get_site_name(self, saas): return self.site_name or '.'.join((saas.site_name, self.site_name_base_domain)) diff --git a/orchestra/apps/saas/services/phplist.py b/orchestra/apps/saas/services/phplist.py index 3cb4a90f..346da96a 100644 --- a/orchestra/apps/saas/services/phplist.py +++ b/orchestra/apps/saas/services/phplist.py @@ -11,4 +11,4 @@ class PHPListForm(SoftwareServiceForm): class PHPListService(SoftwareService): verbose_name = "phpList" form = PHPListForm - icon = 'saas/icons/Phplist.png' + icon = 'orchestra/icons/apps/Phplist.png' diff --git a/orchestra/apps/systemusers/backends.py b/orchestra/apps/systemusers/backends.py index 158d5614..4aaee319 100644 --- a/orchestra/apps/systemusers/backends.py +++ b/orchestra/apps/systemusers/backends.py @@ -115,7 +115,7 @@ class FTPTraffic(ServiceMonitor): USERNAME="$3" LOG_FILE="$4" { - grep "UPLOAD\|DOWNLOAD" ${LOG_FILE} \\ + grep " bytes, " ${LOG_FILE} \\ | grep " \\[${USERNAME}\\] " \\ | awk -v ini="${INI_DATE}" -v end="${END_DATE}" ' BEGIN { @@ -135,7 +135,7 @@ class FTPTraffic(ServiceMonitor): } { # Fri Jul 1 13:23:17 2014 split($4, time, ":") - day = sprintf("%02d", $3) + day = sprintf("%%02d", $3) # line_date = year month day hour minute second line_date = $5 months[$2] day time[1] time[2] time[3] if ( line_date > ini && line_date < end) { diff --git a/orchestra/apps/systemusers/forms.py b/orchestra/apps/systemusers/forms.py index 78318565..c86ae3c4 100644 --- a/orchestra/apps/systemusers/forms.py +++ b/orchestra/apps/systemusers/forms.py @@ -7,6 +7,7 @@ from orchestra.forms import UserCreationForm, UserChangeForm from . import settings from .models import SystemUser + class SystemUserFormMixin(object): MOCK_USERNAME = '' diff --git a/orchestra/apps/webapps/admin.py b/orchestra/apps/webapps/admin.py index 068d5b90..78ef8806 100644 --- a/orchestra/apps/webapps/admin.py +++ b/orchestra/apps/webapps/admin.py @@ -7,8 +7,10 @@ from orchestra.admin import ExtendedModelAdmin from orchestra.admin.utils import change_url from orchestra.apps.accounts.admin import AccountAdminMixin from orchestra.forms.widgets import DynamicHelpTextSelect +from orchestra.plugins.admin import SelectPluginAdminMixin -from . import settings +from . import settings, options +from .applications import App from .models import WebApp, WebAppOption @@ -17,8 +19,7 @@ class WebAppOptionInline(admin.TabularInline): extra = 1 OPTIONS_HELP_TEXT = { - k: str(unicode(v[1])) if len(v) == 3 else '' - for k, v in settings.WEBAPPS_OPTIONS.iteritems() + op.name: str(unicode(op.help_text)) for op in options.get_enabled().values() } class Media: @@ -30,6 +31,12 @@ class WebAppOptionInline(admin.TabularInline): if db_field.name == 'value': kwargs['widget'] = forms.TextInput(attrs={'size':'100'}) if db_field.name == 'name': + if self.parent_object: + plugin = self.parent_object.type_class + else: + request = kwargs['request'] + plugin = App.get_plugin(request.GET['type']) + kwargs['choices'] = plugin.get_options_choices() # Help text based on select widget kwargs['widget'] = DynamicHelpTextSelect( 'this.id.replace("name", "value")', self.OPTIONS_HELP_TEXT @@ -37,20 +44,22 @@ class WebAppOptionInline(admin.TabularInline): return super(WebAppOptionInline, self).formfield_for_dbfield(db_field, **kwargs) -class WebAppAdmin(AccountAdminMixin, ExtendedModelAdmin): +class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin): list_display = ('name', 'type', 'display_websites', 'account_link') list_filter = ('type',) - add_fields = ('account', 'name', 'type') - fields = ('account_link', 'name', 'type') +# add_fields = ('account', 'name', 'type') +# fields = ('account_link', 'name', 'type') inlines = [WebAppOptionInline] readonly_fields = ('account_link',) change_readonly_fields = ('name', 'type') list_prefetch_related = ('content_set__website',) + plugin = App + plugin_field = 'type' + plugin_title = _("Web application type") - TYPE_HELP_TEXT = { - k: str(unicode(v.get('help_text', ''))) - for k, v in settings.WEBAPPS_TYPES.iteritems() - } +# TYPE_HELP_TEXT = { +# app.get_name(): str(unicode(app.help_text)) for app in App.get_plugins() +# } def display_websites(self, webapp): websites = [] @@ -68,14 +77,14 @@ class WebAppAdmin(AccountAdminMixin, ExtendedModelAdmin): display_websites.short_description = _("web sites") display_websites.allow_tags = True - def formfield_for_dbfield(self, db_field, **kwargs): - """ Make value input widget bigger """ - if db_field.name == 'type': - # Help text based on select widget - kwargs['widget'] = DynamicHelpTextSelect( - 'this.id.replace("name", "value")', self.TYPE_HELP_TEXT - ) - kwargs['help_text'] = self.TYPE_HELP_TEXT.get(db_field.default, '') - return super(WebAppAdmin, self).formfield_for_dbfield(db_field, **kwargs) +# def formfield_for_dbfield(self, db_field, **kwargs): +# """ Make value input widget bigger """ +# if db_field.name == 'type': +# # Help text based on select widget +# kwargs['widget'] = DynamicHelpTextSelect( +# 'this.id.replace("name", "value")', self.TYPE_HELP_TEXT +# ) +# kwargs['help_text'] = self.TYPE_HELP_TEXT.get(db_field.default, '') +# return super(WebAppAdmin, self).formfield_for_dbfield(db_field, **kwargs) admin.site.register(WebApp, WebAppAdmin) diff --git a/orchestra/apps/webapps/backends/__init__.py b/orchestra/apps/webapps/backends/__init__.py index eb8d92fb..fb6e432b 100644 --- a/orchestra/apps/webapps/backends/__init__.py +++ b/orchestra/apps/webapps/backends/__init__.py @@ -12,16 +12,8 @@ class WebAppServiceMixin(object): return settings.WEBAPPS_TYPES[webapp.type]['directive'][0] == self.directive def create_webapp_dir(self, context): - self.append(textwrap.dedent(""" - path="" - for dir in $(echo %(app_path)s | tr "/" "\n"); do - path="${path}/${dir}" - [ -d $path ] || { - mkdir "${path}" - chown %(user)s:%(group)s "${path}" - } - done - """ % context)) + self.append("mkdir -p %(app_path)s" % context) + self.append("chown %(user)s:%(group)s %(app_path)s" % context) def get_php_init_vars(self, webapp, per_account=False): """ @@ -54,7 +46,7 @@ class WebAppServiceMixin(object): return { 'user': webapp.get_username(), 'group': webapp.get_groupname(), - 'app_name': webapp.get_name(), + 'app_name': webapp.name, 'type': webapp.type, 'app_path': webapp.get_path().rstrip('/'), 'banner': self.get_banner(), diff --git a/orchestra/apps/webapps/backends/phpfcgid.py b/orchestra/apps/webapps/backends/phpfcgid.py index a811572c..65e15427 100644 --- a/orchestra/apps/webapps/backends/phpfcgid.py +++ b/orchestra/apps/webapps/backends/phpfcgid.py @@ -13,6 +13,7 @@ class PHPFcgidBackend(WebAppServiceMixin, ServiceController): """ Per-webapp fcgid application """ verbose_name = _("PHP-Fcgid") directive = 'fcgi' + default_route_match = "webapp.type.endswith('-fcgi')" def save(self, webapp): if not self.valid_directive(webapp): diff --git a/orchestra/apps/webapps/backends/phpfpm.py b/orchestra/apps/webapps/backends/phpfpm.py index 72f48f43..6841140d 100644 --- a/orchestra/apps/webapps/backends/phpfpm.py +++ b/orchestra/apps/webapps/backends/phpfpm.py @@ -14,6 +14,7 @@ class PHPFPMBackend(WebAppServiceMixin, ServiceController): """ Per-webapp php application """ verbose_name = _("PHP-FPM") directive = 'fpm' + default_route_match = "webapp.type.endswith('-fpm')" def save(self, webapp): if not self.valid_directive(webapp): @@ -26,7 +27,8 @@ class PHPFPMBackend(WebAppServiceMixin, ServiceController): } || { echo -e '%(fpm_config)s' > %(fpm_path)s UPDATEDFPM=1 - }""" % context)) + }""" % context + )) def delete(self, webapp): if not self.valid_directive(webapp): diff --git a/orchestra/apps/webapps/backends/static.py b/orchestra/apps/webapps/backends/static.py index f06c7eef..67d9a526 100644 --- a/orchestra/apps/webapps/backends/static.py +++ b/orchestra/apps/webapps/backends/static.py @@ -8,6 +8,7 @@ from . import WebAppServiceMixin class StaticBackend(WebAppServiceMixin, ServiceController): verbose_name = _("Static") directive = 'static' + default_route_match = "webapp.type == 'static'" def save(self, webapp): if not self.valid_directive(webapp): diff --git a/orchestra/apps/webapps/backends/symboliclink.py b/orchestra/apps/webapps/backends/symboliclink.py new file mode 100644 index 00000000..f97f5c3a --- /dev/null +++ b/orchestra/apps/webapps/backends/symboliclink.py @@ -0,0 +1,27 @@ +from django.utils.translation import ugettext_lazy as _ + +from orchestra.apps.orchestration import ServiceController + +from . import WebAppServiceMixin + + +class SymbolicLinkBackend(WebAppServiceMixin, ServiceController): + verbose_name = _("Symbolic link webapp") + model = 'webapps.WebApp' + default_route_match = "webapp.type == 'symbolic-link'" + + def save(self, webapp): + context = self.get_context(webapp) + self.append("ln -s '%(link_path)s' %(app_path)s" % context) + self.append("chown -h %(user)s:%(group)s %(app_path)s" % context) + + def delete(self, webapp): + context = self.get_context(webapp) + self.delete_webapp_dir(context) + + def get_context(self, webapp): + context = super(SymbolicLinkBackend, self).get_context(webapp) + context.update({ + 'link_path': webapp.data['path'], + }) + return context diff --git a/orchestra/apps/webapps/backends/wordpress.py b/orchestra/apps/webapps/backends/wordpress.py new file mode 100644 index 00000000..e9c4ab24 --- /dev/null +++ b/orchestra/apps/webapps/backends/wordpress.py @@ -0,0 +1,48 @@ +import textwrap + +from django.utils.translation import ugettext_lazy as _ + +from orchestra.apps.orchestration import ServiceController + +from .. import settings + +from . import WebAppServiceMixin + + +class WordPressBackend(WebAppServiceMixin, ServiceController): + verbose_name = _("Wordpress") + model = 'webapps.WebApp' + default_route_match = "webapp.type == 'wordpress'" + + def save(self, webapp): + context = self.get_context(webapp) + self.create_webapp_dir(context) + self.append(textwrap.dedent("""\ + # Check if directory is empty befor doing anything + if [[ ! $(ls -A %(app_path)s) ]]; then + wget http://wordpress.org/latest.tar.gz -O - --no-check-certificate \\ + | tar -xzvf - -C %(app_path)s --strip-components=1 + cp %(app_path)s/wp-config-sample.php %(app_path)s/wp-config.php + sed -i "s/database_name_here/%(db_name)s/" %(app_path)s/wp-config.php + sed -i "s/username_here/%(db_user)s/" %(app_path)s/wp-config.php + sed -i "s/password_here/%(db_pass)s/" %(app_path)s/wp-config.php + sed -i "s/localhost/%(db_host)s/" %(app_path)s/wp-config.php + mkdir %(app_path)s/wp-content/uploads + chmod 750 %(app_path)s/wp-content/uploads + chown -R %(user)s:%(group)s %(app_path)s + fi""" % context + )) + + def delete(self, webapp): + context = self.get_context(webapp) + self.delete_webapp_dir(context) + + def get_context(self, webapp): + context = super(WordPressBackend, self).get_context(webapp) + context.update({ + 'db_name': webapp.data['db_name'], + 'db_user': webapp.data['db_user'], + 'db_pass': webapp.data['db_pass'], + 'db_host': settings.WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST, + }) + return context diff --git a/orchestra/apps/webapps/backends/wordpressmu.py b/orchestra/apps/webapps/backends/wordpressmu.py index 87a5786c..690d1b38 100644 --- a/orchestra/apps/webapps/backends/wordpressmu.py +++ b/orchestra/apps/webapps/backends/wordpressmu.py @@ -11,6 +11,7 @@ from .. import settings class WordpressMuBackend(ServiceController): verbose_name = _("Wordpress multisite") model = 'webapps.WebApp' + default_route_match = "webapp.type == 'wordpress-mu'" @property def script(self): @@ -85,7 +86,7 @@ class WordpressMuBackend(ServiceController): self.validate_response(response) def delete_blog(self, webapp, server): - # OH, I've enjoied so much coding this methods that I want to thanks + # OH, I've enjoied so much coding this methods that I want to thank # the wordpress team for the excellent software they are producing session = requests.Session() self.login(session) diff --git a/orchestra/apps/webapps/migrations/0001_initial.py b/orchestra/apps/webapps/migrations/0001_initial.py new file mode 100644 index 00000000..c6c501b0 --- /dev/null +++ b/orchestra/apps/webapps/migrations/0001_initial.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import orchestra.core.validators +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='WebApp', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=128, verbose_name='name', validators=[orchestra.core.validators.validate_name])), + ('type', models.CharField(max_length=32, verbose_name='type', choices=[(b'dokuwiki-mu', b'DokuWiki (SaaS)'), (b'drupal-mu', b'Drupdal (SaaS)'), (b'php4-fcgi', b'PHP 4 FCGI'), (b'php5.2-fcgi', b'PHP 5.2 FCGI'), (b'php5.5-fpm', b'PHP 5.5 FPM'), (b'static', b'Static'), (b'symlink', b'Symbolic link'), (b'webalizer', b'Webalizer'), (b'wordpress', b'WordPress'), (b'wordpress-mu', b'WordPress (SaaS)')])), + ('account', models.ForeignKey(related_name='webapps', verbose_name='Account', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Web App', + 'verbose_name_plural': 'Web Apps', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='WebAppOption', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=128, verbose_name='name', choices=[(b'PHP-allow_url_fopen', 'PHP - allow_url_fopen'), (b'PHP-allow_url_include', 'PHP - Allow URL include'), (b'PHP-auto_append_file', 'PHP - Auto append file'), (b'PHP-auto_prepend_file', 'PHP - Auto prepend file'), (b'PHP-date.timezone', 'PHP - date.timezone'), (b'PHP-default_socket_timeout', 'PHP - Default socket timeout'), (b'PHP-display_errors', 'PHP - Display errors'), (b'PHP-extension', 'PHP - Extension'), (b'PHP-magic_quotes_gpc', 'PHP - Magic quotes GPC'), (b'PHP-magic_quotes_runtime', 'PHP - Magic quotes runtime'), (b'PHP-magic_quotes_sybase', 'PHP - Magic quotes sybase'), (b'PHP-max_execution_time', 'PHP - Max execution time'), (b'PHP-max_input_time', 'PHP - Max input time'), (b'PHP-max_input_vars', 'PHP - Max input vars'), (b'PHP-memory_limit', 'PHP - Memory limit'), (b'PHP-mysql.connect_timeout', 'PHP - Mysql connect timeout'), (b'PHP-output_buffering', 'PHP - output_buffering'), (b'PHP-post_max_size', 'PHP - Post max size'), (b'PHP-register_globals', 'PHP - Register globals'), (b'PHP-safe_mode', 'PHP - Safe mode'), (b'PHP-sendmail_path', 'PHP - sendmail_path'), (b'PHP-session.auto_start', 'PHP - session.auto_start'), (b'PHP-session.bug_compat_warn', 'PHP - session.bug_compat_warn'), (b'PHP-suhosin.executor.include.whitelist', 'PHP - suhosin.executor.include.whitelist'), (b'PHP-suhosin.get.max_vars', 'PHP - Suhosin GET max vars'), (b'PHP-suhosin.post.max_vars', 'PHP - Suhosin POST max vars'), (b'PHP-suhosin.request.max_vars', 'PHP - Suhosin request max vars'), (b'PHP-suhosin.session.encrypt', 'PHP - suhosin.session.encrypt'), (b'PHP-suhosin.simulation', 'PHP - Suhosin simulation'), (b'PHP-upload_max_filesize', 'PHP - upload_max_filesize'), (b'PHP-zend_extension', 'PHP - zend_extension'), (b'php-enabled_functions', 'PHP - Enabled functions'), (b'processes', 'Number of processes'), (b'public-root', 'Public root'), (b'timeout', 'Process timeout')])), + ('value', models.CharField(max_length=256, verbose_name='value')), + ('webapp', models.ForeignKey(related_name='options', verbose_name='Web application', to='webapps.WebApp')), + ], + options={ + 'verbose_name': 'option', + 'verbose_name_plural': 'options', + }, + bases=(models.Model,), + ), + migrations.AlterUniqueTogether( + name='webappoption', + unique_together=set([('webapp', 'name')]), + ), + migrations.AlterUniqueTogether( + name='webapp', + unique_together=set([('name', 'account')]), + ), + ] diff --git a/orchestra/apps/webapps/migrations/0002_webapp_data.py b/orchestra/apps/webapps/migrations/0002_webapp_data.py new file mode 100644 index 00000000..868b478d --- /dev/null +++ b/orchestra/apps/webapps/migrations/0002_webapp_data.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('webapps', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='webapp', + name='data', + field=jsonfield.fields.JSONField(default={}, help_text='Extra information dependent of each service.', verbose_name='data'), + preserve_default=False, + ), + ] diff --git a/orchestra/apps/webapps/migrations/__init__.py b/orchestra/apps/webapps/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orchestra/apps/webapps/models.py b/orchestra/apps/webapps/models.py index b24063d4..6c0dd08b 100644 --- a/orchestra/apps/webapps/models.py +++ b/orchestra/apps/webapps/models.py @@ -2,23 +2,25 @@ import re from django.core.exceptions import ValidationError from django.db import models +from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ +from jsonfield import JSONField from orchestra.core import validators, services -from orchestra.utils import tuple_setting_to_choices, dict_setting_to_choices from orchestra.utils.functional import cached -from . import settings +from . import settings, options +from .types import AppType class WebApp(models.Model): """ Represents a web application """ name = models.CharField(_("name"), max_length=128, validators=[validators.validate_name]) type = models.CharField(_("type"), max_length=32, - choices=dict_setting_to_choices(settings.WEBAPPS_TYPES), - default=settings.WEBAPPS_DEFAULT_TYPE) + choices=AppType.get_plugin_choices()) account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), related_name='webapps') + data = JSONField(_("data"), help_text=_("Extra information dependent of each service.")) class Meta: unique_together = ('name', 'account') @@ -31,21 +33,25 @@ class WebApp(models.Model): def get_description(self): return self.get_type_display() + @cached_property + def type_class(self): + return AppType.get_plugin(self.type) + + @cached_property + def type_instance(self): + """ Per request lived type_instance """ + return self.type_class() + def clean(self): - # Validate unique webapp names - if self.app_type.get('unique_name', False): - try: - webapp = WebApp.objects.exclude(id=self.pk).get(name=self.name, type=self.type) - except WebApp.DoesNotExist: - pass - else: - raise ValidationError({ - 'name': _("A webapp with this name already exists."), - }) + apptype = self.type_instance + apptype.validate(self) + self.data = apptype.clean_data(self) @cached def get_options(self): - return { opt.name: opt.value for opt in self.options.all() } + return { + opt.name: opt.value for opt in self.options.all() + } @property def app_type(self): @@ -81,7 +87,7 @@ class WebAppOption(models.Model): webapp = models.ForeignKey(WebApp, verbose_name=_("Web application"), related_name='options') name = models.CharField(_("name"), max_length=128, - choices=tuple_setting_to_choices(settings.WEBAPPS_OPTIONS)) + choices=((op.name, op.verbose_name) for op in options.get_enabled().values())) value = models.CharField(_("value"), max_length=256) class Meta: @@ -93,16 +99,24 @@ class WebAppOption(models.Model): return self.name def clean(self): - """ validates name and value according to WEBAPPS_OPTIONS """ - regex = settings.WEBAPPS_OPTIONS[self.name][-1] - if not re.match(regex, self.value): - raise ValidationError({ - 'value': ValidationError(_("'%(value)s' does not match %(regex)s."), - params={ - 'value': self.value, - 'regex': regex - }), - }) + option = options.get_enabled()[self.name] + option.validate(self) services.register(WebApp) + + +# Admin bulk deletion doesn't call model.delete(), we use signals instead of model method overriding + +from django.db.models.signals import pre_save, pre_delete +from django.dispatch import receiver + +@receiver(pre_save, sender=WebApp, dispatch_uid='webapps.type.save') +def type_save(sender, *args, **kwargs): + instance = kwargs['instance'] + instance.type_instance.save(instance) + +@receiver(pre_delete, sender=WebApp, dispatch_uid='webapps.type.delete') +def type_delete(sender, *args, **kwargs): + instance = kwargs['instance'] + instance.type_instance.delete(instance) diff --git a/orchestra/apps/webapps/options.py b/orchestra/apps/webapps/options.py new file mode 100644 index 00000000..b7d89d91 --- /dev/null +++ b/orchestra/apps/webapps/options.py @@ -0,0 +1,300 @@ +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ + +from orchestra.utils.python import import_class + +from . import settings + + +class AppOption(object): + def __init__(self, name, *args, **kwargs): + self.name = name + self.verbose_name = kwargs.pop('verbose_name', name) + self.help_text = kwargs.pop('help_text', '') + for k,v in kwargs.iteritems(): + setattr(self, k, v) + + def validate(self, webapp): + if self.regex and not re.match(self.regex, webapp.value): + raise ValidationError({ + 'value': ValidationError(_("'%(value)s' does not match %(regex)s."), + params={ + 'value': webapp.value, + 'regex': self.regex + }), + }) + + +public_root = AppOption('public-root', + verbose_name=_("Public root"), + help_text=_("Document root relative to webapps/<webapp>/"), + regex=r'[^ ]+' +) + +timeout = AppOption('timeout', + # FCGID FcgidIOTimeout + # FPM pm.request_terminate_timeout + # PHP max_execution_time ini + verbose_name=_("Process timeout"), + help_text=_("Maximum time in seconds allowed for a request to complete (a number between 0 and 999)."), + regex=r'^[0-9]{1,3}$', +) + +processes = AppOption('processes', + # FCGID MaxProcesses + # FPM pm.max_children + verbose_name=_("Number of processes"), + help_text=_("Maximum number of children that can be alive at the same time (a number between 0 and 9)."), + regex=r'^[0-9]$', +) + +php_enabled_functions = AppOption('php-enabled_functions', + verbose_name=_("Enabled functions"), + help_text = ' '.join(settings.WEBAPPS_PHP_DISABLED_FUNCTIONS), + regex=r'^[\w\.,-]+$' +) + +php_allow_url_include = AppOption('PHP-allow_url_include', + verbose_name=_("Allow URL include"), + help_text=_("Allows the use of URL-aware fopen wrappers with include, include_once, require, " + "require_once (On or Off)."), + regex=r'^(On|Off|on|off)$' +) + +php_allow_url_fopen = AppOption('PHP-allow_url_fopen', + verbose_name=_("Allow URL fopen"), + help_text=_("Enables the URL-aware fopen wrappers that enable accessing URL object like files (On or Off)."), + regex=r'^(On|Off|on|off)$' +) + +php_auto_append_file = AppOption('PHP-auto_append_file', + verbose_name=_("Auto append file"), + help_text=_("Specifies the name of a file that is automatically parsed after the main file."), + regex=r'^[\w\.,-/]+$' +) + +php_auto_prepend_file = AppOption('PHP-auto_prepend_file', + verbose_name=_("Auto prepend file"), + help_text=_("Specifies the name of a file that is automatically parsed before the main file."), + regex=r'^[\w\.,-/]+$' +) + +php_date_timezone = AppOption('PHP-date.timezone', + verbose_name=_("date.timezone"), + help_text=_("Sets the default timezone used by all date/time functions (Timezone string 'Europe/London')."), + regex=r'^\w+/\w+$' +) + +php_default_socket_timeout = AppOption('PHP-default_socket_timeout', + verbose_name=_("Default socket timeout"), + help_text=_("Number between 0 and 999."), + regex=r'^[0-9]{1,3}$' +) + +php_display_errors = AppOption('PHP-display_errors', + verbose_name=_("Display errors"), + help_text=_("Determines whether errors should be printed to the screen as part of the output or " + "if they should be hidden from the user (On or Off)."), + regex=r'^(On|Off|on|off)$' +) + +php_extension = AppOption('PHP-extension', + verbose_name=_("Extension"), + regex=r'^[^ ]+$' +) + +php_magic_quotes_gpc = AppOption('PHP-magic_quotes_gpc', + verbose_name=_("Magic quotes GPC"), + help_text=_("Sets the magic_quotes state for GPC (Get/Post/Cookie) operations (On or Off) " + "DEPRECATED as of PHP 5.3.0."), + regex=r'^(On|Off|on|off)$', + deprecated=5.3 +) + +php_magic_quotes_runtime = AppOption('PHP-magic_quotes_runtime', + verbose_name=_("Magic quotes runtime"), + help_text=_("Functions that return data from any sort of external source will have quotes escaped " + "with a backslash (On or Off) DEPRECATED as of PHP 5.3.0."), + regex=r'^(On|Off|on|off)$', + deprecated=5.3 +) + +php_magic_quotes_sybase = AppOption('PHP-magic_quotes_sybase', + verbose_name=_("Magic quotes sybase"), + help_text=_("Single-quote is escaped with a single-quote instead of a backslash (On or Off)."), + regex=r'^(On|Off|on|off)$' +) + +php_max_execution_time = AppOption('PHP-max_execution_time', + verbose_name=_("Max execution time"), + help_text=_("Maximum time in seconds a script is allowed to run before it is terminated by " + "the parser (Integer between 0 and 999)."), + regex=r'^[0-9]{1,3}$' +) + +php_max_input_time = AppOption('PHP-max_input_time', + verbose_name=_("Max input time"), + help_text=_("Maximum time in seconds a script is allowed to parse input data, like POST and GET " + "(Integer between 0 and 999)."), + regex=r'^[0-9]{1,3}$' +) + +php_max_input_vars = AppOption('PHP-max_input_vars', + verbose_name=_("Max input vars"), + help_text=_("How many input variables may be accepted (limit is applied to $_GET, $_POST " + "and $_COOKIE superglobal separately) (Integer between 0 and 9999)."), + regex=r'^[0-9]{1,4}$' +) + +php_memory_limit = AppOption('PHP-memory_limit', + verbose_name=_("Memory limit"), + help_text=_("This sets the maximum amount of memory in bytes that a script is allowed to allocate " + "(Value between 0M and 999M)."), + regex=r'^[0-9]{1,3}M$' +) + +php_mysql_connect_timeout = AppOption('PHP-mysql.connect_timeout', + verbose_name=_("Mysql connect timeout"), + help_text=_("Number between 0 and 999."), + regex=r'^([0-9]){1,3}$' +) + +php_output_buffering = AppOption('PHP-output_buffering', + verbose_name=_("Output buffering"), + help_text=_("Turn on output buffering (On or Off)."), + regex=r'^(On|Off|on|off)$' +) + +php_register_globals = AppOption('PHP-register_globals', + verbose_name=_("Register globals"), + help_text=_("Whether or not to register the EGPCS (Environment, GET, POST, Cookie, Server) " + "variables as global variables (On or Off)."), + regex=r'^(On|Off|on|off)$' +) + +php_post_max_size = AppOption('PHP-post_max_size', + verbose_name=_("Post max size"), + help_text=_("Sets max size of post data allowed (Value between 0M and 999M)."), + regex=r'^[0-9]{1,3}M$' +) + +php_sendmail_path = AppOption('PHP-sendmail_path', + verbose_name=_("sendmail_path"), + help_text=_("Where the sendmail program can be found."), + regex=r'^[^ ]+$' +) + +php_session_bug_compat_warn = AppOption('PHP-session.bug_compat_warn', + verbose_name=_("session.bug_compat_warn"), + help_text=_("Enables an PHP bug on session initialization for legacy behaviour (On or Off)."), + regex=r'^(On|Off|on|off)$' +) + +php_session_auto_start = AppOption('PHP-session.auto_start', + verbose_name=_("session.auto_start"), + help_text=_("Specifies whether the session module starts a session automatically on request " + "startup (On or Off)."), + regex=r'^(On|Off|on|off)$' +) +php_safe_mode = AppOption('PHP-safe_mode', + verbose_name=_("Safe mode"), + help_text=_("Whether to enable PHP's safe mode (On or Off) DEPRECATED as of PHP 5.3.0"), + regex=r'^(On|Off|on|off)$', + deprecated=5.3 +) +php_suhosin_post_max_vars = AppOption('PHP-suhosin.post.max_vars', + verbose_name=_("Suhosin POST max vars"), + help_text=_("Number between 0 and 9999."), + regex=r'^[0-9]{1,4}$' +) +php_suhosin_get_max_vars = AppOption('PHP-suhosin.get.max_vars', + verbose_name=_("Suhosin GET max vars"), + help_text=_("Number between 0 and 9999."), + regex=r'^[0-9]{1,4}$' +) +php_suhosin_request_max_vars = AppOption('PHP-suhosin.request.max_vars', + verbose_name=_("Suhosin request max vars"), + help_text=_("Number between 0 and 9999."), + regex=r'^[0-9]{1,4}$' +) +php_suhosin_session_encrypt = AppOption('PHP-suhosin.session.encrypt', + verbose_name=_("suhosin.session.encrypt"), + help_text=_("On or Off"), + regex=r'^(On|Off|on|off)$' +) +php_suhosin_simulation = AppOption('PHP-suhosin.simulation', + verbose_name=_("Suhosin simulation"), + help_text=_("On or Off"), + regex=r'^(On|Off|on|off)$' +) +php_suhosin_executor_include_whitelist = AppOption('PHP-suhosin.executor.include.whitelist', + verbose_name=_("suhosin.executor.include.whitelist"), + regex=r'.*$' +) +php_upload_max_filesize = AppOption('PHP-upload_max_filesize', + verbose_name=_("upload_max_filesize"), + help_text=_("Value between 0M and 999M."), + regex=r'^[0-9]{1,3}M$' +) +php_zend_extension = AppOption('PHP-post_max_size', + verbose_name=_("zend_extension"), + regex=r'^[^ ]+$' +) + + +filesystem = [ + public_root, +] + +process = [ + timeout, + processes, +] + +php = [ + php_enabled_functions, + php_allow_url_include, + php_allow_url_fopen, + php_auto_append_file, + php_auto_prepend_file, + php_date_timezone, + php_default_socket_timeout, + php_display_errors, + php_extension, + php_magic_quotes_gpc, + php_magic_quotes_runtime, + php_magic_quotes_sybase, + php_max_execution_time, + php_max_input_time, + php_max_input_vars, + php_memory_limit, + php_mysql_connect_timeout, + php_output_buffering, + php_register_globals, + php_post_max_size, + php_sendmail_path, + php_session_bug_compat_warn, + php_session_auto_start, + php_safe_mode, + php_suhosin_post_max_vars, + php_suhosin_get_max_vars, + php_suhosin_request_max_vars, + php_suhosin_session_encrypt, + php_suhosin_simulation, + php_suhosin_executor_include_whitelist, + php_upload_max_filesize, + php_zend_extension, +] + + +_enabled = None + +def get_enabled(): + global _enabled + if _enabled is None: + from . import settings + _enabled = {} + for op in settings.WEBAPPS_ENABLED_OPTIONS: + op = import_class(op) + _enabled[op.name] = op + return _enabled diff --git a/orchestra/apps/webapps/settings.py b/orchestra/apps/webapps/settings.py index 8aa03469..bfb109aa 100644 --- a/orchestra/apps/webapps/settings.py +++ b/orchestra/apps/webapps/settings.py @@ -21,67 +21,26 @@ WEBAPPS_FCGID_PATH = getattr(settings, 'WEBAPPS_FCGID_PATH', '/home/httpd/fcgid/%(user)s/%(app_name)s-wrapper') -WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', { - 'php5.5': { - 'verbose_name': "PHP 5.5 fpm", -# 'fpm', ('unix:/var/run/%(user)s-%(app_name)s.sock|fcgi://127.0.0.1%(app_path)s',), - 'directive': ('fpm', 'fcgi://{}%(app_path)s'.format(WEBAPPS_FPM_LISTEN)), - 'help_text': _("This creates a PHP5.5 application under ~/webapps/<app_name>
" - "PHP-FPM will be used to execute PHP files.") - }, - 'php5.2': { - 'verbose_name': "PHP 5.2 fcgi", - 'directive': ('fcgi', WEBAPPS_FCGID_PATH), - 'help_text': _("This creates a PHP5.2 application under ~/webapps/<app_name>
" - "Apache-mod-fcgid will be used to execute PHP files.") - }, - 'php4': { - 'verbose_name': "PHP 4 fcgi", - 'directive': ('fcgi', WEBAPPS_FCGID_PATH,), - 'help_text': _("This creates a PHP4 application under ~/webapps/<app_name>
" - "Apache-mod-fcgid will be used to execute PHP files.") - }, - 'static': { - 'verbose_name': _("Static"), - 'directive': ('static',), - 'help_text': _("This creates a Static application under ~/webapps/<app_name>
" - "Apache2 will be used to serve static content and execute CGI files.") - }, - 'webalizer': { - 'verbose_name': "Webalizer", - 'directive': ('static', '%(app_path)s%(site_name)s'), - 'help_text': _("This creates a Webalizer application under " - "~/webapps/<app_name>-<site_name>") - }, - 'wordpress-mu': { - 'verbose_name': _("Wordpress (SaaS)"), - 'directive': ('fpm', 'fcgi://127.0.0.1:8990/home/httpd/wordpress-mu/'), - 'help_text': _("This creates a Wordpress site on a multi-tenant Wordpress server.
" - "By default this blog is accessible via <app_name>.blogs.orchestra.lan") - }, - 'dokuwiki-mu': { - 'verbose_name': _("DokuWiki (SaaS)"), - 'directive': ('alias', '/home/httpd/wikifarm/farm/'), - 'help_text': _("This create a Dokuwiki wiki into a shared Dokuwiki server.
" - "By default this wiki is accessible via <app_name>.wikis.orchestra.lan") - }, - 'drupal-mu': { - 'verbose_name': _("Drupdal (SaaS)"), - 'directive': ('fpm', 'fcgi://127.0.0.1:8991/home/httpd/drupal-mu/'), - 'help_text': _("This creates a Drupal site into a multi-tenant Drupal server.
" - "The installation will be completed after visiting " - "http://<app_name>.drupal.orchestra.lan/install.php?profile=standard
" - "By default this site will be accessible via <app_name>.drupal.orchestra.lan") - } -}) +WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', ( + 'orchestra.apps.webapps.types.Php55App', + 'orchestra.apps.webapps.types.Php52App', + 'orchestra.apps.webapps.types.Php4App', + 'orchestra.apps.webapps.types.StaticApp', + 'orchestra.apps.webapps.types.WebalizerApp', + 'orchestra.apps.webapps.types.WordPressMuApp', + 'orchestra.apps.webapps.types.DokuWikiMuApp', + 'orchestra.apps.webapps.types.DrupalMuApp', + 'orchestra.apps.webapps.types.SymbolicLinkApp', + 'orchestra.apps.webapps.types.WordPressApp', +)) -WEBAPPS_TYPES_OVERRIDE = getattr(settings, 'WEBAPPS_TYPES_OVERRIDE', {}) -for webapp_type, value in WEBAPPS_TYPES_OVERRIDE.iteritems(): - if value is None: - WEBAPPS_TYPES.pop(webapp_type, None) - else: - WEBAPPS_TYPES[webapp_type] = value +#WEBAPPS_TYPES_OVERRIDE = getattr(settings, 'WEBAPPS_TYPES_OVERRIDE', {}) +#for webapp_type, value in WEBAPPS_TYPES_OVERRIDE.iteritems(): +# if value is None: +# WEBAPPS_TYPES.pop(webapp_type, None) +# else: +# WEBAPPS_TYPES[webapp_type] = value WEBAPPS_DEFAULT_TYPE = getattr(settings, 'WEBAPPS_DEFAULT_TYPE', 'php5.5') @@ -116,204 +75,43 @@ WEBAPPS_PHP_DISABLED_FUNCTIONS = getattr(settings, 'WEBAPPS_PHP_DISABLED_FUNCTIO ]) -WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', { - # { name: ( verbose_name, [help_text], validation_regex ) } - # Filesystem - 'public-root': ( - _("Public root"), - _("Document root relative to webapps/<webapp>/"), - r'[^ ]+', - ), - # Processes - 'timeout': ( - _("Process timeout"), - _("Maximum time in seconds allowed for a request to complete " - "(a number between 0 and 999)."), - # FCGID FcgidIOTimeout - # FPM pm.request_terminate_timeout - # PHP max_execution_time ini - r'^[0-9]{1,3}$', - ), - 'processes': ( - _("Number of processes"), - _("Maximum number of children that can be alive at the same time " - "(a number between 0 and 9)."), - # FCGID MaxProcesses - # FPM pm.max_children - r'^[0-9]$', - ), - # PHP - 'php-enabled_functions': ( - _("PHP - Enabled functions"), - ' '.join(WEBAPPS_PHP_DISABLED_FUNCTIONS), - r'^[\w\.,-]+$' - ), - 'PHP-allow_url_include': ( - _("PHP - Allow URL include"), - _("Allows the use of URL-aware fopen wrappers with include, include_once, require, " - "require_once (On or Off)."), - r'^(On|Off|on|off)$' - ), - 'PHP-allow_url_fopen': ( - _("PHP - allow_url_fopen"), - _("Enables the URL-aware fopen wrappers that enable accessing URL object like files " - "(On or Off)."), - r'^(On|Off|on|off)$' - ), - 'PHP-auto_append_file': ( - _("PHP - Auto append file"), - _("Specifies the name of a file that is automatically parsed after the main file."), - r'^[\w\.,-/]+$' - ), - 'PHP-auto_prepend_file': ( - _("PHP - Auto prepend file"), - _("Specifies the name of a file that is automatically parsed before the main file."), - r'^[\w\.,-/]+$' - ), - 'PHP-date.timezone': ( - _("PHP - date.timezone"), - _("Sets the default timezone used by all date/time functions " - "(Timezone string 'Europe/London')."), - r'^\w+/\w+$' - ), - 'PHP-default_socket_timeout': ( - _("PHP - Default socket timeout"), - _("Number between 0 and 999."), - r'^[0-9]{1,3}$' - ), - 'PHP-display_errors': ( - _("PHP - Display errors"), - _("determines whether errors should be printed to the screen as part of the output or " - "if they should be hidden from the user (On or Off)."), - r'^(On|Off|on|off)$' - ), - 'PHP-extension': ( - _("PHP - Extension"), - r'^[^ ]+$' - ), - 'PHP-magic_quotes_gpc': ( - _("PHP - Magic quotes GPC"), - _("Sets the magic_quotes state for GPC (Get/Post/Cookie) operations (On or Off) " - "DEPRECATED as of PHP 5.3.0."), - r'^(On|Off|on|off)$' - ), - 'PHP-magic_quotes_runtime': ( - _("PHP - Magic quotes runtime"), - _("Functions that return data from any sort of external source will have quotes escaped " - "with a backslash (On or Off) DEPRECATED as of PHP 5.3.0."), - r'^(On|Off|on|off)$' - ), - 'PHP-magic_quotes_sybase': ( - _("PHP - Magic quotes sybase"), - _("Single-quote is escaped with a single-quote instead of a backslash (On or Off)."), - r'^(On|Off|on|off)$' - ), - 'PHP-max_execution_time': ( - _("PHP - Max execution time"), - _("Maximum time in seconds a script is allowed to run before it is terminated by " - "the parser (Integer between 0 and 999)."), - r'^[0-9]{1,3}$' - ), - 'PHP-max_input_time': ( - _("PHP - Max input time"), - _("Maximum time in seconds a script is allowed to parse input data, like POST and GET " - "(Integer between 0 and 999)."), - r'^[0-9]{1,3}$' - ), - 'PHP-max_input_vars': ( - _("PHP - Max input vars"), - _("How many input variables may be accepted (limit is applied to $_GET, $_POST and $_COOKIE superglobal separately) " - "(Integer between 0 and 9999)."), - r'^[0-9]{1,4}$' - ), - 'PHP-memory_limit': ( - _("PHP - Memory limit"), - _("This sets the maximum amount of memory in bytes that a script is allowed to allocate " - "(Value between 0M and 999M)."), - r'^[0-9]{1,3}M$' - ), - 'PHP-mysql.connect_timeout': ( - _("PHP - Mysql connect timeout"), - _("Number between 0 and 999."), - r'^([0-9]){1,3}$' - ), - 'PHP-output_buffering': ( - _("PHP - output_buffering"), - _("Turn on output buffering (On or Off)."), - r'^(On|Off|on|off)$' - ), - 'PHP-register_globals': ( - _("PHP - Register globals"), - _("Whether or not to register the EGPCS (Environment, GET, POST, Cookie, Server) " - "variables as global variables (On or Off)."), - r'^(On|Off|on|off)$' - ), - 'PHP-post_max_size': ( - _("PHP - Post max size"), - _("Sets max size of post data allowed (Value between 0M and 999M)."), - r'^[0-9]{1,3}M$' - ), - 'PHP-sendmail_path': ( - _("PHP - sendmail_path"), - _("Where the sendmail program can be found."), - r'^[^ ]+$' - ), - 'PHP-session.bug_compat_warn': ( - _("PHP - session.bug_compat_warn"), - _("Enables an PHP bug on session initialization for legacy behaviour (On or Off)."), - r'^(On|Off|on|off)$' - ), - 'PHP-session.auto_start': ( - _("PHP - session.auto_start"), - _("Specifies whether the session module starts a session automatically on request " - "startup (On or Off)."), - r'^(On|Off|on|off)$' - ), - 'PHP-safe_mode': ( - _("PHP - Safe mode"), - _("Whether to enable PHP's safe mode (On or Off) DEPRECATED as of PHP 5.3.0"), - r'^(On|Off|on|off)$' - ), - 'PHP-suhosin.post.max_vars': ( - _("PHP - Suhosin POST max vars"), - _("Number between 0 and 9999."), - r'^[0-9]{1,4}$' - ), - 'PHP-suhosin.get.max_vars': ( - _("PHP - Suhosin GET max vars"), - _("Number between 0 and 9999."), - r'^[0-9]{1,4}$' - ), - 'PHP-suhosin.request.max_vars': ( - _("PHP - Suhosin request max vars"), - _("Number between 0 and 9999."), - r'^[0-9]{1,4}$' - ), - 'PHP-suhosin.session.encrypt': ( - _("PHP - suhosin.session.encrypt"), - _("On or Off"), - r'^(On|Off|on|off)$' - ), - 'PHP-suhosin.simulation': ( - _("PHP - Suhosin simulation"), - _("On or Off"), - r'^(On|Off|on|off)$' - ), - 'PHP-suhosin.executor.include.whitelist': ( - _("PHP - suhosin.executor.include.whitelist"), - r'.*$' - ), - 'PHP-upload_max_filesize': ( - _("PHP - upload_max_filesize"), - _("Value between 0M and 999M."), - r'^[0-9]{1,3}M$' - ), - 'PHP-zend_extension': ( - _("PHP - zend_extension"), - r'^[^ ]+$' - ), -}) - +WEBAPPS_ENABLED_OPTIONS = getattr(settings, 'WEBAPPS_ENABLED_OPTIONS', ( + 'orchestra.apps.webapps.options.public_root', + 'orchestra.apps.webapps.options.timeout', + 'orchestra.apps.webapps.options.processes', + 'orchestra.apps.webapps.options.php_enabled_functions', + 'orchestra.apps.webapps.options.php_allow_url_include', + 'orchestra.apps.webapps.options.php_allow_url_fopen', + 'orchestra.apps.webapps.options.php_auto_append_file', + 'orchestra.apps.webapps.options.php_auto_prepend_file', + 'orchestra.apps.webapps.options.php_date_timezone', + 'orchestra.apps.webapps.options.php_default_socket_timeout', + 'orchestra.apps.webapps.options.php_display_errors', + 'orchestra.apps.webapps.options.php_extension', + 'orchestra.apps.webapps.options.php_magic_quotes_gpc', + 'orchestra.apps.webapps.options.php_magic_quotes_runtime', + 'orchestra.apps.webapps.options.php_magic_quotes_sybase', + 'orchestra.apps.webapps.options.php_max_execution_time', + 'orchestra.apps.webapps.options.php_max_input_time', + 'orchestra.apps.webapps.options.php_max_input_vars', + 'orchestra.apps.webapps.options.php_memory_limit', + 'orchestra.apps.webapps.options.php_mysql_connect_timeout', + 'orchestra.apps.webapps.options.php_output_buffering', + 'orchestra.apps.webapps.options.php_register_globals', + 'orchestra.apps.webapps.options.php_post_max_size', + 'orchestra.apps.webapps.options.php_sendmail_path', + 'orchestra.apps.webapps.options.php_session_bug_compat_warn', + 'orchestra.apps.webapps.options.php_session_auto_start', + 'orchestra.apps.webapps.options.php_safe_mode', + 'orchestra.apps.webapps.options.php_suhosin_post_max_vars', + 'orchestra.apps.webapps.options.php_suhosin_get_max_vars', + 'orchestra.apps.webapps.options.php_suhosin_request_max_vars', + 'orchestra.apps.webapps.options.php_suhosin_session_encrypt', + 'orchestra.apps.webapps.options.php_suhosin_simulation', + 'orchestra.apps.webapps.options.php_suhosin_executor_include_whitelist', + 'orchestra.apps.webapps.options.php_upload_max_filesize', + 'orchestra.apps.webapps.options.php_zend_extension', +)) WEBAPPS_WORDPRESSMU_ADMIN_PASSWORD = getattr(settings, 'WEBAPPS_WORDPRESSMU_ADMIN_PASSWORD', @@ -332,3 +130,7 @@ WEBAPPS_DOKUWIKIMU_FARM_PATH = getattr(settings, 'WEBAPPS_DOKUWIKIMU_FARM_PATH', WEBAPPS_DRUPAL_SITES_PATH = getattr(settings, 'WEBAPPS_DRUPAL_SITES_PATH', '/home/httpd/htdocs/drupal-mu/sites/%(site_name)s') + + +WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST = getattr(settings, 'WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST', + 'mysql.orchestra.lan') diff --git a/orchestra/apps/webapps/types.py b/orchestra/apps/webapps/types.py new file mode 100644 index 00000000..87815836 --- /dev/null +++ b/orchestra/apps/webapps/types.py @@ -0,0 +1,309 @@ +from django import forms +from django.core.exceptions import ValidationError +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext_lazy as _ + +from orchestra import plugins +from orchestra.plugins.forms import PluginDataForm +from orchestra.core import validators +from orchestra.forms import widgets +from orchestra.utils.functional import cached +from orchestra.utils.python import import_class + +from . import options, settings + + +class AppType(plugins.Plugin): + name = None + verbose_name = "" + help_text= "" + form = PluginDataForm + change_form = None + serializer = None + icon = 'orchestra/icons/apps.png' + unique_name = False + options = ( + ('Process', options.process), + ('PHP', options.php), + ('File system', options.filesystem), + ) + + @classmethod + @cached + def get_plugins(cls): + plugins = [] + for cls in settings.WEBAPPS_TYPES: + plugins.append(import_class(cls)) + return plugins + + @classmethod + def clean_data(cls, webapp): + """ model clean, uses cls.serizlier by default """ + if cls.serializer: + serializer = cls.serializer(data=webapp.data) + if not serializer.is_valid(): + raise ValidationError(serializer.errors) + return serializer.data + return {} + + def get_form(self): + self.form.plugin = self + self.form.plugin_field = 'type' + return self.form + + def get_change_form(self): + form = self.change_form or self.form + form.plugin = self + form.plugin_field = 'type' + return form + + def get_serializer(self): + self.serializer.plugin = self + return self.serializer + + def validate(self, instance): + """ Unique name validation """ + if self.unique_name: + if not instance.pk and Webapp.objects.filter(name=instance.name, type=instance.type).exists(): + raise ValidationError({ + 'name': _("A WordPress blog with this name already exists."), + }) + + def get_options(self): + pass + + @classmethod + def get_options_choices(cls): + enabled = options.get_enabled().values() + yield (None, '-------') + for option in cls.options: + if hasattr(option, '__iter__'): + yield (option[0], [(op.name, op.verbose_name) for op in option[1] if op in enabled]) + elif option in enabled: + yield (option.name, option.verbose_name) + + + def save(self, instance): + pass + + def delete(self, instance): + pass + + def get_related_objects(self, instance): + pass + + +class Php55App(AppType): + name = 'php5.5-fpm' + verbose_name = "PHP 5.5 FPM" +# 'fpm', ('unix:/var/run/%(user)s-%(app_name)s.sock|fcgi://127.0.0.1%(app_path)s',), + directive = ('fpm', 'fcgi://{}%(app_path)s'.format(settings.WEBAPPS_FPM_LISTEN)) + help_text = _("This creates a PHP5.5 application under ~/webapps/<app_name>
" + "PHP-FPM will be used to execute PHP files.") + options = ( + ('Process', options.process), + ('PHP', [op for op in options.php if getattr(op, 'deprecated', 99) > 5.5]), + ('File system', options.filesystem), + ) + icon = 'orchestra/icons/apps/PHPFPM.png' + + +class Php52App(AppType): + name = 'php5.2-fcgi' + verbose_name = "PHP 5.2 FCGI" + directive = ('fcgi', settings.WEBAPPS_FCGID_PATH) + help_text = _("This creates a PHP5.2 application under ~/webapps/<app_name>
" + "Apache-mod-fcgid will be used to execute PHP files.") + icon = 'orchestra/icons/apps/PHPFCGI.png' + + +class Php4App(AppType): + name = 'php4-fcgi' + verbose_name = "PHP 4 FCGI" + directive = ('fcgi', settings.WEBAPPS_FCGID_PATH) + help_text = _("This creates a PHP4 application under ~/webapps/<app_name>
" + "Apache-mod-fcgid will be used to execute PHP files.") + icon = 'orchestra/icons/apps/PHPFCGI.png' + + +class StaticApp(AppType): + name = 'static' + verbose_name = "Static" + directive = ('static',) + help_text = _("This creates a Static application under ~/webapps/<app_name>
" + "Apache2 will be used to serve static content and execute CGI files.") + icon = 'orchestra/icons/apps/Static.png' + options = ( + ('File system', options.filesystem), + ) + +class WebalizerApp(AppType): + name = 'webalizer' + verbose_name = "Webalizer" + directive = ('static', '%(app_path)s%(site_name)s') + help_text = _("This creates a Webalizer application under " + "~/webapps/<app_name>-<site_name>") + icon = 'orchestra/icons/apps/Stats.png' + options = () + + +class WordPressMuApp(AppType): + name = 'wordpress-mu' + verbose_name = "WordPress (SaaS)" + directive = ('fpm', 'fcgi://127.0.0.1:8990/home/httpd/wordpress-mu/') + help_text = _("This creates a WordPress site on a multi-tenant WordPress server.
" + "By default this blog is accessible via <app_name>.blogs.orchestra.lan") + icon = 'orchestra/icons/apps/WordPressMu.png' + unique_name = True + options = () + + +class DokuWikiMuApp(AppType): + name = 'dokuwiki-mu' + verbose_name = "DokuWiki (SaaS)" + directive = ('alias', '/home/httpd/wikifarm/farm/') + help_text = _("This create a DokuWiki wiki into a shared DokuWiki server.
" + "By default this wiki is accessible via <app_name>.wikis.orchestra.lan") + icon = 'orchestra/icons/apps/DokuWikiMu.png' + unique_name = True + options = () + + +class MoodleMuApp(AppType): + name = 'moodle-mu' + verbose_name = "Moodle (SaaS)" + directive = ('alias', '/home/httpd/wikifarm/farm/') + help_text = _("This create a Moodle site into a shared Moodle server.
" + "By default this wiki is accessible via <app_name>.moodle.orchestra.lan") + icon = 'orchestra/icons/apps/MoodleMu.png' + unique_name = True + options = () + + +class DrupalMuApp(AppType): + name = 'drupal-mu' + verbose_name = "Drupdal (SaaS)" + directive = ('fpm', 'fcgi://127.0.0.1:8991/home/httpd/drupal-mu/') + help_text = _("This creates a Drupal site into a multi-tenant Drupal server.
" + "The installation will be completed after visiting " + "http://<app_name>.drupal.orchestra.lan/install.php?profile=standard
" + "By default this site will be accessible via <app_name>.drupal.orchestra.lan") + icon = 'orchestra/icons/apps/DrupalMu.png' + unique_name = True + options = () + + +from rest_framework import serializers +from orchestra.forms import widgets +class SymbolicLinkForm(PluginDataForm): + path = forms.CharField(label=_("Path"), widget=forms.TextInput(attrs={'size':'100'}), + help_text=_("Path for the origin of the symbolic link.")) + + +class SymbolicLinkSerializer(serializers.Serializer): + path = serializers.CharField(label=_("Path")) + + +class SymbolicLinkApp(AppType): + name = 'symbolic-link' + verbose_name = "Symbolic link" + form = SymbolicLinkForm + serializer = SymbolicLinkSerializer + icon = 'orchestra/icons/apps/SymbolicLink.png' + change_readonly_fileds = ('path',) + + +class WordPressForm(PluginDataForm): + db_name = forms.CharField(label=_("Database name"), + help_text=_("Database used for this webapp.")) + db_user = forms.CharField(label=_("Database user"),) + db_pass = forms.CharField(label=_("Database user password"), + help_text=_("Initial database password.")) + + +class WordPressSerializer(serializers.Serializer): + db_name = serializers.CharField(label=_("Database name"), required=False) + db_user = serializers.CharField(label=_("Database user"), required=False) + db_pass = serializers.CharField(label=_("Database user password"), required=False) + + +from orchestra.apps.databases.models import Database, DatabaseUser +from orchestra.utils.python import random_ascii + + +class WordPressApp(AppType): + name = 'wordpress' + verbose_name = "WordPress" + icon = 'orchestra/icons/apps/WordPress.png' + change_form = WordPressForm + serializer = WordPressSerializer + change_readonly_fileds = ('db_name', 'db_user', 'db_pass',) + help_text = _("Visit http://<domain.lan>/wp-admin/install.php to finish the installation.") + + def get_db_name(self, webapp): + db_name = 'wp_%s_%s' % (webapp.name, webapp.account) + # Limit for mysql database names + return db_name[:65] + + def get_db_user(self, webapp): + db_name = self.get_db_name(webapp) + # Limit for mysql user names + return db_name[:17] + + def get_db_pass(self): + return random_ascii(10) + + def validate(self, webapp): + create = not webapp.pk + if create: + db = Database(name=self.get_db_name(webapp), account=webapp.account) + user = DatabaseUser(username=self.get_db_user(webapp), password=self.get_db_pass(), + account=webapp.account) + for obj in (db, user): + try: + obj.full_clean() + except ValidationError, e: + raise ValidationError({ + 'name': e.messages, + }) + + def save(self, webapp): + create = not webapp.pk + if create: + db_name = self.get_db_name(webapp) + db_user = self.get_db_user(webapp) + db_pass = self.get_db_pass() + db = Database.objects.create(name=db_name, account=webapp.account) + user = DatabaseUser(username=db_user, account=webapp.account) + user.set_password(db_pass) + user.save() + db.users.add(user) + webapp.data = { + 'db_name': db_name, + 'db_user': db_user, + 'db_pass': db_pass, + } + else: + # Trigger related backends + for related in self.get_related(webapp): + related.save() + + def delete(self, webapp): + for related in self.get_related(webapp): + related.delete() + + def get_related(self, webapp): + related = [] + try: + db_user = DatabaseUser.objects.get(username=webapp.data.get('db_user')) + except DatabaseUser.DoesNotExist: + pass + else: + related.append(db_user) + try: + db = Database.objects.get(name=webapp.data.get('db_name')) + except Database.DoesNotExist: + pass + else: + related.append(db) + return related diff --git a/orchestra/apps/websites/admin.py b/orchestra/apps/websites/admin.py index 45b4fac1..71c4242c 100644 --- a/orchestra/apps/websites/admin.py +++ b/orchestra/apps/websites/admin.py @@ -10,7 +10,7 @@ from orchestra.admin.utils import admin_link, change_url from orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin from orchestra.forms.widgets import DynamicHelpTextSelect -from . import settings +from . import settings, options from .forms import WebsiteAdminForm from .models import Content, Website, WebsiteOption @@ -20,8 +20,7 @@ class WebsiteOptionInline(admin.TabularInline): extra = 1 OPTIONS_HELP_TEXT = { - k: str(unicode(v[1])) if len(v) == 3 else '' - for k, v in settings.WEBSITES_OPTIONS.iteritems() + op.name: str(unicode(op.help_text)) for op in options.get_enabled().values() } # class Media: diff --git a/orchestra/apps/websites/models.py b/orchestra/apps/websites/models.py index ea6ace91..9bc2a59f 100644 --- a/orchestra/apps/websites/models.py +++ b/orchestra/apps/websites/models.py @@ -5,10 +5,9 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from orchestra.core import validators, services -from orchestra.utils import tuple_setting_to_choices from orchestra.utils.functional import cached -from . import settings +from . import settings, options class Website(models.Model): @@ -78,15 +77,16 @@ class Website(models.Model): path = settings.WEBSITES_WEBSITE_WWW_ERROR_LOG_PATH % context return path.replace('//', '/') + class WebsiteOption(models.Model): website = models.ForeignKey(Website, verbose_name=_("web site"), related_name='options') name = models.CharField(_("name"), max_length=128, - choices=tuple_setting_to_choices(settings.WEBSITES_OPTIONS)) + choices=((op.name, op.verbose_name) for op in options.get_enabled().values())) value = models.CharField(_("value"), max_length=256) class Meta: - unique_together = ('website', 'name') +# unique_together = ('website', 'name') verbose_name = _("option") verbose_name_plural = _("options") @@ -94,16 +94,8 @@ class WebsiteOption(models.Model): return self.name def clean(self): - """ validates name and value according to WEBSITES_WEBSITEOPTIONS """ - regex = settings.WEBSITES_OPTIONS[self.name][-1] - if not re.match(regex, self.value): - raise ValidationError({ - 'value': ValidationError(_("'%(value)s' does not match %(regex)s."), - params={ - 'value': self.value, - 'regex': regex - }), - }) + option = options.get_enabled()[self.name] + option.validate(self) class Content(models.Model): diff --git a/orchestra/apps/websites/options.py b/orchestra/apps/websites/options.py new file mode 100644 index 00000000..c768f054 --- /dev/null +++ b/orchestra/apps/websites/options.py @@ -0,0 +1,127 @@ +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ + +from orchestra.utils.python import import_class + +from . import settings + + +# TODO multiple and unique validation support in the formset +class SiteOption(object): + unique = True + + def __init__(self, name, *args, **kwargs): + self.name = name + self.verbose_name = kwargs.pop('verbose_name', name) + self.help_text = kwargs.pop('help_text', '') + for k,v in kwargs.iteritems(): + setattr(self, k, v) + + def validate(self, website): + if self.regex and not re.match(self.regex, website.value): + raise ValidationError({ + 'value': ValidationError(_("'%(value)s' does not match %(regex)s."), + params={ + 'value': website.value, + 'regex': self.regex + }), + }) + + +directory_protection = SiteOption('directory_protection', + verbose_name=_("Directory protection"), + help_text=_("Space separated ..."), + regex=r'^([\w/_]+)\s+(\".*\")\s+([\w/_\.]+)$', +) + +redirect = SiteOption('redirect', + verbose_name=_("Redirection"), + help_text=_("<website path> <destination URL>"), + regex=r'^[^ ]+\s[^ ]+$', +) + +proxy = SiteOption('proxy', + verbose_name=_("Proxy"), + help_text=_("<website path> <target URL>"), + regex=r'^[^ ]+\shttp[^ ]+(timeout=[0-9]{1,3}|retry=[0-9]|\s)*$', +) + +ssl_ca = SiteOption('ssl_ca', + verbose_name=_("SSL CA"), + help_text=_("Filesystem path of the CA certificate file."), + regex=r'^[^ ]+$' +) + +ssl_cert = SiteOption('ssl_cert', + verbose_name=_("SSL cert"), + help_text=_("Filesystem path of the certificate file."), + regex=r'^[^ ]+$', +) + +ssl_key = SiteOption('ssl_key', + verbose_name=_("SSL key"), + help_text=_("Filesystem path of the key file."), + regex=r'^[^ ]+$', +) + +sec_rule_remove = SiteOption('sec_rule_remove', + verbose_name=_("SecRuleRemoveById"), + help_text=_("Space separated ModSecurity rule IDs."), + regex=r'^[0-9\s]+$', +) + +sec_engine = SiteOption('sec_engine', + verbose_name=_("Modsecurity engine"), + help_text=_("On or Off, defaults to On"), + regex=r'^(On|Off)$', +) + +user_group = SiteOption('user_group', + verbose_name=_("SuexecUserGroup"), + help_text=_("user [group], username and optional groupname."), + # TODO validate existing user/group + regex=r'^[\w/_]+(\s[\w/_]+)*$', +) + +error_document = SiteOption('error_document', + verbose_name=_("ErrorDocumentRoot"), + help_text=_("<error code> <URL/path/message>
" + " 500 http://foo.example.com/cgi-bin/tester
" + " 404 /cgi-bin/bad_urls.pl
" + " 401 /subscription_info.html
" + " 403 \"Sorry can't allow you access today\""), + regex=r'[45]0[0-9]\s.*', +) + + +ssl = [ + ssl_ca, + ssl_cert, + ssl_key, +] + +sec = [ + sec_rule_remove, + sec_engine, +] + +httpd = [ + directory_protection, + redirect, + proxy, + user_group, + error_document, +] + + +_enabled = None + +def get_enabled(): + global _enabled + if _enabled is None: + from . import settings + _enabled = {} + for op in settings.WEBSITES_ENABLED_OPTIONS: + op = import_class(op) + _enabled[op.name] = op + return _enabled diff --git a/orchestra/apps/websites/settings.py b/orchestra/apps/websites/settings.py index 7c85c017..1f167224 100644 --- a/orchestra/apps/websites/settings.py +++ b/orchestra/apps/websites/settings.py @@ -6,12 +6,20 @@ WEBSITES_UNIQUE_NAME_FORMAT = getattr(settings, 'WEBSITES_UNIQUE_NAME_FORMAT', '%(account)s-%(name)s') +# TODO 'http', 'https', 'https-only', 'http and https' and rename to PROTOCOL WEBSITES_PORT_CHOICES = getattr(settings, 'WEBSITES_PORT_CHOICES', ( (80, 'HTTP'), (443, 'HTTPS'), )) +WEBSITES_PROTOCOL_CHOICES = getattr(settings, 'WEBSITES_PROTOCOL_CHOICES', ( + ('http', 'HTTP'), + ('https', 'HTTPS'), + ('http-https', 'HTTP and HTTPS), + ('https-only', 'HTTPS only'), +)) + WEBSITES_DEFAULT_PORT = getattr(settings, 'WEBSITES_DEFAULT_PORT', 80) @@ -21,65 +29,18 @@ WEBSITES_DEFAULT_IP = getattr(settings, 'WEBSITES_DEFAULT_IP', '*') WEBSITES_DOMAIN_MODEL = getattr(settings, 'WEBSITES_DOMAIN_MODEL', 'domains.Domain') -WEBSITES_OPTIONS = getattr(settings, 'WEBSITES_OPTIONS', { - # { name: ( verbose_name, [help_text], validation_regex ) } - 'directory_protection': ( - _("HTTPD - Directory protection"), - _("Space separated ..."), - r'^([\w/_]+)\s+(\".*\")\s+([\w/_\.]+)$', - ), - 'redirect': ( - _("HTTPD - Redirection"), - _("<website path> <destination URL>"), - r'^[^ ]+\s[^ ]+$', - ), - 'proxy': ( - _("HTTPD - Proxy"), - _("<website path> <target URL>"), - r'^[^ ]+\shttp[^ ]+(timeout=[0-9]{1,3}|retry=[0-9]|\s)*$', - ), - 'ssl_ca': ( - "HTTPD - SSL CA", - _("Filesystem path of the CA certificate file."), - r'^[^ ]+$' - ), - 'ssl_cert': ( - _("HTTPD - SSL cert"), - _("Filesystem path of the certificate file."), - r'^[^ ]+$' - ), - 'ssl_key': ( - _("HTTPD - SSL key"), - _("Filesystem path of the key file."), - r'^[^ ]+$', - ), - 'sec_rule_remove': ( - "HTTPD - SecRuleRemoveById", - _("Space separated ModSecurity rule IDs."), - r'^[0-9\s]+$', - ), - 'sec_engine': ( - "HTTPD - Modsecurity engine", - _("On or Off, defaults to On"), - r'^(On|Off)$', - ), - 'user_group': ( - "HTTPD - SuexecUserGroup", - _("user [group], username and optional groupname."), - # TODO validate existing user/group - r'^[\w/_]+(\s[\w/_]+)*$', - ), - # TODO backend support - 'error_document': ( - "HTTPD - ErrorDocumentRoot", - _("<error code> <URL/path/message>
" - " 500 http://foo.example.com/cgi-bin/tester
" - " 404 /cgi-bin/bad_urls.pl
" - " 401 /subscription_info.html
" - " 403 \"Sorry can't allow you access today\""), - r'[45]0[0-9]\s.*', - ) -}) +WEBSITES_ENABLED_OPTIONS = getattr(settings, 'WEBSITES_ENABLED_OPTIONS', ( + 'orchestra.apps.websites.options.directory_protection', + 'orchestra.apps.websites.options.redirect', + 'orchestra.apps.websites.options.proxy', + 'orchestra.apps.websites.options.ssl_ca', + 'orchestra.apps.websites.options.ssl_cert', + 'orchestra.apps.websites.options.ssl_key', + 'orchestra.apps.websites.options.sec_rule_remove', + 'orchestra.apps.websites.options.sec_engine', + 'orchestra.apps.websites.options.user_group', + 'orchestra.apps.websites.options.error_document', +)) WEBSITES_BASE_APACHE_CONF = getattr(settings, 'WEBSITES_BASE_APACHE_CONF', diff --git a/orchestra/forms/widgets.py b/orchestra/forms/widgets.py index b4673b2a..5ffbe0e1 100644 --- a/orchestra/forms/widgets.py +++ b/orchestra/forms/widgets.py @@ -69,10 +69,11 @@ def paddingCheckboxSelectMultiple(padding): class DynamicHelpTextSelect(forms.Select): def __init__(self, target, help_text, *args, **kwargs): help_text = self.get_dynamic_help_text(target, help_text) - attrs = { + attrs = kwargs.get('attrs', {}) + attrs.update({ 'onClick': help_text, 'onChange': help_text, - } + }) attrs.update(kwargs.get('attrs', {})) kwargs['attrs'] = attrs super(DynamicHelpTextSelect, self).__init__(*args, **kwargs) @@ -81,8 +82,8 @@ class DynamicHelpTextSelect(forms.Select): return textwrap.dedent("""\ siteoptions = {help_text}; valueelement = $("#" + {target}); + help_text = siteoptions[this.options[this.selectedIndex].value] || "" valueelement.parent().find('p').remove(); - valueelement.parent().append( - "

" + siteoptions[this.options[this.selectedIndex].value] + "

" - );""".format(target=target, help_text=str(help_text)) + valueelement.parent().append("

" + help_text + "

");\ + """.format(target=target, help_text=str(help_text)) ) diff --git a/orchestra/plugins/admin.py b/orchestra/plugins/admin.py index a21d4dca..4435fadd 100644 --- a/orchestra/plugins/admin.py +++ b/orchestra/plugins/admin.py @@ -1,7 +1,6 @@ from django.conf.urls import patterns, url from django.contrib.admin.utils import unquote from django.shortcuts import render, redirect -from django.utils.text import camel_case_to_spaces from django.utils.translation import ugettext_lazy as _ from orchestra.admin.utils import wrap_admin_view @@ -11,14 +10,29 @@ from orchestra.utils.functional import cached class SelectPluginAdminMixin(object): plugin = None plugin_field = None + plugin_title = None def get_form(self, request, obj=None, **kwargs): if obj: - self.form = getattr(obj, '%s_class' % self.plugin_field)().get_form() + plugin = getattr(obj, '%s_instance' % self.plugin_field) + self.form = getattr(plugin, 'get_change_form', plugin.get_form)() else: - self.form = self.plugin.get_plugin(self.plugin_value)().get_form() + plugin = self.plugin.get_plugin(self.plugin_value)() + self.form = plugin.get_form() return super(SelectPluginAdminMixin, self).get_form(request, obj=obj, **kwargs) + def get_fields(self, request, obj=None): + """ Try to maintain original field ordering """ + fields = super(SelectPluginAdminMixin, self).get_fields(request, obj=obj) + head_fields = list(self.get_readonly_fields(request, obj)) + head, tail = [], [] + for field in fields: + if field in head_fields: + head.append(field) + else: + tail.append(field) + return head + tail + def get_urls(self): """ Hooks select account url """ urls = super(SelectPluginAdminMixin, self).get_urls() @@ -34,6 +48,7 @@ class SelectPluginAdminMixin(object): def select_plugin_view(self, request): opts = self.model._meta context = { + 'plugin_title': self.plugin_title or 'Plugins', 'opts': opts, 'app_label': opts.app_label, 'field': self.plugin_field, @@ -52,8 +67,9 @@ class SelectPluginAdminMixin(object): self.plugin_value = plugin_value if not plugin_value: self.plugin_value = self.plugin.get_plugins()[0].get_name() + plugin = self.plugin.get_plugin(self.plugin_value) context = { - 'title': _("Add new %s") % camel_case_to_spaces(self.plugin_value), + 'title': _("Add new %s") % plugin.verbose_name, } context.update(extra_context or {}) return super(SelectPluginAdminMixin, self).add_view(request, form_url=form_url, @@ -62,9 +78,9 @@ class SelectPluginAdminMixin(object): def change_view(self, request, object_id, form_url='', extra_context=None): obj = self.get_object(request, unquote(object_id)) - plugin_value = getattr(obj, self.plugin_field) + plugin = getattr(obj, '%s_class' % self.plugin_field) context = { - 'title': _("Change %s") % camel_case_to_spaces(str(plugin_value)), + 'title': _("Change %s") % plugin.verbose_name, } context.update(extra_context or {}) return super(SelectPluginAdminMixin, self).change_view(request, object_id, diff --git a/orchestra/plugins/forms.py b/orchestra/plugins/forms.py index 6467ca75..4bd1937b 100644 --- a/orchestra/plugins/forms.py +++ b/orchestra/plugins/forms.py @@ -1,23 +1,32 @@ from django import forms +from orchestra.forms.widgets import ReadOnlyWidget + class PluginDataForm(forms.ModelForm): data = forms.CharField(widget=forms.HiddenInput, required=False) def __init__(self, *args, **kwargs): super(PluginDataForm, self).__init__(*args, **kwargs) - # TODO remove it well - try: - self.fields[self.plugin_field].widget = forms.HiddenInput() - except KeyError: - pass + if self.plugin_field in self.fields: + value = self.plugin.get_name() + display = '%s change' % unicode(self.plugin.verbose_name) + self.fields[self.plugin_field].widget = ReadOnlyWidget(value, display) + self.fields[self.plugin_field].help_text = getattr(self.plugin, 'help_text', '') instance = kwargs.get('instance') if instance: for field in self.declared_fields: initial = self.fields[field].initial self.fields[field].initial = instance.data.get(field, initial) + if self.instance.pk: + for field in self.plugin.get_change_readonly_fileds(): + value = getattr(self.instance, field, None) or self.instance.data[field] + self.fields[field].required = False + self.fields[field].widget = ReadOnlyWidget(value) +# self.fields[field].help_text = None def clean(self): + # TODO clean all filed within data??? data = {} for field in self.declared_fields: try: diff --git a/orchestra/plugins/options.py b/orchestra/plugins/options.py index f6e87a69..3f2387b1 100644 --- a/orchestra/plugins/options.py +++ b/orchestra/plugins/options.py @@ -6,10 +6,11 @@ class Plugin(object): # Used on select plugin view class_verbose_name = None icon = None + change_readonly_fileds = () @classmethod def get_name(cls): - return cls.__name__ + return getattr(cls, 'name', cls.__name__) @classmethod def get_plugins(cls): @@ -39,6 +40,10 @@ class Plugin(object): verbose = plugin.get_verbose_name() choices.append((plugin.get_name(), verbose)) return sorted(choices, key=lambda e: e[1]) + + @classmethod + def get_change_readonly_fileds(cls): + return cls.change_readonly_fileds class PluginModelAdapter(Plugin): diff --git a/orchestra/apps/saas/static/saas/icons/BSCW.png b/orchestra/static/orchestra/icons/apps/BSCW.png similarity index 100% rename from orchestra/apps/saas/static/saas/icons/BSCW.png rename to orchestra/static/orchestra/icons/apps/BSCW.png diff --git a/orchestra/static/orchestra/icons/apps/DokuWikiMu.png b/orchestra/static/orchestra/icons/apps/DokuWikiMu.png new file mode 100644 index 0000000000000000000000000000000000000000..38d91789b3410db7a8b6e27d4be8f712e588e7ad GIT binary patch literal 3844 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 + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/apps/saas/static/saas/icons/Dokuwiki.png b/orchestra/static/orchestra/icons/apps/Dokuwiki.png similarity index 100% rename from orchestra/apps/saas/static/saas/icons/Dokuwiki.png rename to orchestra/static/orchestra/icons/apps/Dokuwiki.png diff --git a/orchestra/apps/saas/static/saas/icons/Dokuwiki.svg b/orchestra/static/orchestra/icons/apps/Dokuwiki.svg similarity index 100% rename from orchestra/apps/saas/static/saas/icons/Dokuwiki.svg rename to orchestra/static/orchestra/icons/apps/Dokuwiki.svg diff --git a/orchestra/apps/saas/static/saas/icons/Drupal.png b/orchestra/static/orchestra/icons/apps/Drupal.png similarity index 100% rename from orchestra/apps/saas/static/saas/icons/Drupal.png rename to orchestra/static/orchestra/icons/apps/Drupal.png diff --git a/orchestra/apps/saas/static/saas/icons/Drupal.svg b/orchestra/static/orchestra/icons/apps/Drupal.svg similarity index 100% rename from orchestra/apps/saas/static/saas/icons/Drupal.svg rename to orchestra/static/orchestra/icons/apps/Drupal.svg diff --git a/orchestra/static/orchestra/icons/apps/DrupalMu.png b/orchestra/static/orchestra/icons/apps/DrupalMu.png new file mode 100644 index 0000000000000000000000000000000000000000..b8ca4053def735487754dca429c017f41a30a771 GIT binary patch literal 2313 zcmV+k3HJ7hP)fO zCL!cOcK06txVvFrcOQY74l{md&fLA{`Td^X?>z4cDJAzZl+T;Mo#OujK5yLf0*pf~ zXtCO+0gnSu06BngCrO|J_z*ZySdf0{PDzG>h;|mTSZ(uwcY!Gp7P$x%78ax*i;h}k zh**HdYBK{{fai(c^;BRRu(7ZpeIUxRArX-Pi`AA4l%ToFouCX@QCN_E`3~zDMA)9R zSZzk&Peei9rK)V*dlZ+d`X|i*{_M8qT|p$S@H((Cno3VzyvmwK^Vzh!1XT^N@J!&% zXzB}vkS=7g+QtI4KwLDHp0B>j+^OSPUwn~Kjy7IfyTCt-M4P?-4lDz{31k8$v>9wa zu&1yf-F2q|EWmS-5^wJ4rmel5m%jQCYj&1%rlAX@B(cAp<#`jBot5F8y$NUort4H! z11u>lNWT)Q&2TRC*~Ts{Q3!6vrSQRp&)E3K7=ysj7PH_ICdElcg-kOyzoygYxDZ1Q^yBOP4u? zKb&i2^WNih-0TZiHNbeZF{le&7qVDwj{-+R6+U#fitS&Y%Y@{305046*>SF!H;-Os zz!@+D`M}WI&UfZzl07~xWImt|7+YA7-We=0lndpCs-zG9fu0DZzTJi?>eH z^Xrl;VaWry;*)vzR0Ag~uZ7DR2Ry8o7-}!1E8s$H1G~OEpIDjiHu30_(C)Hj~d4Pqeq3xI#Mqolm$|QMIJ4yU#cGQMenAkk-EufF#F#l)*-m|DKZmQ^D5;_Pa>l8(ApaqpAci<)P{Vr(-OV%8d)+qnI)5x zIZ|Wi`}-=WYwv?wH?^KuUF2k>@cQZkme10L-kLdCq^*COl%x@n&;f05uQ3wg)k;c& ziRvH%wmzQeL*5@>${a=6^K;pBpp4%gsv&)B5}Q}fVO4&XkE@MJj3qyFOnCS6MQ+6R zLi46%P}*1nX5XoFMb3R;%O=Da*tR;CHXeSSBS#Jx$MuSircHB!(NDcn>k7 zB1lb)30qc5C_Yuq`zI?&PK;ygx_P7}`I7DIy+uNNe8kM%m}o@1c$yYT?!{Bn*jIT2 z4B9m8R3(+IJ>-nhccD|Y&HQM`F;3UD491mSYU0StD;b|Ud@yeJi3(d9#XBFCP}9<(SweL&E-8)``I(F!5l?GZ zFUKnzDX(usm6Gg?WPb423|=X=($zP>&z_jY{xh{yHQ6y56rP-uMb)(yzO{UDwB!L^ z6-A|uF~B(>V4tCf=C)3X{_+Xkx12+1)CZ#zV|ZcRSBN!-A4@?nO>y52I`0MiQ#(i?@85+Q_=nL9V;*KdDA z>+0o}1cbKHH5nA7>cVUgq$Uk#(wG#M&Cf*^JBeaF8-QkuPMVG48(Pp%Na1RMjgj*J{_k@${l{ zzzGZrFnWu~U%G18*Pi?NGjR#S9x=w4XA2>sUSaMgRF|{5=SJu6j}^c7!9RBvbpkF= z2(W-xFan0O@tNk8KYndi@|cVqDWyrOs-mi@B0WKB?;zdpQjt=6{KNBTC3F2CP;f>gjM z-P32l-KY@4yO)(9&tSnr*z+450sfWt;5q;!yi}8^3b{U})yvcjuFGBTt`ipR(d3IQGQGX{MEGUFGQ9Tj<@Fwp) jCWwS=P#*XC-);T}@c5C&Q)1!c00000NkvXXu0mjf5d>DU literal 0 HcmV?d00001 diff --git a/orchestra/static/orchestra/icons/apps/DrupalMu.svg b/orchestra/static/orchestra/icons/apps/DrupalMu.svg new file mode 100644 index 00000000..11b394f2 --- /dev/null +++ b/orchestra/static/orchestra/icons/apps/DrupalMu.svg @@ -0,0 +1,160 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/apps/saas/static/saas/icons/Moodle.png b/orchestra/static/orchestra/icons/apps/Moodle.png similarity index 100% rename from orchestra/apps/saas/static/saas/icons/Moodle.png rename to orchestra/static/orchestra/icons/apps/Moodle.png diff --git a/orchestra/apps/saas/static/saas/icons/Moodle.svg b/orchestra/static/orchestra/icons/apps/Moodle.svg similarity index 100% rename from orchestra/apps/saas/static/saas/icons/Moodle.svg rename to orchestra/static/orchestra/icons/apps/Moodle.svg diff --git a/orchestra/static/orchestra/icons/apps/MoodleMu.png b/orchestra/static/orchestra/icons/apps/MoodleMu.png new file mode 100644 index 0000000000000000000000000000000000000000..3881b0ee6ca2981380e45383b09bfadad7d785fa GIT binary patch literal 3265 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 literal 0 HcmV?d00001 diff --git a/orchestra/static/orchestra/icons/apps/MoodleMu.svg b/orchestra/static/orchestra/icons/apps/MoodleMu.svg new file mode 100644 index 00000000..3493ba5b --- /dev/null +++ b/orchestra/static/orchestra/icons/apps/MoodleMu.svg @@ -0,0 +1,538 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/static/orchestra/icons/apps/PHP.svg b/orchestra/static/orchestra/icons/apps/PHP.svg new file mode 100644 index 00000000..ffa40a38 --- /dev/null +++ b/orchestra/static/orchestra/icons/apps/PHP.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/orchestra/static/orchestra/icons/apps/PHPFCGI.png b/orchestra/static/orchestra/icons/apps/PHPFCGI.png new file mode 100644 index 0000000000000000000000000000000000000000..02ea63ca5cd22dec69176ccf22ef731492c8eef2 GIT binary patch literal 4353 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 literal 0 HcmV?d00001 diff --git a/orchestra/static/orchestra/icons/apps/PHPFCGI.svg b/orchestra/static/orchestra/icons/apps/PHPFCGI.svg new file mode 100644 index 00000000..677aac42 --- /dev/null +++ b/orchestra/static/orchestra/icons/apps/PHPFCGI.svg @@ -0,0 +1,182 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FCGI + diff --git a/orchestra/static/orchestra/icons/apps/PHPFPM.png b/orchestra/static/orchestra/icons/apps/PHPFPM.png new file mode 100644 index 0000000000000000000000000000000000000000..71d3fca793b628e056f2340554404bd944ff6cf6 GIT binary patch literal 4282 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 literal 0 HcmV?d00001 diff --git a/orchestra/static/orchestra/icons/apps/PHPFPM.svg b/orchestra/static/orchestra/icons/apps/PHPFPM.svg new file mode 100644 index 00000000..82cd01ea --- /dev/null +++ b/orchestra/static/orchestra/icons/apps/PHPFPM.svg @@ -0,0 +1,182 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FPM + diff --git a/orchestra/apps/saas/static/saas/icons/Phplist.png b/orchestra/static/orchestra/icons/apps/Phplist.png similarity index 100% rename from orchestra/apps/saas/static/saas/icons/Phplist.png rename to orchestra/static/orchestra/icons/apps/Phplist.png diff --git a/orchestra/apps/saas/static/saas/icons/Phplist.svg b/orchestra/static/orchestra/icons/apps/Phplist.svg similarity index 100% rename from orchestra/apps/saas/static/saas/icons/Phplist.svg rename to orchestra/static/orchestra/icons/apps/Phplist.svg diff --git a/orchestra/static/orchestra/icons/apps/Python.svg b/orchestra/static/orchestra/icons/apps/Python.svg new file mode 100644 index 00000000..6f9736f4 --- /dev/null +++ b/orchestra/static/orchestra/icons/apps/Python.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/orchestra/static/orchestra/icons/apps/Static.png b/orchestra/static/orchestra/icons/apps/Static.png new file mode 100644 index 0000000000000000000000000000000000000000..9328f1a6daedba49b23f175b013efc88f88e88b0 GIT binary patch literal 3252 zcmV;l3`_HgP)z>%6m&&cbU}4= zXm4@=N?~htVjxp=Wod3@a_0N@F8}}lA#_DpbW?A2a${uxXmoUNIxjD3X>Dy`V=irV zb7^B}VQg$JV|r<3<6Zy&3&lx9K~!jg)tOstT*rOJe{;^+tClO?DUrHRlqiW5*B9BL zBicz&+l4Pm4I6d@6i5N12m&WI0;Fh>7XyCiOJD>rY6$8}i^Oe;7P6H3QbUef$*H2q zk!)SLq%IUG>Ozs)C8_1^p39!w^nu*j3;qY!nQJNG*p;)ApNGb6=58wAo*PiEUbU01DM zRRP7(DB3TCD1}Y&emNq_@5@KHl9);kEG4?CfW;aq-rtLed+BE}EEYoZ8d>V^V%K*= zsEFmRPfLbSdB>IhUc7j5(GKp0G(cBZSLwa~7YDX(-FgSndkvw5D89Ft8~5$Ng$oy! zO?Ej#y1ToVtW%DAa-f_@x#g9J-b*eN2k*W@sG0){1ub-7xg@J$+qP{LCx~iT(t&b9 zmE2h9!2d-q+*1ye3s@`+&YwTOta{5uPfyPsA#_gyikqv&Mpg(xcXxNyXG@7#YL&k4 z5GpocnkM0JxFV@?2Nomjx*o&BX$A*xl1gRC zuzUCGtX|!SVOlgb)e+EUyN<@L;>R1PUuV(V_W))t&w&HSdHwZ0y!P7j?A*EI{skzC zQX;=_lq*epLI^(n^hGBRbN3ZeHOMP^8Z9qy%=f-xncdaLtp5(~qU*h@_2}nSlJ0uW_g0F-K z2E#m_Xy?htA7ORN?4+S-8lLA74u|;hkG~DDySMlCRm&3Kd0s{03k4;UNs`GVAAE3_ zfh1BLB4S(|Lyp*=&{U>GAldivK@xp`3PQ{yEG4*}#PxhUUlLfkigoMO@CFsIP4R;E@Nqcx0~qbb6A>scBRNk{3tl?%Y6ES2L;9=u^PI zEa^ZISFc{hG|h@4lF1||PxiBQ>so@r0Q--PBAOF8bzRtwhgER7t=m|RkK_8-j)z%r zv1}LH_E3#9^|kZ&J}?`?s;cm-&(6*r=fJTuBM1>d2-X(sElWU4OAB>%^Us!wA`*!N z@pwIvND$BS*s^*BVdEOMCou~ShGmmCZ6#(5!B^+2HBHfqgXcI7j^prfcL%>Zbc$d9_6&KomGz1PPkuxmvMuC}pfvK#;t&GX| zw85d1m+xq|P$-m&=XpH!=tk;mLWC57X%+y+i*=XWN-XtRm`olOu1#i%)E(W9@1z%PNVm zEQ>EMXXsqtfUa8rW5xdOY9+3OL?Xe{Pxp~dXUI;^5Y@o299qN1yn0{XzKukKg{$92 zHw)y=0=hYy6QfoGlgb)0$||mnPh* zJ>p@75Rz0XgAn4QB?T<@QNM5{e(=EudGg8aoIN{CTTGw|$#2fyz%Y!`Y*LPq8!7Vh z1bkJlOnVrHF;9f=xol~OFg`Ykw3Y1-i>~o=NNF)*^M}G2?I^KHgpZRWE6K#5rZe%4BlNr{u)S)N}r}{6j|BQvYq7h#f@H~m@c{uK@ z2czhAJA{hxJP*sVc<#9b zx^8gdSdu52!IK)fT#jru%h^jeD5!DrW`Vq6lQ#<(rj2cx_?C`u>3EjT;N>xF+a{;` zXpY9fz$jBwmw%c{4VXYgl#O0R0YV7jx^BgffQ1WOxl2+?G)>_<-`UQ{$aOyc_an5# z0@T#h@ZX=G;fwJwhG}A%CbntdnmWFf!?*JIg*>v5M>qy~E6nBL0v8AK96vsYWnJ-y zh7KN^<2#R6H3yV(V#|T1X#@fRg25nb*EaI%tGzTg2YCDKkNEJzBSeCY^saAWbwncU zS>n>l<6E=DYc!Y6hA`jgXeLI4MV67Om;+KuZ`ZC}Iny+2 zR<2w*?;n1us#X;d2n5hH4OLZ9Rh8$T53p&|7>5oG6Aa#@y?qrAv^LPYs*Zx=VHO+| z0YO#4w_Vby+gu*F#rXInJw1)=*s+0lyoO*fXah4qZmyIds*Ui=FTeaf;9X7Asxhv5 zSYz8ZwrvZ?ad2H1$MJ;g)(MCUA>tw$jSvn82nGYVu7_pWm}UXjHKddosVLJ@(;OKH z1d1PT?;JjS_@}^(TtK+00DxV)b_E^BDee4B(^N85P% zm_mqxrfIsSX@=){QWcTu}g(uX4}_Jiq}8 mfCcDMN{1!zzb4=hh5rE|A;2(UEo3tQ0000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + HTML Document + + + Jakub Steiner + + + http://jimmac.musichall.cz + + + HTML + hypertext + web + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/static/orchestra/icons/apps/Stats.png b/orchestra/static/orchestra/icons/apps/Stats.png new file mode 100644 index 0000000000000000000000000000000000000000..525a2bb9eee00d875a64afcc152d7161c0d0cf88 GIT binary patch literal 2436 zcmV-~348X5P)1fV;=lGoe|9t=RoikoVO3C-x0{?FSw`<>f z0wKSKw5IrXP-R=ERTScG8&0Q7#kZyQ51lNr=PEt_f||wNg|Oz z2od$`<4eFj@$uuwNhA`UKx|(=pJ&UKEe-93LV-jg(a_)Rw?P*Q1)h24nOeVQnX*{~ z>LyGklcZ89PvCZ4RaKJ7WP@MRG?K|=!yI;hw`rQza>SbLff@k2c!76=^^??cO;MDl zD{tG95ojivW_zL;ux9VAw%N911nhyb*(`=(_ztWn3i*7Va=F~lRwxua{eJ!P`F!0R zx3vfCfl{edoxZ(RT~$>cd+f1>_K}eh`uqDE<`@|np|7vc(|_9%az~)q(`iek*p*0V zMGy!yM6Q)Dvhl8m*JiszhL^6W1?=x>InoHn-ijgrt)I=>Axolbj3Y}o{ylXZ1<)t$s%8(LLW_Uzfy(8o_gnx_5tCDh6V zu32VTiGE{8Uq6Z4Ioz~3kf0WfVk^UYvr!w_6ftFn9cI_#02X)JK50H zMKB!p9CuZ+P$rYXFbvHFr)8PadO!T9hnKi<2S zZF~39{_w*D04W5PY1W=4UFYV^3|F#QUVHFCk_QfO@VVz!=PKb`)mG}p!UBJI>M7#m zxDr_w=6tSVqj2rMCx)q2RXNW@}n>gnOZt}ZTQv;6t-$JrQ(FsLYPuSz+$ zECNnYwuGGKzR2ZrIfjOY5Tz3TJbIMJdwbb?-+dqiNLgEHONfTmmo74xCc8U2xO@G2 zPF%gpIrwl;Q66!VShj?`fZKIdRoT(o%O9V7lAqpn7k8&pP%a}as|L&oQZ^&wX!}Wb zH;R-GoxO3RfrRh>M(r%*XMPfT|L9Q;q*C17-3^9O`4?fij4%vDxr|jVfA=(u`gdn> zw&^+tlS%e$-1v}yS5_2(OeRxzo%Xs?Tpu20;r;hH^1=(qnHj7qh^1EAmXB|nWkI>j z?v4(ICMTbIT2X%Wx|BCsB4C4af020+m>U}6&b~g75~1r@y51-k7nznNB_*BlI9eza znKsP>z@L^R;FZ3bU7bsX0(<)UD!>*Nd^c!qKbR)pc6O6dHX$twVVYQ`i4fJNDZcYr zg6M{|Y%Ua;%3K>z6eT33G?$fyyd>l#aH~+DXjxdhc43vvo~v0$#In!_xAESmhcJYK zX_^>@;rTR8lRG=tvSxS;$+b2>1Lj)Z1Ga?R(s2?PkH;B1f1U>pA4Zra(lETWT_sVx zYXeig20yy|H~xO}P{VHo&ff~uZ~)a;6F-?=}XU;G&JIlFu-r?RyA4S`?4LLT3FpRp*JGDK+&9&3$#yq;1 zN9-SC?Yn6v#O50KHCl)}HpW>udXbN3Lky(PV*)RZ2=YI))dovmMJmE#1|R9)PGKVO$QyJ5L7#Lk_ZefedbD_3TKB2bi$+oOS}hSr2~&w;n9AuGqH;wKQpwX{AXe?GI{F2j zTmg&9#?+~3dydL#F93^FWK)z + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/static/orchestra/icons/apps/SymbolicLink.png b/orchestra/static/orchestra/icons/apps/SymbolicLink.png new file mode 100644 index 0000000000000000000000000000000000000000..d3b23940d7da03c35abe886307575d39ea76cdf9 GIT binary patch literal 1928 zcmV;32Y2|1P)Dy`V=irV zb7^B}VQg$JV|r<3<6Zy&2AN4jK~!jg)mqPQ8$}fUW_P_#Y{yAj+Wd5qq86zMi5f)+ z6-wdC1;L?09Ba|u%0GaFR3!cZD)rEyUJ>d63Blh01tpI2qlHE!O`J5bo3z-Dz47kO z4hMUk^?GJ^<0PQIXyn=1x9@%5%$ql}J0XlQyvic|-vEv(uRH-obmR8z+e#vl_(D;X zPbj4mmSsi$OJD#XNm5ak&$D2np);Nalk`1p8xJRXNE z%Lfk;a$=yJcudnoKA*?T%uIP_XJ?5q_HiMG03uva5#)eB!Y#71)55wX0Knr{x%#V(G)^^dwX9E4-faB zKYyO&ayb}=Q3vM-`Y*1&Q4HDFF7$cgpjJLrvXq&lE4__8gkcp*COHR*?#=mEz3eE z6oO@09YO+bN1|V^`eDQkl1D~F&=%o3-~<)~_)$}<6DSX~$i9#OA*8|3afXrzq)5kW zzvfARH~HK^TOr{UI!@5;PCOI=9^jziD3#P;?Uc6OfwsFIgu31`pt;>>Gj0)@`YmoE^HcS4e+20|wm zLiSSs=UIC_U9*hMaS=d{D#fVjk@h@b{li)*&2AhZ;`Y#i3uU7=oO^r=Rj$5 z$W#?MZMF4?Squw{^C%PxhZ*p1SD2B;R;SM+ zWg)kbjm-^ob#-C%;#gC7@vht3+VSo?@1d`!uL_GpghxWOkqk^gWMpf53m0o)BLeOw zp(qM^di&}ac|-{KU|Svm9udPZ5NU76=~JhhoOzMRWY&?MPc@*fLvoWKfIT7Hz+2u6 z*-|c-aBk#vAyD3P>zQ?=(zA_VSvBF;)4n7BJMB4NWR7JLQ%cd_-|v~BoNeowbq_c@m&X;M!LICfe@*Qm>WD$o$Lz>X-HC4pMf2$#I=fzlhjsW_nV`8 z6ao_vF?QyACo$gjP+|x}iJ=C4##kK?ukI{%yPq8T^E!{5DCK!&DUmI&cAf+Zp`Z@@ z(w}=(<@IoouyKm-I&fI=OMPB8Tk+>f;5g3DPWlY)*pHap-}rfLB2ul8Z6HuA7GF|I z758CA5JzrcZt#BR0eBf1rb6>69Bu-G)*NG3awE}e~86mUtha+O>fc-lDh8PxfAZ_=;+pU zoosDwg>t!^S}vEBQmGWGR4R(D>xy9*3Z+!G3uBDf#Rf$PVUi?SvMf`(sH$qJs%nPA zVKW+y8qsLf?CI&z2L=WVUDtPS-n?mec0j8O4IxAU5JGi3q5zN`MM8C_?4Dy5s+&++ zt5X1G?biTcFvbKQnvR)3lcDN9TfzYWn}jXQQEUPhW6U~|-&g(LZT + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + Symbolic Link + + + emblem + symbolic + link + pointer + io + file + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/apps/saas/static/saas/icons/WordPress.png b/orchestra/static/orchestra/icons/apps/WordPress.png similarity index 100% rename from orchestra/apps/saas/static/saas/icons/WordPress.png rename to orchestra/static/orchestra/icons/apps/WordPress.png diff --git a/orchestra/apps/saas/static/saas/icons/WordPress.svg b/orchestra/static/orchestra/icons/apps/WordPress.svg similarity index 100% rename from orchestra/apps/saas/static/saas/icons/WordPress.svg rename to orchestra/static/orchestra/icons/apps/WordPress.svg diff --git a/orchestra/static/orchestra/icons/apps/WordPressMu.png b/orchestra/static/orchestra/icons/apps/WordPressMu.png new file mode 100644 index 0000000000000000000000000000000000000000..dc60411292d214c027ffaeb854563d60d9ecdc32 GIT binary patch literal 2874 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 YU(a4z;D8@V!T + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/apps/saas/static/saas/icons/gitlab.png b/orchestra/static/orchestra/icons/apps/gitlab.png similarity index 100% rename from orchestra/apps/saas/static/saas/icons/gitlab.png rename to orchestra/static/orchestra/icons/apps/gitlab.png diff --git a/orchestra/static/orchestra/icons/saas.svg b/orchestra/static/orchestra/icons/saas.svg index 5eb15d98..b10f711b 100644 --- a/orchestra/static/orchestra/icons/saas.svg +++ b/orchestra/static/orchestra/icons/saas.svg @@ -16,7 +16,7 @@ width="48" height="48" sodipodi:docname="saas.svg" - inkscape:export-filename="/home/glic3/orchestra/django-orchestra/orchestra/static/orchestra/icons/saas.png" + inkscape:export-filename="/home/glic3rinu/orchestra/django-orchestra/orchestra/static/orchestra/icons/saas.png" inkscape:export-xdpi="90" inkscape:export-ydpi="90"> {% if plugin.icon %}
-

Software as a Service

+

{{ plugin_title }}

    {% for plugin in plugins %} diff --git a/orchestra/utils/humanize.py b/orchestra/utils/humanize.py index c8b0c5cd..4fa44170 100644 --- a/orchestra/utils/humanize.py +++ b/orchestra/utils/humanize.py @@ -174,7 +174,6 @@ UNITS_CONVERSIONS = { } def unit_to_bytes(unit): - unit = unit.upper() for bytes, units in UNITS_CONVERSIONS.iteritems(): if unit in units: return bytes diff --git a/orchestra/utils/python.py b/orchestra/utils/python.py index bf1a68fa..0bfb997c 100644 --- a/orchestra/utils/python.py +++ b/orchestra/utils/python.py @@ -1,4 +1,6 @@ import collections +import random +import string def import_class(cls): @@ -8,6 +10,10 @@ def import_class(cls): return getattr(module, cls) +def random_ascii(length): + return ''.join([random.choice(string.hexdigits) for i in range(0, length)]).lower() + + class OrderedSet(collections.MutableSet): def __init__(self, iterable=None): self.end = end = [] diff --git a/orchestra/utils/tests.py b/orchestra/utils/tests.py index 6fc9f7c1..89f78d40 100644 --- a/orchestra/utils/tests.py +++ b/orchestra/utils/tests.py @@ -1,7 +1,5 @@ import datetime import os -import string -import random from functools import wraps from django.conf import settings @@ -15,9 +13,8 @@ from xvfbwrapper import Xvfb from orchestra.apps.accounts.models import Account +from .python import random_ascii -def random_ascii(length): - return ''.join([random.choice(string.hexdigits) for i in range(0, length)]).lower() class AppDependencyMixin(object):