mirror of
https://github.com/craigerl/aprsd.git
synced 2024-12-18 07:36:34 -05:00
Added flask messages web UI and basic auth
This patch fixes the CTRL-C signal_handler. This patch also adds the new Messages WEB UI page as well as the save url, which are both behind an http basic auth. The flask web service now has users in the config file aprsd: web: users: admin: <password>
This commit is contained in:
parent
0d18e54969
commit
aa290692ab
@ -1,21 +1,65 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
import aprsd
|
||||
from aprsd import messaging, stats
|
||||
import flask
|
||||
import flask_classful
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
auth = HTTPBasicAuth()
|
||||
users = None
|
||||
|
||||
|
||||
# HTTPBasicAuth doesn't work on a class method.
|
||||
# This has to be out here. Rely on the APRSDFlask
|
||||
# class to initialize the users from the config
|
||||
@auth.verify_password
|
||||
def verify_password(username, password):
|
||||
global users
|
||||
|
||||
if username in users and check_password_hash(users.get(username), password):
|
||||
return username
|
||||
|
||||
|
||||
class APRSDFlask(flask_classful.FlaskView):
|
||||
config = None
|
||||
|
||||
def set_config(self, config):
|
||||
global users
|
||||
self.config = config
|
||||
self.users = {}
|
||||
for user in self.config["aprsd"]["web"]["users"]:
|
||||
self.users[user] = generate_password_hash(
|
||||
self.config["aprsd"]["web"]["users"][user],
|
||||
)
|
||||
|
||||
users = self.users
|
||||
|
||||
def index(self):
|
||||
return "Hello"
|
||||
# return flask.render_template("index.html", message=msg)
|
||||
|
||||
@auth.login_required
|
||||
def messages(self):
|
||||
track = messaging.MsgTrack()
|
||||
msgs = []
|
||||
for id in track:
|
||||
LOG.info(track[id].dict())
|
||||
msgs.append(track[id].dict())
|
||||
|
||||
return flask.render_template("messages.html", messages=json.dumps(msgs))
|
||||
|
||||
@auth.login_required
|
||||
def save(self):
|
||||
"""Save the existing queue to disk."""
|
||||
track = messaging.MsgTrack()
|
||||
track.save()
|
||||
return json.dumps({"messages": "saved"})
|
||||
|
||||
def stats(self):
|
||||
stats_obj = stats.APRSDStats()
|
||||
track = messaging.MsgTrack()
|
||||
@ -30,9 +74,16 @@ class APRSDFlask(flask_classful.FlaskView):
|
||||
|
||||
|
||||
def init_flask(config):
|
||||
flask_app = flask.Flask("aprsd")
|
||||
flask_app = flask.Flask(
|
||||
"aprsd",
|
||||
static_url_path="",
|
||||
static_folder="web/static",
|
||||
template_folder="web/templates",
|
||||
)
|
||||
server = APRSDFlask()
|
||||
server.set_config(config)
|
||||
# flask_app.route('/', methods=['GET'])(server.index)
|
||||
flask_app.route("/stats", methods=["GET"])(server.stats)
|
||||
flask_app.route("/messages", methods=["GET"])(server.messages)
|
||||
flask_app.route("/save", methods=["GET"])(server.save)
|
||||
return flask_app
|
||||
|
@ -20,6 +20,7 @@
|
||||
#
|
||||
|
||||
# python included libs
|
||||
import datetime
|
||||
import logging
|
||||
from logging import NullHandler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
@ -27,7 +28,6 @@ import os
|
||||
import queue
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
# local imports here
|
||||
@ -52,7 +52,9 @@ LOG_LEVELS = {
|
||||
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
server_event = threading.Event()
|
||||
flask_enabled = False
|
||||
|
||||
# server_event = threading.Event()
|
||||
|
||||
# localization, please edit:
|
||||
# HOST = "noam.aprs2.net" # north america tier2 servers round robin
|
||||
@ -150,20 +152,23 @@ def install(append, case_insensitive, shell, path):
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
global server_vent
|
||||
global flask_enabled
|
||||
|
||||
LOG.info(
|
||||
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds to exit all threads",
|
||||
)
|
||||
threads.APRSDThreadList().stop_all()
|
||||
server_event.set()
|
||||
LOG.info("EXITING STATS")
|
||||
LOG.info(stats.APRSDStats())
|
||||
# time.sleep(1)
|
||||
signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
|
||||
|
||||
# end signal_handler
|
||||
if "subprocess" not in str(frame):
|
||||
LOG.info(
|
||||
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format(
|
||||
datetime.datetime.now(),
|
||||
),
|
||||
)
|
||||
time.sleep(5)
|
||||
tracker = messaging.MsgTrack()
|
||||
tracker.save()
|
||||
LOG.info(stats.APRSDStats())
|
||||
# signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
# sys.exit(0)
|
||||
if flask_enabled:
|
||||
signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
|
||||
|
||||
# Setup the logging faciility
|
||||
@ -394,9 +399,7 @@ def server(
|
||||
flush,
|
||||
):
|
||||
"""Start the aprsd server process."""
|
||||
global event
|
||||
|
||||
event = threading.Event()
|
||||
global flask_enabled
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
if not quiet:
|
||||
@ -468,6 +471,7 @@ def server(
|
||||
web_enabled = False
|
||||
|
||||
if web_enabled:
|
||||
flask_enabled = True
|
||||
app = flask.init_flask(config)
|
||||
app.run(
|
||||
host=config["aprsd"]["web"]["host"],
|
||||
@ -475,10 +479,8 @@ def server(
|
||||
)
|
||||
|
||||
# If there are items in the msgTracker, then save them
|
||||
tracker = messaging.MsgTrack()
|
||||
tracker.save()
|
||||
LOG.info(stats.APRSDStats())
|
||||
LOG.info("APRSD Exiting.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -53,6 +53,38 @@ class MsgTrack:
|
||||
cls._instance.lock = threading.Lock()
|
||||
return cls._instance
|
||||
|
||||
def __getitem__(self, name):
|
||||
with self.lock:
|
||||
return self.track[name]
|
||||
|
||||
def __iter__(self):
|
||||
with self.lock:
|
||||
return iter(self.track)
|
||||
|
||||
def keys(self):
|
||||
with self.lock:
|
||||
return self.track.keys()
|
||||
|
||||
def items(self):
|
||||
with self.lock:
|
||||
return self.track.items()
|
||||
|
||||
def values(self):
|
||||
with self.lock:
|
||||
return self.track.values()
|
||||
|
||||
def __len__(self):
|
||||
with self.lock:
|
||||
return len(self.track)
|
||||
|
||||
def __str__(self):
|
||||
with self.lock:
|
||||
result = "{"
|
||||
for key in self.track.keys():
|
||||
result += "{}: {}, ".format(key, str(self.track[key]))
|
||||
result += "}"
|
||||
return result
|
||||
|
||||
def add(self, msg):
|
||||
with self.lock:
|
||||
key = int(msg.id)
|
||||
@ -71,24 +103,18 @@ class MsgTrack:
|
||||
if key in self.track.keys():
|
||||
del self.track[key]
|
||||
|
||||
def __len__(self):
|
||||
with self.lock:
|
||||
return len(self.track)
|
||||
|
||||
def __str__(self):
|
||||
with self.lock:
|
||||
result = "{"
|
||||
for key in self.track.keys():
|
||||
result += "{}: {}, ".format(key, str(self.track[key]))
|
||||
result += "}"
|
||||
return result
|
||||
|
||||
def save(self):
|
||||
"""Save this shit to disk?"""
|
||||
"""Save any queued to disk?"""
|
||||
LOG.debug("Save tracker to disk? {}".format(len(self)))
|
||||
if len(self) > 0:
|
||||
LOG.info("Saving {} tracking messages to disk".format(len(self)))
|
||||
pickle.dump(self.dump(), open(utils.DEFAULT_SAVE_FILE, "wb+"))
|
||||
else:
|
||||
LOG.debug(
|
||||
"Nothing to save, flushing old save file '{}'".format(
|
||||
utils.DEFAULT_SAVE_FILE,
|
||||
),
|
||||
)
|
||||
self.flush()
|
||||
|
||||
def dump(self):
|
||||
@ -229,8 +255,17 @@ class RawMessage(Message):
|
||||
super().__init__(None, None, msg_id=None)
|
||||
self.message = message
|
||||
|
||||
def __repr__(self):
|
||||
return self.message
|
||||
def dict(self):
|
||||
now = datetime.datetime.now()
|
||||
return {
|
||||
"type": "raw",
|
||||
"message": self.message.rstrip("\n"),
|
||||
"raw": self.message.rstrip("\n"),
|
||||
"retry_count": self.retry_count,
|
||||
"last_send_attempt": self.last_send_attempt,
|
||||
"last_send_time": str(self.last_send_time),
|
||||
"last_send_age": str(now - self.last_send_time),
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
@ -246,12 +281,12 @@ class RawMessage(Message):
|
||||
cl = client.get_client()
|
||||
log_message(
|
||||
"Sending Message Direct",
|
||||
repr(self).rstrip("\n"),
|
||||
str(self).rstrip("\n"),
|
||||
self.message,
|
||||
tocall=self.tocall,
|
||||
fromcall=self.fromcall,
|
||||
)
|
||||
cl.sendall(repr(self))
|
||||
cl.sendall(str(self))
|
||||
stats.APRSDStats().msgs_sent_inc()
|
||||
|
||||
|
||||
@ -267,7 +302,22 @@ class TextMessage(Message):
|
||||
# an ack? Some messages we don't want to do this ever.
|
||||
self.allow_delay = allow_delay
|
||||
|
||||
def __repr__(self):
|
||||
def dict(self):
|
||||
now = datetime.datetime.now()
|
||||
return {
|
||||
"id": self.id,
|
||||
"type": "text-message",
|
||||
"fromcall": self.fromcall,
|
||||
"tocall": self.tocall,
|
||||
"message": self.message.rstrip("\n"),
|
||||
"raw": str(self).rstrip("\n"),
|
||||
"retry_count": self.retry_count,
|
||||
"last_send_attempt": self.last_send_attempt,
|
||||
"last_send_time": str(self.last_send_time),
|
||||
"last_send_age": str(now - self.last_send_time),
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
"""Build raw string to send over the air."""
|
||||
return "{}>APZ100::{}:{}{{{}\n".format(
|
||||
self.fromcall,
|
||||
@ -276,19 +326,6 @@ class TextMessage(Message):
|
||||
str(self.id),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
delta = "Never"
|
||||
if self.last_send_time:
|
||||
now = datetime.datetime.now()
|
||||
delta = now - self.last_send_time
|
||||
return "{}>{} Msg({})({}): '{}'".format(
|
||||
self.fromcall,
|
||||
self.tocall,
|
||||
self.id,
|
||||
delta,
|
||||
self.message,
|
||||
)
|
||||
|
||||
def _filter_for_send(self):
|
||||
"""Filter and format message string for FCC."""
|
||||
# max? ftm400 displays 64, raw msg shows 74
|
||||
@ -311,12 +348,12 @@ class TextMessage(Message):
|
||||
cl = client.get_client()
|
||||
log_message(
|
||||
"Sending Message Direct",
|
||||
repr(self).rstrip("\n"),
|
||||
str(self).rstrip("\n"),
|
||||
self.message,
|
||||
tocall=self.tocall,
|
||||
fromcall=self.fromcall,
|
||||
)
|
||||
cl.sendall(repr(self))
|
||||
cl.sendall(str(self))
|
||||
stats.APRSDStats().msgs_tx_inc()
|
||||
|
||||
|
||||
@ -370,13 +407,13 @@ class SendMessageThread(threads.APRSDThread):
|
||||
# tracking the time.
|
||||
log_message(
|
||||
"Sending Message",
|
||||
repr(msg).rstrip("\n"),
|
||||
str(msg).rstrip("\n"),
|
||||
msg.message,
|
||||
tocall=self.msg.tocall,
|
||||
retry_number=msg.last_send_attempt,
|
||||
msg_num=msg.id,
|
||||
)
|
||||
cl.sendall(repr(msg))
|
||||
cl.sendall(str(msg))
|
||||
stats.APRSDStats().msgs_tx_inc()
|
||||
msg.last_send_time = datetime.datetime.now()
|
||||
msg.last_send_attempt += 1
|
||||
@ -392,29 +429,40 @@ class AckMessage(Message):
|
||||
def __init__(self, fromcall, tocall, msg_id):
|
||||
super().__init__(fromcall, tocall, msg_id=msg_id)
|
||||
|
||||
def __repr__(self):
|
||||
def dict(self):
|
||||
now = datetime.datetime.now()
|
||||
return {
|
||||
"id": self.id,
|
||||
"type": "ack",
|
||||
"fromcall": self.fromcall,
|
||||
"tocall": self.tocall,
|
||||
"raw": str(self).rstrip("\n"),
|
||||
"retry_count": self.retry_count,
|
||||
"last_send_attempt": self.last_send_attempt,
|
||||
"last_send_time": str(self.last_send_time),
|
||||
"last_send_age": str(now - self.last_send_time),
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return "{}>APZ100::{}:ack{}\n".format(
|
||||
self.fromcall,
|
||||
self.tocall.ljust(9),
|
||||
self.id,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "From({}) TO({}) Ack ({})".format(self.fromcall, self.tocall, self.id)
|
||||
|
||||
def send_thread(self):
|
||||
"""Separate thread to send acks with retries."""
|
||||
cl = client.get_client()
|
||||
for i in range(self.retry_count, 0, -1):
|
||||
log_message(
|
||||
"Sending ack",
|
||||
repr(self).rstrip("\n"),
|
||||
str(self).rstrip("\n"),
|
||||
None,
|
||||
ack=self.id,
|
||||
tocall=self.tocall,
|
||||
retry_number=i,
|
||||
)
|
||||
cl.sendall(repr(self))
|
||||
cl.sendall(str(self))
|
||||
stats.APRSDStats().ack_tx_inc()
|
||||
# aprs duplicate detection is 30 secs?
|
||||
# (21 only sends first, 28 skips middle)
|
||||
@ -433,13 +481,13 @@ class AckMessage(Message):
|
||||
cl = client.get_client()
|
||||
log_message(
|
||||
"Sending ack",
|
||||
repr(self).rstrip("\n"),
|
||||
str(self).rstrip("\n"),
|
||||
None,
|
||||
ack=self.id,
|
||||
tocall=self.tocall,
|
||||
fromcall=self.fromcall,
|
||||
)
|
||||
cl.sendall(repr(self))
|
||||
cl.sendall(str(self))
|
||||
|
||||
|
||||
class SendAckThread(threads.APRSDThread):
|
||||
@ -476,13 +524,13 @@ class SendAckThread(threads.APRSDThread):
|
||||
cl = client.get_client()
|
||||
log_message(
|
||||
"Sending ack",
|
||||
repr(self.ack).rstrip("\n"),
|
||||
str(self.ack).rstrip("\n"),
|
||||
None,
|
||||
ack=self.ack.id,
|
||||
tocall=self.ack.tocall,
|
||||
retry_number=self.ack.last_send_attempt,
|
||||
)
|
||||
cl.sendall(repr(self.ack))
|
||||
cl.sendall(str(self.ack))
|
||||
stats.APRSDStats().ack_tx_inc()
|
||||
self.ack.last_send_attempt += 1
|
||||
self.ack.last_send_time = datetime.datetime.now()
|
||||
|
@ -29,6 +29,9 @@ DEFAULT_CONFIG_DICT = {
|
||||
"enabled": True,
|
||||
"host": "0.0.0.0",
|
||||
"port": 8001,
|
||||
"users": {
|
||||
"admin": "aprsd",
|
||||
},
|
||||
},
|
||||
"email": {
|
||||
"enabled": True,
|
||||
@ -297,6 +300,15 @@ def parse_config(config_file):
|
||||
["aprs", "password"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprs"]["password"],
|
||||
)
|
||||
|
||||
# Ensure they change the admin password
|
||||
if config["aprsd"]["web"]["enabled"] is True:
|
||||
check_option(
|
||||
config,
|
||||
["aprsd", "web", "users", "admin"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["web"]["users"]["admin"],
|
||||
)
|
||||
|
||||
if config["aprsd"]["email"]["enabled"] is True:
|
||||
# Check IMAP server settings
|
||||
check_option(config, ["aprsd", "email", "imap", "host"])
|
||||
|
57
aprsd/web/static/json-viewer/jquery.json-viewer.css
Normal file
57
aprsd/web/static/json-viewer/jquery.json-viewer.css
Normal file
@ -0,0 +1,57 @@
|
||||
/* Root element */
|
||||
.json-document {
|
||||
padding: 1em 2em;
|
||||
}
|
||||
|
||||
/* Syntax highlighting for JSON objects */
|
||||
ul.json-dict, ol.json-array {
|
||||
list-style-type: none;
|
||||
margin: 0 0 0 1px;
|
||||
border-left: 1px dotted #ccc;
|
||||
padding-left: 2em;
|
||||
}
|
||||
.json-string {
|
||||
color: #0B7500;
|
||||
}
|
||||
.json-literal {
|
||||
color: #1A01CC;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Toggle button */
|
||||
a.json-toggle {
|
||||
position: relative;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.json-toggle:focus {
|
||||
outline: none;
|
||||
}
|
||||
a.json-toggle:before {
|
||||
font-size: 1.1em;
|
||||
color: #c0c0c0;
|
||||
content: "\25BC"; /* down arrow */
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
text-align: center;
|
||||
line-height: 1em;
|
||||
left: -1.2em;
|
||||
}
|
||||
a.json-toggle:hover:before {
|
||||
color: #aaa;
|
||||
}
|
||||
a.json-toggle.collapsed:before {
|
||||
/* Use rotated down arrow, prevents right arrow appearing smaller than down arrow in some browsers */
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
/* Collapsable placeholder links */
|
||||
a.json-placeholder {
|
||||
color: #aaa;
|
||||
padding: 0 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.json-placeholder:hover {
|
||||
text-decoration: underline;
|
||||
}
|
158
aprsd/web/static/json-viewer/jquery.json-viewer.js
Normal file
158
aprsd/web/static/json-viewer/jquery.json-viewer.js
Normal file
@ -0,0 +1,158 @@
|
||||
/**
|
||||
* jQuery json-viewer
|
||||
* @author: Alexandre Bodelot <alexandre.bodelot@gmail.com>
|
||||
* @link: https://github.com/abodelot/jquery.json-viewer
|
||||
*/
|
||||
(function($) {
|
||||
|
||||
/**
|
||||
* Check if arg is either an array with at least 1 element, or a dict with at least 1 key
|
||||
* @return boolean
|
||||
*/
|
||||
function isCollapsable(arg) {
|
||||
return arg instanceof Object && Object.keys(arg).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string represents a valid url
|
||||
* @return boolean
|
||||
*/
|
||||
function isUrl(string) {
|
||||
var urlRegexp = /^(https?:\/\/|ftps?:\/\/)?([a-z0-9%-]+\.){1,}([a-z0-9-]+)?(:(\d{1,5}))?(\/([a-z0-9\-._~:/?#[\]@!$&'()*+,;=%]+)?)?$/i;
|
||||
return urlRegexp.test(string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a json object into html representation
|
||||
* @return string
|
||||
*/
|
||||
function json2html(json, options) {
|
||||
var html = '';
|
||||
if (typeof json === 'string') {
|
||||
// Escape tags and quotes
|
||||
json = json
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/"/g, '"');
|
||||
|
||||
if (options.withLinks && isUrl(json)) {
|
||||
html += '<a href="' + json + '" class="json-string" target="_blank">' + json + '</a>';
|
||||
} else {
|
||||
// Escape double quotes in the rendered non-URL string.
|
||||
json = json.replace(/"/g, '\\"');
|
||||
html += '<span class="json-string">"' + json + '"</span>';
|
||||
}
|
||||
} else if (typeof json === 'number') {
|
||||
html += '<span class="json-literal">' + json + '</span>';
|
||||
} else if (typeof json === 'boolean') {
|
||||
html += '<span class="json-literal">' + json + '</span>';
|
||||
} else if (json === null) {
|
||||
html += '<span class="json-literal">null</span>';
|
||||
} else if (json instanceof Array) {
|
||||
if (json.length > 0) {
|
||||
html += '[<ol class="json-array">';
|
||||
for (var i = 0; i < json.length; ++i) {
|
||||
html += '<li>';
|
||||
// Add toggle button if item is collapsable
|
||||
if (isCollapsable(json[i])) {
|
||||
html += '<a href class="json-toggle"></a>';
|
||||
}
|
||||
html += json2html(json[i], options);
|
||||
// Add comma if item is not last
|
||||
if (i < json.length - 1) {
|
||||
html += ',';
|
||||
}
|
||||
html += '</li>';
|
||||
}
|
||||
html += '</ol>]';
|
||||
} else {
|
||||
html += '[]';
|
||||
}
|
||||
} else if (typeof json === 'object') {
|
||||
var keyCount = Object.keys(json).length;
|
||||
if (keyCount > 0) {
|
||||
html += '{<ul class="json-dict">';
|
||||
for (var key in json) {
|
||||
if (Object.prototype.hasOwnProperty.call(json, key)) {
|
||||
html += '<li>';
|
||||
var keyRepr = options.withQuotes ?
|
||||
'<span class="json-string">"' + key + '"</span>' : key;
|
||||
// Add toggle button if item is collapsable
|
||||
if (isCollapsable(json[key])) {
|
||||
html += '<a href class="json-toggle">' + keyRepr + '</a>';
|
||||
} else {
|
||||
html += keyRepr;
|
||||
}
|
||||
html += ': ' + json2html(json[key], options);
|
||||
// Add comma if item is not last
|
||||
if (--keyCount > 0) {
|
||||
html += ',';
|
||||
}
|
||||
html += '</li>';
|
||||
}
|
||||
}
|
||||
html += '</ul>}';
|
||||
} else {
|
||||
html += '{}';
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* jQuery plugin method
|
||||
* @param json: a javascript object
|
||||
* @param options: an optional options hash
|
||||
*/
|
||||
$.fn.jsonViewer = function(json, options) {
|
||||
// Merge user options with default options
|
||||
options = Object.assign({}, {
|
||||
collapsed: false,
|
||||
rootCollapsable: true,
|
||||
withQuotes: false,
|
||||
withLinks: true
|
||||
}, options);
|
||||
|
||||
// jQuery chaining
|
||||
return this.each(function() {
|
||||
|
||||
// Transform to HTML
|
||||
var html = json2html(json, options);
|
||||
if (options.rootCollapsable && isCollapsable(json)) {
|
||||
html = '<a href class="json-toggle"></a>' + html;
|
||||
}
|
||||
|
||||
// Insert HTML in target DOM element
|
||||
$(this).html(html);
|
||||
$(this).addClass('json-document');
|
||||
|
||||
// Bind click on toggle buttons
|
||||
$(this).off('click');
|
||||
$(this).on('click', 'a.json-toggle', function() {
|
||||
var target = $(this).toggleClass('collapsed').siblings('ul.json-dict, ol.json-array');
|
||||
target.toggle();
|
||||
if (target.is(':visible')) {
|
||||
target.siblings('.json-placeholder').remove();
|
||||
} else {
|
||||
var count = target.children('li').length;
|
||||
var placeholder = count + (count > 1 ? ' items' : ' item');
|
||||
target.after('<a href class="json-placeholder">' + placeholder + '</a>');
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Simulate click on toggle button when placeholder is clicked
|
||||
$(this).on('click', 'a.json-placeholder', function() {
|
||||
$(this).siblings('a.json-toggle').click();
|
||||
return false;
|
||||
});
|
||||
|
||||
if (options.collapsed == true) {
|
||||
// Trigger click to collapse all nodes
|
||||
$(this).find('a.json-toggle').click();
|
||||
}
|
||||
});
|
||||
};
|
||||
})(jQuery);
|
15
aprsd/web/templates/messages.html
Normal file
15
aprsd/web/templates/messages.html
Normal file
@ -0,0 +1,15 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
|
||||
<script src="json-viewer/jquery.json-viewer.js"></script>
|
||||
<link href="json-viewer/jquery.json-viewer.css" type="text/css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<pre id="json-viewer"></pre>
|
||||
|
||||
<script>
|
||||
var data = {{ messages | safe }}
|
||||
$('#json-viewer').jsonViewer(data)
|
||||
</script>
|
||||
|
||||
</html>
|
@ -13,4 +13,5 @@ pre-commit
|
||||
pytz
|
||||
opencage
|
||||
flask
|
||||
flask_classful
|
||||
flask-classful
|
||||
flask-httpauth
|
||||
|
@ -37,10 +37,13 @@ filelock==3.0.12
|
||||
# virtualenv
|
||||
flask-classful==0.14.2
|
||||
# via -r requirements.in
|
||||
flask-httpauth==4.2.0
|
||||
# via -r requirements.in
|
||||
flask==1.1.2
|
||||
# via
|
||||
# -r requirements.in
|
||||
# flask-classful
|
||||
# flask-httpauth
|
||||
identify==1.5.13
|
||||
# via pre-commit
|
||||
idna==2.10
|
||||
|
Loading…
Reference in New Issue
Block a user