modify checkbox text in notes
This commit is contained in:
+6
-1
@@ -167,7 +167,8 @@ login.
|
|||||||
Logged-in users manage their own private short notes at `/notes`. Each note has
|
Logged-in users manage their own private short notes at `/notes`. Each note has
|
||||||
a title, a text body and an optional checklist. Unchecked items are shown first;
|
a title, a text body and an optional checklist. Unchecked items are shown first;
|
||||||
when an item is checked it is saved as done, moves to the bottom of the checklist
|
when an item is checked it is saved as done, moves to the bottom of the checklist
|
||||||
and is displayed with a line through it.
|
and is displayed with a line through it. Checklist item text can be edited at any
|
||||||
|
time.
|
||||||
|
|
||||||
Notes are stored in `main_app.db` in the `note` and `note_item` tables. Apply
|
Notes are stored in `main_app.db` in the `note` and `note_item` tables. Apply
|
||||||
the users database migration once (the production launcher does this
|
the users database migration once (the production launcher does this
|
||||||
@@ -184,6 +185,10 @@ The main app (`:8000`) listens on all interfaces; the sensors API (`:8001`)
|
|||||||
listens on `127.0.0.1` by default and is open to every local user without
|
listens on `127.0.0.1` by default and is open to every local user without
|
||||||
authentication.
|
authentication.
|
||||||
|
|
||||||
|
Do not run the main app on the sensors port (`8001`) while `SENSORS_API_URL` is
|
||||||
|
unset: it defaults to `http://localhost:8001`, so the main app would proxy API
|
||||||
|
requests to itself. Both launchers refuse to start in that configuration.
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -174,6 +174,20 @@ def create_app():
|
|||||||
user_db.session.commit()
|
user_db.session.commit()
|
||||||
return _note_redirect(note.id)
|
return _note_redirect(note.id)
|
||||||
|
|
||||||
|
@app.route(
|
||||||
|
"/notes/<int:note_id>/items/<int:item_id>", methods=["POST"]
|
||||||
|
)
|
||||||
|
@login_required
|
||||||
|
def notes_update_item(note_id, item_id):
|
||||||
|
note = _note_or_404(note_id)
|
||||||
|
item = NoteItem.query.filter_by(id=item_id, note_id=note.id).first_or_404()
|
||||||
|
text = request.form.get("text", "").strip()
|
||||||
|
if text:
|
||||||
|
item.text = text[:255]
|
||||||
|
note.touch()
|
||||||
|
user_db.session.commit()
|
||||||
|
return _note_redirect(note.id)
|
||||||
|
|
||||||
@app.route(
|
@app.route(
|
||||||
"/notes/<int:note_id>/items/<int:item_id>/toggle", methods=["POST"]
|
"/notes/<int:note_id>/items/<int:item_id>/toggle", methods=["POST"]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -38,6 +38,16 @@ esac
|
|||||||
PORT="${PORT:-$DEFAULT_PORT}"
|
PORT="${PORT:-$DEFAULT_PORT}"
|
||||||
HOST="${HOST:-$DEFAULT_HOST}"
|
HOST="${HOST:-$DEFAULT_HOST}"
|
||||||
|
|
||||||
|
# Prevent the main app from proxying to itself. Without an explicit
|
||||||
|
# SENSORS_API_URL the client targets localhost:8001, so main must not listen
|
||||||
|
# there as well.
|
||||||
|
if [ "$APP" = "main" ] && [ -z "${SENSORS_API_URL:-}" ] && [ "$PORT" = "8001" ]; then
|
||||||
|
echo "Refusing to start main on port 8001: SENSORS_API_URL would default to" >&2
|
||||||
|
echo "http://localhost:8001 and the app would call itself recursively." >&2
|
||||||
|
echo "Use another port (for example 8000) or set SENSORS_API_URL explicitly." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$APP" = "main" ]; then
|
if [ "$APP" = "main" ]; then
|
||||||
SECRET_FILE=".secret_key"
|
SECRET_FILE=".secret_key"
|
||||||
if [ -z "${SECRET_KEY:-}" ]; then
|
if [ -z "${SECRET_KEY:-}" ]; then
|
||||||
|
|||||||
@@ -38,6 +38,16 @@ esac
|
|||||||
PORT="${PORT:-$DEFAULT_PORT}"
|
PORT="${PORT:-$DEFAULT_PORT}"
|
||||||
HOST="${HOST:-$DEFAULT_HOST}"
|
HOST="${HOST:-$DEFAULT_HOST}"
|
||||||
|
|
||||||
|
# Prevent the main app from proxying to itself. Without an explicit
|
||||||
|
# SENSORS_API_URL the client targets localhost:8001, so main must not listen
|
||||||
|
# there as well.
|
||||||
|
if [ "$APP" = "main" ] && [ -z "${SENSORS_API_URL:-}" ] && [ "$PORT" = "8001" ]; then
|
||||||
|
echo "Refusing to start main on port 8001: SENSORS_API_URL would default to" >&2
|
||||||
|
echo "http://localhost:8001 and the app would call itself recursively." >&2
|
||||||
|
echo "Use another port (for example 8000) or set SENSORS_API_URL explicitly." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -x ".venv/bin/python" ]; then
|
if [ -x ".venv/bin/python" ]; then
|
||||||
PYTHON=".venv/bin/python"
|
PYTHON=".venv/bin/python"
|
||||||
else
|
else
|
||||||
|
|||||||
+19
-3
@@ -343,7 +343,7 @@ textarea {
|
|||||||
padding: 0.15em 0;
|
padding: 0.15em 0;
|
||||||
}
|
}
|
||||||
.check-form {
|
.check-form {
|
||||||
flex: 1;
|
flex: 0 0 auto;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.6em;
|
gap: 0.6em;
|
||||||
@@ -354,8 +354,24 @@ textarea {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
accent-color: var(--mc-yellow);
|
accent-color: var(--mc-yellow);
|
||||||
}
|
}
|
||||||
.item-text { flex: 1; overflow-wrap: anywhere; }
|
.item-edit-form {
|
||||||
.checklist li.done .item-text {
|
flex: 1;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.item-edit-form input { flex: 1; min-width: 0; }
|
||||||
|
.item-edit-form button {
|
||||||
|
background: var(--mc-dim);
|
||||||
|
color: var(--mc-black);
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0.1em 0.4em;
|
||||||
|
font-size: 0.85em;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
.item-edit-form button:hover { background: var(--mc-cyan); }
|
||||||
|
.checklist li.done .item-edit-form input {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
color: var(--mc-dim);
|
color: var(--mc-dim);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,9 +61,15 @@
|
|||||||
{% if item.is_done %}checked{% endif %}
|
{% if item.is_done %}checked{% endif %}
|
||||||
onchange="this.form.submit()"
|
onchange="this.form.submit()"
|
||||||
aria-label="Mark '{{ item.text }}' as done">
|
aria-label="Mark '{{ item.text }}' as done">
|
||||||
<span class="item-text">{{ item.text }}</span>
|
|
||||||
<noscript><button type="submit">[ Update ]</button></noscript>
|
<noscript><button type="submit">[ Update ]</button></noscript>
|
||||||
</form>
|
</form>
|
||||||
|
<form method="post"
|
||||||
|
action="{{ url_for('notes_update_item', note_id=note.id, item_id=item.id) }}"
|
||||||
|
class="item-edit-form">
|
||||||
|
<input type="text" name="text" value="{{ item.text }}"
|
||||||
|
maxlength="255" required aria-label="Checklist item text">
|
||||||
|
<button type="submit">[ Save ]</button>
|
||||||
|
</form>
|
||||||
<form method="post"
|
<form method="post"
|
||||||
action="{{ url_for('notes_delete_item', note_id=note.id, item_id=item.id) }}"
|
action="{{ url_for('notes_delete_item', note_id=note.id, item_id=item.id) }}"
|
||||||
class="item-delete-form">
|
class="item-delete-form">
|
||||||
|
|||||||
@@ -235,11 +235,18 @@ class MainAppTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
with self.main_app.app_context():
|
with self.main_app.app_context():
|
||||||
first, second = (
|
first, _second = (
|
||||||
NoteItem.query.filter_by(note_id=note_id).order_by(NoteItem.id).all()
|
NoteItem.query.filter_by(note_id=note_id).order_by(NoteItem.id).all()
|
||||||
)
|
)
|
||||||
first_id = first.id
|
first_id = first.id
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.client.post(
|
||||||
|
f"/notes/{note_id}/items/{first_id}",
|
||||||
|
data={"text": "renamed first item"},
|
||||||
|
).status_code,
|
||||||
|
302,
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.client.post(
|
self.client.post(
|
||||||
f"/notes/{note_id}/items/{first_id}/toggle",
|
f"/notes/{note_id}/items/{first_id}/toggle",
|
||||||
@@ -253,7 +260,8 @@ class MainAppTest(unittest.TestCase):
|
|||||||
self.assertEqual(page.status_code, 200)
|
self.assertEqual(page.status_code, 200)
|
||||||
self.assertIn('class="done"', html)
|
self.assertIn('class="done"', html)
|
||||||
self.assertIn('value="Test title"', html)
|
self.assertIn('value="Test title"', html)
|
||||||
self.assertLess(html.index("second item"), html.index("first item"))
|
self.assertIn('value="renamed first item"', html)
|
||||||
|
self.assertLess(html.index("second item"), html.index("renamed first item"))
|
||||||
|
|
||||||
with self.main_app.app_context():
|
with self.main_app.app_context():
|
||||||
user_db.session.expire_all()
|
user_db.session.expire_all()
|
||||||
@@ -264,7 +272,7 @@ class MainAppTest(unittest.TestCase):
|
|||||||
self.assertFalse(items[1].is_done)
|
self.assertFalse(items[1].is_done)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
[item.text for item in items[0].note.ordered_items],
|
[item.text for item in items[0].note.ordered_items],
|
||||||
["second item", "first item"],
|
["second item", "renamed first item"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user