Admin interface improvements

This commit is contained in:
Marc 2014-07-22 21:47:01 +00:00
parent ccbda512bf
commit e57226b769
10 changed files with 192 additions and 89 deletions

View File

@ -10,7 +10,7 @@ from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from orchestra.models.utils import get_field_value
from orchestra.utils.time import timesince, timeuntil
from orchestra.utils.humanize import naturaldate
def get_modeladmin(model, import_module=True):
@ -78,7 +78,7 @@ def admin_link(*args, **kwargs):
def display_link(self, instance):
obj = getattr(instance, field, instance)
if not getattr(obj, 'pk', False):
if not getattr(obj, 'pk', None):
return '---'
opts = obj._meta
view_name = 'admin:%s_%s_change' % (opts.app_label, opts.model_name)
@ -95,7 +95,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(obj, field=field_name, colors=colours, verbose=verbose):
def colored_field(modeladmin, obj, field=field_name, colors=colours, verbose=verbose):
value = escape(get_field_value(obj, field))
color = colors.get(value, "black")
if verbose:
@ -113,22 +113,40 @@ def colored(field_name, colours, description='', verbose=False, bold=True):
return colored_field
def display_timesince(date, double=False):
"""
Format date for messages create_on: show a relative time
with contextual helper to show fulltime format.
"""
if not date:
return 'Never'
date_rel = timesince(date)
if not double:
date_rel = date_rel.split(',')[0]
date_rel += ' ago'
date_abs = date.strftime("%Y-%m-%d %H:%M:%S %Z")
return mark_safe("<span title='%s'>%s</span>" % (date_abs, date_rel))
#def display_timesince(date, double=False):
# """
# Format date for messages create_on: show a relative time
# with contextual helper to show fulltime format.
# """
# if not date:
# return 'Never'
# date_rel = timesince(date)
# if not double:
# date_rel = date_rel.split(',')[0]
# date_rel += ' ago'
# date_abs = date.strftime("%Y-%m-%d %H:%M:%S %Z")
# return mark_safe("<span title='%s'>%s</span>" % (date_abs, date_rel))
def display_timeuntil(date):
date_rel = timeuntil(date) + ' left'
date_abs = date.strftime("%Y-%m-%d %H:%M:%S %Z")
return mark_safe("<span title='%s'>%s</span>" % (date_abs, date_rel))
def admin_date(field, **kwargs):
""" utility function for creating admin dates """
default = kwargs.pop('default', '')
order = kwargs.pop('order', field)
def display_date(self, instance):
value = get_field_value(instance, field)
if not value:
return default
return '<div title="{0}">{1}</div>'.format(
escape(str(value)), escape(naturaldate(value)),
)
display_date.short_description = _(field.replace('_', ' '))
display_date.admin_order_field = order
display_date.allow_tags = True
return display_date
#def display_timeuntil(date):
# date_rel = timeuntil(date) + ' left'
# date_abs = date.strftime("%Y-%m-%d %H:%M:%S %Z")
# return mark_safe("<span title='%s'>%s</span>" % (date_abs, date_rel))

View File

View File

@ -12,8 +12,7 @@ from django.utils.translation import ugettext_lazy as _
from markdown import markdown
from orchestra.admin import ChangeListDefaultFilter, ExtendedModelAdmin#, ChangeViewActions
from orchestra.admin.utils import (admin_link, colored, wrap_admin_view,
display_timesince)
from orchestra.admin.utils import admin_link, colored, wrap_admin_view, admin_date
from orchestra.apps.contacts import settings as contacts_settings
from .actions import (reject_tickets, resolve_tickets, take_tickets, close_tickets,
@ -110,6 +109,8 @@ class TicketInline(admin.TabularInline):
creator_link = admin_link('creator')
owner_link = admin_link('owner')
created = admin_link('created_on')
last_modified = admin_link('last_modified_on')
def ticket_id(self, instance):
return '<b>%s</b>' % link()(self, instance)
@ -123,12 +124,6 @@ class TicketInline(admin.TabularInline):
def colored_priority(self, instance):
return colored('priority', PRIORITY_COLORS, bold=False)(instance)
colored_priority.short_description = _("Priority")
def created(self, instance):
return display_timesince(instance.created_on)
def last_modified(self, instance):
return display_timesince(instance.last_modified_on)
class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeViewActions,
@ -327,7 +322,7 @@ class QueueAdmin(admin.ModelAdmin):
}
def num_tickets(self, queue):
num = queue.tickets.count()
num = queue.tickets__count
url = reverse('admin:issues_ticket_changelist')
url += '?my_tickets=False&queue=%i' % queue.pk
return '<a href="%s">%d</a>' % (url, num)

