Hooray! Updates to updates!
This commit is contained in:
parent
85b0e576c7
commit
bcf14cf251
3 changed files with 106 additions and 33 deletions
|
|
@ -649,15 +649,16 @@ class CRUDService(Generic[T]):
|
|||
|
||||
return rows
|
||||
|
||||
def create(self, data: dict, actor=None) -> T:
|
||||
def create(self, data: dict, actor=None, *, commit: bool = True) -> T:
|
||||
session = self.session
|
||||
obj = self.model(**data)
|
||||
session.add(obj)
|
||||
session.commit()
|
||||
self._log_version("create", obj, actor)
|
||||
if commit:
|
||||
session.commit()
|
||||
self._log_version("create", obj, actor, commit=commit)
|
||||
return obj
|
||||
|
||||
def update(self, id: int, data: dict, actor=None) -> T:
|
||||
def update(self, id: int, data: dict, actor=None, *, commit: bool = True) -> T:
|
||||
session = self.session
|
||||
obj = session.get(self.model, id)
|
||||
if not obj:
|
||||
|
|
@ -695,7 +696,8 @@ class CRUDService(Generic[T]):
|
|||
return obj
|
||||
|
||||
# Commit atomically
|
||||
session.commit()
|
||||
if commit:
|
||||
session.commit()
|
||||
|
||||
# AFTER snapshot for audit
|
||||
after = obj.as_dict()
|
||||
|
|
@ -712,10 +714,10 @@ class CRUDService(Generic[T]):
|
|||
return obj
|
||||
|
||||
# Log both what we *intended* and what *actually* happened
|
||||
self._log_version("update", obj, actor, metadata={"diff": actual, "patch": patch})
|
||||
self._log_version("update", obj, actor, metadata={"diff": actual, "patch": patch}, commit=commit)
|
||||
return obj
|
||||
|
||||
def delete(self, id: int, hard: bool = False, actor = None):
|
||||
def delete(self, id: int, hard: bool = False, actor = None, *, commit: bool = True):
|
||||
session = self.session
|
||||
obj = session.get(self.model, id)
|
||||
if not obj:
|
||||
|
|
@ -725,11 +727,12 @@ class CRUDService(Generic[T]):
|
|||
else:
|
||||
soft = cast(_SoftDeletable, obj)
|
||||
soft.is_deleted = True
|
||||
session.commit()
|
||||
self._log_version("delete", obj, actor)
|
||||
if commit:
|
||||
session.commit()
|
||||
self._log_version("delete", obj, actor, commit=commit)
|
||||
return obj
|
||||
|
||||
def _log_version(self, change_type: str, obj: T, actor=None, metadata: dict | None = None):
|
||||
def _log_version(self, change_type: str, obj: T, actor=None, metadata: dict | None = None, *, commit: bool = True):
|
||||
session = self.session
|
||||
try:
|
||||
snapshot = {}
|
||||
|
|
@ -747,7 +750,8 @@ class CRUDService(Generic[T]):
|
|||
meta=to_jsonable(metadata) if metadata else None,
|
||||
)
|
||||
session.add(version)
|
||||
session.commit()
|
||||
if commit:
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
log.warning(f"Version logging failed for {self.model.__name__} id={getattr(obj, "id", "?")}: {str(e)}")
|
||||
log.warning(f"Version logging failed for {self.model.__name__} id={getattr(obj, 'id', '?')}: {str(e)}")
|
||||
session.rollback()
|
||||
|
|
|
|||
|
|
@ -9,6 +9,28 @@ from crudkit.core import normalize_payload
|
|||
|
||||
bp_entry = Blueprint("entry", __name__)
|
||||
|
||||
def _apply_worklog_updates(worklog, updates, delete_ids):
|
||||
note_cls = type(worklog).updates.property.mapper.class_
|
||||
note_svc = crudkit.crud.get_service(note_cls)
|
||||
sess = note_svc.session
|
||||
|
||||
existing = {u.id: u for u in worklog.updates if not getattr(u, "is_deleted", False)}
|
||||
|
||||
for item in updates:
|
||||
uid = item.get("id")
|
||||
content = (item.get("content") or "").strip()
|
||||
if not content and not uid:
|
||||
continue
|
||||
if uid:
|
||||
# per-note version log preserved, but commit deferred.
|
||||
note_svc.update(uid, {"content": content}, actor="bulk_child_update", commit=False)
|
||||
else:
|
||||
note_svc.create({"work_log_id": worklog.id, "content": content}, actor="bulk_child_create", commit=False)
|
||||
|
||||
for uid in delete_ids:
|
||||
if uid in existing:
|
||||
note_svc.delete(uid, actor="bulk_child_delete", commit=False)
|
||||
|
||||
def init_entry_routes(app):
|
||||
|
||||
@bp_entry.get("/entry/<model>/<int:id>")
|
||||
|
|
@ -168,6 +190,9 @@ def init_entry_routes(app):
|
|||
cls = crudkit.crud.get_model(model)
|
||||
payload = normalize_payload(request.get_json(), cls)
|
||||
|
||||
updates = payload.pop("updates", None) or []
|
||||
delete_ids = payload.pop("delete_update_ids", None) or []
|
||||
|
||||
params = {}
|
||||
if model == "inventory":
|
||||
pass
|
||||
|
|
@ -197,13 +222,19 @@ def init_entry_routes(app):
|
|||
else:
|
||||
raise TypeError("Invalid model.")
|
||||
|
||||
print(payload)
|
||||
|
||||
service = crudkit.crud.get_service(cls)
|
||||
service.update(id, data=payload, actor="update_entry")
|
||||
sess = service.session
|
||||
|
||||
obj = service.update(id, data=payload, actor="update_entry", commit=False)
|
||||
|
||||
if model == "worklog" and (updates or delete_ids):
|
||||
_apply_worklog_updates(obj, updates, delete_ids)
|
||||
|
||||
sess.commit()
|
||||
|
||||
return {"status": "success", "payload": payload}
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return {"status": "failure", "error": str(e)}
|
||||
|
||||
app.register_blueprint(bp_entry)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
const fd = new FormData(form);
|
||||
const out = {};
|
||||
|
||||
// base values
|
||||
fd.forEach((value, key) => {
|
||||
if (key in out) {
|
||||
if (!Array.isArray(out[key])) out[key] = [out[key]];
|
||||
|
|
@ -16,21 +17,21 @@
|
|||
}
|
||||
});
|
||||
|
||||
// normalize radios and checkboxes
|
||||
form.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach(el => {
|
||||
if (!el.name) return;
|
||||
|
||||
if (el.type === 'radio') {
|
||||
if (out[el.name] !== undefined) return;
|
||||
if (out[el.name] !== undefined) return; // already set for this group
|
||||
const checked = form.querySelector(`input[type="radio"][name="${CSS.escape(el.name)}"]:checked`);
|
||||
if (checked) out[el.name] = checked.value ?? true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (el.type === 'checkbox') {
|
||||
const group = form.querySelectorAll(`input[type="checkbox"][name="${CSS.escape(el.name)}"]`);
|
||||
if (group.length > 1) {
|
||||
const checkedVals = Array.from(group)
|
||||
.filter(i => i.checked)
|
||||
.map(i => i.value ?? true);
|
||||
const checkedVals = Array.from(group).filter(i => i.checked).map(i => i.value ?? true);
|
||||
out[el.name] = checkedVals;
|
||||
} else {
|
||||
out[el.name] = el.checked;
|
||||
|
|
@ -41,24 +42,61 @@
|
|||
return out;
|
||||
}
|
||||
|
||||
function collectExistingUpdateIds() {
|
||||
return Array.from(document.querySelectorAll('script[type="application/json"][id^="md-"]'))
|
||||
.map(el => Number(el.id.slice(3)))
|
||||
.filter(Number.isFinite);
|
||||
}
|
||||
|
||||
function collectEditedUpdates() {
|
||||
const updates = [];
|
||||
for (const id of collectExistingUpdateIds()) {
|
||||
updates.push({ id, content: getMarkdown(id) }); // ensure getMarkdown exists
|
||||
}
|
||||
for (const md of (window.newDrafts || [])) {
|
||||
if ((md ?? '').trim()) updates.push({ content: md });
|
||||
}
|
||||
return updates;
|
||||
}
|
||||
|
||||
function collectDeletedIds() {
|
||||
return (window.deletedIds || []).filter(Number.isFinite);
|
||||
}
|
||||
|
||||
document.getElementById("{{ field['attrs']['data-model'] }}_form").addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
|
||||
const json = formToJson(e.target);
|
||||
json['id'] = {{ field['template_ctx']['values']['id'] }};
|
||||
json.id = {{ field['template_ctx']['values']['id'] }};
|
||||
|
||||
response = await fetch("{{ url_for('entry.update_entry', id=field['template_ctx']['values']['id'], model=field['attrs']['data-model']) }}", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(json)
|
||||
});
|
||||
// map friendly names to real FK columns if needed
|
||||
if (json.contact && !json.contact_id) json.contact_id = Number(json.contact) || null;
|
||||
if (json.work_item && !json.work_item_id) json.work_item_id = Number(json.work_item) || null;
|
||||
|
||||
reply = await response.json();
|
||||
if (reply['status'] === 'success') {
|
||||
toastMessage("This entry has been successfully saved!", "success");
|
||||
} else {
|
||||
toastMessage(`Unable to save entry: ${reply['error']}`, "danger");
|
||||
// child mutations
|
||||
json.updates = collectEditedUpdates();
|
||||
json.delete_update_ids = collectDeletedIds();
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
"{{ url_for('entry.update_entry', id=field['template_ctx']['values']['id'], model=field['attrs']['data-model']) }}",
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(json),
|
||||
}
|
||||
);
|
||||
|
||||
const reply = await response.json();
|
||||
if (reply.status === 'success') {
|
||||
toastMessage('This entry has been successfully saved!', 'success');
|
||||
window.newDrafts = [];
|
||||
window.deletedIds = [];
|
||||
} else {
|
||||
toastMessage(`Unable to save entry: ${reply.error}`, 'danger');
|
||||
}
|
||||
} catch (err) {
|
||||
toastMessage(`Network error: ${String(err)}`, 'danger');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue