Add optional battery metadata fields
New nullable columns on Battery: size, chemistry, capacity_mah, tested_capacity_mah, tested_date, charge_cycles, purchase_date. Battery detail page shows all populated fields and a full edit form with select dropdowns for size and chemistry (with Other fallback). Capacity health % shown in green/orange/red when both nominal and tested capacity are set. Dashboard gains a Size column.
This commit is contained in:
@@ -77,14 +77,31 @@ def create_app(config_object="config"):
|
||||
# Battery — edit notes
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@app.route("/battery/<int:battery_id>/edit-notes", methods=["POST"])
|
||||
def battery_edit_notes(battery_id):
|
||||
@app.route("/battery/<int:battery_id>/edit-details", methods=["POST"])
|
||||
def battery_edit_details(battery_id):
|
||||
battery = db.get(Battery, battery_id)
|
||||
if battery is None:
|
||||
abort(404)
|
||||
battery.notes = request.form.get("notes", "").strip() or None
|
||||
f = request.form
|
||||
|
||||
def _int(key):
|
||||
v = f.get(key, "").strip()
|
||||
try:
|
||||
return int(v) if v else None
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
battery.notes = f.get("notes", "").strip() or None
|
||||
battery.size = f.get("size", "").strip() or None
|
||||
battery.chemistry = f.get("chemistry", "").strip() or None
|
||||
battery.capacity_mah = _int("capacity_mah")
|
||||
battery.tested_capacity_mah = _int("tested_capacity_mah")
|
||||
battery.tested_date = f.get("tested_date", "").strip() or None
|
||||
battery.charge_cycles = _int("charge_cycles")
|
||||
battery.purchase_date = f.get("purchase_date", "").strip() or None
|
||||
|
||||
db.commit()
|
||||
flash("Notes updated.", "success")
|
||||
flash("Details updated.", "success")
|
||||
return redirect(url_for("battery_detail", battery_id=battery_id))
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@@ -37,6 +37,15 @@ class Battery(Base):
|
||||
device_id = Column(Integer, ForeignKey("device.id", ondelete="SET NULL"), nullable=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
|
||||
# Optional metadata
|
||||
size = Column(String(20), nullable=True) # AA, AAA, 18650, CR2032 …
|
||||
chemistry = Column(String(20), nullable=True) # NiMH, Alkaline, Li-ion …
|
||||
capacity_mah = Column(Integer, nullable=True) # nominal capacity in mAh
|
||||
tested_capacity_mah = Column(Integer, nullable=True) # last measured capacity in mAh
|
||||
tested_date = Column(String(10), nullable=True) # YYYY-MM-DD of last test
|
||||
charge_cycles = Column(Integer, nullable=True) # number of charge cycles
|
||||
purchase_date = Column(String(10), nullable=True) # YYYY-MM-DD when purchased
|
||||
|
||||
device = relationship("Device", back_populates="batteries")
|
||||
|
||||
def is_available(self):
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
{% block content %}
|
||||
<h1>{{ battery.label }}</h1>
|
||||
|
||||
{% macro meta_row(label, value) %}
|
||||
{% if value %}
|
||||
<tr>
|
||||
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">{{ label }}</td>
|
||||
<td style="border:none;">{{ value }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
<div class="card">
|
||||
<table style="width:auto;border:none;">
|
||||
<tr>
|
||||
@@ -28,17 +37,117 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{{ meta_row("Size", battery.size) }}
|
||||
{{ meta_row("Chemistry", battery.chemistry) }}
|
||||
{% if battery.capacity_mah %}
|
||||
<tr>
|
||||
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Capacity</td>
|
||||
<td style="border:none;">
|
||||
{{ battery.capacity_mah }} mAh
|
||||
{% if battery.tested_capacity_mah %}
|
||||
{% set pct = (battery.tested_capacity_mah / battery.capacity_mah * 100)|round|int %}
|
||||
{% if pct >= 80 %}{% set health_color = "#166534" %}
|
||||
{% elif pct >= 60 %}{% set health_color = "#92400e" %}
|
||||
{% else %}{% set health_color = "#991b1b" %}
|
||||
{% endif %}
|
||||
→ tested <strong style="color:{{ health_color }};">{{ battery.tested_capacity_mah }} mAh ({{ pct }}%)</strong>
|
||||
{% if battery.tested_date %}<span class="text-muted">on {{ battery.tested_date }}</span>{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% elif battery.tested_capacity_mah %}
|
||||
<tr>
|
||||
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Tested Capacity</td>
|
||||
<td style="border:none;">
|
||||
{{ battery.tested_capacity_mah }} mAh
|
||||
{% if battery.tested_date %}<span class="text-muted">on {{ battery.tested_date }}</span>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{{ meta_row("Charge Cycles", battery.charge_cycles) }}
|
||||
{{ meta_row("Purchase Date", battery.purchase_date) }}
|
||||
{% if battery.notes %}
|
||||
<tr>
|
||||
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Notes</td>
|
||||
<td style="border:none;">{{ battery.notes }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<!-- Edit Details -->
|
||||
<div class="card">
|
||||
<h2>Notes</h2>
|
||||
<form method="post" action="{{ url_for('battery_edit_notes', battery_id=battery.id) }}">
|
||||
<div class="form-group">
|
||||
<textarea name="notes" placeholder="No notes yet…">{{ battery.notes or '' }}</textarea>
|
||||
<h2>Edit Details</h2>
|
||||
<form method="post" action="{{ url_for('battery_edit_details', battery_id=battery.id) }}">
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0 1rem;">
|
||||
|
||||
<div class="form-group">
|
||||
<label>Size</label>
|
||||
<select id="size-select" onchange="metaSelectChanged(this,'size')">
|
||||
<option value="">— none —</option>
|
||||
{% for opt in ['AA','AAA','C','D','9V','18650','21700','14500','26650','CR2032','CR123A'] %}
|
||||
<option value="{{ opt }}" {% if battery.size == opt %}selected{% endif %}>{{ opt }}</option>
|
||||
{% endfor %}
|
||||
<option value="__new__" {% if battery.size and battery.size not in ['AA','AAA','C','D','9V','18650','21700','14500','26650','CR2032','CR123A'] %}selected{% endif %}>Other…</option>
|
||||
</select>
|
||||
<input type="text" id="size" name="size" value="{{ battery.size or '' }}"
|
||||
placeholder="Enter size"
|
||||
style="display:{% if battery.size and battery.size not in ['AA','AAA','C','D','9V','18650','21700','14500','26650','CR2032','CR123A'] %}''{% else %}none{% endif %};margin-top:0.4rem;">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Chemistry</label>
|
||||
<select id="chemistry-select" onchange="metaSelectChanged(this,'chemistry')">
|
||||
<option value="">— none —</option>
|
||||
{% for opt in ['NiMH','Alkaline','Li-ion','LiFePO4','NiCd','Zinc-Carbon','Li-MnO2'] %}
|
||||
<option value="{{ opt }}" {% if battery.chemistry == opt %}selected{% endif %}>{{ opt }}</option>
|
||||
{% endfor %}
|
||||
<option value="__new__" {% if battery.chemistry and battery.chemistry not in ['NiMH','Alkaline','Li-ion','LiFePO4','NiCd','Zinc-Carbon','Li-MnO2'] %}selected{% endif %}>Other…</option>
|
||||
</select>
|
||||
<input type="text" id="chemistry" name="chemistry" value="{{ battery.chemistry or '' }}"
|
||||
placeholder="Enter chemistry"
|
||||
style="display:{% if battery.chemistry and battery.chemistry not in ['NiMH','Alkaline','Li-ion','LiFePO4','NiCd','Zinc-Carbon','Li-MnO2'] %}''{% else %}none{% endif %};margin-top:0.4rem;">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="capacity_mah">Capacity (mAh)</label>
|
||||
<input type="number" id="capacity_mah" name="capacity_mah" min="0"
|
||||
value="{{ battery.capacity_mah or '' }}" placeholder="e.g. 2000">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="charge_cycles">Charge Cycles</label>
|
||||
<input type="number" id="charge_cycles" name="charge_cycles" min="0"
|
||||
value="{{ battery.charge_cycles or '' }}" placeholder="e.g. 50">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="tested_capacity_mah">Tested Capacity (mAh)</label>
|
||||
<input type="number" id="tested_capacity_mah" name="tested_capacity_mah" min="0"
|
||||
value="{{ battery.tested_capacity_mah or '' }}" placeholder="e.g. 1850">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="tested_date">Test Date</label>
|
||||
<input type="date" id="tested_date" name="tested_date"
|
||||
value="{{ battery.tested_date or '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="purchase_date">Purchase Date</label>
|
||||
<input type="date" id="purchase_date" name="purchase_date"
|
||||
value="{{ battery.purchase_date or '' }}">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Save Notes</button>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="notes">Notes</label>
|
||||
<textarea id="notes" name="notes" placeholder="No notes yet…">{{ battery.notes or '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" type="submit">Save Details</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -67,4 +176,18 @@
|
||||
</div>
|
||||
|
||||
<a class="text-muted" href="{{ url_for('dashboard') }}">← Back to Dashboard</a>
|
||||
|
||||
<script>
|
||||
function metaSelectChanged(sel, inputId) {
|
||||
var input = document.getElementById(inputId);
|
||||
if (sel.value === '__new__') {
|
||||
input.style.display = '';
|
||||
input.value = '';
|
||||
input.focus();
|
||||
} else {
|
||||
input.style.display = 'none';
|
||||
input.value = sel.value;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
<th style="width:1.5rem;"><input type="checkbox" id="select-all" title="Select all"></th>
|
||||
<th>Label</th>
|
||||
<th>Brand</th>
|
||||
<th>Size</th>
|
||||
<th>Status</th>
|
||||
<th>Assigned To</th>
|
||||
<th>Actions</th>
|
||||
@@ -62,6 +63,7 @@
|
||||
<td><input type="checkbox" name="battery_ids" value="{{ b.id }}" class="row-cb"></td>
|
||||
<td><a href="{{ url_for('battery_detail', battery_id=b.id) }}"><strong>{{ b.label }}</strong></a></td>
|
||||
<td>{{ b.brand }}</td>
|
||||
<td>{{ b.size or '—' }}</td>
|
||||
<td>
|
||||
<span class="badge badge-{{ b.status }}">{{ b.status|capitalize }}</span>
|
||||
</td>
|
||||
@@ -94,7 +96,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" class="text-muted" style="text-align:center;padding:1rem;">No batteries found. <a href="{{ url_for('battery_add') }}">Add some.</a></td></tr>
|
||||
<tr><td colspan="7" class="text-muted" style="text-align:center;padding:1rem;">No batteries found. <a href="{{ url_for('battery_add') }}">Add some.</a></td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -90,7 +90,7 @@ def test_battery_detail_not_found(client):
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_edit_notes(seeded_client):
|
||||
seeded_client.post("/battery/1/edit-notes", data={"notes": "test note here"})
|
||||
seeded_client.post("/battery/1/edit-details", data={"notes": "test note here"})
|
||||
resp = seeded_client.get("/battery/1")
|
||||
assert b"test note here" in resp.data
|
||||
|
||||
|
||||
Reference in New Issue
Block a user