Admin interface improvements
This commit is contained in:
parent
ccbda512bf
commit
e57226b769
|
@ -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))
|
||||
|
|
0
orchestra/apps/invoices/__init__.py
Normal file
0
orchestra/apps/invoices/__init__.py
Normal 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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
87
orchestra/utils/humanize.py
Normal file
87
orchestra/utils/humanize.py
Normal 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)
|
Loading…
Reference in a new issue