View File

@ -14,7 +14,7 @@ class MiscServiceAdmin(admin.ModelAdmin):
def num_instances(self, misc):
""" return num slivers as a link to slivers changelist view """
num = misc.instances.count()
num = misc.instances__count
url = reverse('admin:miscellaneous_miscellaneous_changelist')
url += '?service={}'.format(misc.pk)
return mark_safe('<a href="{0}">{1}</a>'.format(url, num))

View File

@ -2,10 +2,9 @@ from django.contrib import admin
from django.core.urlresolvers import reverse
from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _
from djcelery.humanize import naturaldate
from orchestra.admin.html import monospace_format
from orchestra.admin.utils import admin_link
from orchestra.admin.utils import admin_link, admin_date, colored
from .models import Server, Route, BackendLog, BackendOperation
@ -89,13 +88,9 @@ class BackendLogAdmin(admin.ModelAdmin):
readonly_fields = fields
server_link = admin_link('server')
def display_state(self, log):
color = STATE_COLORS.get(log.state, 'grey')
return '<span style="color: %s;">%s</span>' % (color, log.state)
display_state.short_description = _("state")
display_state.allow_tags = True
display_state.admin_order_field = 'state'
display_last_update = admin_date('last_update')
display_created = admin_date('created')
display_state = colored('state', STATE_COLORS)
def mono_script(self, log):
return monospace_format(escape(log.script))
@ -113,20 +108,6 @@ class BackendLogAdmin(admin.ModelAdmin):
return monospace_format(escape(log.traceback))
mono_traceback.short_description = _("traceback")
def display_last_update(self, log):
return '<div title="{0}">{1}</div>'.format(
escape(str(log.last_update)), escape(naturaldate(log.last_update)),
)
display_last_update.short_description = _("last update")
display_last_update.allow_tags = True
def display_created(self, log):
return '<div title="{0}">{1}</div>'.format(
escape(str(log.created)), escape(naturaldate(log.created)),
)
display_created.short_description = _("created")
display_created.allow_tags = True
def get_queryset(self, request):
""" Order by structured name and imporve performance """
qs = super(BackendLogAdmin, self).get_queryset(request)

View File

@ -2,11 +2,12 @@ from django import forms
from django.db import models
from django.contrib import admin
from django.core.urlresolvers import reverse
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ChangeListDefaultFilter
from orchestra.admin.filters import UsedContentTypeFilter
from orchestra.admin.utils import admin_link
from orchestra.admin.utils import admin_link, admin_date
from orchestra.apps.accounts.admin import AccountAdminMixin
from orchestra.core import services
@ -49,9 +50,9 @@ class ServiceAdmin(admin.ModelAdmin):
return super(ServiceAdmin, self).formfield_for_dbfield(db_field, **kwargs)
def num_orders(self, service):
num = service.orders.count()
num = service.orders__count
url = reverse('admin:orders_order_changelist')
url += '?service=%i' % service.pk
url += '?service=%i&is_active=True' % service.pk
return '<a href="%s">%d</a>' % (url, num)
num_orders.short_description = _("Orders")
num_orders.admin_order_field = 'orders__count'
@ -59,20 +60,36 @@ class ServiceAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super(ServiceAdmin, self).get_queryset(request)
qs = qs.annotate(models.Count('orders'))
# Count active orders
qs = qs.extra(select={
'orders__count': (
"SELECT COUNT(*) "
"FROM orders_order "
"WHERE orders_order.service_id = orders_service.id AND ("
" orders_order.cancelled_on IS NULL OR"
" orders_order.cancelled_on > '%s' "
")" % timezone.now()
)
})
return qs
class OrderAdmin(AccountAdminMixin, ChangeListDefaultFilter, admin.ModelAdmin):
list_display = (
'id', 'service', 'account_link', 'content_object_link', 'cancelled_on'
'id', 'service', 'account_link', 'content_object_link',
'display_registered_on', 'display_cancelled_on'
)
list_display_link = ('id', 'service')
list_filter = (ActiveOrderListFilter, 'service',)
date_hierarchy = 'registered_on'
default_changelist_filters = (
('is_active', 'True'),
)
content_object_link = admin_link('content_object')
display_registered_on = admin_date('registered_on')
display_cancelled_on = admin_date('cancelled_on')
class MetricStorageAdmin(admin.ModelAdmin):
list_display = ('order', 'value', 'created_on', 'updated_on')

