Add config editing
This commit is contained in:
parent
1ce1ec9bdc
commit
bd272c2e84
@ -1,5 +1,6 @@
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
@ -34,7 +35,7 @@ def datetimeformat(value):
|
||||
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
@app.get("/")
|
||||
def root():
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Feed view for a single source.
|
||||
@ -146,6 +147,66 @@ def action(source_name, item_id, action):
|
||||
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():
|
||||
# init_default_logging()
|
||||
return app
|
||||
|
@ -26,6 +26,13 @@ class LocalSource:
|
||||
with open(config_path, "r", encoding="utf8") as 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:
|
||||
return (self.source_path / "state").absolute()
|
||||
|
||||
@ -61,10 +68,11 @@ class LocalSource:
|
||||
|
||||
def save_item(self, item: dict) -> None:
|
||||
# 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:
|
||||
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:
|
||||
os.remove(self.get_item_path(item_id))
|
||||
|
79
intake/templates/edit.jinja2
Normal file
79
intake/templates/edit.jinja2
Normal file
@ -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>
|
||||
{% endif %}
|
||||
|
||||
{% if items %}
|
||||
<div class="readable-item">
|
||||
<div style="text-align:center;">
|
||||
<button onclick="javascript:mdeactivate({{ mdeac|safe }})">Deactivate All</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# if items #}
|
||||
{% else %}
|
||||
|
@ -40,11 +40,12 @@ summary:focus {
|
||||
<p>No sources found.</p>
|
||||
{% else %}
|
||||
{% 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 %}
|
||||
{% endif %}
|
||||
</details>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -11,6 +11,7 @@ print("args:", args, file=sys.stderr, flush=True)
|
||||
if args.action == "fetch":
|
||||
print(json.dumps({
|
||||
"id": "updateme",
|
||||
"title": "The count is at 1",
|
||||
"action": {
|
||||
"increment": 1
|
||||
}
|
||||
@ -21,5 +22,6 @@ if args.action == "increment":
|
||||
item = json.loads(item)
|
||||
item["action"]["increment"] += 1
|
||||
item["body"] = f"<p>{item['action']['increment']}</p>"
|
||||
item["title"] = f"The count is at {item['action']['increment']}"
|
||||
print(json.dumps(item))
|
||||
pass
|
||||
|
@ -2,7 +2,9 @@
|
||||
"action": {
|
||||
"fetch": {
|
||||
"exe": "python3",
|
||||
"args": ["update.py"]
|
||||
"args": [
|
||||
"update.py"
|
||||
]
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
|
Loading…
Reference in New Issue
Block a user