From 17974d41fac20bcc10ffea1546954935f8794895 Mon Sep 17 00:00:00 2001 From: Marc Aymerich Date: Thu, 7 May 2015 14:09:37 +0000 Subject: [PATCH] Refactored dashboard icons and menu registration --- TODO.md | 15 + orchestra/admin/dashboard.py | 86 +- orchestra/admin/menu.py | 107 +- .../project_template/project_name/settings.py | 69 -- orchestra/contrib/accounts/apps.py | 4 +- orchestra/contrib/bills/__init__.py | 1 + orchestra/contrib/bills/apps.py | 12 + orchestra/contrib/bills/models.py | 5 +- orchestra/contrib/contacts/__init__.py | 1 + orchestra/contrib/contacts/apps.py | 12 + orchestra/contrib/contacts/models.py | 5 +- orchestra/contrib/databases/__init__.py | 1 + orchestra/contrib/databases/apps.py | 14 + orchestra/contrib/databases/models.py | 6 +- orchestra/contrib/domains/__init__.py | 1 + orchestra/contrib/domains/admin.py | 13 +- orchestra/contrib/domains/apps.py | 12 + orchestra/contrib/domains/models.py | 4 - orchestra/contrib/issues/__init__.py | 2 +- orchestra/contrib/issues/apps.py | 13 + orchestra/contrib/lists/__init__.py | 1 + orchestra/contrib/lists/apps.py | 12 + orchestra/contrib/lists/models.py | 4 - orchestra/contrib/mailboxes/__init__.py | 1 + orchestra/contrib/mailboxes/apps.py | 13 + orchestra/contrib/mailboxes/models.py | 6 - orchestra/contrib/mailer/apps.py | 2 +- orchestra/contrib/mailer/models.py | 2 +- orchestra/contrib/miscellaneous/__init__.py | 1 + orchestra/contrib/miscellaneous/apps.py | 15 + orchestra/contrib/miscellaneous/models.py | 7 - orchestra/contrib/orchestration/__init__.py | 9 +- orchestra/contrib/orchestration/apps.py | 14 + orchestra/contrib/orchestration/backends.py | 2 +- .../management/commands/orchestrate.py | 4 +- orchestra/contrib/orchestration/manager.py | 35 +- .../contrib/orchestration/middlewares.py | 4 +- orchestra/contrib/orders/apps.py | 2 +- orchestra/contrib/payments/__init__.py | 1 + orchestra/contrib/payments/apps.py | 14 + orchestra/contrib/payments/models.py | 5 - orchestra/contrib/plans/__init__.py | 1 + orchestra/contrib/plans/apps.py | 15 + orchestra/contrib/plans/models.py | 7 - orchestra/contrib/resources/apps.py | 5 + orchestra/contrib/resources/backends.py | 4 +- .../migrations/0002_auto_20150502_1429.py | 30 + .../migrations/0003_auto_20150502_1433.py | 14 + .../migrations/0004_auto_20150503_1559.py | 14 + orchestra/contrib/resources/models.py | 5 +- orchestra/contrib/resources/tasks.py | 6 +- orchestra/contrib/saas/apps.py | 4 +- orchestra/contrib/saas/backends/gitlab.py | 2 +- orchestra/contrib/saas/backends/phplist.py | 2 +- orchestra/contrib/services/apps.py | 8 + orchestra/contrib/services/models.py | 4 - orchestra/contrib/settings/admin.py | 2 - orchestra/contrib/settings/apps.py | 8 + orchestra/contrib/systemusers/apps.py | 2 +- orchestra/contrib/tasks/__init__.py | 3 + orchestra/contrib/tasks/apps.py | 14 + orchestra/contrib/tasks/decorators.py | 9 +- .../management/commands/syncperiodictasks.py | 15 +- orchestra/contrib/vps/__init__.py | 1 + orchestra/contrib/vps/apps.py | 12 + orchestra/contrib/vps/models.py | 6 +- orchestra/contrib/webapps/apps.py | 2 +- orchestra/contrib/websites/__init__.py | 1 + orchestra/contrib/websites/apps.py | 17 +- orchestra/contrib/websites/models.py | 5 +- orchestra/core/__init__.py | 33 +- .../static/orchestra/icons/Appointment.png | Bin 0 -> 4203 bytes .../static/orchestra/icons/Appointment.svg | 413 ++++++++ .../orchestra/icons/Edit-check-sheet.png | Bin 0 -> 19927 bytes .../orchestra/icons/Edit-check-sheet.svg | 390 +++++++ .../static/orchestra/icons/Mail-send.png | Bin 0 -> 3237 bytes .../static/orchestra/icons/Mail-send.svg | 999 ++++++++++++++++++ .../icons/Multimedia-volume-control.png | Bin 0 -> 2732 bytes .../icons/Multimedia-volume-control.svg | 242 +++++ 79 files changed, 2515 insertions(+), 317 deletions(-) create mode 100644 orchestra/contrib/bills/apps.py create mode 100644 orchestra/contrib/contacts/apps.py create mode 100644 orchestra/contrib/databases/apps.py create mode 100644 orchestra/contrib/domains/apps.py create mode 100644 orchestra/contrib/issues/apps.py create mode 100644 orchestra/contrib/lists/apps.py create mode 100644 orchestra/contrib/mailboxes/apps.py create mode 100644 orchestra/contrib/miscellaneous/apps.py create mode 100644 orchestra/contrib/orchestration/apps.py create mode 100644 orchestra/contrib/payments/apps.py create mode 100644 orchestra/contrib/plans/apps.py create mode 100644 orchestra/contrib/resources/migrations/0002_auto_20150502_1429.py create mode 100644 orchestra/contrib/resources/migrations/0003_auto_20150502_1433.py create mode 100644 orchestra/contrib/resources/migrations/0004_auto_20150503_1559.py create mode 100644 orchestra/contrib/tasks/apps.py create mode 100644 orchestra/contrib/vps/apps.py create mode 100644 orchestra/static/orchestra/icons/Appointment.png create mode 100644 orchestra/static/orchestra/icons/Appointment.svg create mode 100644 orchestra/static/orchestra/icons/Edit-check-sheet.png create mode 100644 orchestra/static/orchestra/icons/Edit-check-sheet.svg create mode 100644 orchestra/static/orchestra/icons/Mail-send.png create mode 100644 orchestra/static/orchestra/icons/Mail-send.svg create mode 100644 orchestra/static/orchestra/icons/Multimedia-volume-control.png create mode 100644 orchestra/static/orchestra/icons/Multimedia-volume-control.svg diff --git a/TODO.md b/TODO.md index d6bf4848..ecbd5b3b 100644 --- a/TODO.md +++ b/TODO.md @@ -376,3 +376,18 @@ TODO mount the filesystem with "nosuid" option # orchestrate async stdout stderr (inspired on pangea managemengt commands) # orchestra-beat support for uwsgi cron + +# message.log if 1: return changeform + +# generate nginx certs on project dir rather than nginx + +# Register icons + +# send_message doesn't log task + +make django admin taskstate uncollapse fucking traceback, ( if exists ?) + +# receive tass stuck at RECEIVED +# monitor tasks in started and backend already in success? + +# custom message on admin save diff --git a/orchestra/admin/dashboard.py b/orchestra/admin/dashboard.py index 88e30b60..2bbbc51a 100644 --- a/orchestra/admin/dashboard.py +++ b/orchestra/admin/dashboard.py @@ -1,45 +1,61 @@ from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ from fluent_dashboard import dashboard from fluent_dashboard.modules import CmsAppIconList -from orchestra.core import services +from orchestra.core import services, accounts, administration + + +class AppDefaultIconList(CmsAppIconList): + def __init__(self, *args, **kwargs): + self.icons = kwargs.pop('icons') + super(AppDefaultIconList, self).__init__(*args, **kwargs) + + def get_icon_for_model(self, app_name, model_name, default=None): + icon = self.icons.get('.'.join((app_name, model_name))) + return super(AppDefaultIconList, self).get_icon_for_model(app_name, model_name, default=icon) class OrchestraIndexDashboard(dashboard.FluentIndexDashboard): - _registry = {} - - @classmethod - def register_link(cls, module, view_name, title): - registered = cls._registry.get(module, []) - registered.append((view_name, title)) - cls._registry[module] = registered + def process_registered_view(self, module, view_name, options): + app_name, name = view_name.split('_')[:-1] + module.icons['.'.join((app_name, name))] = options.get('icon') + url = reverse('admin:' + view_name) + add_url = '/'.join(url.split('/')[:-2]) + module.children.append({ + 'models': [{ + 'add_url': add_url, + 'app_name': app_name, + 'change_url': url, + 'name': name, + 'title': options.get('verbose_name')}], + 'name': app_name, + 'title': options.get('verbose_name'), + 'url': add_url, + }) def get_application_modules(self): - modules = super(OrchestraIndexDashboard, self).get_application_modules() - models = [] - for model, options in services.get().items(): - if options.get('menu', True): - models.append("%s.%s" % (model.__module__, model._meta.object_name)) - - for module in modules: - registered = self._registry.get(module.title, None) - if registered: - for view_name, title in registered: - # This values are shit, but it is how fluent dashboard will look for the icon - app_name, name = view_name.split('_')[:-1] - url = reverse('admin:' + view_name) - add_url = '/'.join(url.split('/')[:-2]) - module.children.append({ - 'models': [{ - 'add_url': add_url, - 'app_name': app_name, - 'change_url': url, - 'name': name, - 'title': title }], - 'name': app_name, - 'title': title, - 'url': add_url, - }) - service_icon_list = CmsAppIconList('Services', models=models, collapsible=True) - modules.append(service_icon_list) + from fluent_dashboard import appsettings + modules = [] + # Honor settings override, hacky. I Know + if appsettings.FLUENT_DASHBOARD_APP_GROUPS[0][0] != _('CMS'): + modules = super(OrchestraIndexDashboard, self).get_application_modules() + for register in (accounts, administration, services): + title = register.verbose_name + models = [] + icons = {} + views = [] + for model, options in register.get().items(): + if isinstance(model, str): + views.append((model, options)) + elif options.get('dashboard', True): + opts = model._meta + label = "%s.%s" % (model.__module__, opts.object_name) + models.append(label) + label = '.'.join((opts.app_label, opts.model_name)) + icons[label] = options.get('icon') + module = AppDefaultIconList(title, models=models, icons=icons, collapsible=True) + for view_name, options in views: + self.process_registered_view(module, view_name, options) + modules.append(module) return modules diff --git a/orchestra/admin/menu.py b/orchestra/admin/menu.py index 27d3b4df..788336bd 100644 --- a/orchestra/admin/menu.py +++ b/orchestra/admin/menu.py @@ -27,80 +27,37 @@ def api_link(context): return reverse('api-root') -def process_registered_models(register): - childrens = [] - for model, options in register.get().items(): - if options.get('menu', True): +from copy import copy +def process_registry(register): + def get_item(model, options): + if isinstance(model, str): + url = reverse('admin:'+model) + else: opts = model._meta url = reverse('admin:{}_{}_changelist'.format( - opts.app_label, opts.model_name)) - name = capfirst(options.get('verbose_name_plural')) - childrens.append(items.MenuItem(name, url)) - return childrens - - -def get_services(): - childrens = process_registered_models(services) - return sorted(childrens, key=lambda i: i.title) - - -def get_accounts(): - childrens=[] - if isinstalled('orchestra.contrib.payments'): - url = reverse('admin:payments_transactionprocess_changelist') - childrens.append(items.MenuItem(_("Transaction processes"), url)) - if isinstalled('orchestra.contrib.issues'): - url = reverse('admin:issues_ticket_changelist') - childrens.append(items.MenuItem(_("Tickets"), url)) - childrens.extend(process_registered_models(accounts)) - return sorted(childrens, key=lambda i: i.title) - - -def get_administration_items(): - childrens = [] - if isinstalled('orchestra.contrib.settings'): - url = reverse('admin:settings_setting_change') - childrens.append(items.MenuItem(_("Settings"), url)) - if isinstalled('orchestra.contrib.services'): - url = reverse('admin:services_service_changelist') - childrens.append(items.MenuItem(_("Services"), url)) - url = reverse('admin:plans_plan_changelist') - childrens.append(items.MenuItem(_("Plans"), url)) - if isinstalled('orchestra.contrib.orchestration'): - route = reverse('admin:orchestration_route_changelist') - backendlog = reverse('admin:orchestration_backendlog_changelist') - server = reverse('admin:orchestration_server_changelist') - childrens.append(items.MenuItem(_("Orchestration"), route, children=[ - items.MenuItem(_("Routes"), route), - items.MenuItem(_("Backend logs"), backendlog), - items.MenuItem(_("Servers"), server), - ])) - if isinstalled('orchestra.contrib.resources'): - resource = reverse('admin:resources_resource_changelist') - data = reverse('admin:resources_resourcedata_changelist') - monitor = reverse('admin:resources_monitordata_changelist') - childrens.append(items.MenuItem(_("Resources"), resource, children=[ - items.MenuItem(_("Resources"), resource), - items.MenuItem(_("Data"), data), - items.MenuItem(_("Monitoring"), monitor), - ])) - if isinstalled('orchestra.contrib.miscellaneous'): - url = reverse('admin:miscellaneous_miscservice_changelist') - childrens.append(items.MenuItem(_("Miscellaneous"), url)) - if isinstalled('orchestra.contrib.issues'): - url = reverse('admin:issues_queue_changelist') - childrens.append(items.MenuItem(_("Ticket queues"), url)) - if isinstalled('djcelery'): - task = reverse('admin:djcelery_taskstate_changelist') - periodic = reverse('admin:djcelery_periodictask_changelist') - worker = reverse('admin:djcelery_workerstate_changelist') - childrens.append(items.MenuItem(_("Tasks"), task, children=[ - items.MenuItem(_("Logs"), task), - items.MenuItem(_("Periodic tasks"), periodic), - items.MenuItem(_("Workers"), worker), - ])) - childrens.extend(process_registered_models(administration)) - return childrens + opts.app_label, opts.model_name)) + name = capfirst(options.get('verbose_name_plural')) + return items.MenuItem(name, url) + + childrens = {} + for model, options in register.get().items(): + if options.get('menu', True): + parent = options.get('parent') + if parent: + parent_item = childrens.get(parent) + if parent_item: + if not parent_item.children: + parent_item.children.append(copy(parent_item)) + else: + parent_item = get_item(parent, register[parent]) + parent_item.children = [] + parent_item.children.append(get_item(model, options)) + childrens[parent] = parent_item + elif model not in childrens: + childrens[model] = get_item(model, options) + else: + childrens[model].children.insert(0, get_item(model, options)) + return sorted(childrens.values(), key=lambda i: i.title) class OrchestraMenu(Menu): @@ -122,16 +79,16 @@ class OrchestraMenu(Menu): # items.Bookmarks(), items.MenuItem( _("Services"), - children=get_services() + children=process_registry(services) ), items.MenuItem( _("Accounts"), reverse('admin:accounts_account_changelist'), - children=get_accounts() + children=process_registry(accounts) ), items.MenuItem( _("Administration"), - children=get_administration_items() + children=process_registry(administration) ), items.MenuItem("API", api_link(context)), ] diff --git a/orchestra/conf/project_template/project_name/settings.py b/orchestra/conf/project_template/project_name/settings.py index 81462018..6c940c63 100644 --- a/orchestra/conf/project_template/project_name/settings.py +++ b/orchestra/conf/project_template/project_name/settings.py @@ -233,75 +233,6 @@ ADMIN_TOOLS_MENU = 'orchestra.admin.menu.OrchestraMenu' # Fluent dashboard ADMIN_TOOLS_INDEX_DASHBOARD = 'orchestra.admin.dashboard.OrchestraIndexDashboard' FLUENT_DASHBOARD_ICON_THEME = '../orchestra/icons' -FLUENT_DASHBOARD_APP_GROUPS = ( - # Services group is generated by orchestra.admin.dashboard - ('Accounts', { - 'models': ( - 'orchestra.contrib.accounts.models.Account', - 'orchestra.contrib.contacts.models.Contact', - 'orchestra.contrib.orders.models.Order', - 'orchestra.contrib.plans.models.ContractedPlan', - 'orchestra.contrib.bills.models.Bill', - 'orchestra.contrib.payments.models.Transaction', - 'orchestra.contrib.issues.models.Ticket', - ), - 'collapsible': True, - }), - ('Administration', { - 'models': ( - 'djcelery.models.TaskState', - 'orchestra.contrib.orchestration.models.Route', - 'orchestra.contrib.orchestration.models.BackendLog', - 'orchestra.contrib.orchestration.models.Server', - 'orchestra.contrib.resources.models.Resource', - 'orchestra.contrib.resources.models.ResourceData', - 'orchestra.contrib.services.models.Service', - 'orchestra.contrib.plans.models.Plan', - 'orchestra.contrib.miscellaneous.models.MiscService', - ), - 'collapsible': True, - }), -) - -FLUENT_DASHBOARD_APP_ICONS = { - # Services - 'webs/web': 'web.png', - 'mail/address': 'X-office-address-book.png', - 'mailboxes/mailbox': 'email.png', - 'mailboxes/address': 'X-office-address-book.png', - 'lists/list': 'email-alter.png', - 'domains/domain': 'domain.png', - 'multitenance/tenant': 'apps.png', - 'webapps/webapp': 'Applications-other.png', - 'websites/website': 'Applications-internet.png', - 'databases/database': 'database.png', - 'databases/databaseuser': 'postgresql.png', - 'vps/vps': 'TuxBox.png', - 'miscellaneous/miscellaneous': 'applications-other.png', - 'saas/saas': 'saas.png', - 'systemusers/systemuser': 'roleplaying.png', - # Accounts - 'accounts/account': 'Face-monkey.png', - 'contacts/contact': 'contact_book.png', - 'orders/order': 'basket.png', - 'plans/contractedplan': 'ContractedPack.png', - 'services/service': 'price.png', - 'bills/bill': 'invoice.png', - 'payments/paymentsource': 'card_in_use.png', - 'payments/transaction': 'transaction.png', - 'payments/transactionprocess': 'transactionprocess.png', - 'issues/ticket': 'Ticket_star.png', - 'miscellaneous/miscservice': 'Misc-Misc-Box-icon.png', - # Administration - 'settings/setting': 'preferences.png', - 'djcelery/taskstate': 'taskstate.png', - 'orchestration/server': 'vps.png', - 'orchestration/route': 'hal.png', - 'orchestration/backendlog': 'scriptlog.png', - 'resources/resource': "gauge.png", - 'resources/resourcedata': "monitor.png", - 'plans/plan': 'Pack.png', -} # Django-celery diff --git a/orchestra/contrib/accounts/apps.py b/orchestra/contrib/accounts/apps.py index e3d54378..81e73667 100644 --- a/orchestra/contrib/accounts/apps.py +++ b/orchestra/contrib/accounts/apps.py @@ -12,7 +12,7 @@ class AccountConfig(AppConfig): def ready(self): from .management import create_initial_superuser from .models import Account - services.register(Account, menu=False) - accounts.register(Account) + services.register(Account, menu=False, dashboard=False) + accounts.register(Account, icon='Face-monkey.png') post_migrate.connect(create_initial_superuser, dispatch_uid="orchestra.contrib.accounts.management.createsuperuser") diff --git a/orchestra/contrib/bills/__init__.py b/orchestra/contrib/bills/__init__.py index e69de29b..c568ce60 100644 --- a/orchestra/contrib/bills/__init__.py +++ b/orchestra/contrib/bills/__init__.py @@ -0,0 +1 @@ +default_app_config = 'orchestra.contrib.bills.apps.BillsConfig' diff --git a/orchestra/contrib/bills/apps.py b/orchestra/contrib/bills/apps.py new file mode 100644 index 00000000..ecc74588 --- /dev/null +++ b/orchestra/contrib/bills/apps.py @@ -0,0 +1,12 @@ +from django.apps import AppConfig + +from orchestra.core import accounts + + +class BillsConfig(AppConfig): + name = 'orchestra.contrib.bills' + verbose_name = 'Bills' + + def ready(self): + from .models import Bill + accounts.register(Bill, icon='invoice.png') diff --git a/orchestra/contrib/bills/models.py b/orchestra/contrib/bills/models.py index 9ab06ee0..6e924e8d 100644 --- a/orchestra/contrib/bills/models.py +++ b/orchestra/contrib/bills/models.py @@ -13,7 +13,7 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.contrib.accounts.models import Account from orchestra.contrib.contacts.models import Contact -from orchestra.core import accounts, validators +from orchestra.core import validators from orchestra.utils.html import html_to_pdf from . import settings @@ -351,6 +351,3 @@ class BillSubline(models.Model): # if self.line.bill.is_open: # self.line.bill.total = self.line.bill.get_total() # self.line.bill.save(update_fields=['total']) - - -accounts.register(Bill) diff --git a/orchestra/contrib/contacts/__init__.py b/orchestra/contrib/contacts/__init__.py index e69de29b..3af15748 100644 --- a/orchestra/contrib/contacts/__init__.py +++ b/orchestra/contrib/contacts/__init__.py @@ -0,0 +1 @@ +default_app_config = 'orchestra.contrib.contacts.apps.ContactsConfig' diff --git a/orchestra/contrib/contacts/apps.py b/orchestra/contrib/contacts/apps.py new file mode 100644 index 00000000..4ed7fe70 --- /dev/null +++ b/orchestra/contrib/contacts/apps.py @@ -0,0 +1,12 @@ +from django.apps import AppConfig + +from orchestra.core import accounts + + +class ContactsConfig(AppConfig): + name = 'orchestra.contrib.contacts' + verbose_name = 'Contacts' + + def ready(self): + from .models import Contact + accounts.register(Contact, icon='contact_book.png') diff --git a/orchestra/contrib/contacts/models.py b/orchestra/contrib/contacts/models.py index e981b761..42fee3fe 100644 --- a/orchestra/contrib/contacts/models.py +++ b/orchestra/contrib/contacts/models.py @@ -3,7 +3,7 @@ from django.core.validators import RegexValidator from django.db import models from django.utils.translation import ugettext_lazy as _ -from orchestra.core import accounts, validators +from orchestra.core import validators from orchestra.models.fields import MultiSelectField from . import settings @@ -78,6 +78,3 @@ class Contact(models.Model): errors['zipcode'] = error if errors: raise ValidationError(errors) - - -accounts.register(Contact) diff --git a/orchestra/contrib/databases/__init__.py b/orchestra/contrib/databases/__init__.py index e69de29b..f21f8dda 100644 --- a/orchestra/contrib/databases/__init__.py +++ b/orchestra/contrib/databases/__init__.py @@ -0,0 +1 @@ +default_app_config = 'orchestra.contrib.databases.apps.DatabasesConfig' diff --git a/orchestra/contrib/databases/apps.py b/orchestra/contrib/databases/apps.py new file mode 100644 index 00000000..97f8ef4a --- /dev/null +++ b/orchestra/contrib/databases/apps.py @@ -0,0 +1,14 @@ +from django.apps import AppConfig +from django.utils.translation import ugettext_lazy as _ + +from orchestra.core import services + + +class DatabasesConfig(AppConfig): + name = 'orchestra.contrib.databases' + verbose_name = 'Databases' + + def ready(self): + from .models import Database, DatabaseUser + services.register(Database, icon='database.png') + services.register(DatabaseUser, icon='postgresql.png', verbose_name_plural=_("Database users")) diff --git a/orchestra/contrib/databases/models.py b/orchestra/contrib/databases/models.py index b04e1ff1..2270a1f8 100644 --- a/orchestra/contrib/databases/models.py +++ b/orchestra/contrib/databases/models.py @@ -3,7 +3,7 @@ import hashlib from django.db import models from django.utils.translation import ugettext_lazy as _ -from orchestra.core import validators, services +from orchestra.core import validators from . import settings @@ -76,7 +76,3 @@ class DatabaseUser(models.Model): self.password = '*%s' % hexdigest.upper() else: raise TypeError("Database type '%s' not supported" % self.type) - - -services.register(Database) -services.register(DatabaseUser, verbose_name_plural=_("Database users")) diff --git a/orchestra/contrib/domains/__init__.py b/orchestra/contrib/domains/__init__.py index e69de29b..5c85353e 100644 --- a/orchestra/contrib/domains/__init__.py +++ b/orchestra/contrib/domains/__init__.py @@ -0,0 +1 @@ +default_app_config = 'orchestra.contrib.domains.apps.DomainsConfig' diff --git a/orchestra/contrib/domains/admin.py b/orchestra/contrib/domains/admin.py index 78ff706f..1d7e7ce5 100644 --- a/orchestra/contrib/domains/admin.py +++ b/orchestra/contrib/domains/admin.py @@ -11,8 +11,8 @@ from orchestra.contrib.accounts.admin import AccountAdminMixin from orchestra.utils import apps from .actions import view_zone -from .forms import RecordInlineFormSet, BatchDomainCreationAdminForm from .filters import TopDomainListFilter +from .forms import RecordInlineFormSet, BatchDomainCreationAdminForm from .models import Domain, Record @@ -21,11 +21,6 @@ class RecordInline(admin.TabularInline): formset = RecordInlineFormSet verbose_name_plural = _("Extra records") -# class Media: -# css = { -# 'all': ('orchestra/css/hide-inline-id.css',) -# } -# def formfield_for_dbfield(self, db_field, **kwargs): """ Make value input widget bigger """ if db_field.name == 'value': @@ -73,9 +68,9 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin): change_view_actions = [view_zone] def structured_name(self, domain): - if not domain.is_top: - return ' '*4 + domain.name - return domain.name + if domain.is_top: + return domain.name + return ' '*4 + domain.name structured_name.short_description = _("name") structured_name.allow_tags = True structured_name.admin_order_field = 'structured_name' diff --git a/orchestra/contrib/domains/apps.py b/orchestra/contrib/domains/apps.py new file mode 100644 index 00000000..559166c4 --- /dev/null +++ b/orchestra/contrib/domains/apps.py @@ -0,0 +1,12 @@ +from django.apps import AppConfig + +from orchestra.core import services + + +class DomainsConfig(AppConfig): + name = 'orchestra.contrib.domains' + verbose_name = 'Domains' + + def ready(self): + from .models import Domain + services.register(Domain, icon='domain.png') diff --git a/orchestra/contrib/domains/models.py b/orchestra/contrib/domains/models.py index 5b44e476..f73743bb 100644 --- a/orchestra/contrib/domains/models.py +++ b/orchestra/contrib/domains/models.py @@ -2,7 +2,6 @@ from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import ungettext, ugettext_lazy as _ -from orchestra.core import services from orchestra.core.validators import validate_ipv4_address, validate_ipv6_address, validate_ascii from orchestra.utils.python import AttrDict @@ -271,6 +270,3 @@ class Record(models.Model): def get_ttl(self): return self.ttl or settings.DOMAINS_DEFAULT_TTL - - -services.register(Domain) diff --git a/orchestra/contrib/issues/__init__.py b/orchestra/contrib/issues/__init__.py index edea65c4..650ba7fe 100644 --- a/orchestra/contrib/issues/__init__.py +++ b/orchestra/contrib/issues/__init__.py @@ -1 +1 @@ -REQUIRED_APPS = ['slices'] +default_app_config = 'orchestra.contrib.issues.apps.IssuesConfig' diff --git a/orchestra/contrib/issues/apps.py b/orchestra/contrib/issues/apps.py new file mode 100644 index 00000000..fdc35f24 --- /dev/null +++ b/orchestra/contrib/issues/apps.py @@ -0,0 +1,13 @@ +from django.apps import AppConfig + +from orchestra.core import accounts, administration + + +class IssuesConfig(AppConfig): + name = 'orchestra.contrib.issues' + verbose_name = "Issues" + + def ready(self): + from .models import Queue, Ticket + accounts.register(Ticket, icon='Ticket_star.png') + administration.register(Queue, dashboard=False) diff --git a/orchestra/contrib/lists/__init__.py b/orchestra/contrib/lists/__init__.py index e69de29b..413f2e03 100644 --- a/orchestra/contrib/lists/__init__.py +++ b/orchestra/contrib/lists/__init__.py @@ -0,0 +1 @@ +default_app_config = 'orchestra.contrib.lists.apps.ListsConfig' diff --git a/orchestra/contrib/lists/apps.py b/orchestra/contrib/lists/apps.py new file mode 100644 index 00000000..7e6e772c --- /dev/null +++ b/orchestra/contrib/lists/apps.py @@ -0,0 +1,12 @@ +from django.apps import AppConfig + +from orchestra.core import services + + +class ListsConfig(AppConfig): + name = 'orchestra.contrib.lists' + verbose_name = 'Lists' + + def ready(self): + from .models import List + services.register(List, icon='email-alter.png') diff --git a/orchestra/contrib/lists/models.py b/orchestra/contrib/lists/models.py index 8d1ba5ed..cb9caefa 100644 --- a/orchestra/contrib/lists/models.py +++ b/orchestra/contrib/lists/models.py @@ -2,7 +2,6 @@ from django.db import models from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ -from orchestra.core import services from orchestra.core.validators import validate_name from . import settings @@ -70,6 +69,3 @@ class List(models.Model): 'name': self.name } return settings.LISTS_LIST_URL % context - - -services.register(List) diff --git a/orchestra/contrib/mailboxes/__init__.py b/orchestra/contrib/mailboxes/__init__.py index e69de29b..dbf89749 100644 --- a/orchestra/contrib/mailboxes/__init__.py +++ b/orchestra/contrib/mailboxes/__init__.py @@ -0,0 +1 @@ +default_app_config = 'orchestra.contrib.mailboxes.apps.MailboxesConfig' diff --git a/orchestra/contrib/mailboxes/apps.py b/orchestra/contrib/mailboxes/apps.py new file mode 100644 index 00000000..9171c4ea --- /dev/null +++ b/orchestra/contrib/mailboxes/apps.py @@ -0,0 +1,13 @@ +from django.apps import AppConfig + +from orchestra.core import services + + +class MailboxesConfig(AppConfig): + name = 'orchestra.contrib.mailboxes' + verbose_name = 'Mailboxes' + + def ready(self): + from .models import Mailbox, Address + services.register(Mailbox, icon='email.png') + services.register(Address, icon='X-office-address-book.png') diff --git a/orchestra/contrib/mailboxes/models.py b/orchestra/contrib/mailboxes/models.py index 7a3811f6..8f4cad4b 100644 --- a/orchestra/contrib/mailboxes/models.py +++ b/orchestra/contrib/mailboxes/models.py @@ -6,8 +6,6 @@ from django.db import models from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ -from orchestra.core import services - from . import validators, settings @@ -152,7 +150,3 @@ class Autoresponse(models.Model): def __str__(self): return self.address - - -services.register(Mailbox) -services.register(Address) diff --git a/orchestra/contrib/mailer/apps.py b/orchestra/contrib/mailer/apps.py index fd8cd958..c680cef8 100644 --- a/orchestra/contrib/mailer/apps.py +++ b/orchestra/contrib/mailer/apps.py @@ -9,4 +9,4 @@ class MailerConfig(AppConfig): def ready(self): from .models import Message - administration.register(Message) + administration.register(Message, icon='Mail-send.png') diff --git a/orchestra/contrib/mailer/models.py b/orchestra/contrib/mailer/models.py index 6a943f47..21786c1e 100644 --- a/orchestra/contrib/mailer/models.py +++ b/orchestra/contrib/mailer/models.py @@ -43,7 +43,7 @@ class Message(models.Model): # Max tries if self.retries >= len(settings.MAILER_DEFERE_SECONDS): self.state = self.FAILED - self.save(update_fields=('state', 'retries')) + self.save(update_fields=('state', 'retries', 'last_retry')) def sent(self): self.state = self.SENT diff --git a/orchestra/contrib/miscellaneous/__init__.py b/orchestra/contrib/miscellaneous/__init__.py index e69de29b..6294909f 100644 --- a/orchestra/contrib/miscellaneous/__init__.py +++ b/orchestra/contrib/miscellaneous/__init__.py @@ -0,0 +1 @@ +default_app_config = 'orchestra.contrib.miscellaneous.apps.MiscellaneousConfig' diff --git a/orchestra/contrib/miscellaneous/apps.py b/orchestra/contrib/miscellaneous/apps.py new file mode 100644 index 00000000..2f5763ae --- /dev/null +++ b/orchestra/contrib/miscellaneous/apps.py @@ -0,0 +1,15 @@ +from django.apps import AppConfig + +from orchestra.core import services, administration +from orchestra.core.translations import ModelTranslation + + +class MiscellaneousConfig(AppConfig): + name = 'orchestra.contrib.miscellaneous' + verbose_name = 'Miscellaneous' + + def ready(self): + from .models import MiscService, Miscellaneous + services.register(Miscellaneous, icon='applications-other.png') + administration.register(MiscService, icon='Misc-Misc-Box-icon.png') + ModelTranslation.register(MiscService, ('verbose_name',)) diff --git a/orchestra/contrib/miscellaneous/models.py b/orchestra/contrib/miscellaneous/models.py index 663b4232..f3233133 100644 --- a/orchestra/contrib/miscellaneous/models.py +++ b/orchestra/contrib/miscellaneous/models.py @@ -2,8 +2,6 @@ from django.db import models from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ -from orchestra.core import services -from orchestra.core.translations import ModelTranslation from orchestra.core.validators import validate_name from orchestra.models.fields import NullableCharField @@ -69,8 +67,3 @@ class Miscellaneous(models.Model): if self.identifier: self.identifier = self.identifier.strip() self.description = self.description.strip() - - -services.register(Miscellaneous) - -ModelTranslation.register(MiscService, ('verbose_name',)) diff --git a/orchestra/contrib/orchestration/__init__.py b/orchestra/contrib/orchestration/__init__.py index 89d0113b..262c9d81 100644 --- a/orchestra/contrib/orchestration/__init__.py +++ b/orchestra/contrib/orchestration/__init__.py @@ -3,6 +3,9 @@ import copy from .backends import ServiceBackend, ServiceController, replace +default_app_config = 'orchestra.contrib.orchestration.apps.OrchestrationConfig' + + class Operation(): DELETE = 'delete' SAVE = 'save' @@ -30,10 +33,10 @@ class Operation(): self.routes = routes @classmethod - def execute(cls, operations, async=False): + def execute(cls, operations, serialize=False, async=False): from . import manager - scripts, block = manager.generate(operations) - return manager.execute(scripts, block=block, async=async) + scripts, oserialize = manager.generate(operations) + return manager.execute(scripts, serialize=(serialize or oserialize), async=async) @classmethod def execute_action(cls, instance, action): diff --git a/orchestra/contrib/orchestration/apps.py b/orchestra/contrib/orchestration/apps.py new file mode 100644 index 00000000..6de145dd --- /dev/null +++ b/orchestra/contrib/orchestration/apps.py @@ -0,0 +1,14 @@ +from django.apps import AppConfig + +from orchestra.core import administration + + +class OrchestrationConfig(AppConfig): + name = 'orchestra.contrib.orchestration' + verbose_name = "Orchestration" + + def ready(self): + from .models import Server, Route, BackendLog + administration.register(BackendLog, icon='scriptlog.png') + administration.register(Server, parent=BackendLog, icon='vps.png') + administration.register(Route, parent=BackendLog, icon='hal.png') diff --git a/orchestra/contrib/orchestration/backends.py b/orchestra/contrib/orchestration/backends.py index d32a2bdf..cd52aa79 100644 --- a/orchestra/contrib/orchestration/backends.py +++ b/orchestra/contrib/orchestration/backends.py @@ -45,7 +45,7 @@ class ServiceBackend(plugins.Plugin, metaclass=ServiceMount): actions = [] default_route_match = 'True' # Force the backend manager to block in multiple backend executions executing them synchronously - block = False + serialize = False doc_settings = None # By default backend will not run if actions do not generate insctructions, # If your backend uses prepare() or commit() only then you should set force_empty_action_execution = True diff --git a/orchestra/contrib/orchestration/management/commands/orchestrate.py b/orchestra/contrib/orchestration/management/commands/orchestrate.py index 69fc11fc..defeb4c4 100644 --- a/orchestra/contrib/orchestration/management/commands/orchestrate.py +++ b/orchestra/contrib/orchestration/management/commands/orchestrate.py @@ -71,7 +71,7 @@ class Command(BaseCommand): else: for instance in queryset: manager.collect(instance, action, operations=operations, route_cache=route_cache) - scripts, block = manager.generate(operations) + scripts, serialize = manager.generate(operations) servers = [] # Print scripts for key, value in scripts.items(): @@ -96,7 +96,7 @@ class Command(BaseCommand): return break if not dry: - logs = manager.execute(scripts, block=block) + logs = manager.execute(scripts, serialize=serialize) for log in logs: self.stdout.write(log.stdout) self.stderr.write(log.stderr) diff --git a/orchestra/contrib/orchestration/manager.py b/orchestra/contrib/orchestration/manager.py index 9cc86d70..0ee544f4 100644 --- a/orchestra/contrib/orchestration/manager.py +++ b/orchestra/contrib/orchestration/manager.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) router = import_class(settings.ORCHESTRATION_ROUTER) -def as_task(execute, log, operations): +def keep_log(execute, log, operations): def wrapper(*args, **kwargs): """ send report """ # Remember that threads have their oun connection poll @@ -30,14 +30,14 @@ def as_task(execute, log, operations): if log.state != log.SUCCESS: send_report(execute, args, log) except Exception as e: - subject = 'EXCEPTION executing backend(s) %s %s' % (str(args), str(kwargs)) - message = traceback.format_exc() - logger.error(subject) - logger.error(message) - mail_admins(subject, message) + trace = traceback.format_exc() log.state = BackendLog.EXCEPTION - log.stderr = traceback.format_exc() - log.save(update_fields=('state', 'stderr')) + log.stderr = trace + log.save() + subject = 'EXCEPTION executing backend(s) %s %s' % (str(args), str(kwargs)) + logger.error(subject) + logger.error(trace) + mail_admins(subject, trace) # We don't propagate the exception further to avoid transaction rollback finally: # Store and log the operation @@ -56,7 +56,7 @@ def as_task(execute, log, operations): def generate(operations): scripts = OrderedDict() cache = {} - block = False + serialize = False # Generate scripts per route+backend for operation in operations: logger.debug("Queued %s" % str(operation)) @@ -86,18 +86,18 @@ def generate(operations): pre_action.send(**kwargs) method(operation.instance) post_action.send(**kwargs) - if backend.block: - block = True + if backend.serialize: + serialize = True for value in scripts.values(): backend, operations = value backend.set_tail() pre_commit.send(sender=backend.__class__, backend=backend) backend.commit() post_commit.send(sender=backend.__class__, backend=backend) - return scripts, block + return scripts, serialize -def execute(scripts, block=False, async=False): +def execute(scripts, serialize=False, async=False): """ executes the operations on the servers """ if settings.ORCHESTRATION_DISABLE_EXECUTION: logger.info('Orchestration execution is dissabled by ORCHESTRATION_DISABLE_EXECUTION settings.') @@ -110,21 +110,22 @@ def execute(scripts, block=False, async=False): route, __ = key backend, operations = value args = (route.host,) + async = not serialize and (async or route.async) kwargs = { - 'async': async or route.async + 'async': async, } log = backend.create_log(*args, **kwargs) kwargs['log'] = log - task = as_task(backend.execute, log, operations) + task = keep_log(backend.execute, log, operations) logger.debug('%s is going to be executed on %s' % (backend, route.host)) - if block: + if serialize: # Execute one backend at a time, no need for threads task(*args, **kwargs) else: task = close_connection(task) thread = threading.Thread(target=task, args=args, kwargs=kwargs) thread.start() - if not route.async: + if not async: threads_to_join.append(thread) logs.append(log) [ thread.join() for thread in threads_to_join ] diff --git a/orchestra/contrib/orchestration/middlewares.py b/orchestra/contrib/orchestration/middlewares.py index 95d68725..94fa73d3 100644 --- a/orchestra/contrib/orchestration/middlewares.py +++ b/orchestra/contrib/orchestration/middlewares.py @@ -97,14 +97,14 @@ class OperationsMiddleware(object): operations = self.get_pending_operations() if operations: try: - scripts, block = manager.generate(operations) + scripts, serialize = manager.generate(operations) except Exception as exception: self.leave_transaction_management(exception) raise # We commit transaction just before executing operations # because here is when IntegrityError show up self.leave_transaction_management() - logs = manager.execute(scripts, block=block) + logs = manager.execute(scripts, serialize=serialize) if logs and resolve(request.path).app_name == 'admin': message_user(request, logs) return response diff --git a/orchestra/contrib/orders/apps.py b/orchestra/contrib/orders/apps.py index 733d281b..ae588789 100644 --- a/orchestra/contrib/orders/apps.py +++ b/orchestra/contrib/orders/apps.py @@ -10,6 +10,6 @@ class OrdersConfig(AppConfig): def ready(self): from .models import Order - accounts.register(Order) + accounts.register(Order, icon='basket.png') if database_ready(): from . import signals diff --git a/orchestra/contrib/payments/__init__.py b/orchestra/contrib/payments/__init__.py index e69de29b..970bd432 100644 --- a/orchestra/contrib/payments/__init__.py +++ b/orchestra/contrib/payments/__init__.py @@ -0,0 +1 @@ +default_app_config = 'orchestra.contrib.payments.apps.PaymentsConfig' diff --git a/orchestra/contrib/payments/apps.py b/orchestra/contrib/payments/apps.py new file mode 100644 index 00000000..7ae1bae4 --- /dev/null +++ b/orchestra/contrib/payments/apps.py @@ -0,0 +1,14 @@ +from django.apps import AppConfig + +from orchestra.core import accounts + + +class PaymentsConfig(AppConfig): + name = 'orchestra.contrib.payments' + verbose_name = "Payments" + + def ready(self): + from .models import PaymentSource, Transaction, TransactionProcess + accounts.register(PaymentSource, dashboard=False) + accounts.register(Transaction, icon='transaction.png') + accounts.register(TransactionProcess, icon='transactionprocess.png', dashboard=False) diff --git a/orchestra/contrib/payments/models.py b/orchestra/contrib/payments/models.py index 520d681f..79ce714a 100644 --- a/orchestra/contrib/payments/models.py +++ b/orchestra/contrib/payments/models.py @@ -4,7 +4,6 @@ from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from jsonfield import JSONField -from orchestra.core import accounts from orchestra.models.queryset import group_by from . import settings @@ -200,7 +199,3 @@ class TransactionProcess(models.Model): for transaction in self.transactions.processing(): transaction.mark_as_secured() self.save(update_fields=['state']) - - -accounts.register(PaymentSource) -accounts.register(Transaction) diff --git a/orchestra/contrib/plans/__init__.py b/orchestra/contrib/plans/__init__.py index e69de29b..e09642bf 100644 --- a/orchestra/contrib/plans/__init__.py +++ b/orchestra/contrib/plans/__init__.py @@ -0,0 +1 @@ +default_app_config = 'orchestra.contrib.plans.apps.PlansConfig' diff --git a/orchestra/contrib/plans/apps.py b/orchestra/contrib/plans/apps.py new file mode 100644 index 00000000..153501c9 --- /dev/null +++ b/orchestra/contrib/plans/apps.py @@ -0,0 +1,15 @@ +from django.apps import AppConfig + +from orchestra.core import administration, accounts +from orchestra.core.translations import ModelTranslation + + +class PlansConfig(AppConfig): + name = 'orchestra.contrib.plans' + verbose_name = 'Plans' + + def ready(self): + from .models import Plan, ContractedPlan + accounts.register(ContractedPlan, icon='ContractedPack.png') + administration.register(Plan, icon='Pack.png') + ModelTranslation.register(Plan, ('verbose_name',)) diff --git a/orchestra/contrib/plans/models.py b/orchestra/contrib/plans/models.py index 5a9fae38..7caf1a57 100644 --- a/orchestra/contrib/plans/models.py +++ b/orchestra/contrib/plans/models.py @@ -5,7 +5,6 @@ from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from orchestra.core import services, accounts -from orchestra.core.translations import ModelTranslation from orchestra.core.validators import validate_name from orchestra.models import queryset @@ -100,9 +99,3 @@ class Rate(models.Model): for name, method in cls.RATE_METHODS.items(): choices.append((name, method.verbose_name)) return choices - - -accounts.register(ContractedPlan) -services.register(ContractedPlan, menu=False) - -ModelTranslation.register(Plan, ('verbose_name',)) diff --git a/orchestra/contrib/resources/apps.py b/orchestra/contrib/resources/apps.py index ca3c4215..2cfbde65 100644 --- a/orchestra/contrib/resources/apps.py +++ b/orchestra/contrib/resources/apps.py @@ -1,6 +1,7 @@ from django import db from django.apps import AppConfig +from orchestra.core import administration from orchestra.utils.db import database_ready @@ -16,6 +17,10 @@ class ResourcesConfig(AppConfig): except db.utils.OperationalError: # Not ready afterall pass + from .models import Resource, ResourceData, MonitorData + administration.register(Resource, icon='gauge.png') + administration.register(ResourceData, parent=Resource, icon='monitor.png') + administration.register(MonitorData, parent=Resource, dashboard=False) def reload_relations(self): from .admin import insert_resource_inlines diff --git a/orchestra/contrib/resources/backends.py b/orchestra/contrib/resources/backends.py index e5e7103e..41dd3f39 100644 --- a/orchestra/contrib/resources/backends.py +++ b/orchestra/contrib/resources/backends.py @@ -72,7 +72,7 @@ class ServiceMonitor(ServiceBackend): MonitorData.objects.create(monitor=name, object_id=object_id, content_type=ct, value=value, created_at=self.current_date) - def execute(self, server, async=False): - log = super(ServiceMonitor, self).execute(server, async=async) + def execute(self, *args, **kwargs): + log = super(ServiceMonitor, self).execute(*args, **kwargs) self.store(log) return log diff --git a/orchestra/contrib/resources/migrations/0002_auto_20150502_1429.py b/orchestra/contrib/resources/migrations/0002_auto_20150502_1429.py new file mode 100644 index 00000000..83c9b197 --- /dev/null +++ b/orchestra/contrib/resources/migrations/0002_auto_20150502_1429.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('resources', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='CrontabSchedule', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('minute', models.CharField(max_length=64, verbose_name='minute', default='*')), + ('hour', models.CharField(max_length=64, verbose_name='hour', default='*')), + ('day_of_week', models.CharField(max_length=64, verbose_name='day of week', default='*')), + ('day_of_month', models.CharField(max_length=64, verbose_name='day of month', default='*')), + ('month_of_year', models.CharField(max_length=64, verbose_name='month of year', default='*')), + ], + options={ + 'verbose_name': 'crontab', + 'ordering': ('month_of_year', 'day_of_month', 'day_of_week', 'hour', 'minute'), + 'verbose_name_plural': 'crontabs', + }, + ), + ] diff --git a/orchestra/contrib/resources/migrations/0003_auto_20150502_1433.py b/orchestra/contrib/resources/migrations/0003_auto_20150502_1433.py new file mode 100644 index 00000000..7a3f3472 --- /dev/null +++ b/orchestra/contrib/resources/migrations/0003_auto_20150502_1433.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('resources', '0002_auto_20150502_1429'), + ] + + operations = [ + ] diff --git a/orchestra/contrib/resources/migrations/0004_auto_20150503_1559.py b/orchestra/contrib/resources/migrations/0004_auto_20150503_1559.py new file mode 100644 index 00000000..1b717fe6 --- /dev/null +++ b/orchestra/contrib/resources/migrations/0004_auto_20150503_1559.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('resources', '0003_auto_20150502_1433'), + ] + + operations = [ + ] diff --git a/orchestra/contrib/resources/models.py b/orchestra/contrib/resources/models.py index df9ceb29..53a1c7ac 100644 --- a/orchestra/contrib/resources/models.py +++ b/orchestra/contrib/resources/models.py @@ -148,9 +148,8 @@ class Resource(models.Model): def monitor(self, async=True): if async: - print(tasks.monitor.delay) - return tasks.monitor.delay(self.pk, async=async) - return tasks.monitor(self.pk, async=async) + return tasks.monitor.apply_async(self.pk) + return tasks.monitor(self.pk) class ResourceData(models.Model): diff --git a/orchestra/contrib/resources/tasks.py b/orchestra/contrib/resources/tasks.py index ee983fa3..e440a36c 100644 --- a/orchestra/contrib/resources/tasks.py +++ b/orchestra/contrib/resources/tasks.py @@ -7,7 +7,7 @@ from .backends import ServiceMonitor @task(name='resources.Monitor') -def monitor(resource_id, ids=None, async=True): +def monitor(resource_id, ids=None): with LockFile('/dev/shm/resources.monitor-%i.lock' % resource_id, expire=60*60): from .models import ResourceData, Resource resource = Resource.objects.get(pk=resource_id) @@ -29,9 +29,7 @@ def monitor(resource_id, ids=None, async=True): for obj in model.objects.filter(**kwargs): op = Operation(backend, obj, Operation.MONITOR) monitorings.append(op) - # TODO async=True only when running with celery - # monitor.request.id - logs += Operation.execute(monitorings, async=async) + logs += Operation.execute(monitorings, async=False) kwargs = {'id__in': ids} if ids else {} # Update used resources and trigger resource exceeded and revovery diff --git a/orchestra/contrib/saas/apps.py b/orchestra/contrib/saas/apps.py index bed646b0..8ad3f8c8 100644 --- a/orchestra/contrib/saas/apps.py +++ b/orchestra/contrib/saas/apps.py @@ -10,6 +10,4 @@ class SaaSConfig(AppConfig): def ready(self): from . import signals from .models import SaaS - services.register(SaaS) - - + services.register(SaaS, icon='saas.png') diff --git a/orchestra/contrib/saas/backends/gitlab.py b/orchestra/contrib/saas/backends/gitlab.py index 55c13cd5..77b8389a 100644 --- a/orchestra/contrib/saas/backends/gitlab.py +++ b/orchestra/contrib/saas/backends/gitlab.py @@ -12,7 +12,7 @@ class GitLabSaaSBackend(ServiceController): verbose_name = _("GitLab SaaS") model = 'saas.SaaS' default_route_match = "saas.service == 'gitlab'" - block = True + serialize = True actions = ('save', 'delete', 'validate_creation') doc_settings = (settings, ('SAAS_GITLAB_DOMAIN', 'SAAS_GITLAB_ROOT_PASSWORD'), diff --git a/orchestra/contrib/saas/backends/phplist.py b/orchestra/contrib/saas/backends/phplist.py index e7ed0aa0..62b8a39b 100644 --- a/orchestra/contrib/saas/backends/phplist.py +++ b/orchestra/contrib/saas/backends/phplist.py @@ -18,7 +18,7 @@ class PhpListSaaSBackend(ServiceController): verbose_name = _("phpList SaaS") model = 'saas.SaaS' default_route_match = "saas.service == 'phplist'" - block = True + serialize = True def _save(self, saas, server): admin_link = 'http://%s/admin/' % saas.get_site_domain() diff --git a/orchestra/contrib/services/apps.py b/orchestra/contrib/services/apps.py index 51f56168..43b2f55c 100644 --- a/orchestra/contrib/services/apps.py +++ b/orchestra/contrib/services/apps.py @@ -1,6 +1,14 @@ from django.apps import AppConfig +from orchestra.core import administration, accounts +from orchestra.core.translations import ModelTranslation + class ServicesConfig(AppConfig): name = 'orchestra.contrib.services' verbose_name = 'Services' + + def ready(self): + from .models import Service + administration.register(Service, icon='price.png') + ModelTranslation.register(Service, ('description',)) diff --git a/orchestra/contrib/services/models.py b/orchestra/contrib/services/models.py index 27daf1ab..862f026d 100644 --- a/orchestra/contrib/services/models.py +++ b/orchestra/contrib/services/models.py @@ -8,7 +8,6 @@ from django.utils.module_loading import autodiscover_modules from django.utils.translation import string_concat, ugettext_lazy as _ from orchestra.core import caches, validators -from orchestra.core.translations import ModelTranslation from orchestra.utils.python import import_class from . import settings @@ -244,6 +243,3 @@ class Service(models.Model): for instance in queryset: updates += order_model.update_orders(instance, service=self, commit=commit) return updates - - -ModelTranslation.register(Service, ('description',)) diff --git a/orchestra/contrib/settings/admin.py b/orchestra/contrib/settings/admin.py index 3a503591..93e44325 100644 --- a/orchestra/contrib/settings/admin.py +++ b/orchestra/contrib/settings/admin.py @@ -103,7 +103,5 @@ class SettingFileView(generic.TemplateView): return context - admin.site.register_url(r'^settings/setting/view/$', SettingFileView.as_view(), 'settings_setting_view') admin.site.register_url(r'^settings/setting/$', SettingView.as_view(), 'settings_setting_change') -OrchestraIndexDashboard.register_link('Administration', 'settings_setting_change', _("Settings")) diff --git a/orchestra/contrib/settings/apps.py b/orchestra/contrib/settings/apps.py index 83f1b749..f93361c7 100644 --- a/orchestra/contrib/settings/apps.py +++ b/orchestra/contrib/settings/apps.py @@ -1,13 +1,21 @@ from django.apps import AppConfig from django.core.checks import register, Error from django.core.exceptions import ValidationError +from django.utils.translation import ngettext, ugettext_lazy as _ + +from orchestra.core import administration from . import Setting + class SettingsConfig(AppConfig): name = 'orchestra.contrib.settings' verbose_name = 'Settings' + def ready(self): + administration.register_view('settings_setting_change', verbose_name=_("Settings"), + icon='Multimedia-volume-control.png') + @register() def check_settings(app_configs, **kwargs): """ perfroms all the validation """ diff --git a/orchestra/contrib/systemusers/apps.py b/orchestra/contrib/systemusers/apps.py index 418f364e..021d92d9 100644 --- a/orchestra/contrib/systemusers/apps.py +++ b/orchestra/contrib/systemusers/apps.py @@ -12,7 +12,7 @@ class SystemUsersConfig(AppConfig): def ready(self): from .models import SystemUser - services.register(SystemUser) + services.register(SystemUser, icon='roleplaying.png') if 'migrate' in sys.argv and 'accounts' not in sys.argv: post_migrate.connect(self.create_initial_systemuser, dispatch_uid="orchestra.contrib.systemusers.apps.create_initial_systemuser") diff --git a/orchestra/contrib/tasks/__init__.py b/orchestra/contrib/tasks/__init__.py index 210d0ac6..e380d425 100644 --- a/orchestra/contrib/tasks/__init__.py +++ b/orchestra/contrib/tasks/__init__.py @@ -2,3 +2,6 @@ import sys from . import settings from .decorators import task, periodic_task, keep_state, apply_async + + +default_app_config = 'orchestra.contrib.tasks.apps.TasksConfig' diff --git a/orchestra/contrib/tasks/apps.py b/orchestra/contrib/tasks/apps.py new file mode 100644 index 00000000..a6fd8f7e --- /dev/null +++ b/orchestra/contrib/tasks/apps.py @@ -0,0 +1,14 @@ +from django.apps import AppConfig + +from orchestra.core import administration + + +class TasksConfig(AppConfig): + name = 'orchestra.contrib.tasks' + verbose_name = "Tasks" + + def ready(self): + from djcelery.models import PeriodicTask, TaskState, WorkerState + administration.register(TaskState, icon='Edit-check-sheet.png') + administration.register(PeriodicTask, parent=TaskState, icon='Appointment.png') + administration.register(WorkerState, parent=TaskState, dashboard=False) diff --git a/orchestra/contrib/tasks/decorators.py b/orchestra/contrib/tasks/decorators.py index c08af43e..725d4a8e 100644 --- a/orchestra/contrib/tasks/decorators.py +++ b/orchestra/contrib/tasks/decorators.py @@ -6,6 +6,7 @@ from threading import Thread from celery import shared_task as celery_shared_task from celery import states from celery.decorators import periodic_task as celery_periodic_task +from django.core.mail import mail_admins from django.utils import timezone from orchestra.utils.db import close_connection @@ -26,11 +27,15 @@ def keep_state(fn): result = fn(*args, **kwargs) except Exception as exc: state.state = states.FAILURE - state.traceback = traceback.format_exc() + state.traceback = trace state.runtime = (timezone.now()-now).total_seconds() state.save() + subject = 'EXCEPTION executing task %s(args=%s, kwargs=%s)' % (name, str(args), str(kwargs)) + trace = traceback.format_exc() + logger.error(subject) + logger.error(trace) + mail_admins(subject, trace) return - # TODO send email else: state.state = states.SUCCESS state.result = str(result) diff --git a/orchestra/contrib/tasks/management/commands/syncperiodictasks.py b/orchestra/contrib/tasks/management/commands/syncperiodictasks.py index d638d99e..6b2b4412 100644 --- a/orchestra/contrib/tasks/management/commands/syncperiodictasks.py +++ b/orchestra/contrib/tasks/management/commands/syncperiodictasks.py @@ -1,2 +1,15 @@ -# create crontab entries for defines periodic tasks +from django.core.management.base import BaseCommand, CommandError +from djcelery.app import app +from djcelery.schedulers import DatabaseScheduler + +class Command(BaseCommand): + help = 'Runs Orchestra method.' + + def handle(self, *args, **options): + dbschedule = DatabaseScheduler(app=app) + self.stdout.write('\033[1m%i periodic tasks have been syncronized:\033[0m' % len(dbschedule.schedule)) + size = max([len(name) for name in dbschedule.schedule])+1 + for name, task in dbschedule.schedule.items(): + spaces = ' '*(size-len(name)) + self.stdout.write(' %s%s%s' % (name, spaces, task.schedule)) diff --git a/orchestra/contrib/vps/__init__.py b/orchestra/contrib/vps/__init__.py index e69de29b..96cf9729 100644 --- a/orchestra/contrib/vps/__init__.py +++ b/orchestra/contrib/vps/__init__.py @@ -0,0 +1 @@ +default_app_config = 'orchestra.contrib.vps.apps.VPSConfig' diff --git a/orchestra/contrib/vps/apps.py b/orchestra/contrib/vps/apps.py new file mode 100644 index 00000000..919bb54d --- /dev/null +++ b/orchestra/contrib/vps/apps.py @@ -0,0 +1,12 @@ +from django.apps import AppConfig + +from orchestra.core import services + + +class VPSConfig(AppConfig): + name = 'orchestra.contrib.vps' + verbose_name = 'VPS' + + def ready(self): + from .models import VPS + services.register(VPS, icon='TuxBox.png') diff --git a/orchestra/contrib/vps/models.py b/orchestra/contrib/vps/models.py index d647871d..27691f3c 100644 --- a/orchestra/contrib/vps/models.py +++ b/orchestra/contrib/vps/models.py @@ -1,8 +1,7 @@ +from django.contrib.auth.hashers import make_password from django.db import models from django.utils.translation import ugettext_lazy as _ -from django.contrib.auth.hashers import make_password -from orchestra.core import services from orchestra.core.validators import validate_hostname from . import settings @@ -32,6 +31,3 @@ class VPS(models.Model): def get_username(self): return self.hostname - - -services.register(VPS) diff --git a/orchestra/contrib/webapps/apps.py b/orchestra/contrib/webapps/apps.py index 6f7c4650..7eb86eef 100644 --- a/orchestra/contrib/webapps/apps.py +++ b/orchestra/contrib/webapps/apps.py @@ -10,4 +10,4 @@ class WebAppsConfig(AppConfig): def ready(self): from . import signals from .models import WebApp - services.register(WebApp) + services.register(WebApp, icon='Applications-other.png') diff --git a/orchestra/contrib/websites/__init__.py b/orchestra/contrib/websites/__init__.py index e69de29b..93cab2d3 100644 --- a/orchestra/contrib/websites/__init__.py +++ b/orchestra/contrib/websites/__init__.py @@ -0,0 +1 @@ +default_app_config = 'orchestra.contrib.websites.apps.WebsitesConfig' diff --git a/orchestra/contrib/websites/apps.py b/orchestra/contrib/websites/apps.py index 7451c44f..41052863 100644 --- a/orchestra/contrib/websites/apps.py +++ b/orchestra/contrib/websites/apps.py @@ -1,17 +1,20 @@ from django.apps import AppConfig from django.contrib.contenttypes.fields import GenericRelation +from orchestra.core import services from orchestra.utils.db import database_ready -class WebsiteConfig(AppConfig): +class WebsitesConfig(AppConfig): name = 'orchestra.contrib.websites' def ready(self): if database_ready(): - from django.contrib.contenttypes.models import ContentType - from .models import Content - qset = Content.content_type.field.get_limit_choices_to() - for ct in ContentType.objects.filter(qset): - relation = GenericRelation('websites.Content') - ct.model_class().add_to_class('content_set', relation) +# from django.contrib.contenttypes.models import ContentType +# from .models import Content, Website +# qset = Content.content_type.field.get_limit_choices_to() +# for ct in ContentType.objects.filter(qset): +# relation = GenericRelation('websites.Content') +# ct.model_class().add_to_class('content_set', relation) + from .models import Website + services.register(Website, icon='Applications-internet.png') diff --git a/orchestra/contrib/websites/models.py b/orchestra/contrib/websites/models.py index 3ea12672..4dffd743 100644 --- a/orchestra/contrib/websites/models.py +++ b/orchestra/contrib/websites/models.py @@ -5,7 +5,7 @@ from django.db import models from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ -from orchestra.core import validators, services +from orchestra.core import validators from orchestra.utils.functional import cached from . import settings @@ -152,6 +152,3 @@ class Content(models.Model): domain = self.website.domains.first() if domain: return '%s://%s%s' % (self.website.get_protocol(), domain, self.path) - - -services.register(Website) diff --git a/orchestra/core/__init__.py b/orchestra/core/__init__.py index 0badee82..f29687bb 100644 --- a/orchestra/core/__init__.py +++ b/orchestra/core/__init__.py @@ -1,9 +1,12 @@ +from django.utils.translation import string_concat + from ..utils.python import AttrDict class Register(object): - def __init__(self): + def __init__(self, verbose_name=None): self._registry = {} + self.verbose_name = verbose_name def __contains__(self, key): return key in self._registry @@ -13,13 +16,21 @@ class Register(object): def register(self, model, **kwargs): if model in self._registry: - raise KeyError("%s already registered" % str(model)) - plural = kwargs.get('verbose_name_plural', model._meta.verbose_name_plural) - self._registry[model] = AttrDict(**{ - 'verbose_name': kwargs.get('verbose_name', model._meta.verbose_name), - 'verbose_name_plural': plural, - 'menu': kwargs.get('menu', True), - }) + raise KeyError("%s already registered" % model) + if 'verbose_name' not in kwargs: + kwargs['verbose_name'] = model._meta.verbose_name + if 'verbose_name_plural' not in kwargs: + kwargs['verbose_name_plural'] = model._meta.verbose_name_plural + self._registry[model] = AttrDict(**kwargs) + + def register_view(self, view_name, **kwargs): + if view_name in self._registry: + raise KeyError("%s already registered" % view_name) + if 'verbose_name' not in kwargs: + raise KeyError("%s verbose_name is required for views" % view_name) + if 'verbose_name_plural' not in kwargs: + kwargs['verbose_name_plural'] = string_concat(kwargs['verbose_name'], 's') + self._registry[view_name] = AttrDict(**kwargs) def get(self, *args): if args: @@ -27,7 +38,7 @@ class Register(object): return self._registry -services = Register() +services = Register(verbose_name='Services') # TODO rename to something else -accounts = Register() -administration = Register() +accounts = Register(verbose_name='Accounts') +administration = Register(verbose_name='Administration') diff --git a/orchestra/static/orchestra/icons/Appointment.png b/orchestra/static/orchestra/icons/Appointment.png new file mode 100644 index 0000000000000000000000000000000000000000..06e1fe943a394b28c9db124a0570ceb60b17473b GIT binary patch literal 4203 zcmV-x5R~tUP)L2z(yX>N3FWo~px=LWd|000zpMObt} zb#!QNasWzUYjt8EQ*>o%Ze?=j`}Z#Z001F$MObuGZ*_8GWdLY&bZ|N^FKTIRZDC_B zZFO^LV`yP)Y%XJZX=dYI000kqNklufP?C~n5)w*6l4eXwCMl$lbQ}Vdr=hVOCT`jk2+#nn0o#nh z*v81%NS3U(w5xsHecZ=6{X@I*>ajKu+Mb#3oO}1azMu2^opbKlt4JyNpv#S$ZwzHr zUyCMWGn$qMG32SvjNE(gz4izFEaeXc*m}uUy{|d*QIALeyyJ+EOCcIO9*+{J3R%HW z5XZ7HGp3R=vtEJ7DJZ={%6}13?0#g&Pu~821Nh?RTb8Q^x4EwTv!>QYwSDOlZ&OR7 z;`8~DNDBD^`FsHYRa4Pa4a>4gOePo|8n%b~PB{h3iXp`Pj-mYI(MRtv|4#t6Z2sFO zO;>*8@#ve^Z@SRCXz@Z7AQqb>9-AbcHnD9RAp}y08MUr!1cO0B)l~$7LClOve@|~= zbYL(qrM$I$wCcX^KmUE_e+}S^pS$H|RaLgGyKsZ2YxN2}kw`N*IErazky0RqLBTfRH{s-m=-KmWh7NrXe|QxQ%f3vYb`7c?#OJZ59-yiO`~eh2nIQm0QP6dr zwzg(guIk{5Pj1BPH89g@CPs$o=<4!oy8feUKl6=C-ctnr@^d$Tp|-B>-pjALBJj?M ze$r_ZpATAFeOz+!CUjkAvVSK7uis4V!m9{H&PPfYDQ%>1kkUoCF6ly$#Rc|*z^PH5_~mmrjzhGy4Ufm*oi~pr9gkf0=%aUxoizemwrur*a`(ndFA59~ zjgm;D&@`9&2z+$YhtYMNc;DlU9Q`pZtN)HrWIa+^NRdN2S)`jqI3@^_pf-$0XGV*c z*RzYy=cTDB!ey6#7=Y1%Q)s$Qq$wKIEb+tnu9=%cdGn_B&W>O(=x1No12ozA>1hl*U2K>Ea1*-zET|w1?X7P#`+CiNGTaP z)lbucg}SQgpS^DLO-s%QaNXvc>QqI3``q(CW+T9el8*f~qQw&d$Xsih`<#*mUK6Jp1fZY`gy%ZohLku9HJJQxTYg z$ReC9!Zi_27Ac&wrp|oD?>D&Q;}-xhe5#*lORK6V>K}jcv$wQV1W;A=ngxp%8e^m5 z2q~zm(^$QF6{@OEPfjQvR&<@mOE2%`aQ8UZf9-KH=@hP$#kHqQPw8|7$|tsWTU1q{ zYgH$qPyj2JBbPO)4u=&X+$-k=xb{=GMx>-G(hwmw5l0G%->=fr(md}2>LQK&?zz1* zw{>#W^*b0Gjv?G!G51Ye$Ha9qAl`clm5RsES+!;<5=h45)I{ohipn+f0;t}?6-^6T zMLwUWP$=N_LL^d$q9}7(urvhfy3Rj8w3}={FZ}9h7e8>q7(&T&$;&9)`}uo z|3|AqN|Lb&YU(0L!MZPe;j7hi05nDWShTsx7mp{Vru48ud;5Zl?=|K6#x1wAZQIYe z@w@wZ^7+2$j_f*Fgs>}`JX;Y$5UCGics=CvIb6rV7xdePZCp490F*^upN~Q=j}(GX zl|glN%?$IEK!gxne#NJG`ghOqvqy%w=kb#WC(GD)k~a=@&pCg-dseEYlr%L*K|-OB z$K&;C64^QjKp3+a?_6EkjrPM_Z0=g;VPt**o6YRu6u#G07zM{>mCXP3n3*Q zT?Zub$r!I6I>76P4lq73Hm`Y6TyY5@sIRYQ%XMF+ws9TZ$KK(MLr3`1m%hwxxBZ}^ z@zS$@ZiqDC^ZD_4eMB1?E53-8L#R|Jad>+rt8yzl$?9+8g9JtZx|UFp{uKl2OfBU>#x6VdP1xuMNwF^ zpkqcOAr&U$Nu-o`41=7Rm5QWi4uB#V&gb$Ss;*D1m>h|1+Zcwy>@uI}Qi;doeEZwq zp-?EGX)15OeT-lH;vUwn{m`6rJKM-K^&E8?!0>u;Y`dT+%HTW^pg(73KjQWKkWw%? zX=0jLJRVO)^QT9O(=gxq)~$?>kF#OJ`P_Nu9cOLzvjG`iF9ZEUAS7O&AKNO}Qp%yy z^G*+=uK2I`_;@}T3L%7Guz!N_@mR$nbN03nf~TH(nkSxk0b!|Wf^UBF z=3*3Xp|i7N&Ux=O3Wnih&x`w!Qczppz~sa@Qquj%uYVYu1F(JjUBe)I6R|kK>S|me z7#WO{N~J4~{pG)J-~JdQBO{!5-g(?`!(W|o`S@<5;r04>^_7E2DXEP{NyTE8s`6-g zO3z#mNFJLQ9dW9|VWbcoeEkHm*yJ2D^F?6Orj1;A<(2&GXLnDh!XE+=42F2=#aBtC zGWY`_eEtBLbjFdmyDI{?9_PWyu?gEMSX6~;$!GH%KQ=%rm97X;Qdd_O_uR9M_V)I9 zkI&a_nUK$#lM{YQ?xYNs=ayZTQYFN7lzf>l*a#*;kzZG3%}4hi(UailX=`MZhTO*Z=Y0j&v&dT5or^ z-MpwB!|-ylXOL%~d<8R;M@r7xk^{r1NTw1DjSNh^qF3k#rTyxvFp0RyKm1@Dj^on2 za1p`kYEHe=lOLNHxb3w=FH8U)z@wDSoaF#oxt@A{=XVnE*!WOypIo?PDZ0l)_t736 zfB3g#%>qc}j1idq4@9NTuW1@}bqyTY|0cJ8@26zV9FfLG+S)rfapX;FGBN(IPyhZ` z&lY{@Wge$8r!?j&iV_5ZK&VuAELz^NalYiwKKZ)|H*`j5YjnNH#Nd-=qb7qg

