Revision 7c9baa43
Fusion du nouveau VigiGraph basé sur mootools dans le trunk.
git-svn-id: https://vigilo-dev.si.c-s.fr/svn@5124 b22e2e97-25c9-44ff-b637-2e5ceca36478
Makefile | ||
---|---|---|
1 | 1 |
NAME := vigigraph |
2 | 2 |
QOOXDOO_VER := 0.7.3 |
3 | 3 |
|
4 |
all: qooxdoo build
|
|
4 |
all: build |
|
5 | 5 |
|
6 | 6 |
include buildenv/Makefile.common |
7 | 7 |
PKGNAME := $(NAME) |
... | ... | |
9 | 9 |
CODEPATH := $(NAME) |
10 | 10 |
EPYDOC_PARSE := vigigraph\.controllers |
11 | 11 |
|
12 |
qooxdoo_source: javascript/qooxdoo-$(QOOXDOO_VER)-sdk/frontend/Makefile |
|
13 |
javascript/qooxdoo-$(QOOXDOO_VER)-sdk.tar.gz: |
|
14 |
#wget -P javascript/ http://downloads.sourceforge.net/project/qooxdoo/qooxdoo-legacy/$(QOOXDOO_VER)/qooxdoo-$(QOOXDOO_VER)-sdk.tar.gz |
|
15 |
wget -P javascript/ http://vigilo-dev.si.c-s.fr/cache/qooxdoo-$(QOOXDOO_VER)-sdk.tar.gz |
|
16 |
touch --no-create $@ |
|
17 |
javascript/qooxdoo-$(QOOXDOO_VER)-sdk/frontend/Makefile: javascript/qooxdoo-$(QOOXDOO_VER)-sdk.tar.gz |
|
18 |
tar -C javascript/ -xzf javascript/qooxdoo-0.7.3-sdk.tar.gz |
|
19 |
patch -p0 < patches/001_qooxdoo_getBoxObjectFor.diff |
|
20 |
touch --no-create $@ |
|
21 |
|
|
22 |
qooxdoo: vigigraph/public/js/vigigraph.js |
|
23 |
vigigraph/public/js/vigigraph.js: javascript/source/class/vigigraph/Application.js javascript/qooxdoo-$(QOOXDOO_VER)-sdk/frontend/Makefile |
|
24 |
make -C javascript build |
|
25 |
mkdir -p vigigraph/public/js/ |
|
26 |
cp -f javascript/build/script/vigigraph.js vigigraph/public/js/vigigraph.js |
|
27 |
cp -rf javascript/build/resource vigigraph/public/ |
|
28 |
|
|
29 |
clean_qooxdoo: |
|
30 |
$(RM) vigigraph/public/js/vigigraph.js |
|
31 |
$(RM) -r vigigraph/public/resource |
|
32 |
$(RM) -r javascript/build/ |
|
33 |
|
|
34 | 12 |
install: vigigraph/public/js/vigigraph.js $(PYTHON) |
35 | 13 |
$(PYTHON) setup.py install --single-version-externally-managed --root=$(DESTDIR) --record=INSTALLED_FILES |
36 | 14 |
chmod a+rX -R $(DESTDIR)$(PREFIX)/lib*/python*/* |
... | ... | |
54 | 32 |
|
55 | 33 |
lint: lint_pylint |
56 | 34 |
tests: tests_nose |
57 |
clean: clean_python clean_qooxdoo |
|
35 |
clean: clean_python |
babeljs.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
""" |
|
3 |
Provides a class derived from Babel's compile_catalog |
|
4 |
which automatically creates JavaScript files compatible |
|
5 |
with Babel's JavaScript frontend. |
|
6 |
|
|
7 |
This code was taken from: |
|
8 |
http://svn.python.org/projects/doctools/trunk/setup.py |
|
9 |
|
|
10 |
And generates files for use with: |
|
11 |
http://babel.edgewall.org/browser/trunk/contrib/babel.js |
|
12 |
""" |
|
13 |
try: |
|
14 |
from babel.messages.pofile import read_po |
|
15 |
from babel.messages.frontend import compile_catalog |
|
16 |
try: |
|
17 |
from simplejson import dump |
|
18 |
except ImportError: |
|
19 |
from json import dump |
|
20 |
except ImportError: |
|
21 |
pass |
|
22 |
else: |
|
23 |
import os |
|
24 |
from distutils import log |
|
25 |
|
|
26 |
class compile_catalog_plusjs(compile_catalog): |
|
27 |
""" |
|
28 |
An extended command that writes all message strings that occur in |
|
29 |
JavaScript files to a JavaScript file along with the .mo file. |
|
30 |
|
|
31 |
Unfortunately, babel's setup command isn't built very extensible, so |
|
32 |
most of the run() code is duplicated here. |
|
33 |
""" |
|
34 |
|
|
35 |
def run(self): |
|
36 |
compile_catalog.run(self) |
|
37 |
|
|
38 |
po_files = [] |
|
39 |
js_files = [] |
|
40 |
|
|
41 |
if not self.input_file: |
|
42 |
if self.locale: |
|
43 |
po_files.append((self.locale, |
|
44 |
os.path.join(self.directory, self.locale, |
|
45 |
'LC_MESSAGES', |
|
46 |
self.domain + '.po'))) |
|
47 |
js_files.append(os.path.join(self.directory, self.locale, |
|
48 |
'LC_MESSAGES', |
|
49 |
self.domain + '.js')) |
|
50 |
else: |
|
51 |
for locale in os.listdir(self.directory): |
|
52 |
po_file = os.path.join(self.directory, locale, |
|
53 |
'LC_MESSAGES', self.domain + '.po') |
|
54 |
if os.path.exists(po_file): |
|
55 |
po_files.append((locale, po_file)) |
|
56 |
js_files.append(os.path.join(self.directory, locale, |
|
57 |
'LC_MESSAGES', |
|
58 |
self.domain + '.js')) |
|
59 |
else: |
|
60 |
po_files.append((self.locale, self.input_file)) |
|
61 |
if self.output_file: |
|
62 |
js_files.append(self.output_file) |
|
63 |
else: |
|
64 |
js_files.append(os.path.join(self.directory, self.locale, |
|
65 |
'LC_MESSAGES', |
|
66 |
self.domain + '.js')) |
|
67 |
|
|
68 |
for js_file, (locale, po_file) in zip(js_files, po_files): |
|
69 |
infile = open(po_file, 'r') |
|
70 |
try: |
|
71 |
catalog = read_po(infile, locale) |
|
72 |
finally: |
|
73 |
infile.close() |
|
74 |
|
|
75 |
if catalog.fuzzy and not self.use_fuzzy: |
|
76 |
continue |
|
77 |
|
|
78 |
log.info('writing JavaScript strings in catalog %r to %r', |
|
79 |
po_file, js_file) |
|
80 |
|
|
81 |
jscatalog = {} |
|
82 |
for message in catalog: |
|
83 |
# Si le message n'a pas encore été traduit, |
|
84 |
# on ne l'ajoute pas. Le message d'origine |
|
85 |
# (non traduit) sera renvoyé. |
|
86 |
if not message.string: |
|
87 |
continue |
|
88 |
|
|
89 |
# On n'ajoute le message au fichier de traduction JS |
|
90 |
# auto-généré que si le message est utilisé dans du |
|
91 |
# code JavaScript. |
|
92 |
if any(x[0].endswith('.js') for x in message.locations): |
|
93 |
msgid = message.id |
|
94 |
if isinstance(msgid, (list, tuple)): |
|
95 |
msgid = msgid[0] |
|
96 |
jscatalog[msgid] = message.string |
|
97 |
|
|
98 |
outfile = open(js_file, 'wb') |
|
99 |
try: |
|
100 |
outfile.write('babel.Translations.load('); |
|
101 |
dump(dict( |
|
102 |
messages=jscatalog, |
|
103 |
plural_expr=catalog.plural_expr, |
|
104 |
locale=str(catalog.locale) |
|
105 |
), outfile) |
|
106 |
outfile.write(').install();') |
|
107 |
finally: |
|
108 |
outfile.close() |
|
109 |
|
development.ini | ||
---|---|---|
23 | 23 |
use = egg:vigigraph |
24 | 24 |
|
25 | 25 |
db_basename=vigilo_ |
26 |
password_hashing_function=md5 |
|
26 |
;password_hashing_function=md5
|
|
27 | 27 |
lang=fr |
28 | 28 |
use_kerberos=False |
29 | 29 |
help_link=http://foo.bar/help |
... | ... | |
33 | 33 |
|
34 | 34 |
; Emplacement des applications (vigirrd, Nagios, ...) |
35 | 35 |
; sur les serveurs distants. |
36 |
app_path.vigirrd = /vigilo/vigirrd/
|
|
36 |
app_path.vigirrd = / |
|
37 | 37 |
app_path.nagios = /nagios/ |
38 | 38 |
|
39 | 39 |
; Schéma ("protocol") à utiliser pour se connecter |
... | ... | |
47 | 47 |
; Port de connexion au serveur distant hébergeant |
48 | 48 |
; l'application. |
49 | 49 |
; Par défaut : 80 |
50 |
app_port.vigirrd = 80 |
|
50 |
app_port.vigirrd = 8084
|
|
51 | 51 |
app_port.nagios = 80 |
52 | 52 |
|
53 | 53 |
# Délai de rafraîchissement (en secondes) |
patches/001_qooxdoo_getBoxObjectFor.diff | ||
---|---|---|
1 |
--- javascript/qooxdoo-0.7.3-sdk/frontend/framework/source/class/qx/html/Location.js.old 2010-04-20 14:18:32.000000000 +0200 |
|
2 |
+++ javascript/qooxdoo-0.7.3-sdk/frontend/framework/source/class/qx/html/Location.js 2010-04-20 14:42:14.000000000 +0200 |
|
3 |
@@ -419,7 +419,7 @@ |
|
4 |
getPageAreaLeft : qx.core.Variant.select("qx.client", |
|
5 |
{ |
|
6 |
"gecko" : function(el) { |
|
7 |
- return el.ownerDocument.getBoxObjectFor(el).x; |
|
8 |
+ return el.getBoundingClientRect().left; |
|
9 |
}, |
|
10 |
|
|
11 |
"default" : function(el) { |
|
12 |
@@ -439,7 +439,7 @@ |
|
13 |
getPageAreaTop : qx.core.Variant.select("qx.client", |
|
14 |
{ |
|
15 |
"gecko" : function(el) { |
|
16 |
- return el.ownerDocument.getBoxObjectFor(el).y; |
|
17 |
+ return el.getBoundingClientRect().top; |
|
18 |
}, |
|
19 |
|
|
20 |
"default" : function(el) { |
pkg/vigigraph.mandriva.spec | ||
---|---|---|
93 | 93 |
|
94 | 94 |
%prep |
95 | 95 |
%setup -q -n %{module}-%{version} |
96 |
make qooxdoo_source |
|
97 | 96 |
|
98 | 97 |
%build |
99 |
make qooxdoo |
|
100 | 98 |
make PYTHON=%{_bindir}/python SYSCONFDIR=%{_sysconfdir} |
101 | 99 |
|
102 | 100 |
%install |
pkg/vigigraph.redhat.spec | ||
---|---|---|
37 | 37 |
%setup -q -n %{module}-%{version} |
38 | 38 |
# A cause des permissions sur /var/log/httpd sur Red Hat |
39 | 39 |
sed -i -e '/<IfModule mod_wsgi\.c>/a WSGISocketPrefix run/wsgi' deployment/%{module}.conf |
40 |
make qooxdoo_source |
|
41 | 40 |
|
42 | 41 |
%build |
43 |
make qooxdoo |
|
44 | 42 |
make PYTHON=%{__python} SYSCONFDIR=%{_sysconfdir} |
45 | 43 |
|
46 | 44 |
%install |
setup.py | ||
---|---|---|
1 | 1 |
# -*- coding: utf-8 -*- |
2 | 2 |
# vim:set expandtab tabstop=4 shiftwidth=4: |
3 |
|
|
3 | 4 |
import os |
5 |
|
|
4 | 6 |
try: |
5 | 7 |
from setuptools import setup, find_packages |
6 | 8 |
except ImportError: |
... | ... | |
8 | 10 |
use_setuptools() |
9 | 11 |
from setuptools import setup, find_packages |
10 | 12 |
|
13 |
cmdclass = {} |
|
14 |
try: |
|
15 |
from babeljs import compile_catalog_plusjs |
|
16 |
except ImportError: |
|
17 |
pass |
|
18 |
else: |
|
19 |
cmdclass['compile_catalog'] = compile_catalog_plusjs |
|
20 |
|
|
11 | 21 |
sysconfdir = os.getenv("SYSCONFDIR", "/etc") |
12 | 22 |
|
13 | 23 |
tests_require = [ |
... | ... | |
38 | 48 |
package_data={ |
39 | 49 |
'vigigraph': [ |
40 | 50 |
'i18n/*/LC_MESSAGES/*.mo', |
51 |
'i18n/*/LC_MESSAGES/*.js', |
|
41 | 52 |
'templates/*/*', |
42 | 53 |
'public/js/*.js', |
43 |
# 'public/*/*', |
|
44 | 54 |
], |
45 | 55 |
}, |
46 | 56 |
message_extractors={ |
47 | 57 |
'vigigraph': [ |
48 | 58 |
('**.py', 'python', None), |
59 |
('**/public/js/*.js', 'javascript', None), |
|
49 | 60 |
], |
50 | 61 |
}, |
51 | 62 |
|
... | ... | |
61 | 72 |
], |
62 | 73 |
}, |
63 | 74 |
|
75 |
cmdclass=cmdclass, |
|
64 | 76 |
data_files=[ |
65 | 77 |
(os.path.join(sysconfdir, 'vigilo/vigigraph/'), [ |
66 | 78 |
'deployment/vigigraph.conf', |
vigigraph/controllers/root.py | ||
---|---|---|
4 | 4 |
|
5 | 5 |
import logging |
6 | 6 |
from tg import expose, flash, require, request, redirect |
7 |
from pylons.i18n import ugettext as _, lazy_ugettext as l_ |
|
7 |
from pylons.i18n import ugettext as _, lazy_ugettext as l_, get_lang
|
|
8 | 8 |
from repoze.what.predicates import Any, All, not_anonymous, \ |
9 | 9 |
has_permission, in_group |
10 | 10 |
|
... | ... | |
45 | 45 |
"""Handle the front-page.""" |
46 | 46 |
return dict(page='index') |
47 | 47 |
|
48 |
@expose() |
|
49 |
def i18n(self): |
|
50 |
import gettext |
|
51 |
import pylons |
|
52 |
import os.path |
|
53 |
|
|
54 |
# Repris de pylons.i18n.translation:_get_translator. |
|
55 |
conf = pylons.config.current_conf() |
|
56 |
try: |
|
57 |
rootdir = conf['pylons.paths']['root'] |
|
58 |
except KeyError: |
|
59 |
rootdir = conf['pylons.paths'].get('root_path') |
|
60 |
localedir = os.path.join(rootdir, 'i18n') |
|
61 |
|
|
62 |
# Localise le fichier *.mo actuellement chargé |
|
63 |
# et génère le chemin jusqu'au *.js correspondant. |
|
64 |
filename = gettext.find(conf['pylons.package'], localedir, |
|
65 |
languages=get_lang()) |
|
66 |
js = filename[:-3] + '.js' |
|
67 |
|
|
68 |
# Récupère et envoie le contenu du fichier de traduction *.js. |
|
69 |
fhandle = open(js, 'r') |
|
70 |
translations = fhandle.read() |
|
71 |
fhandle.close() |
|
72 |
return translations |
|
73 |
|
|
48 | 74 |
@expose('login.html') |
49 | 75 |
def login(self, came_from='/'): |
50 | 76 |
"""Start the user login.""" |
vigigraph/controllers/rpc.py | ||
---|---|---|
19 | 19 |
in_group, Any, All |
20 | 20 |
from formencode import validators, schema |
21 | 21 |
from sqlalchemy import or_ |
22 |
from sqlalchemy.sql.expression import literal_column |
|
22 | 23 |
|
23 | 24 |
from vigilo.turbogears.controllers import BaseController |
24 | 25 |
from vigilo.turbogears.helpers import get_current_user |
... | ... | |
35 | 36 |
from vigilo.models.tables.secondary_tables import GRAPH_PERFDATASOURCE_TABLE |
36 | 37 |
from vigilo.models.functions import sql_escape_like |
37 | 38 |
|
38 |
from vigigraph.widgets.searchhostform import SearchHostForm |
|
39 |
|
|
40 | 39 |
LOGGER = logging.getLogger(__name__) |
41 | 40 |
|
42 | 41 |
__all__ = ['RpcController'] |
... | ... | |
78 | 77 |
flash("'%s': %s" % (k, tmpl_context.form_errors[k]), 'error') |
79 | 78 |
redirect(request.environ.get('HTTP_REFERER', '/')) |
80 | 79 |
|
81 |
@expose('json') |
|
82 |
def maingroups(self, nocache=None): |
|
83 |
""" |
|
84 |
Determination des groupes principaux (sans parent) |
|
85 |
|
|
86 |
@return: Dictionnaire dont la clé "items" contient une liste |
|
87 |
de tuples contenant le nom et l'ID des groupes d'éléments |
|
88 |
au sommet de la hiérarchie et auquels l'utilisateur a accès. |
|
89 |
@rtype: C{dict} |
|
90 |
@note: L'ID des groupes est converti en chaîne de caractères |
|
91 |
dans le résultat. |
|
92 |
""" |
|
93 |
user = get_current_user() |
|
94 |
if user is None: |
|
95 |
return dict(items=[]) |
|
96 |
|
|
97 |
groups_with_parents = DBSession.query( |
|
98 |
GroupHierarchy.idparent, |
|
99 |
).distinct() |
|
100 |
|
|
101 |
# Les managers ont accès à tout. |
|
102 |
# Les autres ont un accès restreint. |
|
103 |
is_manager = in_group('managers').is_met(request.environ) |
|
104 |
if not is_manager: |
|
105 |
supitemgroups = [sig[0] for sig in user.supitemgroups() if sig[1]] |
|
106 |
groups_with_parents = groups_with_parents.filter( |
|
107 |
GroupHierarchy.idchild.in_(supitemgroups)) |
|
108 |
|
|
109 |
groups_with_parents = [g.idparent for g in groups_with_parents.all()] |
|
110 |
children = DBSession.query( |
|
111 |
SupItemGroup |
|
112 |
).distinct( |
|
113 |
).join( |
|
114 |
(GroupHierarchy, GroupHierarchy.idchild == SupItemGroup.idgroup) |
|
115 |
).filter(GroupHierarchy.hops > 0) |
|
116 |
|
|
117 |
topgroups = DBSession.query( |
|
118 |
SupItemGroup, |
|
119 |
).filter(SupItemGroup.idgroup.in_(groups_with_parents) |
|
120 |
).except_(children).order_by(SupItemGroup.name) |
|
121 |
topgroups = [(sig.name, str(sig.idgroup)) for sig in topgroups.all()] |
|
122 |
return dict(items=topgroups) |
|
123 |
|
|
124 |
class HostgroupsSchema(schema.Schema): |
|
125 |
"""Schéma de validation pour la méthode L{hostgroups}.""" |
|
126 |
maingroupid = validators.Int(not_empty=True) |
|
127 |
nocache = validators.String(if_missing=None) |
|
128 |
|
|
129 |
# @TODO définir un error_handler différent pour remonter l'erreur via JS. |
|
130 |
@validate( |
|
131 |
validators = HostgroupsSchema(), |
|
132 |
error_handler = process_form_errors) |
|
133 |
@expose('json') |
|
134 |
def hostgroups(self, maingroupid, nocache): |
|
135 |
""" |
|
136 |
Determination des groupes associes au groupe parent |
|
137 |
dont identificateur = argument |
|
138 |
|
|
139 |
@param maingroupid: identificateur d un groupe principal |
|
140 |
@type maingroupid: C{int} |
|
141 |
|
|
142 |
@return: Dictionnaire dont la clé "items" contient une liste |
|
143 |
de tuples avec le nom et l'ID des groupes d'éléments |
|
144 |
auxquels l'utilisateur a accès. |
|
145 |
@rtype: C{dict} |
|
146 |
@note: L'ID des groupes est converti en chaîne de caractères |
|
147 |
dans le résultat. |
|
148 |
""" |
|
149 |
user = get_current_user() |
|
150 |
if user is None: |
|
151 |
return dict(items=[]) |
|
152 |
|
|
153 |
hostgroups = DBSession.query( |
|
154 |
SupItemGroup.name, |
|
155 |
SupItemGroup.idgroup, |
|
156 |
).distinct().join( |
|
157 |
(GroupHierarchy, GroupHierarchy.idchild == \ |
|
158 |
SupItemGroup.idgroup), |
|
159 |
).filter(GroupHierarchy.idparent == maingroupid |
|
160 |
).filter(GroupHierarchy.hops == 1 |
|
161 |
).order_by(SupItemGroup.name.asc()) |
|
162 |
|
|
163 |
# Les managers ont accès à tout. |
|
164 |
# Les autres ont un accès restreint. |
|
165 |
is_manager = in_group('managers').is_met(request.environ) |
|
166 |
if not is_manager: |
|
167 |
supitemgroups = [sig[0] for sig in user.supitemgroups() if sig[1]] |
|
168 |
hostgroups = hostgroups.filter( |
|
169 |
SupItemGroup.idgroup.in_(supitemgroups)) |
|
170 |
|
|
171 |
hostgroups = [(hg.name, str(hg.idgroup)) for hg in hostgroups.all()] |
|
172 |
hostgroups.insert(0, (_('No subgroup'), str(maingroupid))) |
|
173 |
return dict(items=hostgroups) |
|
174 |
|
|
175 |
class HostsSchema(schema.Schema): |
|
176 |
"""Schéma de validation pour la méthode L{hosts}.""" |
|
177 |
othergroupid = validators.Int(not_empty=True) |
|
178 |
nocache = validators.String(if_missing=None) |
|
179 |
|
|
180 |
# @TODO définir un error_handler différent pour remonter l'erreur via JS. |
|
181 |
@validate( |
|
182 |
validators = HostsSchema(), |
|
183 |
error_handler = process_form_errors) |
|
184 |
@expose('json') |
|
185 |
def hosts(self, othergroupid, nocache): |
|
186 |
""" |
|
187 |
Determination des hotes associes au groupe |
|
188 |
dont identificateur = argument |
|
189 |
|
|
190 |
@param othergroupid : identificateur d un groupe |
|
191 |
@type othergroupid : int |
|
192 |
|
|
193 |
@return: hotes |
|
194 |
@rtype: document json (sous forme de dict) |
|
195 |
""" |
|
196 |
user = get_current_user() |
|
197 |
if user is None: |
|
198 |
return dict(items=[]) |
|
199 |
|
|
200 |
groups_with_parents = DBSession.query( |
|
201 |
GroupHierarchy.idparent, |
|
202 |
).distinct() |
|
203 |
|
|
204 |
# Les managers ont accès à tout. |
|
205 |
# Les autres ont un accès restreint. |
|
206 |
is_manager = in_group('managers').is_met(request.environ) |
|
207 |
if not is_manager: |
|
208 |
supitemgroups = [sig[0] for sig in user.supitemgroups() if sig[1]] |
|
209 |
groups_with_parents = groups_with_parents.filter( |
|
210 |
GroupHierarchy.idchild.in_(supitemgroups)) |
|
211 |
|
|
212 |
groups_with_parents = [g.idparent for g in groups_with_parents.all()] |
|
213 |
hosts = DBSession.query( |
|
214 |
Host.name, |
|
215 |
Host.idhost, |
|
216 |
).distinct( |
|
217 |
).join( |
|
218 |
(SUPITEM_GROUP_TABLE, SUPITEM_GROUP_TABLE.c.idsupitem == \ |
|
219 |
Host.idhost), |
|
220 |
).filter(SUPITEM_GROUP_TABLE.c.idgroup == othergroupid |
|
221 |
).filter(SUPITEM_GROUP_TABLE.c.idgroup.in_(groups_with_parents) |
|
222 |
).order_by( |
|
223 |
Host.name.asc(), |
|
224 |
).all() |
|
225 |
|
|
226 |
hosts = [(h.name, str(h.idhost)) for h in hosts] |
|
227 |
return dict(items=hosts) |
|
228 |
|
|
229 |
class GraphGroupsSchema(schema.Schema): |
|
230 |
"""Schéma de validation pour la méthode L{graphgroups}.""" |
|
231 |
idhost = validators.Int(not_empty=True) |
|
232 |
nocache = validators.String(if_missing=None) |
|
233 |
|
|
234 |
# @TODO définir un error_handler différent pour remonter l'erreur via JS. |
|
235 |
@validate( |
|
236 |
validators = GraphGroupsSchema(), |
|
237 |
error_handler = process_form_errors) |
|
238 |
@expose('json') |
|
239 |
def graphgroups(self, idhost, nocache): |
|
240 |
""" |
|
241 |
Determination des groupes de graphes associes a l hote |
|
242 |
dont identificateur = argument |
|
243 |
|
|
244 |
@param idhost : identificateur d un hote |
|
245 |
@type idhost : int |
|
246 |
|
|
247 |
@return: groupes de service |
|
248 |
@rtype: document json (sous forme de dict) |
|
249 |
""" |
|
250 |
user = get_current_user() |
|
251 |
if user is None: |
|
252 |
return dict(items=[]) |
|
253 |
|
|
254 |
graphgroups = DBSession.query( |
|
255 |
GraphGroup.name, |
|
256 |
GraphGroup.idgroup, |
|
257 |
).distinct( |
|
258 |
).join( |
|
259 |
(GRAPH_GROUP_TABLE, GRAPH_GROUP_TABLE.c.idgroup == \ |
|
260 |
GraphGroup.idgroup), |
|
261 |
(Graph, Graph.idgraph == GRAPH_GROUP_TABLE.c.idgraph), |
|
262 |
(GRAPH_PERFDATASOURCE_TABLE, \ |
|
263 |
GRAPH_PERFDATASOURCE_TABLE.c.idgraph == Graph.idgraph), |
|
264 |
(PerfDataSource, PerfDataSource.idperfdatasource == \ |
|
265 |
GRAPH_PERFDATASOURCE_TABLE.c.idperfdatasource), |
|
266 |
(SUPITEM_GROUP_TABLE, SUPITEM_GROUP_TABLE.c.idsupitem == \ |
|
267 |
PerfDataSource.idhost), |
|
268 |
).filter(PerfDataSource.idhost == idhost |
|
269 |
).order_by(GraphGroup.name.asc()) |
|
270 |
|
|
271 |
# Les managers ont accès à tout. |
|
272 |
# Les autres ont un accès restreint. |
|
273 |
is_manager = in_group('managers').is_met(request.environ) |
|
274 |
if not is_manager: |
|
275 |
supitemgroups = [sig[0] for sig in user.supitemgroups() if sig[1]] |
|
276 |
graphgroups = graphgroups.filter( |
|
277 |
SUPITEM_GROUP_TABLE.c.idgroup.in_(supitemgroups)) |
|
278 |
|
|
279 |
graphgroups = [(gg.name, str(gg.idgroup)) for gg in graphgroups.all()] |
|
280 |
return dict(items=graphgroups) |
|
281 |
|
|
282 |
class GraphsSchema(schema.Schema): |
|
283 |
"""Schéma de validation pour la méthode L{graphs}.""" |
|
284 |
idgraphgroup = validators.Int(not_empty=True) |
|
285 |
idhost = validators.Int(not_empty=True) |
|
286 |
nocache = validators.String(if_missing=None) |
|
287 |
|
|
288 |
# @TODO définir un error_handler différent pour remonter l'erreur via JS. |
|
289 |
@validate( |
|
290 |
validators = GraphsSchema(), |
|
291 |
error_handler = process_form_errors) |
|
292 |
@expose('json') |
|
293 |
def graphs(self, idgraphgroup, idhost, nocache): |
|
294 |
""" |
|
295 |
Determination des graphes |
|
296 |
avec un service dont identificateur = argument |
|
297 |
|
|
298 |
@param idgraph : identificateur d un service |
|
299 |
@type idgraph : int |
|
300 |
|
|
301 |
@return: graphes |
|
302 |
@rtype: document json (sous forme de dict) |
|
303 |
""" |
|
304 |
user = get_current_user() |
|
305 |
if user is None: |
|
306 |
return dict(items=[]) |
|
307 |
|
|
308 |
graphs = DBSession.query( |
|
309 |
Graph.name, |
|
310 |
Graph.idgraph, |
|
311 |
).distinct().join( |
|
312 |
(GRAPH_GROUP_TABLE, GRAPH_GROUP_TABLE.c.idgraph == \ |
|
313 |
Graph.idgraph), |
|
314 |
(GraphGroup, GraphGroup.idgroup == \ |
|
315 |
GRAPH_GROUP_TABLE.c.idgroup), |
|
316 |
(GRAPH_PERFDATASOURCE_TABLE, \ |
|
317 |
GRAPH_PERFDATASOURCE_TABLE.c.idgraph == Graph.idgraph), |
|
318 |
(PerfDataSource, PerfDataSource.idperfdatasource == \ |
|
319 |
GRAPH_PERFDATASOURCE_TABLE.c.idperfdatasource), |
|
320 |
(SUPITEM_GROUP_TABLE, SUPITEM_GROUP_TABLE.c.idsupitem == \ |
|
321 |
PerfDataSource.idhost), |
|
322 |
).filter(GraphGroup.idgroup == idgraphgroup |
|
323 |
).filter(PerfDataSource.idhost == idhost |
|
324 |
).order_by(Graph.name.asc()) |
|
325 |
|
|
326 |
# Les managers ont accès à tout. |
|
327 |
# Les autres ont un accès restreint. |
|
328 |
is_manager = in_group('managers').is_met(request.environ) |
|
329 |
if not is_manager: |
|
330 |
supitemgroups = [sig[0] for sig in user.supitemgroups() if sig[1]] |
|
331 |
graphs = graphs.filter( |
|
332 |
SUPITEM_GROUP_TABLE.c.idgroup.in_(supitemgroups)) |
|
333 |
|
|
334 |
graphs = [(pds.name, str(pds.idgraph)) for pds in graphs.all()] |
|
335 |
return dict(items=graphs) |
|
336 |
|
|
337 | 80 |
class SearchHostAndGraphSchema(schema.Schema): |
338 | 81 |
"""Schéma de validation pour la méthode L{searchHostAndGraph}.""" |
339 |
host = validators.String(if_missing=None) |
|
340 |
graph = validators.String(if_missing=None) |
|
341 |
nocache = validators.String(if_missing=None) |
|
82 |
search_form_host = validators.String(if_missing=None) |
|
83 |
search_form_graph = validators.String(if_missing=None) |
|
342 | 84 |
|
343 | 85 |
# @TODO définir un error_handler différent pour remonter l'erreur via JS. |
344 | 86 |
@validate( |
345 | 87 |
validators = SearchHostAndGraphSchema(), |
346 | 88 |
error_handler = process_form_errors) |
347 | 89 |
@expose('json') |
348 |
def searchHostAndGraph(self, host, graph, nocache):
|
|
90 |
def searchHostAndGraph(self, search_form_host, search_form_graph):
|
|
349 | 91 |
""" |
350 | 92 |
Determination des couples (hote-graphe) repondant aux criteres de |
351 | 93 |
recherche sur hote et/ou graphe. |
... | ... | |
369 | 111 |
# On a un nom d'indicateur, mais pas de nom d'hôte, |
370 | 112 |
# on considère que l'utilisateur veut tous les indicateurs |
371 | 113 |
# correspondant au motif, quel que soit l'hôte. |
372 |
if graph is not None:
|
|
373 |
if host is None:
|
|
374 |
host = '*' |
|
114 |
if search_form_graph:
|
|
115 |
if not search_form_host:
|
|
116 |
search_form_host = '*'
|
|
375 | 117 |
|
376 |
host = sql_escape_like(host)
|
|
377 |
graph = sql_escape_like(graph)
|
|
118 |
search_form_host = sql_escape_like(search_form_host)
|
|
119 |
search_form_graph = sql_escape_like(search_form_graph)
|
|
378 | 120 |
|
379 | 121 |
items = DBSession.query( |
122 |
Host.idhost.label('idhost'), |
|
380 | 123 |
Host.name.label('hostname'), |
124 |
Graph.idgraph.label('idgraph'), |
|
381 | 125 |
Graph.name.label('graphname'), |
382 | 126 |
).distinct().join( |
383 | 127 |
(PerfDataSource, PerfDataSource.idhost == Host.idhost), |
... | ... | |
388 | 132 |
GRAPH_PERFDATASOURCE_TABLE.c.idgraph), |
389 | 133 |
(SUPITEM_GROUP_TABLE, SUPITEM_GROUP_TABLE.c.idsupitem == \ |
390 | 134 |
Host.idhost), |
391 |
).filter(Host.name.ilike(host) |
|
392 |
).filter(Graph.name.ilike(graph) |
|
135 |
).filter(Host.name.ilike(search_form_host)
|
|
136 |
).filter(Graph.name.ilike(search_form_graph)
|
|
393 | 137 |
).order_by( |
394 | 138 |
Host.name.asc(), |
395 | 139 |
Graph.name.asc(), |
... | ... | |
398 | 142 |
# On a ni hôte, ni indicateur. On renvoie une liste vide. |
399 | 143 |
# Si l'utilisateur voulait vraiment quelque chose, |
400 | 144 |
# il n'avait qu'à le demander. |
401 |
elif host is None:
|
|
145 |
elif not search_form_host:
|
|
402 | 146 |
return [] |
403 | 147 |
|
404 | 148 |
# Sinon, on a juste un motif pour un hôte. |
405 | 149 |
# On renvoie la liste des hôtes correspondant. |
406 | 150 |
else: |
407 |
host = sql_escape_like(host)
|
|
151 |
search_form_host = sql_escape_like(search_form_host)
|
|
408 | 152 |
items = DBSession.query( |
153 |
Host.idhost.label('idhost'), |
|
409 | 154 |
Host.name.label('hostname'), |
410 | 155 |
).distinct().join( |
411 | 156 |
(SUPITEM_GROUP_TABLE, SUPITEM_GROUP_TABLE.c.idsupitem == \ |
412 | 157 |
Host.idhost), |
413 |
).filter(Host.name.ilike(host) |
|
158 |
).filter(Host.name.ilike(search_form_host)
|
|
414 | 159 |
).order_by(Host.name.asc()) |
415 | 160 |
|
416 | 161 |
# Les managers ont accès à tout. |
... | ... | |
423 | 168 |
SUPITEM_GROUP_TABLE.c.idgroup.in_(supitemgroups)) |
424 | 169 |
|
425 | 170 |
items = items.limit(100).all() # pylint: disable-msg=E1103 |
426 |
if graph is None: |
|
427 |
items = [(item.hostname, "") for item in items] |
|
171 |
if not search_form_graph: |
|
172 |
ids = [(item.idhost, None) for item in items] |
|
173 |
labels = [(item.hostname, None) for item in items] |
|
428 | 174 |
else: |
429 |
items = [(item.hostname, item.graphname) for item in items] |
|
430 |
return dict(items=items) |
|
431 |
|
|
432 |
class SelectHostAndGraphSchema(schema.Schema): |
|
433 |
"""Schéma de validation pour la méthode L{selectHostAndGraph}.""" |
|
434 |
host = validators.String(if_missing=None) |
|
435 |
graph = validators.String(if_missing=None) |
|
436 |
nocache = validators.String(if_missing=None) |
|
437 |
|
|
438 |
# @TODO définir un error_handler différent pour remonter l'erreur via JS. |
|
439 |
@validate( |
|
440 |
validators = SelectHostAndGraphSchema(), |
|
441 |
error_handler = process_form_errors) |
|
442 |
@expose('json') |
|
443 |
def selectHostAndGraph(self, host, graph, nocache): |
|
444 |
""" |
|
445 |
Renvoie les valeurs à sélectionner dans les comboboxes |
|
446 |
de VigiGraph pour afficher les données de l'hôte ou du |
|
447 |
couple hôte/graphe sélectionné. |
|
448 |
|
|
449 |
La clé "items" du dictionnaire renvoyé contient une liste avec |
|
450 |
2 éléments, chacun de ces éléments étant lui-même une liste. |
|
451 |
La 1ère liste contient les noms des groupes d'hôtes à sélectionner. |
|
452 |
La 2ème liste contient la liste des groupes de graphes à sélectionner. |
|
453 |
|
|
454 |
Pour le moment, la 2ème liste contiendra au plus 1 élément car |
|
455 |
les groupes de graphes ne sont pas récursifs. L'utilisation d'une |
|
456 |
liste permet d'assurer facilement une évolution vers des groupes |
|
457 |
de graphes récursifs. |
|
458 |
""" |
|
459 |
user = get_current_user() |
|
460 |
if user is None: |
|
461 |
return dict(items=[[], []]) |
|
462 |
|
|
463 |
# Ce cas ne devrait pas se produire, mais on tente |
|
464 |
# d'avoir un comportement gracieux malgré tout. |
|
465 |
if (not host) and (not graph): |
|
466 |
return dict(items=[[], []]) |
|
467 |
|
|
468 |
if graph and not host: |
|
469 |
raise HTTPPreconditionFailed(_("No host given")) |
|
470 |
|
|
471 |
selected_hostgroups = [] |
|
472 |
selected_graphgroups = [] |
|
473 |
is_manager = in_group('managers').is_met(request.environ) |
|
474 |
|
|
475 |
supitemgroups = [] |
|
476 |
if not is_manager: |
|
477 |
supitemgroups = [sig[0] for sig in user.supitemgroups() if sig[1]] |
|
478 |
|
|
479 |
if host: |
|
480 |
# Sélectionne l'identifiant du premier SupItemGroup auquel |
|
481 |
# l'utilisateur a accès et auquel l'hôte donné appartient. |
|
482 |
idsupitemgroup = DBSession.query( |
|
483 |
SupItemGroup.idgroup, |
|
484 |
).distinct().join( |
|
485 |
(SUPITEM_GROUP_TABLE, SUPITEM_GROUP_TABLE.c.idgroup == \ |
|
486 |
SupItemGroup.idgroup), |
|
487 |
(Host, Host.idhost == SUPITEM_GROUP_TABLE.c.idsupitem), |
|
488 |
).filter(Host.name == host) |
|
489 |
|
|
490 |
# On prend en compte les permissions de l'utilisateur. |
|
491 |
if not is_manager: |
|
492 |
idsuitemgroup = idsupitemgroup.filter( |
|
493 |
SupItemGroup.idgroup.in_(supitemgroups)) |
|
494 |
|
|
495 |
idsupitemgroup = idsupitemgroup.scalar() |
|
496 |
|
|
497 |
# Si on a trouvé un tel groupe, on renvoie les noms des |
|
498 |
# groupes de la hiérarchie à sélectionner pour arriver |
|
499 |
# à celui-ci. |
|
500 |
if idsupitemgroup is not None: |
|
501 |
selected_hostgroups = DBSession.query( |
|
502 |
SupItemGroup.name, |
|
503 |
).distinct().join( |
|
504 |
(GroupHierarchy, GroupHierarchy.idparent == \ |
|
505 |
GraphGroup.idgroup), |
|
506 |
).filter(GroupHierarchy.idchild == idsupitemgroup |
|
507 |
).order_by( |
|
508 |
GroupHierarchy.hops.desc() |
|
509 |
).all() |
|
510 |
|
|
511 |
if graph: |
|
512 |
# Le principe est le même que pour l'hôte, en considérant |
|
513 |
# cette fois les GraphGroup à la place des SupItemGroup. |
|
514 |
idgraphgroup = DBSession.query( |
|
515 |
GraphGroup.idgroup, |
|
516 |
).join( |
|
517 |
(GRAPH_GROUP_TABLE, GRAPH_GROUP_TABLE.c.idgroup == \ |
|
518 |
GraphGroup.idgroup), |
|
519 |
(Graph, Graph.idgraph == GRAPH_GROUP_TABLE.c.idgraph), |
|
520 |
).filter(Graph.name == graph) |
|
521 |
|
|
522 |
# On prend en compte les permissions de l'utilisateur. |
|
523 |
if not is_manager: |
|
524 |
idgraphgroup = idgraphgroup.filter( |
|
525 |
GraphGroup.idgroup.in_(supitemgroups)) |
|
526 |
|
|
527 |
idgraphgroup = idgraphgroup.scalar() |
|
528 |
|
|
529 |
# Même principe que pour l'hôte. |
|
530 |
if idgraphgroup is not None: |
|
531 |
selected_graphgroups = DBSession.query( |
|
532 |
GraphGroup.name, |
|
533 |
).join( |
|
534 |
(GroupHierarchy, GroupHierarchy.idparent == \ |
|
535 |
GraphGroup.idgroup), |
|
536 |
).filter(GroupHierarchy.idchild == idgraphgroup |
|
537 |
).order_by( |
|
538 |
GroupHierarchy.hops.desc() |
|
539 |
).all() |
|
540 |
|
|
541 |
hostgroups = [hg.name for hg in selected_hostgroups] |
|
542 |
# @FIXME: Ce test est nécessaire tant que l'interface Qooxdoo |
|
543 |
# monolithique est conservée (ie: 2 niveaux de profondeur figés). |
|
544 |
if len(hostgroups) == 1: |
|
545 |
hostgroups.append(_('No subgroup')) |
|
546 |
graphgroups = [gg.name for gg in selected_graphgroups] |
|
547 |
return dict(items=[hostgroups, graphgroups]) |
|
175 |
ids = [(item.idhost, item.idgraph) for item in items] |
|
176 |
labels = [(item.hostname, item.graphname) for item in items] |
|
177 |
return dict(labels=labels, ids=ids) |
|
548 | 178 |
|
549 | 179 |
@expose('graphslist.html') |
550 | 180 |
def graphsList(self, nocache=None, **kwargs): |
... | ... | |
584 | 214 |
graphslist.append(graph) |
585 | 215 |
return dict(graphslist=graphslist) |
586 | 216 |
|
587 |
@expose(content_type='text/plain')
|
|
217 |
@expose('json')
|
|
588 | 218 |
def tempoDelayRefresh(self, nocache=None): |
589 | 219 |
""" |
590 | 220 |
Determination valeur temporisation pour le rafraichissement automatique |
... | ... | |
598 | 228 |
delay = int(config['refresh_delay']) |
599 | 229 |
except (ValueError, KeyError): |
600 | 230 |
delay = 30 |
601 |
return str(delay)
|
|
231 |
return {'delay': delay}
|
|
602 | 232 |
|
603 | 233 |
class GetIndicatorsSchema(schema.Schema): |
604 | 234 |
"""Schéma de validation pour la méthode L{getIndicators}.""" |
... | ... | |
738 | 368 |
return dict(host=host, graph=graph, start=start, duration=duration, \ |
739 | 369 |
presets=self.presets) |
740 | 370 |
|
741 |
@expose('searchhostform.html') |
|
742 |
def searchHostForm(self): |
|
743 |
""" |
|
744 |
Formulaire de recherche sur les hotes |
|
745 |
|
|
746 |
@return: page avec formulaire de recherche |
|
747 |
@rtype: page html |
|
748 |
""" |
|
749 |
searchhostform = SearchHostForm('search_host_form', \ |
|
750 |
submit_text=None) |
|
751 |
|
|
752 |
return dict(searchhostform=searchhostform) |
|
753 |
|
|
754 | 371 |
@expose('searchhost.html') |
755 | 372 |
@paginate('hosts', items_per_page=10) |
756 | 373 |
def searchHost(self, *args, **kwargs): |
... | ... | |
806 | 423 |
""" |
807 | 424 |
return dict() |
808 | 425 |
|
426 |
@expose('json') |
|
427 |
def hosttree(self, parent_id=None): |
|
428 |
|
|
429 |
# Si l'identifiant du groupe parent n'est pas |
|
430 |
# spécifié, on retourne la liste des groupes racines, |
|
431 |
# fournie par la méthode get_root_hosts_groups. |
|
432 |
if parent_id is None: |
|
433 |
return self.get_root_host_groups() |
|
434 |
|
|
435 |
# TODO: Utiliser un schéma de validation |
|
436 |
parent_id = int(parent_id) |
|
437 |
|
|
438 |
# On vérifie si le groupe parent fait partie des |
|
439 |
# groupes auxquel l'utilisateur a accès, et on |
|
440 |
# retourne une liste vide dans le cas contraire |
|
441 |
is_manager = in_group('managers').is_met(request.environ) |
|
442 |
if not is_manager: |
|
443 |
direct_access = False |
|
444 |
user = get_current_user() |
|
445 |
user_groups = dict(user.supitemgroups()) |
|
446 |
# On regarde d'abord si le groupe fait partie de ceux |
|
447 |
# auquels l'utilisateur a explicitement accès, ou s'il |
|
448 |
# est un parent des groupes auxquels l'utilisateur a accès |
|
449 |
if parent_id in user_groups.keys(): |
|
450 |
direct_access = user_groups[parent_id] |
|
451 |
# Dans le cas contraire, on vérifie si le groupe est un |
|
452 |
# sous-groupe des groupes auxquels l'utilisateur a accès |
|
453 |
else: |
|
454 |
id_list = [ug for ug in user_groups.keys() if user_groups[ug]] |
|
455 |
child_groups = DBSession.query(SupItemGroup.idgroup |
|
456 |
).distinct( |
|
457 |
).join( |
|
458 |
(GroupHierarchy, |
|
459 |
GroupHierarchy.idchild == SupItemGroup.idgroup), |
|
460 |
).filter(GroupHierarchy.idparent.in_(id_list) |
|
461 |
).filter(GroupHierarchy.hops > 0 |
|
462 |
).all() |
|
463 |
for ucg in child_groups: |
|
464 |
if ucg.idgroup == parent_id: |
|
465 |
direct_access = True |
|
466 |
break |
|
467 |
# Sinon, l'utilisateur n'a pas accès à ce groupe |
|
468 |
else: |
|
469 |
return dict(groups = [], leaves = []) |
|
470 |
|
|
471 |
# On récupère la liste des groupes dont |
|
472 |
# l'identifiant du parent est passé en paramètre |
|
473 |
db_groups = DBSession.query( |
|
474 |
SupItemGroup |
|
475 |
).join( |
|
476 |
(GroupHierarchy, GroupHierarchy.idchild == \ |
|
477 |
SupItemGroup.idgroup), |
|
478 |
).filter(GroupHierarchy.hops == 1 |
|
479 |
).filter(GroupHierarchy.idparent == parent_id |
|
480 |
).order_by(SupItemGroup.name.asc()) |
|
481 |
if not is_manager and not direct_access: |
|
482 |
id_list = [ug for ug in user_groups.keys() if not user_groups[ug]] |
|
483 |
db_groups = db_groups.filter( |
|
484 |
SupItemGroup.idgroup.in_(id_list)) |
|
485 |
groups = [] |
|
486 |
for group in db_groups.all(): |
|
487 |
#if group.has_children() or len(group.hosts)>0: |
|
488 |
groups.append({ |
|
489 |
'id' : group.idgroup, |
|
490 |
'name' : group.name, |
|
491 |
}) |
|
492 |
|
|
493 |
# On récupère la liste des hôtes appartenant au |
|
494 |
# groupe dont l'identifiant est passé en paramètre |
|
495 |
hosts = [] |
|
496 |
if is_manager or direct_access: |
|
497 |
db_hosts = DBSession.query( |
|
498 |
Host.idhost, |
|
499 |
Host.name, |
|
500 |
).join( |
|
501 |
(SUPITEM_GROUP_TABLE, |
|
502 |
SUPITEM_GROUP_TABLE.c.idsupitem == Host.idhost |
|
503 |
), |
|
504 |
).filter(SUPITEM_GROUP_TABLE.c.idgroup == parent_id |
|
505 |
).order_by(Host.name.asc()) |
|
506 |
hosts = [] |
|
507 |
for host in db_hosts.all(): |
|
508 |
hosts.append({ |
|
509 |
'id' : host.idhost, |
|
510 |
'name' : host.name, |
|
511 |
}) |
|
512 |
|
|
513 |
return dict(groups = groups, leaves = hosts) |
|
514 |
|
|
515 |
@expose('json') |
|
516 |
def graphtree(self, host_id=None, parent_id=None): |
|
517 |
|
|
518 |
# Si l'identifiant de l'hôte n'est pas spécifié, on |
|
519 |
# retourne un dictionnaire contenant deux listes vides |
|
520 |
if host_id is None: |
|
521 |
return dict(groups = [], graphs=[]) |
|
522 |
|
|
523 |
# On vérifie les permissions sur l'hôte |
|
524 |
# TODO: Utiliser un schéma de validation |
|
525 |
host_id = int(host_id) |
|
526 |
host = DBSession.query(Host |
|
527 |
).filter(Host.idhost == host_id |
|
528 |
).first() |
|
529 |
if host is None: |
|
530 |
return dict(groups = [], graphs=[]) |
|
531 |
user = get_current_user() |
|
532 |
if not host.is_allowed_for(user): |
|
533 |
return dict(groups = [], graphs=[]) |
|
534 |
|
|
535 |
# On récupère la liste des groupes de graphes associé à l'hôte |
|
536 |
host_graph_groups = DBSession.query( |
|
537 |
GraphGroup |
|
538 |
).distinct( |
|
539 |
).join( |
|
540 |
(GRAPH_GROUP_TABLE, \ |
|
541 |
GRAPH_GROUP_TABLE.c.idgroup == GraphGroup.idgroup), |
|
542 |
(Graph, Graph.idgraph == GRAPH_GROUP_TABLE.c.idgraph), |
|
543 |
(GRAPH_PERFDATASOURCE_TABLE, \ |
|
544 |
GRAPH_PERFDATASOURCE_TABLE.c.idgraph == Graph.idgraph), |
|
545 |
(PerfDataSource, PerfDataSource.idperfdatasource == \ |
|
546 |
GRAPH_PERFDATASOURCE_TABLE.c.idperfdatasource), |
|
547 |
(SUPITEM_GROUP_TABLE, \ |
|
548 |
SUPITEM_GROUP_TABLE.c.idsupitem == PerfDataSource.idhost), |
|
549 |
).filter(PerfDataSource.idhost == host_id |
|
550 |
).order_by(GraphGroup.name.asc()) |
|
551 |
|
|
552 |
# Si l'identifiant du groupe parent n'est pas spécifié, |
|
553 |
# on récupère la liste des groupes de graphes racines. |
|
554 |
if parent_id is None: |
|
555 |
graph_groups = GraphGroup.get_top_groups() |
|
556 |
|
|
557 |
# Sinon on récupère la liste des graphes dont le |
|
558 |
# groupe passé en paramètre est le parent direct |
|
559 |
else: |
|
560 |
# TODO: Utiliser un schéma de validation |
|
561 |
parent_id = int(parent_id) |
|
562 |
graph_groups = DBSession.query( |
|
563 |
GraphGroup |
|
564 |
).join( |
|
565 |
(GroupHierarchy, GroupHierarchy.idchild == \ |
|
566 |
GraphGroup.idgroup), |
|
567 |
).filter(GroupHierarchy.hops == 1 |
|
568 |
).filter(GroupHierarchy.idparent == parent_id |
|
569 |
).order_by(GraphGroup.name.asc()) |
|
570 |
|
|
571 |
# On réalise l'intersection des deux listes |
|
572 |
groups = [] |
|
573 |
for gg in graph_groups: |
|
574 |
if gg in host_graph_groups: |
|
575 |
groups.append({ |
|
576 |
'id' : gg.idgroup, |
|
577 |
'name' : gg.name, |
|
578 |
}) |
|
579 |
|
|
580 |
# On récupère la liste des graphes appartenant au |
|
581 |
# groupe dont l'identifiant est passé en paramètre |
|
582 |
graphs = [] |
|
583 |
if parent_id: |
|
584 |
db_graphs = DBSession.query( |
|
585 |
Graph.idgraph, |
|
586 |
Graph.name, |
|
587 |
).join( |
|
588 |
(GRAPH_GROUP_TABLE, |
|
589 |
GRAPH_GROUP_TABLE.c.idgraph == Graph.idgraph |
|
590 |
), |
|
591 |
).filter(GRAPH_GROUP_TABLE.c.idgroup == parent_id |
|
592 |
).order_by(Graph.name.asc()) |
|
593 |
for graph in db_graphs.all(): |
|
594 |
graphs.append({ |
|
595 |
'id' : graph.idgraph, |
|
596 |
'name' : graph.name, |
|
597 |
}) |
|
598 |
|
|
599 |
return dict(groups = groups, leaves = graphs) |
|
600 |
|
|
601 |
def get_root_host_groups(self): |
|
602 |
""" |
|
603 |
Retourne tous les groupes racines (c'est à dire n'ayant |
|
604 |
aucun parent) d'hôtes auquel l'utilisateur a accès. |
|
605 |
|
|
606 |
@return: Un dictionnaire contenant la liste de ces groupes. |
|
607 |
@rtype : C{dict} of C{list} of C{dict} of C{mixed} |
|
608 |
""" |
|
609 |
|
|
610 |
# On récupère tous les groupes qui ont un parent. |
|
611 |
children = DBSession.query( |
|
612 |
SupItemGroup, |
|
613 |
).distinct( |
|
614 |
).join( |
|
615 |
(GroupHierarchy, GroupHierarchy.idchild == SupItemGroup.idgroup) |
|
616 |
).filter(GroupHierarchy.hops > 0) |
|
617 |
|
|
618 |
# Ensuite on les exclut de la liste des groupes, |
|
619 |
# pour ne garder que ceux qui sont au sommet de |
|
620 |
# l'arbre et qui constituent nos "root groups". |
|
621 |
root_groups = DBSession.query( |
|
622 |
SupItemGroup, |
|
623 |
).except_(children |
|
624 |
).order_by(SupItemGroup.name) |
|
625 |
|
|
626 |
# On filtre ces groupes racines afin de ne |
|
627 |
# retourner que ceux auquels l'utilisateur a accès |
|
628 |
user = get_current_user() |
|
629 |
is_manager = in_group('managers').is_met(request.environ) |
|
630 |
if not is_manager: |
|
631 |
user_groups = [ug[0] for ug in user.supitemgroups() if ug[1]] |
|
632 |
root_groups = root_groups.filter( |
|
633 |
SupItemGroup.idgroup.in_(user_groups)) |
|
634 |
|
|
635 |
groups = [] |
|
636 |
for group in root_groups.all(): |
|
637 |
#if group.has_children() or len(group.hosts)>0: |
|
638 |
groups.append({ |
|
639 |
'id' : group.idgroup, |
|
640 |
'name' : group.name, |
|
641 |
}) |
|
642 |
|
|
643 |
return dict(groups = groups, hosts=[]) |
|
644 |
|
|
645 |
def get_root_graph_groups(self, host_id): |
|
646 |
""" |
|
647 |
Retourne tous les groupes racines (c'est à dire n'ayant |
|
648 |
aucun parent) de graphes auquel l'utilisateur a accès et |
|
649 |
concernant l'hôte dont l'identifiant est passé en paramètre. |
|
650 |
|
|
651 |
@return: Un dictionnaire contenant la liste de ces groupes. |
|
652 |
@rtype : C{dict} of C{list} of C{dict} of C{mixed} |
|
653 |
""" |
|
654 |
|
|
655 |
# On récupère tous les groupes qui ont un parent. |
|
656 |
children = DBSession.query( |
|
657 |
Graph, |
|
658 |
).distinct( |
|
659 |
).join( |
|
660 |
(GroupHierarchy, GroupHierarchy.idchild == SupItemGroup.idgroup) |
|
661 |
).filter(GroupHierarchy.hops > 0) |
|
662 |
|
|
663 |
# On récupère tous les groupes racines de graphes portant sur l'hôte |
|
664 |
# souhaité, en éliminant les groupes récupérés à l'étape précédente |
|
665 |
graph_groups = DBSession.query( |
|
666 |
GraphGroup |
|
667 |
).distinct( |
|
668 |
).join( |
|
669 |
(GRAPH_GROUP_TABLE, \ |
|
670 |
GRAPH_GROUP_TABLE.c.idgroup == GraphGroup.idgroup), |
|
671 |
(Graph, Graph.idgraph == GRAPH_GROUP_TABLE.c.idgraph), |
|
672 |
(GRAPH_PERFDATASOURCE_TABLE, \ |
|
673 |
GRAPH_PERFDATASOURCE_TABLE.c.idgraph == Graph.idgraph), |
|
674 |
(PerfDataSource, PerfDataSource.idperfdatasource == \ |
|
675 |
GRAPH_PERFDATASOURCE_TABLE.c.idperfdatasource), |
|
676 |
(SUPITEM_GROUP_TABLE, \ |
|
677 |
SUPITEM_GROUP_TABLE.c.idsupitem == PerfDataSource.idhost), |
|
678 |
).filter(PerfDataSource.idhost == host_id |
|
679 |
# ).except_(children |
|
680 |
).order_by(GraphGroup.name.asc()) |
|
681 |
|
|
682 |
# On filtre ces groupes racines afin de ne |
|
683 |
# retourner que ceux auquels l'utilisateur a accès |
|
684 |
user = get_current_user() |
|
685 |
is_manager = in_group('managers').is_met(request.environ) |
|
686 |
if not is_manager: |
|
687 |
user_groups = [ug[0] for ug in user.supitemgroups() if ug[1]] |
|
688 |
graph_groups = graph_groups.filter( |
|
689 |
SUPITEM_GROUP_TABLE.c.idgroup.in_(user_groups)) |
|
690 |
|
|
691 |
groups = [] |
|
692 |
for group in graph_groups.all(): |
|
693 |
#if group.has_children() or len(group.graphs)>0: |
|
694 |
groups.append({ |
|
695 |
'id' : group.idgroup, |
|
696 |
'name' : group.name, |
|
697 |
}) |
|
698 |
|
|
699 |
return dict(groups = groups, graphs=[]) |
|
700 |
|
|
809 | 701 |
def getListIndicators(self, host, graph): |
810 | 702 |
""" |
811 | 703 |
Liste d indicateurs associes a un graphe |
... | ... | |
833 | 725 |
).filter(Host.name == host |
834 | 726 |
).all() |
835 | 727 |
return indicators |
728 |
|
vigigraph/i18n/en/LC_MESSAGES/vigigraph.po | ||
---|---|---|
7 | 7 |
msgstr "" |
8 | 8 |
"Project-Id-Version: vigigraph 2.0.0\n" |
9 | 9 |
"Report-Msgid-Bugs-To: contact@projet-vigilo.org\n" |
10 |
"POT-Creation-Date: 2010-10-29 14:32+0200\n"
|
|
11 |
"PO-Revision-Date: 2010-10-29 14:32+0200\n"
|
|
10 |
"POT-Creation-Date: 2010-11-03 17:22+0100\n"
|
|
11 |
"PO-Revision-Date: 2010-11-03 17:23+0100\n"
|
|
12 | 12 |
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" |
13 | 13 |
"Language-Team: en <LL@li.org>\n" |
14 | 14 |
"Plural-Forms: nplurals=2; plural=(n != 1)\n" |
... | ... | |
26 | 26 |
msgid "You don't have access to VigiGraph" |
27 | 27 |
msgstr "You don't have access to VigiGraph" |
28 | 28 |
|
29 |
#: vigigraph/controllers/root.py:55
|
|
29 |
#: vigigraph/controllers/root.py:81
|
|
30 | 30 |
msgid "Wrong credentials" |
31 | 31 |
msgstr "Wrong credentials" |
32 | 32 |
|
33 |
#: vigigraph/controllers/root.py:70
|
|
33 |
#: vigigraph/controllers/root.py:96
|
|
34 | 34 |
#, python-format |
35 | 35 |
msgid "\"%(username)s\" logged in (from %(IP)s)" |
36 | 36 |
msgstr "\"%(username)s\" logged in (from %(IP)s)" |
37 | 37 |
|
38 |
#: vigigraph/controllers/root.py:74
|
|
38 |
#: vigigraph/controllers/root.py:100
|
|
39 | 39 |
#, python-format |
40 | 40 |
msgid "Welcome back, %s!" |
41 | 41 |
msgstr "Welcome back, %s!" |
42 | 42 |
|
43 |
#: vigigraph/controllers/root.py:84
|
|
43 |
#: vigigraph/controllers/root.py:110
|
|
44 | 44 |
#, python-format |
45 | 45 |
msgid "Some user logged out (from %(IP)s)" |
46 | 46 |
msgstr "Some user logged out (from %(IP)s)" |
47 | 47 |
|
48 |
#: vigigraph/controllers/root.py:87
|
|
48 |
#: vigigraph/controllers/root.py:113
|
|
49 | 49 |
msgid "We hope to see you soon!" |
50 | 50 |
msgstr "We hope to see you soon!" |
51 | 51 |
|
... | ... | |
65 | 65 |
msgid "Last %d months" |
66 | 66 |
msgstr "Last %d months" |
67 | 67 |
|
68 |
#: vigigraph/controllers/rpc.py:69 |
|
68 |
#: vigigraph/controllers/rpc.py:69 vigigraph/public/js/graph.js:42
|
|
69 | 69 |
msgid "Last year" |
70 | 70 |
msgstr "Last year" |
71 | 71 |
|
... | ... | |
83 | 83 |
msgid "%a, %d %b %Y %H:%M:%S" |
84 | 84 |
msgstr "%a, %d %b %Y %H:%M:%S" |
85 | 85 |
|
86 |
#: vigigraph/widgets/host_picker.py:13 |
|
86 |
#: vigigraph/public/js/graph.js:12 |
|
87 |
msgid "Reload the graph" |
|
88 |
msgstr "Reload the graph" |
|
89 |
|
|
90 |
#: vigigraph/public/js/graph.js:16 |
|
91 |
msgid "Timeframe" |
|
92 |
msgstr "Timeframe" |
|
93 |
|
|
94 |
#: vigigraph/public/js/graph.js:18 |
|
95 |
msgid "Timeframe menu" |
|
96 |
msgstr "Timeframe menu" |
|
97 |
|
|
98 |
#: vigigraph/public/js/graph.js:21 |
|
99 |
msgid "Last 12 hours" |
|
100 |
msgstr "Last 12 hours" |
|
101 |
|
|
102 |
#: vigigraph/public/js/graph.js:24 |
|
103 |
msgid "Last 24 hours" |
|
104 |
msgstr "Last 24 hours" |
|
105 |
|
|
106 |
#: vigigraph/public/js/graph.js:27 |
|
107 |
msgid "Last 48 hours" |
|
108 |
msgstr "Last 48 hours" |
|
109 |
|
|
110 |
#: vigigraph/public/js/graph.js:30 |
|
111 |
msgid "Last 7 days" |
|
112 |
msgstr "Last 7 days" |
|
113 |
|
|
114 |
#: vigigraph/public/js/graph.js:33 |
|
115 |
msgid "Last 14 days" |
|
116 |
msgstr "Last 14 days" |
|
117 |
|
|
118 |
#: vigigraph/public/js/graph.js:36 |
|
119 |
msgid "Last 3 months" |
|
120 |
msgstr "Last 3 months" |
|
121 |
|
|
122 |
#: vigigraph/public/js/graph.js:39 |
|
123 |
msgid "Last 6 months" |
|
124 |
msgstr "Last 6 months" |
|
125 |
|
|
126 |
#: vigigraph/public/js/graph.js:47 |
|
127 |
msgid "Graph start" |
|
128 |
msgstr "Graph start" |
|
129 |
|
|
130 |
#: vigigraph/public/js/graph.js:54 |
|
131 |
msgid "Previous section" |
|
132 |
msgstr "Previous section" |
|
133 |
|
|
134 |
#: vigigraph/public/js/graph.js:61 |
|
135 |
msgid "Next section" |
|
136 |
msgstr "Next section" |
|
137 |
|
|
138 |
#: vigigraph/public/js/graph.js:68 |
|
139 |
msgid "Graph end" |
|
140 |
msgstr "Graph end" |
|
141 |
|
|
142 |
#: vigigraph/public/js/graph.js:75 |
|
143 |
msgid "Zoom in" |
|
144 |
msgstr "Zoom in" |
|
145 |
|
|
146 |
#: vigigraph/public/js/graph.js:82 |
|
147 |
msgid "Zoom out" |
|
148 |
msgstr "Zoom out" |
|
149 |
|
|
150 |
#: vigigraph/public/js/graph.js:88 |
|
151 |
msgid "Export to CSV" |
|
152 |
msgstr "Export to CSV" |
|
153 |
|
|
154 |
#: vigigraph/public/js/graph.js:90 |
|
155 |
msgid "Export the content of this graph to CSV" |
|
156 |
msgstr "Export the content of this graph to CSV" |
|
157 |
|
|
158 |
#: vigigraph/public/js/graph.js:106 |
|
159 |
msgid "All" |
|
160 |
msgstr "All" |
|
161 |
|
|
162 |
#: vigigraph/public/js/graph.js:111 |
|
163 |
msgid "Print graph" |
|
164 |
msgstr "Print graph" |
|
165 |
|
|
166 |
#: vigigraph/public/js/graph.js:119 vigigraph/widgets/host_picker.py:37 |
|
167 |
#: vigigraph/widgets/host_picker.py:76 |
|
168 |
msgid "Graph" |
|
169 |
msgstr "Graph" |
|
170 |
|
|
171 |
#: vigigraph/public/js/host_picker.js:3 |
|
172 |
msgid "Host Picker" |
|
173 |
msgstr "Host Picker" |
|
174 |
|
|
175 |
#: vigigraph/widgets/host_picker.py:13 vigigraph/widgets/host_picker.py:52 |
|
87 | 176 |
msgid "Choose" |
88 | 177 |
msgstr "Choose" |
89 | 178 |
|
90 |
#: vigigraph/widgets/host_picker.py:14 |
|
179 |
#: vigigraph/widgets/host_picker.py:14 vigigraph/widgets/host_picker.py:53
|
|
91 | 180 |
msgid "→" |
92 | 181 |
msgstr "→" |
93 | 182 |
|
94 |
#: vigigraph/widgets/host_picker.py:36 |
|
183 |
#: vigigraph/widgets/host_picker.py:36 vigigraph/widgets/host_picker.py:75
|
|
95 | 184 |
msgid "Host" |
96 | 185 |
msgstr "Host" |
97 | 186 |
|
98 |
#: vigigraph/widgets/host_picker.py:37 |
|
99 |
msgid "Graph" |
|
100 |
msgstr "Graph" |
|
101 |
|
vigigraph/i18n/fr/LC_MESSAGES/vigigraph.po | ||
---|---|---|
8 | 8 |
"Project-Id-Version: vigigraph 0.1\n" |
9 | 9 |
"Report-Msgid-Bugs-To: contact@projet-vigilo.org\n" |
10 | 10 |
"POT-Creation-Date: 2010-02-15 13:41+0100\n" |
11 |
"PO-Revision-Date: 2010-10-29 14:32+0200\n"
|
|
11 |
"PO-Revision-Date: 2010-11-03 17:22+0100\n"
|
|
12 | 12 |
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" |
13 | 13 |
"Language-Team: fr_FR <LL@li.org>\n" |
14 | 14 |
"Plural-Forms: nplurals=2; plural=(n > 1)\n" |
... | ... | |
26 | 26 |
msgid "You don't have access to VigiGraph" |
27 | 27 |
msgstr "Vous n'avez pas accès à VigiGraph" |
28 | 28 |
|
29 |
#: vigigraph/controllers/root.py:55
|
|
29 |
#: vigigraph/controllers/root.py:81
|
|
30 | 30 |
msgid "Wrong credentials" |
31 | 31 |
msgstr "Identifiant ou mot de passe incorrect" |
32 | 32 |
|
33 |
#: vigigraph/controllers/root.py:70
|
|
33 |
#: vigigraph/controllers/root.py:96
|
|
34 | 34 |
#, python-format |
35 | 35 |
msgid "\"%(username)s\" logged in (from %(IP)s)" |
36 | 36 |
msgstr "\"%(username)s\" s'est connecté (depuis %(IP)s)" |
37 | 37 |
|
38 |
#: vigigraph/controllers/root.py:74
|
|
38 |
#: vigigraph/controllers/root.py:100
|
|
39 | 39 |
#, python-format |
40 | 40 |
msgid "Welcome back, %s!" |
41 | 41 |
msgstr "Bienvenue, %s !" |
42 | 42 |
|
43 |
#: vigigraph/controllers/root.py:84
|
|
43 |
#: vigigraph/controllers/root.py:110
|
|
44 | 44 |
#, python-format |
45 | 45 |
msgid "Some user logged out (from %(IP)s)" |
46 | 46 |
msgstr "Un utilisateur s'est déconnecté (depuis %(IP)s)" |
47 | 47 |
|
48 |
#: vigigraph/controllers/root.py:87
|
|
48 |
#: vigigraph/controllers/root.py:113
|
|
49 | 49 |
msgid "We hope to see you soon!" |
50 | 50 |
msgstr "Au plaisir de vous revoir !" |
51 | 51 |
|
... | ... | |
65 | 65 |
msgid "Last %d months" |
66 | 66 |
msgstr "Les %d derniers mois" |
67 | 67 |
|
68 |
#: vigigraph/controllers/rpc.py:69 |
|
68 |
#: vigigraph/controllers/rpc.py:69 vigigraph/public/js/graph.js:42
|
|
69 | 69 |
msgid "Last year" |
70 | 70 |
msgstr "L'an dernier" |
71 | 71 |
|
... | ... | |
83 | 83 |
msgid "%a, %d %b %Y %H:%M:%S" |
84 | 84 |
msgstr "%d/%m/%Y à %H:%M:%S" |
85 | 85 |
|
86 |
#: vigigraph/widgets/host_picker.py:13 |
|
86 |
#: vigigraph/public/js/graph.js:12 |
|
87 |
msgid "Reload the graph" |
|
88 |
msgstr "Recharger le graphe" |
|
89 |
|
|
90 |
#: vigigraph/public/js/graph.js:16 |
|
91 |
msgid "Timeframe" |
|
92 |
msgstr "Période" |
|
93 |
|
|
94 |
#: vigigraph/public/js/graph.js:18 |
|
95 |
msgid "Timeframe menu" |
|
96 |
msgstr "Menu de choix de la période" |
|
97 |
|
|
98 |
#: vigigraph/public/js/graph.js:21 |
|
99 |
msgid "Last 12 hours" |
|
100 |
msgstr "12 dernières heures" |
|
101 |
|
|
102 |
#: vigigraph/public/js/graph.js:24 |
|
103 |
msgid "Last 24 hours" |
|
104 |
msgstr "24 dernières heures" |
|
105 |
|
|
106 |
#: vigigraph/public/js/graph.js:27 |
|
107 |
msgid "Last 48 hours" |
|
108 |
msgstr "48 dernières heures" |
|
109 |
|
|
110 |
#: vigigraph/public/js/graph.js:30 |
|
111 |
msgid "Last 7 days" |
|
112 |
msgstr "7 derniers jours" |
|
113 |
|
|
114 |
#: vigigraph/public/js/graph.js:33 |
|
115 |
msgid "Last 14 days" |
|
116 |
msgstr "14 derniers jours" |
|
117 |
|
|
118 |
#: vigigraph/public/js/graph.js:36 |
|
119 |
msgid "Last 3 months" |
|
120 |
msgstr "3 derniers mois" |
|
121 |
|
|
122 |
#: vigigraph/public/js/graph.js:39 |
|
123 |
msgid "Last 6 months" |
|
124 |
msgstr "6 derniers mois" |
|
125 |
|
|
126 |
#: vigigraph/public/js/graph.js:47 |
|
127 |
msgid "Graph start" |
|
128 |
msgstr "Début du graphe" |
|
129 |
|
|
130 |
#: vigigraph/public/js/graph.js:54 |
|
131 |
msgid "Previous section" |
|
132 |
msgstr "Section précédente" |
|
133 |
|
|
134 |
#: vigigraph/public/js/graph.js:61 |
|
135 |
msgid "Next section" |
|
136 |
msgstr "Section suivante" |
|
137 |
|
|
138 |
#: vigigraph/public/js/graph.js:68 |
|
139 |
msgid "Graph end" |
|
140 |
msgstr "Fin du graphe" |
|
141 |
|
|
142 |
#: vigigraph/public/js/graph.js:75 |
|
143 |
msgid "Zoom in" |
|
144 |
msgstr "Zoomer" |
|
145 |
|
|
146 |
#: vigigraph/public/js/graph.js:82 |
|
147 |
msgid "Zoom out" |
|
148 |
msgstr "Dézoomer" |
|
149 |
|
|
150 |
#: vigigraph/public/js/graph.js:88 |
|
151 |
msgid "Export to CSV" |
|
152 |
msgstr "Export CSV" |
|
153 |
|
|
154 |
#: vigigraph/public/js/graph.js:90 |
|
155 |
msgid "Export the content of this graph to CSV" |
|
156 |
msgstr "Exporte le contenu du graphe au format CSV" |
|
157 |
|
|
158 |
#: vigigraph/public/js/graph.js:106 |
|
159 |
msgid "All" |
|
160 |
msgstr "Tous" |
|
161 |
|
|
162 |
#: vigigraph/public/js/graph.js:111 |
|
163 |
msgid "Print graph" |
|
164 |
msgstr "Imprimer le graphe" |
|
165 |
|
|
166 |
#: vigigraph/public/js/graph.js:119 vigigraph/widgets/host_picker.py:37 |
|
167 |
#: vigigraph/widgets/host_picker.py:76 |
|
168 |
msgid "Graph" |
|
169 |
msgstr "Graphe" |
|
170 |
|
|
171 |
#: vigigraph/public/js/host_picker.js:3 |
|
172 |
msgid "Host Picker" |
|
173 |
msgstr "Sélection d'un hôte" |
|
174 |
|
|
175 |
#: vigigraph/widgets/host_picker.py:13 vigigraph/widgets/host_picker.py:52 |
|
87 | 176 |
msgid "Choose" |
88 | 177 |
msgstr "Choisir" |
89 | 178 |
|
90 |
#: vigigraph/widgets/host_picker.py:14 |
|
179 |
#: vigigraph/widgets/host_picker.py:14 vigigraph/widgets/host_picker.py:53
|
|
91 | 180 |
msgid "→" |
92 | 181 |
msgstr "→" |
93 | 182 |
|
94 |
#: vigigraph/widgets/host_picker.py:36 |
Also available in: Unified diff