Use JSON patch the correct way!

This commit is contained in:
Patrick Jentsch 2019-08-30 13:31:00 +02:00
parent 7702de8770
commit 8f3c53dad2
7 changed files with 158 additions and 5035 deletions

View File

@ -45,8 +45,8 @@ def background_task(user_id, session_id):
with app.app_context(): with app.app_context():
user = db.session.query(User).filter_by(id=user_id).first() user = db.session.query(User).filter_by(id=user_id).first()
''' Get current values from the database. ''' ''' Get current values from the database. '''
corpora = list(map(lambda x: x.to_dict(), user.corpora)) corpora = user.corpora_as_dict()
jobs = list(map(lambda x: x.to_dict(), user.jobs)) jobs = user.jobs_as_dict()
''' Send initial values. ''' ''' Send initial values. '''
socketio.emit('init-corpora', socketio.emit('init-corpora',
json.dumps(corpora), json.dumps(corpora),
@ -59,8 +59,8 @@ def background_task(user_id, session_id):
# print(session_id + ' running') # print(session_id + ' running')
# socketio.emit('message', 'heartbeat', room=session_id) # socketio.emit('message', 'heartbeat', room=session_id)
''' Get current values from the database ''' ''' Get current values from the database '''
new_corpora = list(map(lambda x: x.to_dict(), user.corpora)) new_corpora = user.corpora_as_dict()
new_jobs = list(map(lambda x: x.to_dict(), user.jobs)) new_jobs = user.jobs_as_dict()
''' Compute JSON patches. ''' ''' Compute JSON patches. '''
corpus_patch = jsonpatch.JsonPatch.from_diff(corpora, new_corpora) corpus_patch = jsonpatch.JsonPatch.from_diff(corpora, new_corpora)
jobs_patch = jsonpatch.JsonPatch.from_diff(jobs, new_jobs) jobs_patch = jsonpatch.JsonPatch.from_diff(jobs, new_jobs)

View File

@ -204,6 +204,17 @@ class User(UserMixin, db.Model):
""" """
return self.can(Permission.ADMIN) return self.can(Permission.ADMIN)
def corpora_as_dict(self):
corpora = {}
for corpus in self.corpora:
corpora[str(corpus.id)] = corpus.to_dict()
return corpora
def jobs_as_dict(self):
jobs = {}
for job in self.jobs:
jobs[str(job.id)] = job.to_dict()
return jobs
class AnonymousUser(AnonymousUserMixin): class AnonymousUser(AnonymousUserMixin):
""" """

View File

@ -5,30 +5,59 @@ class CorpusList extends List {
} }
init() { _init() {
var corpus; for (let [id, corpus] of Object.entries(corpora)) {
this.addCorpus(corpus);
for (corpus of corpora) {
this.list.appendChild(this.createCorpusElement(corpus));
} }
this.reIndex();
this.update();
List.updatePagination(this); List.updatePagination(this);
} }
createCorpusElement(corpus) { _update(patch) {
var item, operation, pathArray, valueName;
for (operation of patch) {
pathArray = operation.path.split("/").slice(1);
switch(operation.op) {
case "add":
this.addCorpus(operation.value);
break;
case "remove":
if (pathArray.length != 1) {break;}
this.remove("id", pathArray[0]);
break;
case "replace":
if (pathArray.length != 2) {break;}
item = this.get("id", pathArray[0])[0];
valueName = pathArray[1];
switch(valueName) {
case "description":
item.values({"description": operation.value});
break;
case "title":
item.values({"title": operation.value});
break;
default:
break;
}
default:
break;
}
}
}
addCorpus(corpus) {
var corpusDescriptionElement, corpusElement, corpusIconElement, var corpusDescriptionElement, corpusElement, corpusIconElement,
corpusTitleElement; corpusTitleElement;
corpusDescriptionElement = document.createElement("p"); corpusDescriptionElement = document.createElement("p");
corpusDescriptionElement.dataset.key = "description"; corpusDescriptionElement.classList.add("description");
corpusDescriptionElement.innerText = corpus.description; corpusDescriptionElement.innerText = corpus.description;
corpusElement = document.createElement("a"); corpusElement = document.createElement("a");
corpusElement.classList.add("avatar", "collection-item"); corpusElement.classList.add("avatar", "collection-item");
corpusElement.dataset.key = "id"; corpusElement.dataset.id = corpus.id;
corpusElement.dataset.value = corpus.id;
corpusElement.href = `/corpora/${corpus.id}`; corpusElement.href = `/corpora/${corpus.id}`;
corpusIconElement = document.createElement("i"); corpusIconElement = document.createElement("i");
corpusIconElement.classList.add("circle", "material-icons"); corpusIconElement.classList.add("circle", "material-icons");
@ -42,35 +71,9 @@ class CorpusList extends List {
corpusElement.appendChild(corpusTitleElement); corpusElement.appendChild(corpusTitleElement);
corpusElement.appendChild(corpusDescriptionElement); corpusElement.appendChild(corpusDescriptionElement);
return corpusElement; this.add(
} [{description: corpus.description, id: corpus.id, title: corpus.title}],
function(items) {items[0].elm = corpusElement;}
);
updateWithPatch(delta) {
var corpusElement, key, listItem;
for (key in delta) {
if (key === "_t") {continue;}
if (key.startsWith("_")) {
this.remove("id", delta[key][0].id);
} else if (Array.isArray(delta[key])) {
corpusElement = this.createCorpusElement(delta[key][0]);
listItem = this.add({"description": delta[key][0].description,
"title": delta[key][0].title,
"id": delta[key][0].id})[0];
if (listItem.elm) {
listItem.elm.replaceWith(corpusElement);
}
listItem.elm = corpusElement;
} else {
listItem = this.get("id", corpora[parseInt(key)].id)[0];
if (delta[key]["description"]) {
listItem.values({"description": delta[key]["description"][1]});
}
if (delta[key]["title"]) {
listItem.values({"title": delta[key]["title"][1]});
}
}
}
} }
} }

