Added Admin global search view

This commit is contained in:
Marc Aymerich 2015-10-07 22:05:00 +00:00
parent 257b627a3e
commit eb4673b3c4
14 changed files with 142 additions and 13 deletions

11
TODO.md
View File

@ -93,7 +93,7 @@ Php binaries should have this format: /usr/bin/php5.2-cgi
* logs on panel/logs/ ? mkdir ~webapps, backend post save signal?
* <IfModule security2_module> and other IfModule on backend SecRule
* Orchestra global search box on the page head, based https://github.com/django/django/blob/master/django/contrib/admin/options.py#L866 and iterating over all registered services and inspectin its admin.search_fields
# Orchestra global search box on the page head, based https://github.com/django/django/blob/master/django/contrib/admin/options.py#L866 and iterating over all registered services and inspectin its admin.search_fields
* contain error on plugin missing key (plugin dissabled): NOP, fail hard is better than silently, perhaps fail at starttime? apploading machinary
@ -131,6 +131,7 @@ require_once(/etc/moodles/.$moodle_host.config.php);``` moodle/drupl
* document service help things: discount/refound/compensation effect and metric table
* Document metric interpretation help_text
* document plugin serialization, data_serializer?
* Document strong input validation
# bill line managemente, remove, undo (only when possible), move, copy, paste
* budgets: no undo feature
@ -415,3 +416,11 @@ mkhomedir_helper or create ssh homes with bash.rc and such
# setupforbiddendomains --url alexa -n 5000
* remove welcome box on dashboard?
# account contacts inline, show provided fields and ignore the rest?
# email usage -webkit-column-count:3;-moz-column-count:3;column-count:3;
# resources on service report

View File

@ -19,6 +19,15 @@ class AppDefaultIconList(CmsAppIconList):
class OrchestraIndexDashboard(dashboard.FluentIndexDashboard):
""" Gets application modules from services, accounts and administration registries """
def __init__(self, **kwargs):
super(dashboard.FluentIndexDashboard, self).__init__(**kwargs)
self.children.append(self.get_personal_module())
self.children.extend(self.get_application_modules())
recent_actions = self.get_recent_actions_module()
recent_actions.enabled = True
self.children.append(recent_actions)
def process_registered_view(self, module, view_name, options):
app_name, name = view_name.split('_')[:-1]
module.icons['.'.join((app_name, name))] = options.get('icon')
@ -44,7 +53,7 @@ class OrchestraIndexDashboard(dashboard.FluentIndexDashboard):
# Honor settings override, hacky. I Know
if appsettings.FLUENT_DASHBOARD_APP_GROUPS[0][0] != _('CMS'):
modules = super(OrchestraIndexDashboard, self).get_application_modules()
for register in (accounts, administration, services):
for register in (accounts, services, administration):
title = register.verbose_name
models = []
icons = {}

View File

@ -49,7 +49,7 @@ def service_report(modeladmin, request, queryset):
model = field.related_model
if model in registered_services and model != queryset.model:
fields.append((model, name))
sorted(fields, key=lambda f: f[0]._meta.verbose_name_plural.lower())
fields = sorted(fields, key=lambda f: f[0]._meta.verbose_name_plural.lower())
fields = [field for model, field in fields]
for account in queryset.prefetch_related(*fields):

View File

@ -1,4 +1,4 @@
{% load utils i18n %}
{% load i18n admin_urls utils %}
<html>
<head>
<title>{% block title %}Account service report{% endblock %}</title>
@ -50,13 +50,29 @@
<div class="account-content">
{{ account.get_type_display }} {% trans "account registered on" %} {{ account.date_joined | date }}<br>
<ul class="items-ul">
<li class="item-title">{% trans 'Resources' %}</li>
{% if account.resources %}
<ul>
{% for resource in account.resources %}
<li><a href="{{ resource|admin_url }}">{{ resource.verbose_name }} {% if resource.used != None %}<span title="{% trans 'Used' %}">{{ resource.used }}</span>{% endif %}{% if resource.allocated != None %}{% if resource.used != None %} / {% endif %}<span title="{% trans 'Allocated' %}">{{ resource.allocated }}</span>{% endif %}</a> {{ resource.unit }}</li>
{% endfor %}
</ul>
{% endif %}
{% for opts, related in items %}
<li class="item-title">{{ opts.verbose_name_plural|capfirst }}</li>
<li class="item-title"><a href="{% url opts|admin_urlname:'changelist' %}?account_id={{ account.pk }}">{{ opts.verbose_name_plural|capfirst }}</a></li>
<ul>
{% for obj in related %}
<li class="related"><a href="{{ obj|admin_url }}">{{ obj }}</a>
{% if not obj|isactive %} ({% trans "disabled" %}){% endif %}
{{ obj.get_description|capfirst }}
{% if obj.resources %}
<ul>
{% for resource in obj.resources %}
<li><a href="{{ resource|admin_url }}">{{ resource.verbose_name }} {% if resource.used != None %}<span title="{% trans 'Used' %}">{{ resource.used }}</span>{% endif %}{% if resource.allocated != None %}{% if resource.used != None %} / {% endif %}<span title="{% trans 'Allocated' %}">{{ resource.allocated }}</span>{% endif %}</a> {{ resource.unit }}</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>

View File

@ -9,5 +9,5 @@ class OrdersConfig(AppConfig):
def ready(self):
from .models import Order
accounts.register(Order, icon='basket.png')
accounts.register(Order, icon='basket.png', search=False)
from . import signals

View File

@ -10,5 +10,5 @@ class PaymentsConfig(AppConfig):
def ready(self):
from .models import PaymentSource, Transaction, TransactionProcess
accounts.register(PaymentSource, dashboard=False)
accounts.register(Transaction, icon='transaction.png')
accounts.register(TransactionProcess, icon='transactionprocess.png', dashboard=False)
accounts.register(Transaction, icon='transaction.png', search=False)
accounts.register(TransactionProcess, icon='transactionprocess.png', dashboard=False, search=False)

View File

@ -202,6 +202,10 @@ class ResourceData(models.Model):
def unit(self):
return self.resource.unit
@property
def verbose_name(self):
return self.resource.verbose_name
def get_used(self):
resource = self.resource
total = 0
@ -289,6 +293,8 @@ def create_resource_relation():
""" account.resources.web """
def __getattr__(self, attr):
""" get or build ResourceData """
if attr.startswith('_'):
raise AttributeError
try:
return self.obj.__resource_cache[attr]
except AttributeError:
@ -317,6 +323,9 @@ def create_resource_relation():
""" proxy handled object """
self.obj = obj
return self
def __iter__(self):
return iter(self.obj.resource_set.all())
# Clean previous state
for related in Resource._related:

View File

@ -15,6 +15,7 @@ class VPSAdmin(ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdmin):
form = NonStoredUserChangeForm
add_form = UserCreationForm
readonly_fields = ('account_link',)
search_fields = ('hostname', 'account__username', 'template')
change_readonly_fields = ('account', 'hostname', 'type', 'template')
fieldsets = (
(None, {

View File

@ -14,6 +14,9 @@ class Register(object):
def __getitem__(self, key):
return self._registry[key]
def __iter__(self):
return iter(self._registry.values())
def register(self, model, **kwargs):
if model in self._registry:
raise KeyError("%s already registered" % model)
@ -23,6 +26,8 @@ class Register(object):
kwargs['verbose_name_plural'] = model._meta.verbose_name_plural
defaults = {
'menu': True,
'search': True,
'model': model,
}
defaults.update(kwargs)
self._registry[model] = AttrDict(**defaults)

View File

@ -6,12 +6,12 @@ body {
#header #branding h1 {
margin: 0;
padding: 2px 15px;
background: transparent url(/static/orchestra/images/orchestra-logo.png) 10px 2px no-repeat;
background: transparent url(/static/orchestra/images/orchestra-logo.png) 5px 2px no-repeat;
text-indent: 0;
height: 31px;
font-size: 16px;
/* font-weight: bold;*/
padding-left: 50px;
padding-left: 45px;
line-height: 30px;
border-right: 1px solid #ededed;
}

View File

@ -48,9 +48,12 @@
<div style="max-width: 1170px; margin:auto;">
<div id="branding"><a href="/admin/"></a><h1 id="site-name"><a href="/admin/">{{ ORCHESTRA_SITE_VERBOSE_NAME }}<span class="version">0.0.1a1</span></a></h1></div>
{% for item in menu.children %}{% admin_tools_render_menu_item item forloop.counter %}{% endfor %}
<span style="float:right;color:grey;padding:10px;font-size:11px;">{% trans 'Welcome' %},
<form action="/search" method="get" name="top_search" style="display: inline;">
<input type="text" id="searchbox" style="margin-left:15px;margin-top:7px;" name="q" placeholder="Search" size="25" value="{{ query }}" {% if search_autofocus or app_list %}autofocus="autofocus"{% endif %} title="Use 'username!' for account direct access.">
</form>
<span style="float:right;color:grey;margin:10px;font-size:11px;">
{% url 'admin:accounts_account_change' user.pk as user_change_url %}
<a href="{{ user_change_url }}" style="color:#555;"><strong>{% filter force_escape %}{% firstof user.get_short_name user.username %}{% endfilter %}</strong></a>.
<a href="{{ user_change_url }}" style="color:#555;"><strong>{% filter force_escape %}{% firstof user.get_short_name user.username %}{% endfilter %}</strong></a>
<a href="{% url 'admin:password_change' %}" style="color:#555;">Change password</a> / <a href="{% url 'admin:logout' %}" style="color:#555;">Log out</a></span>
</div>
</ul>

View File

@ -0,0 +1,19 @@
{% extends "admin/base_site.html" %}
{% load i18n l10n %}
{% load url from future %}
{% load admin_urls static utils %}
{% block content %}
<div>
<div style="margin:20px;-webkit-column-count:{{ columns }};-moz-column-count:{{ columns }};column-count:{{ columns }};">
{% for opts, qs in results.items %}
<h3><a href="{% url opts|admin_urlname:'changelist' %}?q={{ query }}">{{ opts.verbose_name_plural|capfirst }}</a>
<span style="font-size:11px"> {{ qs|length }} results</span></h3>
<ul>
{% for instance in qs %}
<li><a href="{{ instance|admin_url }}">{{ instance }}</a></li>
{% endfor %}
</ul>
{% endfor %}
</div>
{% endblock %}

View File

@ -24,6 +24,7 @@ urlpatterns = [
'orchestra.views.serve_private_media',
name='private-media'
),
url(r'search', 'orchestra.views.search', name='search'),
]

View File

@ -1,10 +1,22 @@
import itertools
from collections import OrderedDict
from django.http import Http404
from django.contrib import admin
from django.contrib.admin.utils import unquote
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.db.models import get_model
from django.shortcuts import get_object_or_404
from django.shortcuts import get_object_or_404, render, redirect
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from django.views.static import serve
from orchestra.contrib.accounts.models import Account
from .core import accounts, services
from .utils.python import OrderedSet
def serve_private_media(request, app_label, model_name, field_name, object_id, filename):
model = get_model(app_label, model_name)
@ -18,3 +30,48 @@ def serve_private_media(request, app_label, model_name, field_name, object_id, f
return serve(request, field.name, document_root=field.storage.location)
else:
raise PermissionDenied()
def search(request):
query = request.GET.get('q', '')
if query.endswith('!'):
# Account direct access
query = query.replace('!', '')
try:
account = Account.objects.get(username=query)
except Account.DoesNotExist:
pass
else:
account_url = reverse('admin:accounts_account_change', args=(account.pk,))
return redirect(account_url)
results = OrderedDict()
models = set()
for service in itertools.chain(services, accounts):
if service.search:
models.add(service.model)
models = sorted(models, key=lambda m: m._meta.verbose_name_plural.lower())
total = 0
for model in models:
try:
modeladmin = admin.site._registry[model]
except KeyError:
pass
else:
qs = modeladmin.get_queryset(request)
qs, search_use_distinct = modeladmin.get_search_results(request, qs, query)
if search_use_distinct:
qs = qs.distinct()
num = len(qs)
if num:
total += num
results[model._meta] = qs
title = _("{total} search results for '<tt>{query}</tt>'").format(total=total, query=query)
context = {
'title': mark_safe(title),
'total': total,
'columns': min(int(total/17), 3),
'query': query,
'results': results,
'search_autofocus': True,
}
return render(request, 'admin/orchestra/search.html', context)