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? * 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

View File

@ -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 = {}

View File

@ -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):

View File

@ -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>

View File

@ -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

View File

@ -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)

View File

@ -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:
@ -318,6 +324,9 @@ def create_resource_relation():
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:
try: try:

View File

@ -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, {

View File

@ -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)

View File

@ -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;
} }

View File

@ -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>

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', 'orchestra.views.serve_private_media',
name='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.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)