View File

@ -10,7 +10,7 @@ from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from orchestra.core import caches
from orchestra.core import caches, services
from orchestra.utils.apps import autodiscover
from . import settings
@ -195,14 +195,22 @@ class Service(models.Model):
except IndexError:
pass
else:
for attr in ['matches', 'get_metric']:
try:
getattr(self.handler, attr)(obj)
except Exception as exception:
name = type(exception).__name__
message = exception.message
msg = "{0} {1}: {2}".format(attr, name, message)
raise ValidationError(msg)
attr = None
try:
bool(self.handler.matches(obj))
except Exception as exception:
attr = "Matches"
try:
metric = self.handler.get_metric(obj)
if metric is not None:
int(metric)
except Exception as exception:
attr = "Get metric"
if attr is not None:
name = type(exception).__name__
message = exception.message
msg = "{0} {1}: {2}".format(attr, name, message)
raise ValidationError(msg)
class OrderQuerySet(models.QuerySet):
@ -222,9 +230,6 @@ class OrderQuerySet(models.QuerySet):
class Order(models.Model):
SAVE = 'SAVE'
DELETE = 'DELETE'
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='orders')
content_type = models.ForeignKey(ContentType)
@ -303,14 +308,14 @@ class MetricStorage(models.Model):
@receiver(pre_delete, dispatch_uid="orders.cancel_orders")
def cancel_orders(sender, **kwargs):
if sender not in [MetricStorage, LogEntry, Order, Service]:
if sender in services:
instance = kwargs['instance']
for order in Order.objects.by_object(instance).active():
order.cancel()
@receiver(post_save, dispatch_uid="orders.update_orders")
@receiver(post_delete, dispatch_uid="orders.update_orders")
@receiver(post_delete, dispatch_uid="orders.update_orders_post_delete")
def update_orders(sender, **kwargs):
if sender not in [MetricStorage, LogEntry, Order, Service]:
instance = kwargs['instance']

View File

@ -1,13 +1,11 @@
from django.contrib import admin, messages
from django.contrib.contenttypes import generic
from django.utils.functional import cached_property
from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _
from djcelery.humanize import naturaldate
from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.filters import UsedContentTypeFilter
from orchestra.admin.utils import insertattr, get_modeladmin, admin_link
from orchestra.admin.utils import insertattr, get_modeladmin, admin_link, admin_date
from orchestra.core import services
from orchestra.utils import running_syncdb
@ -91,13 +89,17 @@ admin.site.register(MonitorData, MonitorDataAdmin)
def resource_inline_factory(resources):
class ResourceInlineFormSet(generic.BaseGenericInlineFormSet):
def total_form_count(self):
def total_form_count(self, resources=resources):
return len(resources)
@cached_property
def forms(self):
def forms(self, resources=resources):
forms = []
for i, resource in enumerate(resources):
resources_copy = list(resources)
for i, data in enumerate(self.queryset):
forms.append(self._construct_form(i, resource=data.resource))
resources_copy.remove(data.resource)
for i, resource in enumerate(resources_copy, len(self.queryset)):
forms.append(self._construct_form(i, resource=resource))
return forms
@ -117,16 +119,11 @@ def resource_inline_factory(resources):
'all': ('orchestra/css/hide-inline-id.css',)
}
display_last_update = admin_date('last_update', default=_("Never"))
def has_add_permission(self, *args, **kwargs):
""" Hidde add another """
return False
def display_last_update(self, data):
return '<div title="{0}">{1}</div>'.format(
escape(str(data.last_update)), escape(naturaldate(data.last_update)),
)
display_last_update.short_description = _("last update")
display_last_update.allow_tags = True
return ResourceInline

