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 django.utils.translation import ugettext_lazy as _
from orchestra.models.utils import get_field_value 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): def get_modeladmin(model, import_module=True):
@ -78,7 +78,7 @@ def admin_link(*args, **kwargs):
def display_link(self, instance): def display_link(self, instance):
obj = getattr(instance, field, instance) obj = getattr(instance, field, instance)
if not getattr(obj, 'pk', False): if not getattr(obj, 'pk', None):
return '---' return '---'
opts = obj._meta opts = obj._meta
view_name = 'admin:%s_%s_change' % (opts.app_label, opts.model_name) 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): def colored(field_name, colours, description='', verbose=False, bold=True):
""" returns a method that will render obj with colored html """ """ 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)) value = escape(get_field_value(obj, field))
color = colors.get(value, "black") color = colors.get(value, "black")
if verbose: if verbose:
@ -113,22 +113,40 @@ def colored(field_name, colours, description='', verbose=False, bold=True):
return colored_field return colored_field
def display_timesince(date, double=False): #def display_timesince(date, double=False):
""" # """
Format date for messages create_on: show a relative time # Format date for messages create_on: show a relative time
with contextual helper to show fulltime format. # with contextual helper to show fulltime format.
""" # """
if not date: # if not date:
return 'Never' # return 'Never'
date_rel = timesince(date) # date_rel = timesince(date)
if not double: # if not double:
date_rel = date_rel.split(',')[0] # date_rel = date_rel.split(',')[0]
date_rel += ' ago' # date_rel += ' ago'
date_abs = date.strftime("%Y-%m-%d %H:%M:%S %Z") # date_abs = date.strftime("%Y-%m-%d %H:%M:%S %Z")
return mark_safe("<span title='%s'>%s</span>" % (date_abs, date_rel)) # return mark_safe("<span title='%s'>%s</span>" % (date_abs, date_rel))
def display_timeuntil(date): def admin_date(field, **kwargs):
date_rel = timeuntil(date) + ' left' """ utility function for creating admin dates """
date_abs = date.strftime("%Y-%m-%d %H:%M:%S %Z") default = kwargs.pop('default', '')
return mark_safe("<span title='%s'>%s</span>" % (date_abs, date_rel)) 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 markdown import markdown
from orchestra.admin import ChangeListDefaultFilter, ExtendedModelAdmin#, ChangeViewActions from orchestra.admin import ChangeListDefaultFilter, ExtendedModelAdmin#, ChangeViewActions
from orchestra.admin.utils import (admin_link, colored, wrap_admin_view, from orchestra.admin.utils import admin_link, colored, wrap_admin_view, admin_date
display_timesince)
from orchestra.apps.contacts import settings as contacts_settings from orchestra.apps.contacts import settings as contacts_settings
from .actions import (reject_tickets, resolve_tickets, take_tickets, close_tickets, from .actions import (reject_tickets, resolve_tickets, take_tickets, close_tickets,
@ -110,6 +109,8 @@ class TicketInline(admin.TabularInline):
creator_link = admin_link('creator') creator_link = admin_link('creator')
owner_link = admin_link('owner') owner_link = admin_link('owner')
created = admin_link('created_on')
last_modified = admin_link('last_modified_on')
def ticket_id(self, instance): def ticket_id(self, instance):
return '<b>%s</b>' % link()(self, instance) return '<b>%s</b>' % link()(self, instance)
@ -124,12 +125,6 @@ class TicketInline(admin.TabularInline):
return colored('priority', PRIORITY_COLORS, bold=False)(instance) return colored('priority', PRIORITY_COLORS, bold=False)(instance)
colored_priority.short_description = _("Priority") 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, class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeViewActions,
list_display = [ list_display = [
@ -327,7 +322,7 @@ class QueueAdmin(admin.ModelAdmin):
} }
def num_tickets(self, queue): def num_tickets(self, queue):
num = queue.tickets.count() num = queue.tickets__count
url = reverse('admin:issues_ticket_changelist') url = reverse('admin:issues_ticket_changelist')
url += '?my_tickets=False&queue=%i' % queue.pk url += '?my_tickets=False&queue=%i' % queue.pk
return '<a href="%s">%d</a>' % (url, num) return '<a href="%s">%d</a>' % (url, num)

View File

@ -14,7 +14,7 @@ class MiscServiceAdmin(admin.ModelAdmin):
def num_instances(self, misc): def num_instances(self, misc):
""" return num slivers as a link to slivers changelist view """ """ 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 = reverse('admin:miscellaneous_miscellaneous_changelist')
url += '?service={}'.format(misc.pk) url += '?service={}'.format(misc.pk)
return mark_safe('<a href="{0}">{1}</a>'.format(url, num)) 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.core.urlresolvers import reverse
from django.utils.html import escape from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from djcelery.humanize import naturaldate
from orchestra.admin.html import monospace_format 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 from .models import Server, Route, BackendLog, BackendOperation
@ -89,13 +88,9 @@ class BackendLogAdmin(admin.ModelAdmin):
readonly_fields = fields readonly_fields = fields
server_link = admin_link('server') server_link = admin_link('server')
display_last_update = admin_date('last_update')
def display_state(self, log): display_created = admin_date('created')
color = STATE_COLORS.get(log.state, 'grey') display_state = colored('state', STATE_COLORS)
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'
def mono_script(self, log): def mono_script(self, log):
return monospace_format(escape(log.script)) return monospace_format(escape(log.script))
@ -113,20 +108,6 @@ class BackendLogAdmin(admin.ModelAdmin):
return monospace_format(escape(log.traceback)) return monospace_format(escape(log.traceback))
mono_traceback.short_description = _("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): def get_queryset(self, request):
""" Order by structured name and imporve performance """ """ Order by structured name and imporve performance """
qs = super(BackendLogAdmin, self).get_queryset(request) qs = super(BackendLogAdmin, self).get_queryset(request)

View File

@ -2,11 +2,12 @@ from django import forms
from django.db import models from django.db import models
from django.contrib import admin from django.contrib import admin
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ChangeListDefaultFilter from orchestra.admin import ChangeListDefaultFilter
from orchestra.admin.filters import UsedContentTypeFilter 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.apps.accounts.admin import AccountAdminMixin
from orchestra.core import services from orchestra.core import services
@ -49,9 +50,9 @@ class ServiceAdmin(admin.ModelAdmin):
return super(ServiceAdmin, self).formfield_for_dbfield(db_field, **kwargs) return super(ServiceAdmin, self).formfield_for_dbfield(db_field, **kwargs)
def num_orders(self, service): def num_orders(self, service):
num = service.orders.count() num = service.orders__count
url = reverse('admin:orders_order_changelist') 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) return '<a href="%s">%d</a>' % (url, num)
num_orders.short_description = _("Orders") num_orders.short_description = _("Orders")
num_orders.admin_order_field = 'orders__count' num_orders.admin_order_field = 'orders__count'
@ -59,20 +60,36 @@ class ServiceAdmin(admin.ModelAdmin):
def get_queryset(self, request): def get_queryset(self, request):
qs = super(ServiceAdmin, self).get_queryset(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 return qs
class OrderAdmin(AccountAdminMixin, ChangeListDefaultFilter, admin.ModelAdmin): class OrderAdmin(AccountAdminMixin, ChangeListDefaultFilter, admin.ModelAdmin):
list_display = ( 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',) list_filter = (ActiveOrderListFilter, 'service',)
date_hierarchy = 'registered_on'
default_changelist_filters = ( default_changelist_filters = (
('is_active', 'True'), ('is_active', 'True'),
) )
content_object_link = admin_link('content_object') 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): class MetricStorageAdmin(admin.ModelAdmin):
list_display = ('order', 'value', 'created_on', 'updated_on') 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.functional import cached_property
from django.utils.translation import ugettext_lazy as _ 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 orchestra.utils.apps import autodiscover
from . import settings from . import settings
@ -195,14 +195,22 @@ class Service(models.Model):
except IndexError: except IndexError:
pass pass
else: else:
for attr in ['matches', 'get_metric']: attr = None
try: try:
getattr(self.handler, attr)(obj) bool(self.handler.matches(obj))
except Exception as exception: except Exception as exception:
name = type(exception).__name__ attr = "Matches"
message = exception.message try:
msg = "{0} {1}: {2}".format(attr, name, message) metric = self.handler.get_metric(obj)
raise ValidationError(msg) 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): class OrderQuerySet(models.QuerySet):
@ -222,9 +230,6 @@ class OrderQuerySet(models.QuerySet):
class Order(models.Model): class Order(models.Model):
SAVE = 'SAVE'
DELETE = 'DELETE'
account = models.ForeignKey('accounts.Account', verbose_name=_("account"), account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='orders') related_name='orders')
content_type = models.ForeignKey(ContentType) content_type = models.ForeignKey(ContentType)
@ -303,14 +308,14 @@ class MetricStorage(models.Model):
@receiver(pre_delete, dispatch_uid="orders.cancel_orders") @receiver(pre_delete, dispatch_uid="orders.cancel_orders")
def cancel_orders(sender, **kwargs): def cancel_orders(sender, **kwargs):
if sender not in [MetricStorage, LogEntry, Order, Service]: if sender in services:
instance = kwargs['instance'] instance = kwargs['instance']
for order in Order.objects.by_object(instance).active(): for order in Order.objects.by_object(instance).active():
order.cancel() order.cancel()
@receiver(post_save, dispatch_uid="orders.update_orders") @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): def update_orders(sender, **kwargs):
if sender not in [MetricStorage, LogEntry, Order, Service]: if sender not in [MetricStorage, LogEntry, Order, Service]:
instance = kwargs['instance'] instance = kwargs['instance']

View File

@ -1,13 +1,11 @@
from django.contrib import admin, messages from django.contrib import admin, messages
from django.contrib.contenttypes import generic from django.contrib.contenttypes import generic
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from djcelery.humanize import naturaldate
from orchestra.admin import ExtendedModelAdmin from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.filters import UsedContentTypeFilter 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.core import services
from orchestra.utils import running_syncdb from orchestra.utils import running_syncdb
@ -91,13 +89,17 @@ admin.site.register(MonitorData, MonitorDataAdmin)
def resource_inline_factory(resources): def resource_inline_factory(resources):
class ResourceInlineFormSet(generic.BaseGenericInlineFormSet): class ResourceInlineFormSet(generic.BaseGenericInlineFormSet):
def total_form_count(self): def total_form_count(self, resources=resources):
return len(resources) return len(resources)
@cached_property @cached_property
def forms(self): def forms(self, resources=resources):
forms = [] 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)) forms.append(self._construct_form(i, resource=resource))
return forms return forms
@ -117,17 +119,12 @@ def resource_inline_factory(resources):
'all': ('orchestra/css/hide-inline-id.css',) 'all': ('orchestra/css/hide-inline-id.css',)
} }
display_last_update = admin_date('last_update', default=_("Never"))
def has_add_permission(self, *args, **kwargs): def has_add_permission(self, *args, **kwargs):
""" Hidde add another """ """ Hidde add another """
return False 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 return ResourceInline
if not running_syncdb(): if not running_syncdb():

View File

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