Added Admin global search view
This commit is contained in:
parent
257b627a3e
commit
eb4673b3c4
11
TODO.md
11
TODO.md
|
@ -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?
|
* logs on panel/logs/ ? mkdir ~webapps, backend post save signal?
|
||||||
* <IfModule security2_module> and other IfModule on backend SecRule
|
* <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
|
* 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 service help things: discount/refound/compensation effect and metric table
|
||||||
* Document metric interpretation help_text
|
* Document metric interpretation help_text
|
||||||
* document plugin serialization, data_serializer?
|
* document plugin serialization, data_serializer?
|
||||||
|
* Document strong input validation
|
||||||
|
|
||||||
# bill line managemente, remove, undo (only when possible), move, copy, paste
|
# bill line managemente, remove, undo (only when possible), move, copy, paste
|
||||||
* budgets: no undo feature
|
* budgets: no undo feature
|
||||||
|
@ -415,3 +416,11 @@ mkhomedir_helper or create ssh homes with bash.rc and such
|
||||||
|
|
||||||
|
|
||||||
# setupforbiddendomains --url alexa -n 5000
|
# 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
|
||||||
|
|
|
@ -19,6 +19,15 @@ class AppDefaultIconList(CmsAppIconList):
|
||||||
|
|
||||||
class OrchestraIndexDashboard(dashboard.FluentIndexDashboard):
|
class OrchestraIndexDashboard(dashboard.FluentIndexDashboard):
|
||||||
""" Gets application modules from services, accounts and administration registries """
|
""" 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):
|
def process_registered_view(self, module, view_name, options):
|
||||||
app_name, name = view_name.split('_')[:-1]
|
app_name, name = view_name.split('_')[:-1]
|
||||||
module.icons['.'.join((app_name, name))] = options.get('icon')
|
module.icons['.'.join((app_name, name))] = options.get('icon')
|
||||||
|
@ -44,7 +53,7 @@ class OrchestraIndexDashboard(dashboard.FluentIndexDashboard):
|
||||||
# Honor settings override, hacky. I Know
|
# Honor settings override, hacky. I Know
|
||||||
if appsettings.FLUENT_DASHBOARD_APP_GROUPS[0][0] != _('CMS'):
|
if appsettings.FLUENT_DASHBOARD_APP_GROUPS[0][0] != _('CMS'):
|
||||||
modules = super(OrchestraIndexDashboard, self).get_application_modules()
|
modules = super(OrchestraIndexDashboard, self).get_application_modules()
|
||||||
for register in (accounts, administration, services):
|
for register in (accounts, services, administration):
|
||||||
title = register.verbose_name
|
title = register.verbose_name
|
||||||
models = []
|
models = []
|
||||||
icons = {}
|
icons = {}
|
||||||
|
|
|
@ -49,7 +49,7 @@ def service_report(modeladmin, request, queryset):
|
||||||
model = field.related_model
|
model = field.related_model
|
||||||
if model in registered_services and model != queryset.model:
|
if model in registered_services and model != queryset.model:
|
||||||
fields.append((model, name))
|
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]
|
fields = [field for model, field in fields]
|
||||||
|
|
||||||
for account in queryset.prefetch_related(*fields):
|
for account in queryset.prefetch_related(*fields):
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% load utils i18n %}
|
{% load i18n admin_urls utils %}
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>{% block title %}Account service report{% endblock %}</title>
|
<title>{% block title %}Account service report{% endblock %}</title>
|
||||||
|
@ -50,13 +50,29 @@
|
||||||
<div class="account-content">
|
<div class="account-content">
|
||||||
{{ account.get_type_display }} {% trans "account registered on" %} {{ account.date_joined | date }}<br>
|
{{ account.get_type_display }} {% trans "account registered on" %} {{ account.date_joined | date }}<br>
|
||||||
<ul class="items-ul">
|
<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 %}
|
{% 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>
|
<ul>
|
||||||
{% for obj in related %}
|
{% for obj in related %}
|
||||||
<li class="related"><a href="{{ obj|admin_url }}">{{ obj }}</a>
|
<li class="related"><a href="{{ obj|admin_url }}">{{ obj }}</a>
|
||||||
{% if not obj|isactive %} ({% trans "disabled" %}){% endif %}
|
{% if not obj|isactive %} ({% trans "disabled" %}){% endif %}
|
||||||
{{ obj.get_description|capfirst }}
|
{{ 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>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -9,5 +9,5 @@ class OrdersConfig(AppConfig):
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from .models import Order
|
from .models import Order
|
||||||
accounts.register(Order, icon='basket.png')
|
accounts.register(Order, icon='basket.png', search=False)
|
||||||
from . import signals
|
from . import signals
|
||||||
|
|
|
@ -10,5 +10,5 @@ class PaymentsConfig(AppConfig):
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from .models import PaymentSource, Transaction, TransactionProcess
|
from .models import PaymentSource, Transaction, TransactionProcess
|
||||||
accounts.register(PaymentSource, dashboard=False)
|
accounts.register(PaymentSource, dashboard=False)
|
||||||
accounts.register(Transaction, icon='transaction.png')
|
accounts.register(Transaction, icon='transaction.png', search=False)
|
||||||
accounts.register(TransactionProcess, icon='transactionprocess.png', dashboard=False)
|
accounts.register(TransactionProcess, icon='transactionprocess.png', dashboard=False, search=False)
|
||||||
|
|
|
@ -202,6 +202,10 @@ class ResourceData(models.Model):
|
||||||
def unit(self):
|
def unit(self):
|
||||||
return self.resource.unit
|
return self.resource.unit
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verbose_name(self):
|
||||||
|
return self.resource.verbose_name
|
||||||
|
|
||||||
def get_used(self):
|
def get_used(self):
|
||||||
resource = self.resource
|
resource = self.resource
|
||||||
total = 0
|
total = 0
|
||||||
|
@ -289,6 +293,8 @@ def create_resource_relation():
|
||||||
""" account.resources.web """
|
""" account.resources.web """
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
""" get or build ResourceData """
|
""" get or build ResourceData """
|
||||||
|
if attr.startswith('_'):
|
||||||
|
raise AttributeError
|
||||||
try:
|
try:
|
||||||
return self.obj.__resource_cache[attr]
|
return self.obj.__resource_cache[attr]
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -317,6 +323,9 @@ def create_resource_relation():
|
||||||
""" proxy handled object """
|
""" proxy handled object """
|
||||||
self.obj = obj
|
self.obj = obj
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self.obj.resource_set.all())
|
||||||
|
|
||||||
# Clean previous state
|
# Clean previous state
|
||||||
for related in Resource._related:
|
for related in Resource._related:
|
||||||
|
|
|
@ -15,6 +15,7 @@ class VPSAdmin(ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdmin):
|
||||||
form = NonStoredUserChangeForm
|
form = NonStoredUserChangeForm
|
||||||
add_form = UserCreationForm
|
add_form = UserCreationForm
|
||||||
readonly_fields = ('account_link',)
|
readonly_fields = ('account_link',)
|
||||||
|
search_fields = ('hostname', 'account__username', 'template')
|
||||||
change_readonly_fields = ('account', 'hostname', 'type', 'template')
|
change_readonly_fields = ('account', 'hostname', 'type', 'template')
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {
|
(None, {
|
||||||
|
|
|
@ -14,6 +14,9 @@ class Register(object):
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
return self._registry[key]
|
return self._registry[key]
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self._registry.values())
|
||||||
|
|
||||||
def register(self, model, **kwargs):
|
def register(self, model, **kwargs):
|
||||||
if model in self._registry:
|
if model in self._registry:
|
||||||
raise KeyError("%s already registered" % model)
|
raise KeyError("%s already registered" % model)
|
||||||
|
@ -23,6 +26,8 @@ class Register(object):
|
||||||
kwargs['verbose_name_plural'] = model._meta.verbose_name_plural
|
kwargs['verbose_name_plural'] = model._meta.verbose_name_plural
|
||||||
defaults = {
|
defaults = {
|
||||||
'menu': True,
|
'menu': True,
|
||||||
|
'search': True,
|
||||||
|
'model': model,
|
||||||
}
|
}
|
||||||
defaults.update(kwargs)
|
defaults.update(kwargs)
|
||||||
self._registry[model] = AttrDict(**defaults)
|
self._registry[model] = AttrDict(**defaults)
|
||||||
|
|
|
@ -6,12 +6,12 @@ body {
|
||||||
#header #branding h1 {
|
#header #branding h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 2px 15px;
|
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;
|
text-indent: 0;
|
||||||
height: 31px;
|
height: 31px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
/* font-weight: bold;*/
|
/* font-weight: bold;*/
|
||||||
padding-left: 50px;
|
padding-left: 45px;
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
border-right: 1px solid #ededed;
|
border-right: 1px solid #ededed;
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,9 +48,12 @@
|
||||||
<div style="max-width: 1170px; margin:auto;">
|
<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>
|
<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 %}
|
{% 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 %}
|
{% 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>
|
<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>
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
19
orchestra/templates/admin/orchestra/search.html
Normal file
19
orchestra/templates/admin/orchestra/search.html
Normal 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 %}
|
|
@ -24,6 +24,7 @@ urlpatterns = [
|
||||||
'orchestra.views.serve_private_media',
|
'orchestra.views.serve_private_media',
|
||||||
name='private-media'
|
name='private-media'
|
||||||
),
|
),
|
||||||
|
url(r'search', 'orchestra.views.search', name='search'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,22 @@
|
||||||
|
import itertools
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
from django.contrib import admin
|
||||||
from django.contrib.admin.utils import unquote
|
from django.contrib.admin.utils import unquote
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
from django.db.models import get_model
|
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 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):
|
def serve_private_media(request, app_label, model_name, field_name, object_id, filename):
|
||||||
model = get_model(app_label, model_name)
|
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)
|
return serve(request, field.name, document_root=field.storage.location)
|
||||||
else:
|
else:
|
||||||
raise PermissionDenied()
|
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)
|
||||||
|
|
Loading…
Reference in a new issue