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?
|
||||
* <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
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
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',
|
||||
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.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)
|
||||
|
|
Loading…
Reference in a new issue