Lots and lots of logic.
This commit is contained in:
parent
4c8a8d4ac7
commit
2845d340da
2 changed files with 215 additions and 46 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,15 +166,16 @@
|
||||||
{% 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');
|
||||||
|
|
@ -203,9 +204,9 @@
|
||||||
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() {
|
window.enableDTAddButton = function enableDTAddButton() {
|
||||||
const addButton = document.getElementById('add-devicetype');
|
const addButton = document.getElementById('add-devicetype');
|
||||||
if (addButton.classList.contains('d-none')) return;
|
if (addButton.classList.contains('d-none')) return;
|
||||||
|
|
||||||
|
|
@ -215,5 +216,176 @@
|
||||||
} else {
|
} else {
|
||||||
addButton.classList.remove('disabled');
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
input.value = '';
|
||||||
|
|
||||||
|
row.scrollIntoView({ block: 'nearest' });
|
||||||
|
|
||||||
|
toastMessage(`Created new device type: ${description}`, 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
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 %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue