Add config editing

This commit is contained in:
Tim Van Baak 2023-06-02 16:57:03 -07:00
parent 1ce1ec9bdc
commit bd272c2e84
7 changed files with 159 additions and 8 deletions

View File

@ -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

View File

@ -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))

View 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>

View File

@ -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 %}

View File

@ -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>

View File

@ -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

View File

@ -2,7 +2,9 @@
"action": { "action": {
"fetch": { "fetch": {
"exe": "python3", "exe": "python3",
"args": ["update.py"] "args": [
"update.py"
]
} }
}, },
"env": { "env": {