View File

@ -6,6 +6,7 @@ from django.utils.translation import ugettext_lazy as _
from djcelery.models import PeriodicTask, CrontabSchedule
from orchestra.models.fields import MultiSelectField
from orchestra.utils.functional import cached
from . import helpers
from .backends import ServiceMonitor
@ -164,15 +165,17 @@ def create_resource_relation():
class ResourceHandler(object):
""" account.resources.web """
def __getattr__(self, attr):
""" get or create ResourceData """
""" get or build ResourceData """
try:
return self.obj.resource_set.get(resource__name=attr)
data = self.obj.resource_set.get(resource__name=attr)
except ResourceData.DoesNotExist:
model = self.obj._meta.model_name
resource = Resource.objects.get(content_type__model=model,
name=attr, is_active=True)
return ResourceData.objects.create(content_object=self.obj,
resource=resource)
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
def __get__(self, obj, cls):
self.obj = obj

View File

@ -0,0 +1,87 @@
from datetime import datetime
from django.utils import timezone
from django.utils.translation import ungettext, ugettext as _
def pluralize_year(n):
return ungettext(_('{num:.1f} year ago'), _('{num:.1f} years ago'), n)
def pluralize_month(n):
return ungettext(_('{num:.1f} month ago'), _('{num:.1f} months ago'), n)
def pluralize_week(n):
return ungettext(_('{num:.1f} week ago'), _('{num:.1f} weeks ago'), n)
def pluralize_day(n):
return ungettext(_('{num:.1f} day ago'), _('{num:.1f} days ago'), n)
OLDER_CHUNKS = (
(365.0, pluralize_year),
(30.0, pluralize_month),
(7.0, pluralize_week),
)
def _un(singular__plural, n=None):
singular, plural = singular__plural
return ungettext(singular, plural, n)
def naturaldate(date, include_seconds=False):
"""Convert datetime into a human natural date string."""
if not date:
return ''
right_now = timezone.now()
today = datetime(right_now.year, right_now.month,
right_now.day, tzinfo=right_now.tzinfo)
delta = right_now - date
delta_midnight = today - date
days = delta.days
hours = int(round(delta.seconds / 3600, 0))
minutes = delta.seconds / 60
seconds = delta.seconds
if days < 0:
return _('just now')
if days == 0:
if hours == 0:
if minutes > 0:
minutes += float(seconds)/60
return ungettext(
_('{minutes:.1f} minute ago'),
_('{minutes:.1f} minutes ago'), minutes
).format(minutes=minutes)
else:
if include_seconds and seconds:
return ungettext(
_('{seconds} second ago'),
_('{seconds} seconds ago'), seconds
).format(seconds=seconds)
return _('just now')
else:
hours += float(minutes)/60
return ungettext(
_('{hours:.1f} hour ago'), _('{hours:.1f} hours ago'), hours
).format(hours=hours)
if delta_midnight.days == 0:
return _('yesterday at {time}').format(time=date.strftime('%H:%M'))
count = 0
for chunk, pluralizefun in OLDER_CHUNKS:
if days < 7.0:
count = days + float(hours)/24
fmt = pluralize_day(count)
return fmt.format(num=count)
if days >= chunk:
count = (delta_midnight.days + 1) / chunk
fmt = pluralizefun(count)
return fmt.format(num=count)