View File

@ -5,22 +5,69 @@ class JobList extends List {
} }
init() { _init() {
var job; for (let [id, job] of Object.entries(jobs)) {
this.addJob(job);
for (job of jobs) {
this.list.appendChild(this.createJobElement(job));
} }
this.reIndex();
this.update();
List.updatePagination(this); List.updatePagination(this);
} }
createJobElement(job) { _update(patch) {
var item, jobStatusElement, newStatusColor, operation, pathArray, status,
statusColor, valueName;
for (operation of patch) {
pathArray = operation.path.split("/").slice(1);
switch(operation.op) {
case "add":
this.addJob(operation.value);
break;
case "remove":
if (pathArray.length != 1) {break;}
this.remove("id", pathArray[0]);
break;
case "replace":
if (pathArray.length != 2) {break;}
item = this.get("id", pathArray[0])[0];
valueName = pathArray[1];
switch(valueName) {
case "description":
item.values({"description": operation.value});
break;
case "status":
jobStatusElement = item.elm.querySelector(".status");
status = jobStatusElement.innerHTML;
if (JobList.STATUS_COLORS.hasOwnProperty(status)) {
statusColor = JobList.STATUS_COLORS[status];
} else {
statusColor = JobList.STATUS_COLORS['default'];
}
if (JobList.STATUS_COLORS.hasOwnProperty(operation.value)) {
newStatusColor = JobList.STATUS_COLORS[operation.value];
} else {
newStatusColor = JobList.STATUS_COLORS['default'];
}
jobStatusElement.classList.remove(statusColor);
jobStatusElement.classList.add(newStatusColor);
jobStatusElement.innerHTML = operation.value;
case "title":
item.values({"title": operation.value});
break;
default:
break;
}
default:
break;
}
}
}
addJob(job) {
var jobDescriptionElement, jobElement, jobServiceElement, jobStatusElement, var jobDescriptionElement, jobElement, jobServiceElement, jobStatusElement,
jobTitleElement; jobTitleElement, serviceColor, serviceIcon, statusColor;
jobDescriptionElement = document.createElement("p"); jobDescriptionElement = document.createElement("p");
jobDescriptionElement.classList.add("description"); jobDescriptionElement.classList.add("description");
@ -29,11 +76,26 @@ class JobList extends List {
jobElement.classList.add("avatar", "collection-item"); jobElement.classList.add("avatar", "collection-item");
jobElement.dataset.id = job.id; jobElement.dataset.id = job.id;
jobElement.href = `/jobs/${job.id}`; jobElement.href = `/jobs/${job.id}`;
if (JobList.SERVICE_COLORS.hasOwnProperty(job.service)) {
serviceColor = JobList.SERVICE_COLORS[job.service];
} else {
serviceColor = JobList.SERVICE_COLORS['default'];
}
if (JobList.SERVICE_ICONS.hasOwnProperty(job.service)) {
serviceIcon = JobList.SERVICE_ICONS[job.service];
} else {
serviceIcon = JobList.SERVICE_ICONS['default'];
}
jobServiceElement = document.createElement("i"); jobServiceElement = document.createElement("i");
jobServiceElement.classList.add("circle", "material-icons", "service-icon", JobList.SERVICE_COLORS[job.service]); jobServiceElement.classList.add("circle", "material-icons", "service-icon", serviceColor);
jobServiceElement.innerText = JobList.SERVICE_ICONS[job.service]; jobServiceElement.innerText = serviceIcon;
if (JobList.STATUS_COLORS.hasOwnProperty(job.status)) {
statusColor = JobList.STATUS_COLORS[job.status];
} else {
statusColor = JobList.STATUS_COLORS['default'];
}
jobStatusElement = document.createElement("span"); jobStatusElement = document.createElement("span");
jobStatusElement.classList.add("badge", "new", "status", JobList.STATUS_COLORS[job.status]); jobStatusElement.classList.add("badge", "new", "status", statusColor);
jobStatusElement.dataset.badgeCaption = ""; jobStatusElement.dataset.badgeCaption = "";
jobStatusElement.innerText = job.status; jobStatusElement.innerText = job.status;
jobTitleElement = document.createElement("span"); jobTitleElement = document.createElement("span");
@ -45,45 +107,12 @@ class JobList extends List {
jobElement.appendChild(jobTitleElement); jobElement.appendChild(jobTitleElement);
jobElement.appendChild(jobDescriptionElement); jobElement.appendChild(jobDescriptionElement);
return jobElement; this.add(
} [{description: job.description, id: job.id, title: job.title}],
function(items) {items[0].elm = jobElement;}
);
updateWithPatch(delta) {
var jobElement, jobStatusElement, key, listItem;
for (key in delta) {
if (key === "_t") {continue;}
if (key.startsWith("_")) {
this.remove("id", delta[key][0].id);
} else if (Array.isArray(delta[key])) {
jobElement = this.createJobElement(delta[key][0]);
listItem = this.add({"description": delta[key][0].description,
"title": delta[key][0].title,
"id": delta[key][0].id})[0];
if (listItem.elm) {
listItem.elm.replaceWith(jobElement);
}
listItem.elm = jobElement;
} else {
listItem = this.get("id", jobs[parseInt(key)].id)[0];
if (delta[key]["status"]) {
jobStatusElement = listItem.elm.querySelector(".status");
jobStatusElement.classList.remove(JobList.STATUS_COLORS[delta[key]["status"][0]]);
jobStatusElement.classList.add(JobList.STATUS_COLORS[delta[key]["status"][1]]);
jobStatusElement.innerHTML = delta[key]["status"][1];
}
if (delta[key]["description"]) {
listItem.values({"description": delta[key]["description"][1]});
}
if (delta[key]["title"]) {
listItem.values({"title": delta[key]["title"][1]});
}
}
}
} }
} }
JobList.SERVICE_COLORS = {"nlp": "blue", JobList.SERVICE_COLORS = {"nlp": "blue",
"ocr": "green", "ocr": "green",
"default": "red"}; "default": "red"};

