From 8c13e75d5d6ffd2ecfa25115bd8060b5da2334c2 Mon Sep 17 00:00:00 2001 From: Marc Date: Wed, 23 Jul 2014 18:28:40 +0000 Subject: [PATCH] Improved admin dashboard --- orchestra/admin/menu.py | 83 +- orchestra/admin/utils.py | 10 +- orchestra/apps/accounts/admin.py | 3 +- orchestra/apps/issues/admin.py | 17 +- orchestra/apps/issues/models.py | 2 +- orchestra/apps/resources/models.py | 1 - orchestra/conf/base_settings.py | 4 +- orchestra/static/orchestra/icons/Pack.png | Bin 0 -> 2857 bytes orchestra/static/orchestra/icons/Pack.svg | 4350 +++++++++++++++++ .../static/orchestra/icons/Taskstate.svg | 512 ++ .../orchestra/icons/applications-other.png | Bin 0 -> 2625 bytes .../orchestra/icons/applications-other.svg | 807 +++ orchestra/static/orchestra/icons/invoice.png | Bin 0 -> 2164 bytes orchestra/static/orchestra/icons/invoice.svg | 679 +++ .../static/orchestra/icons/taskstate.png | Bin 3868 -> 3242 bytes 15 files changed, 6418 insertions(+), 50 deletions(-) create mode 100644 orchestra/static/orchestra/icons/Pack.png create mode 100644 orchestra/static/orchestra/icons/Pack.svg create mode 100644 orchestra/static/orchestra/icons/Taskstate.svg create mode 100644 orchestra/static/orchestra/icons/applications-other.png create mode 100644 orchestra/static/orchestra/icons/applications-other.svg create mode 100644 orchestra/static/orchestra/icons/invoice.png create mode 100644 orchestra/static/orchestra/icons/invoice.svg diff --git a/orchestra/admin/menu.py b/orchestra/admin/menu.py index 3fb6bffc..c3e7b4f1 100644 --- a/orchestra/admin/menu.py +++ b/orchestra/admin/menu.py @@ -38,49 +38,71 @@ def get_services(): return sorted(result, key=lambda i: i.title) -def get_accounts(): - accounts = [ +def get_account_items(): + childrens = [ items.MenuItem(_("Accounts"), reverse('admin:accounts_account_changelist')) ] if isinstalled('orchestra.apps.contacts'): url = reverse('admin:contacts_contact_changelist') - accounts.append(items.MenuItem(_("Contacts"), url)) + childrens.append(items.MenuItem(_("Contacts"), url)) if isinstalled('orchestra.apps.users'): url = reverse('admin:users_user_changelist') users = [items.MenuItem(_("Users"), url)] if isinstalled('rest_framework.authtoken'): tokens = reverse('admin:authtoken_token_changelist') users.append(items.MenuItem(_("Tokens"), tokens)) - accounts.append(items.MenuItem(_("Users"), url, children=users)) + childrens.append(items.MenuItem(_("Users"), url, children=users)) if isinstalled('orchestra.apps.prices'): url = reverse('admin:prices_pack_changelist') - accounts.append(items.MenuItem(_("Packs"), url)) + childrens.append(items.MenuItem(_("Packs"), url)) if isinstalled('orchestra.apps.orders'): url = reverse('admin:orders_order_changelist') - accounts.append(items.MenuItem(_("Orders"), url)) - return accounts - - -def get_administration(): - administration = [] - return administration - - -def get_administration_models(): - administration_models = [] - if isinstalled('orchestra.apps.orchestration'): - administration_models.append('orchestra.apps.orchestration.*') - if isinstalled('djcelery'): - administration_models.append('djcelery.*') + childrens.append(items.MenuItem(_("Orders"), url)) if isinstalled('orchestra.apps.issues'): - administration_models.append('orchestra.apps.issues.*') - if isinstalled('orchestra.apps.resources'): - administration_models.append('orchestra.apps.resources.*') - if isinstalled('orchestra.apps.miscellaneous'): - administration_models.append('orchestra.apps.miscellaneous.models.MiscService') + url = reverse('admin:issues_ticket_changelist') + childrens.append(items.MenuItem(_("Tickets"), url)) + return childrens + + +def get_administration_items(): + childrens = [] if isinstalled('orchestra.apps.orders'): - administration_models.append('orchestra.apps.orders.models.Service') - return administration_models + url = reverse('admin:orders_service_changelist') + childrens.append(items.MenuItem(_("Services"), url)) + if isinstalled('orchestra.apps.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.apps.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.apps.miscellaneous'): + url = reverse('admin:miscellaneous_miscservice_changelist') + childrens.append(items.MenuItem(_("Miscellaneous"), url)) + if isinstalled('orchestra.apps.issues'): + url = reverse('admin:issues_queue_changelist') + childrens.append(items.MenuItem(_("Issue 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(_("Celery"), task, children=[ + items.MenuItem(_("Tasks"), task), + items.MenuItem(_("Periodic tasks"), periodic), + items.MenuItem(_("Workers"), worker), + ])) + return childrens class OrchestraMenu(Menu): @@ -99,12 +121,11 @@ class OrchestraMenu(Menu): items.MenuItem( _("Accounts"), reverse('admin:accounts_account_changelist'), - children=get_accounts() + children=get_account_items() ), - items.AppList( + items.MenuItem( _("Administration"), - models=get_administration_models(), - children=get_administration() + children=get_administration_items() ), items.MenuItem("API", api_link(context)) ] diff --git a/orchestra/admin/utils.py b/orchestra/admin/utils.py index 9340e8ed..70a0c77d 100644 --- a/orchestra/admin/utils.py +++ b/orchestra/admin/utils.py @@ -76,7 +76,8 @@ def admin_link(*args, **kwargs): order = kwargs.pop('order', field) popup = kwargs.pop('popup', False) - def display_link(self, instance): + def display_link(*args): + instance = args[-1] obj = getattr(instance, field, instance) if not getattr(obj, 'pk', None): return '---' @@ -95,7 +96,7 @@ def admin_link(*args, **kwargs): def colored(field_name, colours, description='', verbose=False, bold=True): """ returns a method that will render obj with colored html """ - def colored_field(modeladmin, obj, field=field_name, colors=colours, verbose=verbose): + def colored_field(obj, field=field_name, colors=colours, verbose=verbose): value = escape(get_field_value(obj, field)) color = colors.get(value, "black") if verbose: @@ -133,11 +134,12 @@ def admin_date(field, **kwargs): default = kwargs.pop('default', '') order = kwargs.pop('order', field) - def display_date(self, instance): + def display_date(*args): + instance = args[-1] value = get_field_value(instance, field) if not value: return default - return '
{1}
'.format( + return '{1}'.format( escape(str(value)), escape(naturaldate(value)), ) display_date.short_description = _(field.replace('_', ' ')) diff --git a/orchestra/apps/accounts/admin.py b/orchestra/apps/accounts/admin.py index 2f8f7024..fd046721 100644 --- a/orchestra/apps/accounts/admin.py +++ b/orchestra/apps/accounts/admin.py @@ -60,8 +60,9 @@ class AccountAdmin(ExtendedModelAdmin): if not account.is_active: messages.warning(request, 'This account is disabled.') context = { + # TODO not services but everythin (payments, bills, etc) 'services': sorted( - [ model._meta for model in services.get() ], + [ model._meta for model in services.get() if model is not Account ], key=lambda i: i.verbose_name_plural.lower() ) } diff --git a/orchestra/apps/issues/admin.py b/orchestra/apps/issues/admin.py index 4f37dc7c..d0c43ea8 100644 --- a/orchestra/apps/issues/admin.py +++ b/orchestra/apps/issues/admin.py @@ -55,8 +55,8 @@ class MessageReadOnlyInline(admin.TabularInline): def content_html(self, msg): context = { 'number': msg.number, - 'time': display_timesince(msg.created_on), - 'author': link('author')(self, msg) if msg.author else msg.author_name, + 'time': admin_date('created_on')(msg), + 'author': admin_link('author')(msg) if msg.author else msg.author_name, } summary = _("#%(number)i Updated by %(author)s about %(time)s") % context header = '%s
' % summary @@ -113,7 +113,7 @@ class TicketInline(admin.TabularInline): last_modified = admin_link('last_modified_on') def ticket_id(self, instance): - return '%s' % link()(self, instance) + return '%s' % admin_link()(instance) ticket_id.short_description = '#' ticket_id.allow_tags = True @@ -197,17 +197,18 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView display_creator = admin_link('creator') display_queue = admin_link('queue') display_owner = admin_link('owner') + last_modified = admin_date('last_modified_on') def display_summary(self, ticket): context = { - 'creator': link('creator')(self, ticket) if ticket.creator else ticket.creator_name, - 'created': display_timesince(ticket.created_on), + 'creator': admin_link('creator')(self, ticket) if ticket.creator else ticket.creator_name, + 'created': admin_date('created_on')(ticket), 'updated': '', } msg = ticket.messages.last() if msg: context.update({ - 'updated': display_timesince(msg.created_on), + 'updated': admin_date('created_on')(msg), 'updater': admin_link('author')(self, msg) if msg.author else msg.author_name, }) context['updated'] = '. Updated by %(updater)s about %(updated)s' % context @@ -245,10 +246,6 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView bold_subject.short_description = _("Subject") bold_subject.admin_order_field = 'subject' - def last_modified(self, instance): - return display_timesince(instance.last_modified_on) - last_modified.admin_order_field = 'last_modified_on' - def formfield_for_dbfield(self, db_field, **kwargs): """ Make value input widget bigger """ if db_field.name == 'subject': diff --git a/orchestra/apps/issues/models.py b/orchestra/apps/issues/models.py index 7922b2de..e0a8b142 100644 --- a/orchestra/apps/issues/models.py +++ b/orchestra/apps/issues/models.py @@ -85,7 +85,7 @@ class Ticket(models.Model): if self.owner: emails.append(self.owner.email) for contact in self.creator.account.contacts.all(): - if self.queue and set(contact.email_usage).union(set(self.queue.nofify)): + if self.queue and set(contact.email_usage).union(set(self.queue.notify)): emails.append(contact.email) for message in self.messages.distinct('author'): emails.append(message.author.email) diff --git a/orchestra/apps/resources/models.py b/orchestra/apps/resources/models.py index 51a0c664..dde199c3 100644 --- a/orchestra/apps/resources/models.py +++ b/orchestra/apps/resources/models.py @@ -173,7 +173,6 @@ def create_resource_relation(): resource = Resource.objects.get(content_type__model=model, name=attr, is_active=True) data = ResourceData(content_object=self.obj, resource=resource) - print data.resource_id, data.content_type_id, data.object_id setattr(self, attr, data) return data diff --git a/orchestra/conf/base_settings.py b/orchestra/conf/base_settings.py index 2672a067..213deac4 100644 --- a/orchestra/conf/base_settings.py +++ b/orchestra/conf/base_settings.py @@ -145,6 +145,7 @@ FLUENT_DASHBOARD_APP_GROUPS = ( 'orchestra.apps.prices.models.Pack', 'orchestra.apps.bills.models.Bill', 'orchestra.apps.payments.models.Transaction', + 'orchestra.apps.issues.models.Ticket', ), 'collapsible': True, }), @@ -154,7 +155,6 @@ FLUENT_DASHBOARD_APP_GROUPS = ( 'orchestra.apps.orchestration.models.Route', 'orchestra.apps.orchestration.models.BackendLog', 'orchestra.apps.orchestration.models.Server', - 'orchestra.apps.issues.models.Ticket', 'orchestra.apps.resources.models.Resource', 'orchestra.apps.resources.models.Monitor', 'orchestra.apps.orders.models.Service', @@ -185,13 +185,13 @@ FLUENT_DASHBOARD_APP_ICONS = { 'prices/pack': 'Pack.png', 'bills/bill': 'invoice.png', 'payments/transaction': 'transaction.png', + 'issues/ticket': 'Ticket_star.png', # Administration 'users/user': 'Mr-potato.png', 'djcelery/taskstate': 'taskstate.png', 'orchestration/server': 'vps.png', 'orchestration/route': 'hal.png', 'orchestration/backendlog': 'scriptlog.png', - 'issues/ticket': 'Ticket_star.png', 'resources/resource': "gauge.png", 'resources/monitor': "Utilities-system-monitor.png", } diff --git a/orchestra/static/orchestra/icons/Pack.png b/orchestra/static/orchestra/icons/Pack.png new file mode 100644 index 0000000000000000000000000000000000000000..24d48ced937fd2e60c69e7c55a5eb5b4b0965790 GIT binary patch literal 2857 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 Hu0mjf$L*9A literal 0 HcmV?d00001 diff --git a/orchestra/static/orchestra/icons/Pack.svg b/orchestra/static/orchestra/icons/Pack.svg new file mode 100644 index 00000000..7afdb383 --- /dev/null +++ b/orchestra/static/orchestra/icons/Pack.svg @@ -0,0 +1,4350 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/static/orchestra/icons/Taskstate.svg b/orchestra/static/orchestra/icons/Taskstate.svg new file mode 100644 index 00000000..0471b135 --- /dev/null +++ b/orchestra/static/orchestra/icons/Taskstate.svg @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/orchestra/static/orchestra/icons/applications-other.png b/orchestra/static/orchestra/icons/applications-other.png new file mode 100644 index 0000000000000000000000000000000000000000..8e909384ae88d045237b1f80666660bd6a480275 GIT binary patch literal 2625 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 + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/static/orchestra/icons/invoice.png b/orchestra/static/orchestra/icons/invoice.png new file mode 100644 index 0000000000000000000000000000000000000000..6a5daa2cf4cb1be6c42f727c427d381a2a15144a GIT binary patch literal 2164 zcmV-)2#fcLP)uf~v|xRaNavQxqv}9x5Kn3+h9Y-c&&vM3HS0N-+r+W56-B4Z`=b zyWsWC?9S!%VK403V|#B8e5E6K&d&VK_dDlzn`0rRWFh+d`_DR#^V*^xsthS5mStgC z7OJWe2m~&=uKVNe?(Un#+7Mm>E?>Sp+S=N>DHse^jjRwNM>>nF0LO6Or;V?~2O@XPYsUt#&pUDM; zJOyNlXBTuFhgd91ZEYPspXW{%P5#qQFEBGZ!;!;BbL4ehXYJaxtX{nu%d&A@m#kJH z6l!W#F*P+M07VGl=8ITXKz2+_PVmcL{eqwVj(JL%hI+JcHPE-6b>0*u61&`+8_>W`PymjO)jvPKpds{mCEES{ki8g&6nWhmThNBj)A4{^;f<@Z(k1`d-l@M z&_G>X9nUsD%e%*ajqAF!v}~oNrG?MGxB@^h7-WC=~+ zlPCXxPuDqm_$ZqgG{bU9>wJX?_$7Ca#Ywrz9k z)-6u~%hrMXYumPpCZuV_7dx-MD1S~>)m+;3LQ#dxG!xt%x>gh+UH7wn=krAi$Y0Y7 zLI_;fUD6YZ3gSQ@#B+PPOD0tftd!D=+5saGXXy5ost{`4w1b8<>x#aAVJcsUqGBKz zs$oy(*Gio+zb)93!jxFL*l7M`;jziF|@ZCeLPbNiqF7^8l2Y9>NpFWC^l1K9?l9okd zO#mDh+i^K{y|Ct#90O9yf|eR7kq7=* zxscbnkZHuYKX#{L0ZAiTU}ruaBWW6>Qpw!fmTfL_;j((6e1vKmY1!UU5qSXp{G|dr zvFHp&BF;1G(?^z$XTT7VZ>+fH$R>{dnOA?+gh zcIM`yBuxX$GIPH#ELvp?$RCQLvZk@}RstxnqiKFjV?H;R{g36s9Vq#uK0lDt4-0~! z)fJPMQWn@rX0lIqE_;NMFQDY3MXxP041<+^$`_s+&ebp%7hNT@}zSA1h_Q6@&O`K*k$9 zzhM`((?ST + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + Invoice + + + + + diff --git a/orchestra/static/orchestra/icons/taskstate.png b/orchestra/static/orchestra/icons/taskstate.png index 777c58989462c1b767a27a19d2ca1be90061cfeb..9280cb7f64e8d71a9222273613e5c42216bf2eda 100755 GIT binary patch delta 3214 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 A$N&HU delta 3845 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 Hu0mjf2Mn3w