Lots and lots of logic.

This commit is contained in:
Yaro Kasear 2025-10-27 15:57:07 -05:00
parent 4c8a8d4ac7
commit 2845d340da
2 changed files with 215 additions and 46 deletions

View file

@ -60,11 +60,8 @@ def generate_crud_blueprint(model, service, *, base_prefix: str | None = None, r
DELETE /api/<model>/delete?id=123[&hard=1] DELETE /api/<model>/delete?id=123[&hard=1]
""" """
model_name = model.__name__.lower() model_name = model.__name__.lower()
# bikeshed if you want pluralization; this is the least-annoying default
collection = (base_prefix or model_name).lower()
plural = collection if collection.endswith('s') else f"{collection}s"
bp = Blueprint(plural, __name__, url_prefix=f"/api/{plural}") bp = Blueprint(model_name, __name__, url_prefix=f"/api/{model_name}")
@bp.errorhandler(Exception) @bp.errorhandler(Exception)
def _handle_any(e: Exception): def _handle_any(e: Exception):
@ -105,7 +102,7 @@ def generate_crud_blueprint(model, service, *, base_prefix: str | None = None, r
obj = service.create(payload) obj = service.create(payload)
resp = jsonify(obj.as_dict()) resp = jsonify(obj.as_dict())
resp.status_code = 201 resp.status_code = 201
resp.headers["Location"] = url_for(f"{plural}.rest_get", obj_id=obj.id, _external=False) resp.headers["Location"] = url_for(f"{bp.name}.rest_get", obj_id=obj.id, _external=False)
return resp return resp
except Exception as e: except Exception as e:
return _json_error(e) return _json_error(e)

View file

@ -98,12 +98,12 @@
oninput="enableDTAddButton()"> oninput="enableDTAddButton()">
<button type="button" <button type="button"
class="btn btn-primary border-bottom-0 rounded-bottom-0 rounded-start-0 rounded-end-0 disabled" class="btn btn-primary border-bottom-0 rounded-bottom-0 rounded-start-0 rounded-end-0 disabled"
id="add-devicetype" onclick="console.log('No implementation yet.')" disabled>Add</button> id="add-devicetype" onclick="addDTItem()" disabled>Add</button>
<button type="button" <button type="button"
class="btn btn-info border-bottom-0 rounded-bottom-0 rounded-start-0 rounded-end-0 d-none" class="btn btn-info border-bottom-0 rounded-bottom-0 rounded-start-0 rounded-end-0 d-none"
id="edit-devicetype" onclick="console.log('No implementation yet.')">Edit</button> id="edit-devicetype" onclick="editDTItem()">Edit</button>
<button type="button" class="btn btn-danger border-bottom-0 rounded-bottom-0 rounded-start-0 disabled" <button type="button" class="btn btn-danger border-bottom-0 rounded-bottom-0 rounded-start-0 disabled"
id="remove-devicetype" onclick="console.log('No implementation yet.')" disabled>Remove</button> id="remove-devicetype" onclick="deleteDTItem()" disabled>Remove</button>
</div> </div>
<div class="border h-100 ps-3 pe-0 overflow-auto" id="device-type-list"> <div class="border h-100 ps-3 pe-0 overflow-auto" id="device-type-list">
{% for t in device_types %} {% for t in device_types %}
@ -111,7 +111,7 @@
class="d-flex justify-content-between align-items-center user-select-none dt-option"> class="d-flex justify-content-between align-items-center user-select-none dt-option">
<span class="align-middle dt-label">{{ t['description'] }}</span> <span class="align-middle dt-label">{{ t['description'] }}</span>
<input type="number" <input type="number"
class="form-control form-control-sm border border-top-0 {% if loop.index == device_types|length %}border-bottom-0{% endif %} rounded-0 dt-target" class="form-control form-control-sm dt-target"
id="devicetype-target-{{ t['id'] }}" name="devicetype-target-{{ t['id'] }}" id="devicetype-target-{{ t['id'] }}" name="devicetype-target-{{ t['id'] }}"
value="{{ t['target'] if t['target'] else 0 }}" min="0" max="999"> value="{{ t['target'] if t['target'] else 0 }}" min="0" max="999">
</div> </div>
@ -166,54 +166,226 @@
{% endblock %} {% endblock %}
{% block script %} {% block script %}
const brands = document.getElementById('brand'); const brands = document.getElementById('brand');
const dtlist = document.getElementById('device-type-list'); const dtlist = document.getElementById('device-type-list');
const dtinput = document.getElementById('input-devicetype'); const dtinput = document.getElementById('input-devicetype');
const height = getComputedStyle(brands).height; const height = getComputedStyle(brands).height;
dtlist.style.height = height; dtlist.style.height = height;
dtlist.style.maxHeight = height; dtlist.style.maxHeight = height;
dtlist.style.minHeight = height;
document.addEventListener('click', (ev) => { document.addEventListener('click', (ev) => {
const addButton = document.getElementById('add-devicetype'); const addButton = document.getElementById('add-devicetype');
const editButton = document.getElementById('edit-devicetype'); const editButton = document.getElementById('edit-devicetype');
const deleteButton = document.getElementById('remove-devicetype'); const deleteButton = document.getElementById('remove-devicetype');
if (!ev.target.closest('#device-type-list')) return; if (!ev.target.closest('#device-type-list')) return;
// Do not toggle selection when interacting with the input itself // Do not toggle selection when interacting with the input itself
if (ev.target.closest('.dt-target')) return; if (ev.target.closest('.dt-target')) return;
const node = ev.target.closest('.dt-option'); const node = ev.target.closest('.dt-option');
if (!node) return; if (!node) return;
// clear others // clear others
document.querySelectorAll('.dt-option') document.querySelectorAll('.dt-option')
.forEach(n => { n.classList.remove('selected', 'active'); n.removeAttribute('aria-selected'); }); .forEach(n => { n.classList.remove('selected', 'active'); n.removeAttribute('aria-selected'); });
// select this one // select this one
node.classList.add('selected', 'active'); node.classList.add('selected', 'active');
node.setAttribute('aria-selected', 'true'); node.setAttribute('aria-selected', 'true');
// set the visible input to the label, not the whole row // set the visible input to the label, not the whole row
const label = node.querySelector('.dt-label'); const label = node.querySelector('.dt-label');
dtinput.value = (label ? label.textContent : node.textContent).replace(/\s+/g, ' ').trim(); dtinput.value = (label ? label.textContent : node.textContent).replace(/\s+/g, ' ').trim();
addButton.classList.add('d-none'); addButton.classList.add('d-none');
editButton.classList.remove('d-none'); editButton.classList.remove('d-none');
deleteButton.classList.remove('disabled'); deleteButton.classList.remove('disabled');
deleteButton.disabled = false; deleteButton.disabled = false;
});
window.enableDTAddButton = function enableDTAddButton() {
const addButton = document.getElementById('add-devicetype');
if (addButton.classList.contains('d-none')) return;
addButton.disabled = dtinput.value === '';
if (addButton.disabled) {
addButton.classList.add('disabled');
} else {
addButton.classList.remove('disabled');
}
};
window.addDTItem = async function addDTItem() {
const input = document.getElementById('input-devicetype');
const list = document.getElementById('device-type-list');
const addButton = document.getElementById('add-devicetype');
const editButton = document.getElementById('edit-devicetype');
const value = (input.value || '').trim();
if (!value) {
toastMessage('Type a device type first.', 'warning');
return;
}
addButton.disabled = true;
addButton.classList.add('disabled');
let res, data;
try {
const res = await fetch('{{ url_for("crudkit.devicetype.rest_create") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify({ description: value, target: 0 })
});
const ct = res.headers.get('Content-Type') || '';
if (ct.includes('application/json')) {
data = await res.json();
}
if (res.status !== 201) {
const msg = data?.error || `Create failed (${res.status})`;
toastMessage(msg, 'danger');
return;
}
} catch (err) {
toastMessage('Network error creating device type.', 'danger');
return;
} finally {
addButton.disabled = false;
addButton.classList.remove('disabled');
}
const id = data?.id ?? data?.obj?.id;
const description = String(data?.description ?? value);
const row = document.createElement('div');
row.id = `devicetype-option-${id}`;
row.dataset.invId = id;
row.className = 'd-flex justify-content-between align-items-center user-select-none dt-option';
const label = document.createElement('span');
label.className = 'align-middle dt-label';
label.textContent = description;
const qty = document.createElement('input');
qty.type = 'number';
qty.min = '0';
qty.max = '999';
qty.value = '0';
qty.id = `devicetype-target-${id}`;
qty.name = `devicetype-target-${id}`;
qty.className = 'form-control form-control-sm dt-target';
row.append(label, qty);
list.appendChild(row);
list.querySelectorAll('.dt-option').forEach(n => {
n.classList.remove('selected', 'active');
n.removeAttribute('aria-selected');
}); });
window.enableDTAddButton = function enableDTAddButton() { input.value = '';
const addButton = document.getElementById('add-devicetype');
if (addButton.classList.contains('d-none')) return;
addButton.disabled = dtinput.value === ''; row.scrollIntoView({ block: 'nearest' });
if (addButton.disabled) {
addButton.classList.add('disabled'); toastMessage(`Created new device type: ${description}`, 'success');
} else { };
addButton.classList.remove('disabled');
} window.editDTItem = async function editDTItem() {
const input = document.getElementById('input-devicetype');
const addButton = document.getElementById('add-devicetype');
const editButton = document.getElementById('edit-devicetype');
const option = document.querySelector('.dt-option.selected');
const value = (input.value || option.dataset.invId).trim();
if (!value) {
toastMessage('Type a device type first.', 'warning');
return;
} }
let res, data;
try {
const res = await fetch(`{{ url_for('crudkit.devicetype.rest_list') }}${option.dataset.invId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify({ description: value })
});
const ct = res.headers.get('Content-Type') || '';
if (ct.includes('application/json')) {
data = await res.json();
}
if (res.status !== 200) {
const msg = data?.error || `Create failed (${res.status})`;
toastMessage(msg, 'danger');
return;
}
} catch (err) {
toastMessage('Network error creating device type.', 'danger');
return;
} finally {
editButton.disabled = true;
editButton.classList.add('disabled', 'd-none');
addButton.classList.remove('d-none');
input.value = '';
}
option.querySelector('.dt-label').textContent = value;
toastMessage(`Updated device type: ${value}`, 'success');
}
window.deleteDTItem = async function deleteDTItem() {
const input = document.getElementById('input-devicetype');
const addButton = document.getElementById('add-devicetype');
const editButton = document.getElementById('edit-devicetype');
const option = document.querySelector('.dt-option.selected');
const deleteButton = document.getElementById('remove-devicetype');
const value = (input.value || '').trim();
let res, data;
try {
const res = await fetch(`{{ url_for('crudkit.devicetype.rest_list') }}${option.dataset.invId}`, {
method: 'DELETE',
headers: { 'Accept': 'application/json' }
});
if (res.ok) {
option.remove();
toastMessage(`Deleted ${value} successfully.`, 'success');
editButton.disabled = true;
editButton.classList.add('disabled', 'd-none');
deleteButton.disabled = true;
deleteButton.classList.add('disabled');
addButton.classList.remove('d-none');
input.value = '';
return;
}
let msg = 'Delete failed.';
try {
const err = await res.json();
msg = err?.error || msg;
} catch {
const txt = await res.text();
if (txt) msg = txt;
}
toastMessage(msg, 'danger');
} catch (e) {
toastMessage(`Delete failed: ${e?.message || e}`, 'danger');
}
}
{% endblock %} {% endblock %}