File diff suppressed because it is too large Load Diff

View File

@ -11,9 +11,8 @@
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='fonts/material-icons/material-icons.css') }}"> <link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='fonts/material-icons/material-icons.css') }}">
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='css/materialize.min.css') }}" media="screen,projection"/> <link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='css/materialize.min.css') }}" media="screen,projection"/>
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='css/opaque.css') }}" media="screen,projection"/> <link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='css/opaque.css') }}" media="screen,projection"/>
<script src="{{ url_for('static', filename='js/socket.io.js') }}"></script>
<script src="{{ url_for('static', filename='js/jsonpatch.min.js') }}"></script> <script src="{{ url_for('static', filename='js/jsonpatch.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/jsondiffpatch.umd.js') }}"></script> <script src="{{ url_for('static', filename='js/socket.io.js') }}"></script>
<script src="{{ url_for('static', filename='js/list.js') }}"></script> <script src="{{ url_for('static', filename='js/list.js') }}"></script>
<script src="{{ url_for('static', filename='js/list.utils.js') }}"></script> <script src="{{ url_for('static', filename='js/list.utils.js') }}"></script>
<script src="{{ url_for('static', filename='js/CorpusList.js') }}"></script> <script src="{{ url_for('static', filename='js/CorpusList.js') }}"></script>
@ -29,7 +28,7 @@
var subscriber; var subscriber;
corpora = JSON.parse(msg); corpora = JSON.parse(msg);
for (subscriber of corporaSubscribers) {subscriber.init();} for (subscriber of corporaSubscribers) {subscriber._init();}
}); });
@ -37,7 +36,7 @@
var subscriber; var subscriber;
jobs = JSON.parse(msg); jobs = JSON.parse(msg);
for (subscriber of jobsSubscribers) {subscriber.init();} for (subscriber of jobsSubscribers) {subscriber._init();}
}); });
@ -45,12 +44,8 @@
var patch, patchedCorpora, subscriber; var patch, patchedCorpora, subscriber;
patch = JSON.parse(msg); patch = JSON.parse(msg);
patchedCorpora = jsonpatch.apply_patch(corpora, patch); corpora = jsonpatch.apply_patch(corpora, patch);
delta = jsondiffpatch.diff(corpora, patchedCorpora); for (subscriber of corporaSubscribers) {subscriber._update(patch);}
corpora = patchedCorpora;
for (subscriber of corporaSubscribers) {
subscriber.updateWithPatch(delta);
}
}); });
@ -58,12 +53,8 @@
var patch, patchedJobs, subscriber; var patch, patchedJobs, subscriber;
patch = JSON.parse(msg); patch = JSON.parse(msg);
patchedJobs = jsonpatch.apply_patch(jobs, patch); jobs = jsonpatch.apply_patch(jobs, patch);
delta = jsondiffpatch.diff(jobs, patchedJobs); for (subscriber of jobsSubscribers) {subscriber._update(patch);}
jobs = patchedJobs;
for (subscriber of jobsSubscribers) {
subscriber.updateWithPatch(delta);
}
}); });

