Admin cosmetics

This commit is contained in:
Marc 2014-07-21 15:43:36 +00:00
parent c731a73889
commit ccbda512bf
19 changed files with 194 additions and 105 deletions

View File

@ -62,15 +62,15 @@ def wrap_admin_view(modeladmin, view):
def set_default_filter(queryarg, request, value):
""" set default filters for changelist_view """
if queryarg not in request.GET:
q = request.GET.copy()
request_copy = request.GET.copy()
if callable(value):
value = value(request)
q[queryarg] = value
request.GET = q
request_copy[queryarg] = value
request.GET = request_copy
request.META['QUERY_STRING'] = request.GET.urlencode()
def link(*args, **kwargs):
def admin_link(*args, **kwargs):
""" utility function for creating admin links """
field = args[0] if args else ''
order = kwargs.pop('order', field)
@ -88,7 +88,7 @@ def link(*args, **kwargs):
extra = 'onclick="return showAddAnotherPopup(this);"'
return '<a href="%s" %s>%s</a>' % (url, extra, obj)
display_link.allow_tags = True
display_link.short_description = _(field)
display_link.short_description = _(field.replace('_', ' '))
display_link.admin_order_field = order
return display_link

View File

@ -94,6 +94,8 @@ class LinkHeaderRouter(DefaultRouter):
for _prefix, viewset, __ in self.registry:
if _prefix == prefix_or_model or viewset.model == prefix_or_model:
return viewset
msg = "%s does not have a regiestered viewset" % prefix_or_model
raise KeyError(msg)
def insert(self, prefix_or_model, name, field, **kwargs):
""" Dynamically add new fields to an existing serializer """

View File

@ -8,7 +8,7 @@ from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.utils import wrap_admin_view, link
from orchestra.admin.utils import wrap_admin_view, admin_link
from orchestra.core import services
from .filters import HasMainUserListFilter
@ -42,7 +42,7 @@ class AccountAdmin(ExtendedModelAdmin):
add_form = AccountCreationForm
form = AccountChangeForm
user_link = link('user', order='user__username')
user_link = admin_link('user', order='user__username')
def name(self, account):
return account.name

View File

