Add config editing
This commit is contained in:
parent
1ce1ec9bdc
commit
bd272c2e84
|
@ -1,5 +1,6 @@
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
@ -34,7 +35,7 @@ def datetimeformat(value):
|
||||||
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.get("/")
|
||||||
def root():
|
def root():
|
||||||
"""
|
"""
|
||||||
Navigation home page.
|
Navigation home page.
|
||||||
|
@ -52,7 +53,7 @@ def root():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/source/<string:source_name>")
|
@app.get("/source/<string:source_name>")
|
||||||
def source_feed(source_name):
|
def source_feed(source_name):
|
||||||
"""
|
"""
|
||||||
Feed view for a single source.
|
Feed view for a single source.
|
||||||
|
@ -146,6 +147,66 @@ def action(source_name, item_id, action):
|
||||||
return jsonify(item)
|
return jsonify(item)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/edit/source/<string:source_name>", methods=["GET", "POST"])
|
||||||
|
def source_edit(source_name):
|
||||||
|
"""
|
||||||
|
Config editor for a source
|
||||||
|
"""
|
||||||
|
source = LocalSource(intake_data_dir(), source_name)
|
||||||
|
if not source.source_path.exists():
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
# For POST, check if the config is valid
|
||||||
|
error_message: str = None
|
||||||
|
if request.method == "POST":
|
||||||
|
config_str = request.form.get("config", "")
|
||||||
|
error_message, config = try_parse_config(config_str)
|
||||||
|
print(config_str)
|
||||||
|
print(error_message)
|
||||||
|
print(config)
|
||||||
|
if not error_message:
|
||||||
|
source.save_config(config)
|
||||||
|
return redirect(url_for("root"))
|
||||||
|
|
||||||
|
# For GET, load the config
|
||||||
|
if request.method == "GET":
|
||||||
|
config = source.get_config()
|
||||||
|
config_str = json.dumps(config, indent=2)
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"edit.jinja2",
|
||||||
|
source=source,
|
||||||
|
config=config_str,
|
||||||
|
error_message=error_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def try_parse_config(config_str: str):
|
||||||
|
if not config_str:
|
||||||
|
return ("Config required", {})
|
||||||
|
try:
|
||||||
|
parsed = json.loads(config_str)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return ("Invalid JSON", {})
|
||||||
|
if not isinstance(parsed, dict):
|
||||||
|
return ("Invalid config format", {})
|
||||||
|
if "action" not in parsed:
|
||||||
|
return ("No actions defined", {})
|
||||||
|
action = parsed["action"]
|
||||||
|
if "fetch" not in action:
|
||||||
|
return ("No fetch action defined", {})
|
||||||
|
fetch = action["fetch"]
|
||||||
|
if "exe" not in fetch:
|
||||||
|
return ("No fetch exe", {})
|
||||||
|
return (
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"action": parsed["action"],
|
||||||
|
"env": parsed["env"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def wsgi():
|
def wsgi():
|
||||||
# init_default_logging()
|
# init_default_logging()
|
||||||
return app
|
return app
|
||||||
|
|
|
@ -26,6 +26,13 @@ class LocalSource:
|
||||||
with open(config_path, "r", encoding="utf8") as config_file:
|
with open(config_path, "r", encoding="utf8") as config_file:
|
||||||
return json.load(config_file)
|
return json.load(config_file)
|
||||||
|
|
||||||
|
def save_config(self, config: dict) -> None:
|
||||||
|
config_path = self.source_path / "intake.json"
|
||||||
|
tmp_path = config_path.with_name(f"{config_path.name}.tmp")
|
||||||
|
with tmp_path.open("w") as f:
|
||||||
|
f.write(json.dumps(config, indent=2))
|
||||||
|
os.rename(tmp_path, config_path)
|
||||||
|
|
||||||
def get_state_path(self) -> Path:
|
def get_state_path(self) -> Path:
|
||||||
return (self.source_path / "state").absolute()
|
return (self.source_path / "state").absolute()
|
||||||
|
|
||||||
|
@ -61,10 +68,11 @@ class LocalSource:
|
||||||
|
|
||||||
def save_item(self, item: dict) -> None:
|
def save_item(self, item: dict) -> None:
|
||||||
# Write to a tempfile first to avoid losing the item on write failure
|
# Write to a tempfile first to avoid losing the item on write failure
|
||||||
tmp_path = self.source_path / f"{item['id']}.item.tmp"
|
item_path = self.get_item_path(item["id"])
|
||||||
|
tmp_path = item_path.with_name(f"{item_path.name}.tmp")
|
||||||
with tmp_path.open("w") as f:
|
with tmp_path.open("w") as f:
|
||||||
f.write(json.dumps(item, indent=2))
|
f.write(json.dumps(item, indent=2))
|
||||||
os.rename(tmp_path, self.get_item_path(item["id"]))
|
os.rename(tmp_path, item_path)
|
||||||
|
|
||||||
def delete_item(self, item_id) -> None:
|
def delete_item(self, item_id) -> None:
|
||||||
os.remove(self.get_item_path(item_id))
|
os.remove(self.get_item_path(item_id))
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Intake - {{ source.source_name }}</title>
|
||||||
|
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwgAADsIBFShKgAAAABh0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMS41ZEdYUgAAAGFJREFUOE+lkFEKwDAIxXrzXXB3ckMm9EnAV/YRCxFCcUXEL3Jc77NDjpDA/VGL3RFWYEICfeGC8oQc9IPuCAnQDcoRVmBCAn3hgvKEHPSD7ggJ0A3KEVZgQgJ94YLSJ9YDUzNGDXGZ/JEAAAAASUVORK5CYII=">
|
||||||
|
<style>
|
||||||
|
div#wrapper {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.readable-item {
|
||||||
|
border: 1px solid black; border-radius: 6px;
|
||||||
|
padding: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.item-title {
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
.item-button {
|
||||||
|
font-size: 1em;
|
||||||
|
float:right;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
.item-link {
|
||||||
|
text-decoration: none;
|
||||||
|
float:right;
|
||||||
|
font-size: 1em;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.item-info {
|
||||||
|
color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
.readable-item img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.readable-item textarea {
|
||||||
|
width: 100%;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
button, summary {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
summary {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
summary:focus {
|
||||||
|
outline: 1px dotted gray;
|
||||||
|
}
|
||||||
|
.strikethru span, .strikethru p {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
.fade span, .fade p {
|
||||||
|
color: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
table.feed-control td {
|
||||||
|
font-family: monospace; padding: 5px 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="wrapper">
|
||||||
|
|
||||||
|
<div class="readable-item">
|
||||||
|
<form method="post">
|
||||||
|
<label for="config" class="item-title">Source Editor</label>
|
||||||
|
<textarea autofocus id="config" name="config" rows=20>{{config}}</textarea>
|
||||||
|
<p><input type="submit" value="Submit"></p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -178,13 +178,11 @@ var doAction = function (source, itemid, action) {
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if items %}
|
|
||||||
<div class="readable-item">
|
<div class="readable-item">
|
||||||
<div style="text-align:center;">
|
<div style="text-align:center;">
|
||||||
<button onclick="javascript:mdeactivate({{ mdeac|safe }})">Deactivate All</button>
|
<button onclick="javascript:mdeactivate({{ mdeac|safe }})">Deactivate All</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{# if items #}
|
{# if items #}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -40,11 +40,12 @@ summary:focus {
|
||||||
<p>No sources found.</p>
|
<p>No sources found.</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% for source in sources %}
|
{% for source in sources %}
|
||||||
<p><a href="{{ url_for('source_feed', source_name=source.source_name) }}">{{ source.source_name|safe }}</a></p>
|
<p><a href="{{ url_for('source_feed', source_name=source.source_name) }}">{{ source.source_name|safe }}</a> (<a href="{{ url_for('source_edit', source_name=source.source_name) }}">edit</a>)</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -11,6 +11,7 @@ print("args:", args, file=sys.stderr, flush=True)
|
||||||
if args.action == "fetch":
|
if args.action == "fetch":
|
||||||
print(json.dumps({
|
print(json.dumps({
|
||||||
"id": "updateme",
|
"id": "updateme",
|
||||||
|
"title": "The count is at 1",
|
||||||
"action": {
|
"action": {
|
||||||
"increment": 1
|
"increment": 1
|
||||||
}
|
}
|
||||||
|
@ -21,5 +22,6 @@ if args.action == "increment":
|
||||||
item = json.loads(item)
|
item = json.loads(item)
|
||||||
item["action"]["increment"] += 1
|
item["action"]["increment"] += 1
|
||||||
item["body"] = f"<p>{item['action']['increment']}</p>"
|
item["body"] = f"<p>{item['action']['increment']}</p>"
|
||||||
|
item["title"] = f"The count is at {item['action']['increment']}"
|
||||||
print(json.dumps(item))
|
print(json.dumps(item))
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
"action": {
|
"action": {
|
||||||
"fetch": {
|
"fetch": {
|
||||||
"exe": "python3",
|
"exe": "python3",
|
||||||
"args": ["update.py"]
|
"args": [
|
||||||
|
"update.py"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
|
|
Loading…
Reference in New Issue