@saltcorn/server 0.8.7-beta.6 → 0.8.8-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/auth/admin.js +20 -16
- package/auth/routes.js +12 -8
- package/load_plugins.js +14 -1
- package/locales/en.json +29 -1
- package/locales/si.json +1197 -0
- package/package.json +8 -8
- package/public/diagram_utils.js +21 -1
- package/public/relationship_diagram_utils.js +32 -10
- package/public/saltcorn-common.js +40 -19
- package/public/saltcorn.js +11 -5
- package/routes/admin.js +2 -0
- package/routes/common_lists.js +34 -19
- package/routes/diagram.js +214 -199
- package/routes/fields.js +113 -6
- package/routes/index.js +2 -0
- package/routes/models.js +492 -0
- package/routes/page.js +10 -6
- package/routes/tables.js +105 -47
- package/routes/view.js +10 -6
- package/routes/viewedit.js +27 -5
- package/tests/plugins.test.js +103 -2
- package/tests/view.test.js +217 -0
- package/public/vis-network.min.js +0 -49
package/routes/models.js
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @category server
|
|
3
|
+
* @module routes/models
|
|
4
|
+
* @subcategory routes
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const Router = require("express-promise-router");
|
|
8
|
+
|
|
9
|
+
const { error_catcher, is_relative_url, isAdmin } = require("./utils.js");
|
|
10
|
+
const Table = require("@saltcorn/data/models/table");
|
|
11
|
+
const Form = require("@saltcorn/data/models/form");
|
|
12
|
+
const Model = require("@saltcorn/data/models/model");
|
|
13
|
+
const ModelInstance = require("@saltcorn/data/models/model_instance");
|
|
14
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
15
|
+
const db = require("@saltcorn/data/db");
|
|
16
|
+
const moment = require("moment");
|
|
17
|
+
|
|
18
|
+
const { mkTable, renderForm, post_delete_btn } = require("@saltcorn/markup");
|
|
19
|
+
const {
|
|
20
|
+
span,
|
|
21
|
+
h4,
|
|
22
|
+
p,
|
|
23
|
+
a,
|
|
24
|
+
div,
|
|
25
|
+
i,
|
|
26
|
+
form,
|
|
27
|
+
label,
|
|
28
|
+
input,
|
|
29
|
+
text,
|
|
30
|
+
script,
|
|
31
|
+
domReady,
|
|
32
|
+
code,
|
|
33
|
+
iframe,
|
|
34
|
+
style,
|
|
35
|
+
pre,
|
|
36
|
+
} = require("@saltcorn/markup/tags");
|
|
37
|
+
|
|
38
|
+
const router = new Router();
|
|
39
|
+
module.exports = router;
|
|
40
|
+
|
|
41
|
+
const newModelForm = (table, req) => {
|
|
42
|
+
return new Form({
|
|
43
|
+
action: "/models/new/" + table.id,
|
|
44
|
+
fields: [
|
|
45
|
+
{ name: "name", label: "Name", type: "String", required: true },
|
|
46
|
+
{
|
|
47
|
+
name: "modelpattern",
|
|
48
|
+
label: "Model pattern",
|
|
49
|
+
type: "String",
|
|
50
|
+
required: true,
|
|
51
|
+
attributes: { options: Object.keys(getState().modelpatterns) },
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
router.get(
|
|
58
|
+
"/new/:table_id",
|
|
59
|
+
isAdmin,
|
|
60
|
+
error_catcher(async (req, res) => {
|
|
61
|
+
const { table_id } = req.params;
|
|
62
|
+
const table = await Table.findOne({ id: table_id });
|
|
63
|
+
res.sendWrap(req.__(`New model`), {
|
|
64
|
+
above: [
|
|
65
|
+
{
|
|
66
|
+
type: "breadcrumbs",
|
|
67
|
+
crumbs: [
|
|
68
|
+
{ text: req.__("Tables"), href: "/table" },
|
|
69
|
+
{ href: `/table/${table.id}`, text: table.name },
|
|
70
|
+
{ text: req.__(`New model`) },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: "card",
|
|
75
|
+
class: "mt-0",
|
|
76
|
+
title: req.__(`New model`),
|
|
77
|
+
contents: renderForm(newModelForm(table, req), req.csrfToken()),
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
router.post(
|
|
85
|
+
"/new/:table_id",
|
|
86
|
+
isAdmin,
|
|
87
|
+
error_catcher(async (req, res) => {
|
|
88
|
+
const { table_id } = req.params;
|
|
89
|
+
const table = await Table.findOne({ id: table_id });
|
|
90
|
+
const form = newModelForm(table, req);
|
|
91
|
+
form.validate(req.body);
|
|
92
|
+
if (form.hasErrors) {
|
|
93
|
+
res.sendWrap(req.__(`New model`), renderForm(form, req.csrfToken()));
|
|
94
|
+
} else {
|
|
95
|
+
const model = await Model.create({ ...form.values, table_id: table.id });
|
|
96
|
+
if (model.templateObj.configuration_workflow)
|
|
97
|
+
res.redirect(`/models/config/${model.id}`);
|
|
98
|
+
else res.redirect(`/models/show/${model.id}`);
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const respondWorkflow = (model, table, wf, wfres, req, res) => {
|
|
104
|
+
const wrap = (contents, noCard, previewURL) => ({
|
|
105
|
+
above: [
|
|
106
|
+
{
|
|
107
|
+
type: "breadcrumbs",
|
|
108
|
+
crumbs: [
|
|
109
|
+
{ text: req.__("Tables"), href: "/table" },
|
|
110
|
+
{ href: `/table/${table.id || table.name}`, text: table.name },
|
|
111
|
+
{ href: `/models/show/${model.id}`, text: model.name },
|
|
112
|
+
{ text: req.__("Configuration") },
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
type: noCard ? "container" : "card",
|
|
117
|
+
class: !noCard && "mt-0",
|
|
118
|
+
title: wfres.title,
|
|
119
|
+
titleAjaxIndicator: true,
|
|
120
|
+
contents,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
if (wfres.flash) req.flash(wfres.flash[0], wfres.flash[1]);
|
|
125
|
+
if (wfres.renderForm)
|
|
126
|
+
res.sendWrap(
|
|
127
|
+
{
|
|
128
|
+
title: req.__(`%s configuration`, model.name),
|
|
129
|
+
headers: [
|
|
130
|
+
{
|
|
131
|
+
script: `/static_assets/${db.connectObj.version_tag}/jquery-menu-editor.min.js`,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
script: `/static_assets/${db.connectObj.version_tag}/iconset-fontawesome5-3-1.min.js`,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
script: `/static_assets/${db.connectObj.version_tag}/bootstrap-iconpicker.js`,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
css: `/static_assets/${db.connectObj.version_tag}/bootstrap-iconpicker.min.css`,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
wrap(
|
|
145
|
+
renderForm(wfres.renderForm, req.csrfToken()),
|
|
146
|
+
false,
|
|
147
|
+
wfres.previewURL
|
|
148
|
+
)
|
|
149
|
+
);
|
|
150
|
+
else res.redirect(wfres.redirect);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const get_model_workflow = (model, req) => {
|
|
154
|
+
const workflow = model.templateObj.configuration_workflow(req);
|
|
155
|
+
workflow.action = `/models/config/${model.id}`;
|
|
156
|
+
const oldOnDone = workflow.onDone || ((c) => c);
|
|
157
|
+
workflow.onDone = async (ctx) => {
|
|
158
|
+
const { id, ...configuration } = await oldOnDone(ctx);
|
|
159
|
+
await model.update({ configuration });
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
redirect: `/models/show/${model.id}`,
|
|
163
|
+
flash: ["success", `Model ${this.name || ""} saved`],
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
return workflow;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
router.get(
|
|
170
|
+
"/config/:id",
|
|
171
|
+
isAdmin,
|
|
172
|
+
error_catcher(async (req, res) => {
|
|
173
|
+
const { id } = req.params;
|
|
174
|
+
const { step } = req.query;
|
|
175
|
+
|
|
176
|
+
const model = await Model.findOne({ id });
|
|
177
|
+
const table = await Table.findOne({ id: model.table_id });
|
|
178
|
+
const configFlow = get_model_workflow(model, req);
|
|
179
|
+
const wfres = await configFlow.run(
|
|
180
|
+
{
|
|
181
|
+
...model.configuration,
|
|
182
|
+
id: model.id,
|
|
183
|
+
table_id: model.table_id,
|
|
184
|
+
...(step ? { stepName: step } : {}),
|
|
185
|
+
},
|
|
186
|
+
req
|
|
187
|
+
);
|
|
188
|
+
respondWorkflow(model, table, configFlow, wfres, req, res);
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
router.post(
|
|
193
|
+
"/config/:id",
|
|
194
|
+
isAdmin,
|
|
195
|
+
error_catcher(async (req, res) => {
|
|
196
|
+
const { id } = req.params;
|
|
197
|
+
const { step } = req.query;
|
|
198
|
+
|
|
199
|
+
const model = await Model.findOne({ id });
|
|
200
|
+
const table = await Table.findOne({ id: model.table_id });
|
|
201
|
+
if (!table) {
|
|
202
|
+
req.flash("error", `Table not found`);
|
|
203
|
+
res.redirect(`/table`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const workflow = get_model_workflow(model, req);
|
|
207
|
+
const wfres = await workflow.run(req.body, req);
|
|
208
|
+
respondWorkflow(model, table, workflow, wfres, req, res);
|
|
209
|
+
})
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
router.get(
|
|
213
|
+
"/show/:id",
|
|
214
|
+
isAdmin,
|
|
215
|
+
error_catcher(async (req, res) => {
|
|
216
|
+
const { id } = req.params;
|
|
217
|
+
const model = await Model.findOne({ id });
|
|
218
|
+
const table = await Table.findOne({ id: model.table_id });
|
|
219
|
+
const instances = await ModelInstance.find({ model_id: model.id });
|
|
220
|
+
const metrics = model.templateObj.metrics || {};
|
|
221
|
+
const metricCols = Object.entries(metrics).map(([k, v]) => ({
|
|
222
|
+
label: k,
|
|
223
|
+
key: (inst) => inst.metric_values?.[k]?.toPrecision(6),
|
|
224
|
+
}));
|
|
225
|
+
const anyReport = instances.some((i) => !!i.report);
|
|
226
|
+
res.sendWrap(req.__(`Show model`), {
|
|
227
|
+
above: [
|
|
228
|
+
{
|
|
229
|
+
type: "breadcrumbs",
|
|
230
|
+
crumbs: [
|
|
231
|
+
{ text: req.__("Tables"), href: "/table" },
|
|
232
|
+
{ href: `/table/${table.id}`, text: table.name },
|
|
233
|
+
{ text: model.name },
|
|
234
|
+
],
|
|
235
|
+
after: a(
|
|
236
|
+
{ href: `/models/config/${model.id}` },
|
|
237
|
+
req.__("Edit"),
|
|
238
|
+
i({ class: "ms-1 fas fa-edit" })
|
|
239
|
+
),
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
type: "card",
|
|
243
|
+
class: "mt-0",
|
|
244
|
+
title: req.__("Model instances"),
|
|
245
|
+
contents: div(
|
|
246
|
+
mkTable(
|
|
247
|
+
[
|
|
248
|
+
{ label: req.__("Name"), key: "name" },
|
|
249
|
+
{
|
|
250
|
+
label: req.__("Trained"),
|
|
251
|
+
key: (inst) => moment(inst.trained_on).fromNow(),
|
|
252
|
+
},
|
|
253
|
+
...(anyReport
|
|
254
|
+
? [
|
|
255
|
+
{
|
|
256
|
+
label: req.__("Report"),
|
|
257
|
+
key: (inst) =>
|
|
258
|
+
inst.report
|
|
259
|
+
? a(
|
|
260
|
+
{ href: `/models/show-report/${inst.id}` },
|
|
261
|
+
i({ class: "fas fa-file-alt" })
|
|
262
|
+
)
|
|
263
|
+
: "",
|
|
264
|
+
},
|
|
265
|
+
]
|
|
266
|
+
: []),
|
|
267
|
+
...metricCols,
|
|
268
|
+
{
|
|
269
|
+
label: req.__("Default"),
|
|
270
|
+
key: (inst) =>
|
|
271
|
+
form(
|
|
272
|
+
{
|
|
273
|
+
action: `/models/make-default-instance/${inst.id}`,
|
|
274
|
+
method: "POST",
|
|
275
|
+
},
|
|
276
|
+
span(
|
|
277
|
+
{ class: "form-switch" },
|
|
278
|
+
input({
|
|
279
|
+
class: ["form-check-input"],
|
|
280
|
+
type: "checkbox",
|
|
281
|
+
onChange: "this.form.submit()",
|
|
282
|
+
role: "switch",
|
|
283
|
+
name: "enabled",
|
|
284
|
+
...(inst.is_default && { checked: true }),
|
|
285
|
+
})
|
|
286
|
+
),
|
|
287
|
+
input({
|
|
288
|
+
type: "hidden",
|
|
289
|
+
name: "_csrf",
|
|
290
|
+
value: req.csrfToken(),
|
|
291
|
+
})
|
|
292
|
+
),
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
label: req.__("Delete"),
|
|
296
|
+
key: (r) =>
|
|
297
|
+
post_delete_btn(
|
|
298
|
+
`/models/delete-instance/${encodeURIComponent(r.id)}`,
|
|
299
|
+
req
|
|
300
|
+
),
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
instances
|
|
304
|
+
),
|
|
305
|
+
a(
|
|
306
|
+
{ href: `/models/train/${model.id}`, class: "btn btn-primary" },
|
|
307
|
+
i({ class: "fas fa-graduation-cap me-1" }),
|
|
308
|
+
req.__("Train new instance")
|
|
309
|
+
)
|
|
310
|
+
),
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
});
|
|
314
|
+
})
|
|
315
|
+
);
|
|
316
|
+
const model_train_form = (model, table, req) => {
|
|
317
|
+
const hyperparameter_fields =
|
|
318
|
+
model.templateObj.hyperparameter_fields?.({
|
|
319
|
+
table,
|
|
320
|
+
...model,
|
|
321
|
+
}) || [];
|
|
322
|
+
return new Form({
|
|
323
|
+
action: `/models/train/${model.id}`,
|
|
324
|
+
onSubmit: "press_store_button(this)",
|
|
325
|
+
fields: [
|
|
326
|
+
{
|
|
327
|
+
name: "name",
|
|
328
|
+
label: req.__("Name"),
|
|
329
|
+
type: "String",
|
|
330
|
+
required: true,
|
|
331
|
+
},
|
|
332
|
+
...hyperparameter_fields,
|
|
333
|
+
],
|
|
334
|
+
});
|
|
335
|
+
};
|
|
336
|
+
router.post(
|
|
337
|
+
"/delete/:id",
|
|
338
|
+
isAdmin,
|
|
339
|
+
error_catcher(async (req, res) => {
|
|
340
|
+
const { id } = req.params;
|
|
341
|
+
const model = await Model.findOne({ id });
|
|
342
|
+
await model.delete();
|
|
343
|
+
req.flash("success", req.__("Model %s deleted", model.name));
|
|
344
|
+
res.redirect(`/table/${model.table_id}`);
|
|
345
|
+
})
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
router.post(
|
|
349
|
+
"/delete-instance/:id",
|
|
350
|
+
isAdmin,
|
|
351
|
+
error_catcher(async (req, res) => {
|
|
352
|
+
const { id } = req.params;
|
|
353
|
+
const model_inst = await ModelInstance.findOne({ id });
|
|
354
|
+
await model_inst.delete();
|
|
355
|
+
req.flash("success", req.__("Model instance %s deleted", model_inst.name));
|
|
356
|
+
res.redirect(`/models/show/${model_inst.model_id}`);
|
|
357
|
+
})
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
router.get(
|
|
361
|
+
"/train/:id",
|
|
362
|
+
isAdmin,
|
|
363
|
+
error_catcher(async (req, res) => {
|
|
364
|
+
const { id } = req.params;
|
|
365
|
+
const model = await Model.findOne({ id });
|
|
366
|
+
const table = await Table.findOne({ id: model.table_id });
|
|
367
|
+
const form = model_train_form(model, table, req);
|
|
368
|
+
res.sendWrap(req.__(`Train model`), {
|
|
369
|
+
above: [
|
|
370
|
+
{
|
|
371
|
+
type: "breadcrumbs",
|
|
372
|
+
crumbs: [
|
|
373
|
+
{ text: req.__("Tables"), href: "/table" },
|
|
374
|
+
{ href: `/table/${table.id}`, text: table.name },
|
|
375
|
+
{ href: `/models/show/${model.id}`, text: model.name },
|
|
376
|
+
{ text: req.__(`Train`) },
|
|
377
|
+
],
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
type: "card",
|
|
381
|
+
class: "mt-0",
|
|
382
|
+
title: req.__(`New model`),
|
|
383
|
+
contents: renderForm(form, req.csrfToken()),
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
});
|
|
387
|
+
})
|
|
388
|
+
);
|
|
389
|
+
router.post(
|
|
390
|
+
"/train/:id",
|
|
391
|
+
isAdmin,
|
|
392
|
+
error_catcher(async (req, res) => {
|
|
393
|
+
const { id } = req.params;
|
|
394
|
+
const model = await Model.findOne({ id });
|
|
395
|
+
const table = Table.findOne({ id: model.table_id });
|
|
396
|
+
const form = model_train_form(model, table, req);
|
|
397
|
+
form.validate(req.body);
|
|
398
|
+
if (form.hasErrors) {
|
|
399
|
+
res.sendWrap(req.__(`Train model`), renderForm(form, req.csrfToken()));
|
|
400
|
+
} else {
|
|
401
|
+
const { name, ...hyperparameters } = form.values;
|
|
402
|
+
|
|
403
|
+
const train_res = await model.train_instance(name, hyperparameters, {});
|
|
404
|
+
if (typeof train_res === "string") {
|
|
405
|
+
res.sendWrap(req.__(`Model training error`), {
|
|
406
|
+
above: [
|
|
407
|
+
{
|
|
408
|
+
type: "breadcrumbs",
|
|
409
|
+
crumbs: [
|
|
410
|
+
{ text: req.__("Tables"), href: "/table" },
|
|
411
|
+
{ href: `/table/${table.id}`, text: table.name },
|
|
412
|
+
{ href: `/models/show/${model.id}`, text: model.name },
|
|
413
|
+
{ text: req.__(`Training error`) },
|
|
414
|
+
],
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
type: "card",
|
|
418
|
+
class: "mt-0",
|
|
419
|
+
title: req.__(`Training error`),
|
|
420
|
+
contents: pre(train_res),
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
});
|
|
424
|
+
} else {
|
|
425
|
+
req.flash("success", "Model trained");
|
|
426
|
+
res.redirect(`/models/show/${model.id}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
})
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
router.post(
|
|
433
|
+
"/make-default-instance/:id",
|
|
434
|
+
isAdmin,
|
|
435
|
+
error_catcher(async (req, res) => {
|
|
436
|
+
const { id } = req.params;
|
|
437
|
+
const model_instance = await ModelInstance.findOne({ id });
|
|
438
|
+
await model_instance.make_default(!req.body.enabled);
|
|
439
|
+
res.redirect(`/models/show/${model_instance.model_id}`);
|
|
440
|
+
})
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const encode = (s) =>
|
|
444
|
+
s.replace(
|
|
445
|
+
//https://stackoverflow.com/a/57448862/19839414
|
|
446
|
+
/[&<>'"]/g,
|
|
447
|
+
(tag) =>
|
|
448
|
+
({
|
|
449
|
+
"&": "&",
|
|
450
|
+
"<": "<",
|
|
451
|
+
">": ">",
|
|
452
|
+
"'": "'",
|
|
453
|
+
'"': """,
|
|
454
|
+
}[tag])
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
router.get(
|
|
458
|
+
"/show-report/:id",
|
|
459
|
+
isAdmin,
|
|
460
|
+
error_catcher(async (req, res) => {
|
|
461
|
+
const { id } = req.params;
|
|
462
|
+
const model_instance = await ModelInstance.findOne({ id });
|
|
463
|
+
const model = await Model.findOne({ id: model_instance.model_id });
|
|
464
|
+
const table = Table.findOne({ id: model.table_id });
|
|
465
|
+
res.sendWrap(req.__(`Train model`), {
|
|
466
|
+
above: [
|
|
467
|
+
{
|
|
468
|
+
type: "breadcrumbs",
|
|
469
|
+
crumbs: [
|
|
470
|
+
{ text: req.__("Tables"), href: "/table" },
|
|
471
|
+
{ href: `/table/${table.id}`, text: table.name },
|
|
472
|
+
{ href: `/models/show/${model.id}`, text: model.name },
|
|
473
|
+
{ text: model_instance.name },
|
|
474
|
+
{ text: req.__(`Report`) },
|
|
475
|
+
],
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
type: "card",
|
|
479
|
+
class: "mt-0",
|
|
480
|
+
title: req.__(`Model training report`),
|
|
481
|
+
contents:
|
|
482
|
+
iframe({
|
|
483
|
+
id: "trainreport",
|
|
484
|
+
width: "100%",
|
|
485
|
+
height: "100vh",
|
|
486
|
+
srcdoc: encode(model_instance.report),
|
|
487
|
+
}) + style(`iframe#trainreport { height: 100vh}`),
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
});
|
|
491
|
+
})
|
|
492
|
+
);
|
package/routes/page.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
const Router = require("express-promise-router");
|
|
8
8
|
|
|
9
9
|
const Page = require("@saltcorn/data/models/page");
|
|
10
|
+
const Trigger = require("@saltcorn/data/models/trigger");
|
|
10
11
|
const { getState } = require("@saltcorn/data/db/state");
|
|
11
12
|
const {
|
|
12
13
|
error_catcher,
|
|
@@ -40,18 +41,21 @@ router.get(
|
|
|
40
41
|
const { pagename } = req.params;
|
|
41
42
|
const state = getState();
|
|
42
43
|
state.log(3, `Route /page/${pagename} user=${req.user?.id}`);
|
|
43
|
-
const tic =
|
|
44
|
+
const tic = new Date();
|
|
44
45
|
|
|
45
46
|
const role = req.user && req.user.id ? req.user.role_id : 100;
|
|
46
47
|
const db_page = await Page.findOne({ name: pagename });
|
|
47
48
|
if (db_page && role <= db_page.min_role) {
|
|
48
49
|
const contents = await db_page.run(req.query, { res, req });
|
|
49
50
|
const title = scan_for_page_title(contents, db_page.title);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
const tock = new Date();
|
|
52
|
+
const ms = tock.getTime() - tic.getTime();
|
|
53
|
+
Trigger.emitEvent("PageLoad", null, req.user, {
|
|
54
|
+
text: req.__("Page '%s' was loaded", pagename),
|
|
55
|
+
type: "page",
|
|
56
|
+
name: pagename,
|
|
57
|
+
render_time: ms,
|
|
58
|
+
});
|
|
55
59
|
res.sendWrap(
|
|
56
60
|
{
|
|
57
61
|
title,
|