View File

@ -31,11 +31,12 @@
</div> </div>
</div> </div>
<script> <script>
var corpusListOptions = {item: '<a><span class="title"></span><span class="description"></span></a>', var corpusList = new CorpusList("corpus-list", {
page: 4, item: '<div><span class="title"></span><span class="description"></span></div>',
pagination: true, page: 4,
valueNames: ["description", "title", {data: ["id"]}]} pagination: true,
var corpusList = new CorpusList("corpus-list", corpusListOptions); valueNames: ["description", "title", {data: ["id"]}]
});
corpusList.on("filterComplete", List.updatePagination); corpusList.on("filterComplete", List.updatePagination);
corpusList.on("searchComplete", List.updatePagination); corpusList.on("searchComplete", List.updatePagination);
</script> </script>
@ -74,13 +75,14 @@
</div> </div>
</div> </div>
<script> <script>
jobListOptions = {item: '<a><span class="title"></span><span class="description"></span></a>', var jobList = new JobList("job-list", {
page: 4, item: '<div><span class="title"></span><span class="description"></span></div>',
pagination: true, page: 4,
valueNames: ["description", "title", {data: ["id"]}]} pagination: true,
var jobList = new JobList("job-list", jobListOptions); valueNames: ["description", "title", {data: ["id"]}]
jobList.on("filterComplete", List.updatePagination); });
jobList.on("searchComplete", List.updatePagination); jobList.on("filterComplete", List.updatePagination);
jobList.on("searchComplete", List.updatePagination);
</script> </script>
<div id="new-corpus-modal" class="modal"> <div id="new-corpus-modal" class="modal">