Add location field to devices with dropdown selector
This commit is contained in:
@@ -643,17 +643,20 @@ def create_app(config_object="config"):
|
|||||||
def device_add():
|
def device_add():
|
||||||
all_devices = db.query(Device).all()
|
all_devices = db.query(Device).all()
|
||||||
device_types = sorted({d.device_type for d in all_devices if d.device_type})
|
device_types = sorted({d.device_type for d in all_devices if d.device_type})
|
||||||
|
device_locations = sorted({d.location for d in all_devices if d.location})
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
name = request.form.get("name", "").strip()
|
name = request.form.get("name", "").strip()
|
||||||
slots_raw = request.form.get("battery_slots", "1").strip()
|
slots_raw = request.form.get("battery_slots", "1").strip()
|
||||||
notes = request.form.get("notes", "").strip() or None
|
notes = request.form.get("notes", "").strip() or None
|
||||||
device_type = request.form.get("device_type", "").strip() or None
|
device_type = request.form.get("device_type", "").strip() or None
|
||||||
|
location = request.form.get("location", "").strip() or None
|
||||||
|
|
||||||
if not name:
|
if not name:
|
||||||
flash("Device name is required.", "error")
|
flash("Device name is required.", "error")
|
||||||
return render_template("device_add.html",
|
return render_template("device_add.html",
|
||||||
device_types=device_types), 400
|
device_types=device_types,
|
||||||
|
device_locations=device_locations), 400
|
||||||
|
|
||||||
try:
|
try:
|
||||||
slots = int(slots_raw)
|
slots = int(slots_raw)
|
||||||
@@ -663,6 +666,7 @@ def create_app(config_object="config"):
|
|||||||
flash("Battery slots must be a positive integer.", "error")
|
flash("Battery slots must be a positive integer.", "error")
|
||||||
return render_template("device_add.html",
|
return render_template("device_add.html",
|
||||||
device_types=device_types,
|
device_types=device_types,
|
||||||
|
device_locations=device_locations,
|
||||||
form_name=name, form_notes=notes or "",
|
form_name=name, form_notes=notes or "",
|
||||||
form_device_type=request.form.get("device_type", "")), 400
|
form_device_type=request.form.get("device_type", "")), 400
|
||||||
|
|
||||||
@@ -670,17 +674,20 @@ def create_app(config_object="config"):
|
|||||||
flash(f"A device named '{name}' already exists.", "error")
|
flash(f"A device named '{name}' already exists.", "error")
|
||||||
return render_template("device_add.html",
|
return render_template("device_add.html",
|
||||||
device_types=device_types,
|
device_types=device_types,
|
||||||
|
device_locations=device_locations,
|
||||||
form_name=name, form_slots=slots,
|
form_name=name, form_slots=slots,
|
||||||
form_notes=notes or "",
|
form_notes=notes or "",
|
||||||
form_device_type=request.form.get("device_type", "")), 400
|
form_device_type=request.form.get("device_type", "")), 400
|
||||||
|
|
||||||
device = Device(name=name, battery_slots=slots, notes=notes, device_type=device_type)
|
device = Device(name=name, battery_slots=slots, notes=notes,
|
||||||
|
device_type=device_type, location=location)
|
||||||
db.add(device)
|
db.add(device)
|
||||||
db.commit()
|
db.commit()
|
||||||
flash(f"Device '{name}' added.", "success")
|
flash(f"Device '{name}' added.", "success")
|
||||||
return redirect(url_for("device_list"))
|
return redirect(url_for("device_list"))
|
||||||
|
|
||||||
return render_template("device_add.html", device_types=device_types)
|
return render_template("device_add.html", device_types=device_types,
|
||||||
|
device_locations=device_locations)
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Devices — detail
|
# Devices — detail
|
||||||
@@ -696,6 +703,7 @@ def create_app(config_object="config"):
|
|||||||
.filter_by(status="available")
|
.filter_by(status="available")
|
||||||
.order_by(Battery.label).all())
|
.order_by(Battery.label).all())
|
||||||
device_types = sorted({d.device_type for d in db.query(Device).all() if d.device_type})
|
device_types = sorted({d.device_type for d in db.query(Device).all() if d.device_type})
|
||||||
|
device_locations = sorted({d.location for d in db.query(Device).all() if d.location})
|
||||||
ha_live_pct = None
|
ha_live_pct = None
|
||||||
if ha_client.enabled and device.ha_entity_id:
|
if ha_client.enabled and device.ha_entity_id:
|
||||||
ha_live_pct = ha_client.get_state(device.ha_entity_id, timeout=1)
|
ha_live_pct = ha_client.get_state(device.ha_entity_id, timeout=1)
|
||||||
@@ -716,6 +724,7 @@ def create_app(config_object="config"):
|
|||||||
return render_template("device_detail.html", device=device, brands=brands,
|
return render_template("device_detail.html", device=device, brands=brands,
|
||||||
available_batteries=available_batteries,
|
available_batteries=available_batteries,
|
||||||
device_types=device_types,
|
device_types=device_types,
|
||||||
|
device_locations=device_locations,
|
||||||
ha_enabled=ha_client.enabled,
|
ha_enabled=ha_client.enabled,
|
||||||
ha_live_pct=ha_live_pct)
|
ha_live_pct=ha_live_pct)
|
||||||
|
|
||||||
@@ -752,6 +761,7 @@ def create_app(config_object="config"):
|
|||||||
device.battery_slots = slots
|
device.battery_slots = slots
|
||||||
device.notes = notes
|
device.notes = notes
|
||||||
device.device_type = device_type
|
device.device_type = device_type
|
||||||
|
device.location = request.form.get("location", "").strip() or None
|
||||||
device.ha_entity_id = request.form.get("ha_entity_id", "").strip() or None
|
device.ha_entity_id = request.form.get("ha_entity_id", "").strip() or None
|
||||||
db.commit()
|
db.commit()
|
||||||
flash("Device updated.", "success")
|
flash("Device updated.", "success")
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class Device(Base):
|
|||||||
name = Column(String(100), nullable=False, unique=True)
|
name = Column(String(100), nullable=False, unique=True)
|
||||||
battery_slots = Column(Integer, nullable=False, default=1)
|
battery_slots = Column(Integer, nullable=False, default=1)
|
||||||
device_type = Column(String(50), nullable=True)
|
device_type = Column(String(50), nullable=True)
|
||||||
|
location = Column(String(100), nullable=True)
|
||||||
notes = Column(Text, nullable=True)
|
notes = Column(Text, nullable=True)
|
||||||
ha_entity_id = Column(String(100), nullable=True) # e.g. "sensor.tv_remote_battery"
|
ha_entity_id = Column(String(100), nullable=True) # e.g. "sensor.tv_remote_battery"
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,20 @@
|
|||||||
style="display:{% if _cur_type and _cur_type not in _preset_types %}''{% else %}none{% endif %};margin-top:0.4rem;">
|
style="display:{% if _cur_type and _cur_type not in _preset_types %}''{% else %}none{% endif %};margin-top:0.4rem;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Location</label>
|
||||||
|
<select id="location-select" onchange="metaSelectChanged(this,'location')">
|
||||||
|
<option value="">— none —</option>
|
||||||
|
{% for loc in device_locations|default([]) %}
|
||||||
|
<option value="{{ loc }}">{{ loc }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
<option value="__new__">➕ New location…</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" id="location" name="location" value=""
|
||||||
|
placeholder="e.g. Living Room, Bedroom"
|
||||||
|
style="display:none;margin-top:0.4rem;">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="notes">Notes</label>
|
<label for="notes">Notes</label>
|
||||||
<textarea id="notes" name="notes" placeholder="Optional notes…">{{ form_notes|default('') }}</textarea>
|
<textarea id="notes" name="notes" placeholder="Optional notes…">{{ form_notes|default('') }}</textarea>
|
||||||
|
|||||||
@@ -45,6 +45,12 @@
|
|||||||
<td style="border:none;"><span class="badge badge-warning">⚠ Mixed brands installed</span></td>
|
<td style="border:none;"><span class="badge badge-warning">⚠ Mixed brands installed</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if device.location %}
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Location</td>
|
||||||
|
<td style="border:none;">{{ device.location }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% if device.notes %}
|
{% if device.notes %}
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Notes</td>
|
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Notes</td>
|
||||||
@@ -109,6 +115,14 @@ function editTypeSelectChanged(sel) {
|
|||||||
input.style.display = 'none'; input.value = sel.value;
|
input.style.display = 'none'; input.value = sel.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function editLocationSelectChanged(sel) {
|
||||||
|
var input = document.getElementById('edit-location');
|
||||||
|
if (sel.value === '__new__') {
|
||||||
|
input.style.display = ''; input.value = ''; input.focus();
|
||||||
|
} else {
|
||||||
|
input.style.display = 'none'; input.value = sel.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
function brandSelectChanged(sel) {
|
function brandSelectChanged(sel) {
|
||||||
var input = sel.nextElementSibling;
|
var input = sel.nextElementSibling;
|
||||||
if (sel.value === '__new__') {
|
if (sel.value === '__new__') {
|
||||||
@@ -224,6 +238,24 @@ function addInstallRow() {
|
|||||||
placeholder="Enter device type"
|
placeholder="Enter device type"
|
||||||
style="display:{% if device.device_type and device.device_type not in _preset_types %}''{% else %}none{% endif %};margin-top:0.4rem;">
|
style="display:{% if device.device_type and device.device_type not in _preset_types %}''{% else %}none{% endif %};margin-top:0.4rem;">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Location</label>
|
||||||
|
<select id="edit-location-select" onchange="editLocationSelectChanged(this)">
|
||||||
|
<option value="">— none —</option>
|
||||||
|
{% for loc in device_locations|default([]) %}
|
||||||
|
<option value="{{ loc }}" {% if device.location == loc %}selected{% endif %}>{{ loc }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
{% if device.location and device.location not in device_locations|default([]) %}
|
||||||
|
<option value="{{ device.location }}" selected>{{ device.location }}</option>
|
||||||
|
{% endif %}
|
||||||
|
<option value="__new__">➕ New location…</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" id="edit-location" name="location"
|
||||||
|
value="{{ device.location or '' }}"
|
||||||
|
placeholder="e.g. Living Room, Bedroom"
|
||||||
|
style="display:{% if device.location and device.location not in device_locations|default([]) %}''{% else %}none{% endif %};margin-top:0.4rem;">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="edit-notes">Notes</label>
|
<label for="edit-notes">Notes</label>
|
||||||
<textarea id="edit-notes" name="notes">{{ device.notes or '' }}</textarea>
|
<textarea id="edit-notes" name="notes">{{ device.notes or '' }}</textarea>
|
||||||
|
|||||||
@@ -498,6 +498,25 @@ def test_edit_device_type(client):
|
|||||||
assert b"Flashlight" in resp.data
|
assert b"Flashlight" in resp.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_device_with_location(client):
|
||||||
|
resp = client.post("/device/add",
|
||||||
|
data={"name": "Bedroom Remote", "battery_slots": "2", "location": "Bedroom"},
|
||||||
|
follow_redirects=True)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert b"Bedroom Remote" in resp.data
|
||||||
|
resp = client.get("/device/1")
|
||||||
|
assert b"Bedroom" in resp.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_device_location(client):
|
||||||
|
client.post("/device/add", data={"name": "Living Room Clock", "battery_slots": "1"})
|
||||||
|
resp = client.post("/device/1/edit",
|
||||||
|
data={"name": "Living Room Clock", "battery_slots": "1", "location": "Living Room"},
|
||||||
|
follow_redirects=True)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert b"Living Room" in resp.data
|
||||||
|
|
||||||
|
|
||||||
def test_add_capacity_test(client):
|
def test_add_capacity_test(client):
|
||||||
client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
|
client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
|
||||||
resp = client.post("/battery/1/capacity-test/add",
|
resp = client.post("/battery/1/capacity-test/add",
|
||||||
|
|||||||
Reference in New Issue
Block a user