diff --git a/orchestra/admin/utils.py b/orchestra/admin/utils.py
index d378968d..9340e8ed 100644
--- a/orchestra/admin/utils.py
+++ b/orchestra/admin/utils.py
@@ -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("%s" % (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("%s" % (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("%s" % (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 '
{1}
'.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("%s" % (date_abs, date_rel))
diff --git a/orchestra/apps/invoices/__init__.py b/orchestra/apps/invoices/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/orchestra/apps/issues/admin.py b/orchestra/apps/issues/admin.py
index 5e0ef9ff..4f37dc7c 100644
--- a/orchestra/apps/issues/admin.py
+++ b/orchestra/apps/issues/admin.py
@@ -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 '%s' % 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 '%d' % (url, num)
diff --git a/orchestra/apps/miscellaneous/admin.py b/orchestra/apps/miscellaneous/admin.py
index dc623b1a..1b506b30 100644
--- a/orchestra/apps/miscellaneous/admin.py
+++ b/orchestra/apps/miscellaneous/admin.py
@@ -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('{1}'.format(url, num))
diff --git a/orchestra/apps/orchestration/admin.py b/orchestra/apps/orchestration/admin.py
index fa06ebe9..6cdd0059 100644
--- a/orchestra/apps/orchestration/admin.py
+++ b/orchestra/apps/orchestration/admin.py
@@ -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 '%s' % (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 '{1}
'.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 '{1}
'.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)
diff --git a/orchestra/apps/orders/admin.py b/orchestra/apps/orders/admin.py
index d949dd19..b79badc7 100644
--- a/orchestra/apps/orders/admin.py
+++ b/orchestra/apps/orders/admin.py
@@ -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 '%d' % (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')
diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py
index a24d24ba..817835cd 100644
--- a/orchestra/apps/orders/models.py
+++ b/orchestra/apps/orders/models.py
@@ -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']
diff --git a/orchestra/apps/resources/admin.py b/orchestra/apps/resources/admin.py
index 7bbb55f7..eefb22e6 100644
--- a/orchestra/apps/resources/admin.py
+++ b/orchestra/apps/resources/admin.py
@@ -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 '{1}
'.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
diff --git a/orchestra/apps/resources/models.py b/orchestra/apps/resources/models.py
index 14a54027..51a0c664 100644
--- a/orchestra/apps/resources/models.py
+++ b/orchestra/apps/resources/models.py
@@ -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
diff --git a/orchestra/utils/humanize.py b/orchestra/utils/humanize.py
new file mode 100644
index 00000000..cbfd944f
--- /dev/null
+++ b/orchestra/utils/humanize.py
@@ -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)