@ -7,7 +7,7 @@ from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.utils import link
from orchestra.admin.utils import admin_link
from orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin
from .forms import (DatabaseUserChangeForm, DatabaseUserCreationForm,
@ -21,7 +21,7 @@ class UserInline(admin.TabularInline):
readonly_fields = ('user_link',)
extra = 0
user_link = link('user')
user_link = admin_link('user')
def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """
@ -38,7 +38,7 @@ class PermissionInline(AccountAdminMixin, admin.TabularInline):
extra = 0
filter_by_account_fields = ['database']
database_link = link('database', popup=True)
database_link = admin_link('database', popup=True)
def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """

View File

@ -8,7 +8,7 @@ from django.template.response import TemplateResponse
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ChangeListDefaultFilter, ExtendedModelAdmin
from orchestra.admin.utils import wrap_admin_view, link
from orchestra.admin.utils import wrap_admin_view, admin_link
from orchestra.apps.accounts.admin import AccountAdminMixin
from orchestra.utils import apps
@ -41,7 +41,7 @@ class DomainInline(admin.TabularInline):
extra = 0
verbose_name_plural = _("Subdomains")
domain_link = link()
domain_link = admin_link()
domain_link.short_description = _("Name")
def has_add_permission(self, *args, **kwargs):

View File

@ -12,7 +12,8 @@ from django.utils.translation import ugettext_lazy as _
from markdown import markdown
from orchestra.admin import ChangeListDefaultFilter, ExtendedModelAdmin#, ChangeViewActions
from orchestra.admin.utils import (link, colored, wrap_admin_view, display_timesince)
from orchestra.admin.utils import (admin_link, colored, wrap_admin_view,
display_timesince)
from orchestra.apps.contacts import settings as contacts_settings
from .actions import (reject_tickets, resolve_tickets, take_tickets, close_tickets,
@ -107,8 +108,8 @@ class TicketInline(admin.TabularInline):
extra = 0
max_num = 0
creator_link = link('creator')
owner_link = link('owner')
creator_link = admin_link('creator')
owner_link = admin_link('owner')
def ticket_id(self, instance):
return '<b>%s</b>' % link()(self, instance)
@ -198,9 +199,9 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView
'issues/js/ticket-admin.js',
)
display_creator = link('creator')
display_queue = link('queue')
display_owner = link('owner')
display_creator = admin_link('creator')
display_queue = admin_link('queue')
display_owner = admin_link('owner')
def display_summary(self, ticket):
context = {
@ -212,7 +213,7 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView
if msg:
context.update({
'updated': display_timesince(msg.created_on),
'updater': link('author')(self, msg) if msg.author else msg.author_name,
'updater': admin_link('author')(self, msg) if msg.author else msg.author_name,
})
context['updated'] = '. Updated by %(updater)s about %(updated)s' % context
return '<h4>Added by %(creator)s about %(created)s%(updated)s</h4>' % context

View File

@ -1,4 +1,5 @@
from django.contrib.admin import SimpleListFilter
from django.utils.translation import ugettext_lazy as _
from .models import Ticket
@ -10,14 +11,20 @@ class MyTicketsListFilter(SimpleListFilter):
def lookups(self, request, model_admin):
return (
('True', 'My Tickets'),
('False', 'All'),
('True', _("My Tickets")),
('False', _("All")),
)
def queryset(self, request, queryset):
if self.value() == 'True':
return queryset.involved_by(request.user)
def choices(self, cl):
""" Remove default All """
choices = iter(super(MyTicketsListFilter, self).choices(cl))
choices.next()
return choices
class TicketStateListFilter(SimpleListFilter):
title = 'State'
@ -25,14 +32,14 @@ class TicketStateListFilter(SimpleListFilter):
def lookups(self, request, model_admin):
return (
('OPEN', "Open"),
(Ticket.NEW, "New"),
(Ticket.IN_PROGRESS, "In Progress"),
(Ticket.RESOLVED, "Resolved"),
(Ticket.FEEDBACK, "Feedback"),
(Ticket.REJECTED, "Rejected"),
(Ticket.CLOSED, "Closed"),
('False', 'All'),
('OPEN', _("Open")),
(Ticket.NEW, _("New")),
(Ticket.IN_PROGRESS, _("In Progress")),
(Ticket.RESOLVED, _("Resolved")),
(Ticket.FEEDBACK, _("Feedback")),
(Ticket.REJECTED, _("Rejected")),
(Ticket.CLOSED, _("Closed")),
('False', _("All")),
)
def queryset(self, request, queryset):
@ -41,3 +48,10 @@ class TicketStateListFilter(SimpleListFilter):
elif self.value() == 'False':
return queryset
return queryset.filter(state=self.value())
def choices(self, cl):
""" Remove default All """
choices = iter(super(TicketStateListFilter, self).choices(cl))
choices.next()
return choices

View File

@ -4,7 +4,7 @@ from django.contrib.auth.admin import UserAdmin
from django.utils.translation import ugettext, ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.utils import link
from orchestra.admin.utils import admin_link
from orchestra.apps.accounts.admin import SelectAccountAdminMixin
from .forms import ListCreationForm, ListChangeForm
@ -47,7 +47,7 @@ class ListAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
add_form = ListCreationForm
filter_by_account_fields = ['address_domain']
address_domain_link = link('address_domain', order='address_domain__name')
address_domain_link = admin_link('address_domain', order='address_domain__name')
def get_urls(self):
useradmin = UserAdmin(List, self.admin_site)

View File

@ -5,7 +5,7 @@ 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 link
from orchestra.admin.utils import admin_link
from .models import Server, Route, BackendLog, BackendOperation
@ -60,7 +60,7 @@ class BackendOperationInline(admin.TabularInline):
def instance_link(self, operation):
try:
return link('instance')(self, operation)
return admin_link('instance')(self, operation)
except:
return _("deleted {0} {1}").format(
escape(operation.content_type), escape(operation.object_id)
@ -88,11 +88,7 @@ class BackendLogAdmin(admin.ModelAdmin):
]
readonly_fields = fields
def server_link(self, log):
url = reverse('admin:orchestration_server_change', args=(log.server.pk,))
return '<a href="%s">%s</a>' % (url, log.server.name)
server_link.short_description = _("server")
server_link.allow_tags = True
server_link = admin_link('server')
def display_state(self, log):
color = STATE_COLORS.get(log.state, 'grey')

View File

@ -1,18 +1,29 @@
from django import forms
from django.db import models
from django.contrib import admin
from django.core.urlresolvers import reverse
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.apps.accounts.admin import AccountAdminMixin
from orchestra.core import services
from .filters import ActiveOrderListFilter
from .models import Service, Order, MetricStorage
class ServiceAdmin(admin.ModelAdmin):
list_display = (
'description', 'content_type', 'handler_type', 'num_orders', 'is_active'
)
list_filter = ('is_active', 'handler_type', UsedContentTypeFilter)
fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('description', 'content_type', 'match', 'handler', 'is_active')
'fields': ('description', 'content_type', 'match', 'handler_type',
'is_active')
}),
(_("Billing options"), {
'classes': ('wide',),
@ -37,12 +48,32 @@ class ServiceAdmin(admin.ModelAdmin):
kwargs['widget'] = forms.TextInput(attrs={'size':'160'})
return super(ServiceAdmin, self).formfield_for_dbfield(db_field, **kwargs)
def num_orders(self, service):
num = service.orders.count()
url = reverse('admin:orders_order_changelist')
url += '?service=%i' % service.pk
return '<a href="%s">%d</a>' % (url, num)
num_orders.short_description = _("Orders")
num_orders.admin_order_field = 'orders__count'
num_orders.allow_tags = True
class OrderAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = ('id', 'service', 'account_link', 'cancelled_on')
list_filter = ('service',)
def get_queryset(self, request):
qs = super(ServiceAdmin, self).get_queryset(request)
qs = qs.annotate(models.Count('orders'))
return qs
class OrderAdmin(AccountAdminMixin, ChangeListDefaultFilter, admin.ModelAdmin):
list_display = (
'id', 'service', 'account_link', 'content_object_link', 'cancelled_on'
)
list_filter = (ActiveOrderListFilter, 'service',)
default_changelist_filters = (
('is_active', 'True'),
)
content_object_link = admin_link('content_object')
class MetricStorageAdmin(admin.ModelAdmin):
list_display = ('order', 'value', 'created_on', 'updated_on')
list_filter = ('order__service',)

View File

@ -0,0 +1,28 @@
from django.contrib.admin import SimpleListFilter
from django.utils.translation import ugettext_lazy as _
class ActiveOrderListFilter(SimpleListFilter):
""" Filter tickets by created_by according to request.user """
title = 'Orders'
parameter_name = 'is_active'
def lookups(self, request, model_admin):
return (
('True', _("Active")),
('False', _("Inactive")),
('None', _("All")),
)
def queryset(self, request, queryset):
if self.value() == 'True':
return queryset.active()
elif self.value() == 'False':
return queryset.inactive()
return queryset
def choices(self, cl):
""" Remove default All """
choices = iter(super(ActiveOrderListFilter, self).choices(cl))
choices.next()
return choices

View File

@ -12,13 +12,19 @@ class ServiceHandler(plugins.Plugin):
def __init__(self, service):
self.service = service
def __getattr__(self, attr):
return getattr(self.service, attr)
@classmethod
def get_plugin_choices(cls):
choices = super(ServiceHandler, cls).get_plugin_choices()
return [('', _("Default"))] + choices
def __getattr__(self, attr):
return getattr(self.service, attr)
def get_content_type(self):
if not self.model:
return self.content_type
app_label, model = self.model.split('.')
return ContentType.objects.get_by_natural_key(app_label, model.lower())
def matches(self, instance):
safe_locals = {
@ -27,13 +33,8 @@ class ServiceHandler(plugins.Plugin):
return eval(self.match, safe_locals)
def get_metric(self, instance):
safe_locals = {
instance._meta.model_name: instance
}
return eval(self.metric, safe_locals)
def get_content_type(self):
if not self.model:
return self.content_type
app_label, model = self.model.split('.')
return ContentType.objects.get_by_natural_key(app_label, model.lower())
if self.metric:
safe_locals = {
instance._meta.model_name: instance
}
return eval(self.metric, safe_locals)

View File

@ -31,7 +31,7 @@ def search_for_related(origin, max_depth=2):
if hasattr(node, 'account') or isinstance(node, Account):
return node
for related in related_iterator(node):
if related not in models:
if related and related not in models:
new_models = list(models)
new_models.append(related)
queue.append(new_models)

View File

@ -45,8 +45,10 @@ class Service(models.Model):
description = models.CharField(_("description"), max_length=256, unique=True)
content_type = models.ForeignKey(ContentType, verbose_name=_("content type"))
match = models.CharField(_("match"), max_length=256, blank=True)
handler = models.CharField(_("handler"), max_length=256, blank=True,
help_text=_("Handler used to process this Service."),
handler_type = models.CharField(_("handler"), max_length=256, blank=True,
help_text=_("Handler used for processing this Service. A handler "
"enables customized behaviour far beyond what options "
"here allow to."),
choices=ServiceHandler.get_plugin_choices())
is_active = models.BooleanField(_("is active"), default=True)
# Billing
@ -172,14 +174,16 @@ class Service(models.Model):
cache.set(ct, services)
return services
# FIXME some times caching is nasty, do we really have to? make get_plugin more efficient?
@cached_property
def proxy(self):
if self.handler:
return ServiceHandler.get_plugin(self.handler)(self)
def handler(self):
""" Accessor of this service handler instance """
if self.handler_type:
return ServiceHandler.get_plugin(self.handler_type)(self)
return ServiceHandler(self)
def clean(self):
content_type = self.proxy.get_content_type()
content_type = self.handler.get_content_type()
if self.content_type != content_type:
msg =_("Content type must be equal to '%s'." % str(content_type))
raise ValidationError(msg)
@ -191,22 +195,30 @@ class Service(models.Model):
except IndexError:
pass
else:
try:
self.proxy.matches(obj)
except Exception as e:
raise ValidationError(_(str(e)))
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)
class OrderQuerySet(models.QuerySet):
def by_object(self, obj, *args, **kwargs):
def by_object(self, obj, **kwargs):
ct = ContentType.objects.get_for_model(obj)
return self.filter(object_id=obj.pk, content_type=ct)
return self.filter(object_id=obj.pk, content_type=ct, **kwargs)
def active(self, *args, **kwargs):
def active(self, **kwargs):
""" return active orders """
return self.filter(
Q(cancelled_on__isnull=True) | Q(cancelled_on__gt=timezone.now())
).filter(*args, **kwargs)
).filter(**kwargs)
def inactive(self, **kwargs):
""" return inactive orders """
return self.filter(cancelled_on__lt=timezone.now(), **kwargs)
class Order(models.Model):
@ -234,10 +246,12 @@ class Order(models.Model):
def update(self):
instance = self.content_object
if self.service.metric:
metric = self.service.get_metric(instance)
MetricStorage.store(self, metric)
description = "{}: {}".format(self.service.description, str(instance))
handler = self.service.handler
if handler.metric:
metric = handler.get_metric(instance)
if metric is not None:
MetricStorage.store(self, metric)
description = "{}: {}".format(handler.description, str(instance))
if self.description != description:
self.description = description
self.save()
@ -246,7 +260,7 @@ class Order(models.Model):
def update_orders(cls, instance):
for service in Service.get_services(instance):
orders = Order.objects.by_object(instance, service=service).active()
if service.matches(instance):
if service.handler.matches(instance):
if not orders:
account_id = getattr(instance, 'account_id', instance.pk)
order = cls.objects.create(content_object=instance,
@ -289,23 +303,20 @@ class MetricStorage(models.Model):
@receiver(pre_delete, dispatch_uid="orders.cancel_orders")
def cancel_orders(sender, **kwargs):
if (not sender in [MetricStorage, LogEntry, Order, Service] and
not Service in sender.__mro__):
instance = kwargs['instance']
for order in Order.objects.by_object(instance).active():
order.cancel()
if sender not in [MetricStorage, LogEntry, Order, Service]:
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")
def update_orders(sender, **kwargs):
if (not sender in [MetricStorage, LogEntry, Order, Service] and
not Service in sender.__mro__):
instance = kwargs['instance']
print kwargs
if instance.pk:
# post_save
Order.update_orders(instance)
related = search_for_related(instance)
if related:
Order.update_orders(related)
if sender not in [MetricStorage, LogEntry, Order, Service]:
instance = kwargs['instance']
if instance.pk:
# post_save
Order.update_orders(instance)
related = search_for_related(instance)
if related:
Order.update_orders(related)

View File

@ -7,7 +7,7 @@ from djcelery.humanize import naturaldate
from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.filters import UsedContentTypeFilter
from orchestra.admin.utils import insertattr, get_modeladmin, link
from orchestra.admin.utils import insertattr, get_modeladmin, admin_link
from orchestra.core import services
from orchestra.utils import running_syncdb
@ -71,10 +71,7 @@ class ResourceDataAdmin(admin.ModelAdmin):
list_filter = ('resource',)
readonly_fields = ('content_object_link',)
def content_object_link(self, data):
return link('content_object')(self, data)
content_object_link.allow_tags = True
content_object_link.short_description = _("Content object")
content_object_link = admin_link('content_object')
class MonitorDataAdmin(admin.ModelAdmin):
@ -82,10 +79,7 @@ class MonitorDataAdmin(admin.ModelAdmin):
list_filter = ('monitor',)
readonly_fields = ('content_object_link',)
def content_object_link(self, data):
return link('content_object')(self, data)
content_object_link.allow_tags = True
content_object_link.short_description = _("Content object")
content_object_link = admin_link('content_object')
admin.site.register(Resource, ResourceAdmin)

View File

@ -164,7 +164,15 @@ def create_resource_relation():
class ResourceHandler(object):
""" account.resources.web """
def __getattr__(self, attr):
return self.obj.resource_set.get(resource__name=attr)
""" get or create ResourceData """
try:
return 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)
def __get__(self, obj, cls):
self.obj = obj

View File

@ -27,7 +27,10 @@ if not running_syncdb():
# TODO why this is even loaded during syncdb?
for resources in Resource.group_by_content_type():
model = resources[0].content_type.model_class()
router.insert(model, 'resources', ResourceSerializer, required=False, many=True)
try:
router.insert(model, 'resources', ResourceSerializer, required=False, many=True)
except KeyError:
continue
def validate_resources(self, attrs, source, _resources=resources):
""" Creates missing resources """

View File

@ -7,7 +7,7 @@ from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.utils import insertattr, link
from orchestra.admin.utils import insertattr, admin_link
from orchestra.apps.accounts.admin import SelectAccountAdminMixin
from orchestra.apps.domains.forms import DomainIterator
from orchestra.apps.users.roles.admin import RoleAdmin
@ -79,7 +79,7 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
filter_by_account_fields = ['domain']
filter_horizontal = ['mailboxes']
domain_link = link('domain', order='domain__name')
domain_link = admin_link('domain', order='domain__name')
def email_link(self, address):
link = self.domain_link(address)

View File

@ -5,7 +5,7 @@ from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.utils import link
from orchestra.admin.utils import admin_link
from orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin
from orchestra.apps.accounts.widgets import account_related_field_widget_factory
@ -35,7 +35,7 @@ class ContentInline(AccountAdminMixin, admin.TabularInline):
readonly_fields = ('webapp_link', 'webapp_type')
filter_by_account_fields = ['webapp']
webapp_link = link('webapp', popup=True)
webapp_link = admin_link('webapp', popup=True)
webapp_link.short_description = _("Web App")
def webapp_type(self, content):