jx`_-6qGfzXnURVyx7_p!eZN9wEFmo3*uPW2NT9RVOx zALf&rFQaW?Gf07uF1BsW04X=Esw#m%5KZ-4ARaL5MLe$iR$mI$o;z@GFj*Kj&kWO7R%7V@%ejiX2rQCF>_7%7NfbX0Y8&a=+ zumng&RW!pDP9~X*@9Q6WXU~!D{fAxG%>x!tC<2sg8bnqwde*O$ilP`r0B@OjqgZ>2 z+j>d$wTBVE_XAMzW>a9Z1!Vx0a{2w1{{cFPUecP={G0#)002ovPDHLkV1lE; B5S0J` literal 0 HcmV?d00001 diff --git a/orchestra/static/orchestra/icons/Appointment.svg b/orchestra/static/orchestra/icons/Appointment.svg new file mode 100644 index 00000000..772b37ac --- /dev/null +++ b/orchestra/static/orchestra/icons/Appointment.svg @@ -0,0 +1,413 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + New Appointment + + + appointment + new + meeting + rvsp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/static/orchestra/icons/Edit-check-sheet.png b/orchestra/static/orchestra/icons/Edit-check-sheet.png new file mode 100644 index 0000000000000000000000000000000000000000..1fcec45a1c9a3fdf81410241cc9b825396d50969 GIT binary patch literal 19927 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 i$jd0|s^0XK)&B<{5eh=XODtLd0000 + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/static/orchestra/icons/Mail-send.png b/orchestra/static/orchestra/icons/Mail-send.png new file mode 100644 index 0000000000000000000000000000000000000000..2f8fd1f9384af865c565d543c64b1996d241e981 GIT binary patch literal 3237 zcmV;W3|jMvP)bmtK4o#zJW}MU;XC|3=;>P)-Go4PFnI_Xm{^?8_ zwc|L67dta$693VrX_;6Q6RSvu27_#2Ad+|p5D0-*TCsQUIsIcF+7}>U$7#>ZZ}!~R zx!>>aeeMdaHNVRdjzsIE6kpJqr?l2PhWh&w%X!}XZ2`O82uGqje75;D!;p^(Fg5zS zgWq2sc=(VBgd@>%A^4na8h;k>TMgY?n*AN?>gD(UWhmh({SSAt=7&Nc9Eo<>mhqe+ zUj)LDXtQC+FPVn?Qe$12-P_$3 z2({N^nv&^Rmv_%y=Z34uqDxPzf;ML_#W;K6nxWyP_k&^C1j3Q10D66v`E{RV_T0Cv zS?>rn+f6k=X69YaPA@PUcT;!^^r`_NpekVT>aR|^LWq}#`agU9Ln9E5M4z!tOGH0bYH#4=g}JP@fg)EaMWiZdG4$>v^<|#*=!V%t{r~j6qL(bCJJ4vou&^*CmfK2% zBhh;dL+<;+W7~r3n`<~Z86%NY1&bFFNlpBgL0e-Pe;0j|uRa;#<6BqdfzDtloEH)k zLJ*6^czMqck;_644E6U7dY=0I7hgLvfAY)>Ya4<{2})}`l@gT}PiKxnDX1>989j3q z*G;lxb0bPAlu~$}hf*pJcxDJ85JF_URHuRG645Cl8mMqA-u(3hpSr&dAr>`VD7j=xT|0jKI2$)^1h_K-;YhT_G~~Cp zcQ#XB>2Q8}0i{R**QJS61}vto#W{KA3Xk5?vLvv%0wAU2`0?YkwY6oj+({Z;Zy566 zj;_|4hr8Drr!U2rjVJRmjVA8^O)7Z*>&Q4gp(ZMWc1a)4K2qYGVHk{$kF#RM3Zz`r z`?4$$jzqT@QXYD6`?{K4;a211g%~q)?xOqT`Ml0r#IX#t(i|Q+%TqmVOVW6*NGZ8? z?HY+hf?zP1M{vmm!jb6RhLpeRy?1@Zj!?7l-i2AN$KAz%O-ohKVs)*9mkwN|G*x95 z$9^+KV|9Ry&6T$VTj)}TVUS297#$s@si~`^yooT%F^OAL*j0#!tdkoSmBG z%GDSf8en{U45dGv?lrKK@P^ZyUU(SiiS=^k8 zs{sM5ta0$0lD+$mvAWLT+t2L4Gz8yyzHH^qm0Yckj*hZs%^DUK z7VxCUr7M?iSriGI+ zwjl+1+WPapkCJdbp6FRuq`BNL*z$t#m2&nIbaY`)oZI*&m2 zvwQy1w#;vQx;Nza+ZLxLW2kf+kcnC$1kH60uIury`$yQ>wUVzq(V1O5{{`?DpSv4F zijqwkq^j%8%nYV!V%s)}L;}+^sjR4^vZ9j7sY%ApkI~Z9QsgN@h(#B==h;{O-fvqk z?CuTuP1EG`#Tc%q@_HH#G}k)J&nI~8m&bUtdktTHe9Ns2X@k*F{X*&htpeUIM^+@-hbCOw(LaQ)XEf0|NtW-n_Z=Gk}l+pU=Lj1RRIi+3RSnh0t0Pjzm8xrTpn9c6`LY zqNa?AEAyCAu)5Y^d~$}tBd7W1)7#m)wyG$b#N%<^e*0~9?%WALB9SOrq%6xqDaF9R z09&?f!RPZ8S;=u6lve!c<(J}UJyy~LdDdkjyC)7uqK_L=zS4VthrhZafMp2YJ2S2S z`^YKpJI`!4@7mCq2QK%;uIqC6$YC~b+Dt=3Ls2{cOifKOe0Z4l_I6g(ugD9;Tmo8a ze*E%FZ$9_c=kL880mvz2L;Za}3rC_)|Hm8e{NR&4oBWrq&Z*&}W3l^IUEaUBrTPyF zt6&y<(q-hm5n9_?5kheE=uzVFILTx(yWjOZk67$ZlsXJVli4r0*4^V0Ggb-f(oR?Xvq^Z*1VhkaK?I|yNkLP*B6LC5=bRZC1zI++ib(d7U zO4SIT?PFELDz09?O43U%UQpTq{5j$)l(c0;kp)D`Rz^m&(h^cFvSArC)iyCbJ&o&T zKH%P3>$e+12x_Zpi7&)aN~N=tmVixK+oTkoDPvCz6 XN7&{$o2Wm&00000NkvXXu0mjf0(UCu literal 0 HcmV?d00001 diff --git a/orchestra/static/orchestra/icons/Mail-send.svg b/orchestra/static/orchestra/icons/Mail-send.svg new file mode 100644 index 00000000..b114286a --- /dev/null +++ b/orchestra/static/orchestra/icons/Mail-send.svg @@ -0,0 +1,999 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Send and Receive Mail + + + Jakub Steiner + + + + + Andreas Nilsson, Garrett LeSage + + + + + + mail + e-mail + send + receive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/static/orchestra/icons/Multimedia-volume-control.png b/orchestra/static/orchestra/icons/Multimedia-volume-control.png new file mode 100644 index 0000000000000000000000000000000000000000..9e841739b2230fe9d7a35fe0526e2e76a859651a GIT binary patch literal 2732 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + Volume Control + + + multimedia + sound + volume + knob + control + mixer + + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + + + + + + + + + + + + + + + + + + +