sdc_client 0.158.8 → 0.159.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/LICENSE +21 -0
  3. package/dist/index.js +49 -24
  4. package/dist/ugly.index.js +2 -1
  5. package/dist/ugly.index.js.LICENSE.txt +1 -8
  6. package/gulp/gulp.jsx +12 -42
  7. package/package.json +39 -31
  8. package/src/index.js +27 -7
  9. package/src/simpleDomControl/AbstractSDC.js +24 -11
  10. package/src/simpleDomControl/sdc_controller.js +7 -4
  11. package/src/simpleDomControl/sdc_dom_events.js +5 -10
  12. package/src/simpleDomControl/sdc_events.js +3 -3
  13. package/src/simpleDomControl/sdc_main.js +34 -13
  14. package/src/simpleDomControl/sdc_model.js +209 -84
  15. package/src/simpleDomControl/sdc_params.js +0 -3
  16. package/src/simpleDomControl/sdc_server_call.js +21 -4
  17. package/src/simpleDomControl/sdc_test_utils.js +0 -1
  18. package/src/simpleDomControl/sdc_utils.js +59 -7
  19. package/src/simpleDomControl/sdc_view.js +21 -1
  20. package/.github/workflows/node.js.yml +0 -38
  21. package/.idea/inspectionProfiles/Project_Default.xml +0 -17
  22. package/.idea/jsLibraryMappings.xml +0 -6
  23. package/.idea/misc.xml +0 -6
  24. package/.idea/modules.xml +0 -8
  25. package/.idea/vcs.xml +0 -6
  26. package/.idea/workspace.xml +0 -403
  27. package/.readthedocs.yaml +0 -13
  28. package/.tool-versions +0 -1
  29. package/SimpleDomControlClient.iml +0 -12
  30. package/docs/api-reference.rst +0 -225
  31. package/docs/conf.py +0 -11
  32. package/docs/controllers.rst +0 -228
  33. package/docs/events-and-dom.rst +0 -120
  34. package/docs/getting-started.rst +0 -143
  35. package/docs/index.rst +0 -22
  36. package/docs/models.rst +0 -351
  37. package/docs/overview.rst +0 -66
  38. package/docs/requirements.txt +0 -1
  39. package/eslint.config.js +0 -60
  40. package/gulpfile.jsx +0 -25
  41. package/jest.config.mjs +0 -197
  42. package/test/controller.test.js +0 -93
  43. package/test/dist.test.js +0 -127
  44. package/test/model.test.js +0 -330
  45. package/test/models/Author.js +0 -145
  46. package/test/models/Book.js +0 -112
  47. package/test/models/BookContent.js +0 -114
  48. package/test/models/SdcUser.js +0 -75
  49. package/test/models/src.js +0 -8
  50. package/test/sdc_model_dates.ai.test.js +0 -57
  51. package/test/sdc_server_call.ai.test.js +0 -267
  52. package/test/utils.js +0 -88
  53. package/test/view.test.js +0 -159
  54. package/test-setup.js +0 -4
  55. package/webpack.config/webpack.default.config.cjs +0 -22
  56. package/webpack.config/webpack.development.config.cjs +0 -14
  57. package/webpack.config/webpack.production.config.cjs +0 -11
@@ -15,7 +15,6 @@ function checkIfEventFits(ev_type, e, target) {
15
15
  target !== elementUnderMouse && !target.contains(elementUnderMouse)
16
16
  );
17
17
  case "mouseenter":
18
- case "mousein":
19
18
  elementUnderMouse = document.elementFromPoint(e.clientX, e.clientY);
20
19
  return (
21
20
  target !== elementUnderMouse && !target.contains(elementUnderMouse)
@@ -28,7 +27,7 @@ function checkIfEventFits(ev_type, e, target) {
28
27
  export function windowEventHandler(event) {
29
28
  let ev_type = event.type;
30
29
  if (
31
- event.hasOwnProperty("namespace") &&
30
+ Object.hasOwn(event, "namespace") &&
32
31
  event.namespace &&
33
32
  event.namespace.length
34
33
  )
@@ -97,16 +96,12 @@ export function initEvents() {
97
96
  * @param {AbstractSDC} controller
98
97
  */
99
98
  export function setControllerEvents(controller) {
100
- if (controller._isEventsSet) {
101
- return;
102
- }
103
-
104
99
  const events = controller.getEvents();
105
100
  for (let ev_type in events) {
106
- if (events.hasOwnProperty(ev_type)) {
101
+ if (Object.hasOwn(events, ev_type)) {
107
102
  let eventList = events[ev_type];
108
103
  for (let domSelector in eventList) {
109
- if (eventList.hasOwnProperty(domSelector)) {
104
+ if (Object.hasOwn(eventList, domSelector)) {
110
105
  controller.find(domSelector).each(function () {
111
106
  let $elements = $(this);
112
107
  let event_list = $elements.attr(`sdc_${ev_type}`) || null;
@@ -122,6 +117,6 @@ export function setControllerEvents(controller) {
122
117
  }
123
118
  }
124
119
  }
125
- // TODO: Is it needed
126
- //controller._isEventsSet = true;
120
+ // Not cached on purpose: a refresh can add new elements that match the selectors,
121
+ // so the attributes are set again on every refresh (existing entries are skipped).
127
122
  }
@@ -23,7 +23,7 @@ let eventList = {};
23
23
  */
24
24
  export function on(name, controller) {
25
25
  setEvent(name);
26
- if (!eventList.hasOwnProperty(name)) {
26
+ if (!Object.hasOwn(eventList, name)) {
27
27
  return console.log("No event: " + name, controller);
28
28
  }
29
29
 
@@ -61,7 +61,7 @@ export function setEvent(name, functionName) {
61
61
  */
62
62
  export function allOff(controller) {
63
63
  for (let eventName in handlerList) {
64
- if (handlerList.hasOwnProperty(eventName)) {
64
+ if (Object.hasOwn(handlerList, eventName)) {
65
65
  for (let i = handlerList[eventName].length - 1; i >= 0; i--) {
66
66
  if (controller === handlerList[eventName][i]) {
67
67
  handlerList[eventName].splice(i, 1);
@@ -82,7 +82,7 @@ export function allOff(controller) {
82
82
  export function trigger(name) {
83
83
  let args = Array.apply(null, arguments);
84
84
  name = args.shift();
85
- if (!handlerList.hasOwnProperty(name) || !eventList.hasOwnProperty(name)) {
85
+ if (!Object.hasOwn(handlerList, name) || !Object.hasOwn(eventList, name)) {
86
86
  return promiseDummyFactory();
87
87
  }
88
88
  let handler = handlerList[name];
@@ -47,7 +47,10 @@ let sdcDomFragment = function (element, props) {
47
47
  if (props) {
48
48
  Object.entries(props).forEach(([k, v]) => {
49
49
  if (k.startsWith("on")) {
50
- $new_elem[0].addEventListener(k.substring(2).toLowerCase(), v);
50
+ const type = k.substring(2).toLowerCase();
51
+ $new_elem[0].addEventListener(type, v);
52
+ // Remembered so reconcile() can move the listeners to a node it keeps.
53
+ ($new_elem[0]._sdcListeners ??= []).push([type, v]);
51
54
  } else {
52
55
  if (PROPERTIES_UPDATE[k.toLowerCase()]) {
53
56
  k = PROPERTIES_UPDATE[k.toLowerCase()];
@@ -75,6 +78,14 @@ window.sdcDom = function (tagName, props, ...children) {
75
78
  return $new_elem;
76
79
  };
77
80
 
81
+ const redirector = (a) => {
82
+ if (a["url-link"]) {
83
+ trigger("onNavLink", a["url-link"]);
84
+ } else {
85
+ window.location.href = a["url"];
86
+ }
87
+ };
88
+
78
89
  export let app = {
79
90
  CSRF_TOKEN: window.CSRF_TOKEN || "",
80
91
  LANGUAGE_CODE: window.LANGUAGE_CODE || "en",
@@ -87,6 +98,7 @@ export let app = {
87
98
  _isInit: false,
88
99
  _origin_trigger: null,
89
100
  _globalControllerClasses: [],
101
+ _createdGlobals: [],
90
102
 
91
103
  init_sdc: () => {
92
104
  if (!app._isInit) {
@@ -119,8 +131,12 @@ export let app = {
119
131
 
120
132
  app.tagNames = tagList();
121
133
 
134
+ // Global controllers are created only once, even if init_sdc() is called again.
135
+ const newGlobals = Global.filter((tagName) => !app._createdGlobals.includes(tagName));
136
+ app._createdGlobals.push(...newGlobals);
137
+
122
138
  const $globalDiv = $("<div></div>");
123
- Global.forEach((tagName) => {
139
+ newGlobals.forEach((tagName) => {
124
140
  const ControllerClass = controllerList[tagName][0];
125
141
  $globalDiv.append(
126
142
  `<${ControllerClass.prototype._tagName}></${ControllerClass.prototype._tagName}>`,
@@ -169,9 +185,12 @@ export let app = {
169
185
  * @param {AbstractSDC} Controller
170
186
  */
171
187
  registerGlobal: (Controller) => {
172
- app.register(Controller);
188
+ const registration = app.register(Controller);
173
189
  let tagName = Controller.prototype._tagName;
174
- Global.push(tagName);
190
+ if (!Global.includes(tagName)) {
191
+ Global.push(tagName);
192
+ }
193
+ return registration;
175
194
  },
176
195
 
177
196
  cleanCache: () => {
@@ -260,14 +279,23 @@ export let app = {
260
279
  .then((a, b, c) => {
261
280
  resolve(a, b, c);
262
281
  if (a.status === "redirect") {
263
- trigger("onNavLink", a["url-link"]);
282
+ redirector(a);
264
283
  } else {
265
284
  p.then(() => {
266
285
  app.refresh(controller.$container);
267
286
  });
268
287
  }
269
288
  })
270
- .catch(reject);
289
+ .catch((a, b, c) => {
290
+ // send_redirect() answers with HTTP 301 and the redirect in the JSON body.
291
+ if (a.status === 301 && a.responseJSON) {
292
+ a = a.responseJSON;
293
+ redirector(a);
294
+ resolve(a, b, c);
295
+ } else {
296
+ reject(a, b, c);
297
+ }
298
+ });
271
299
  });
272
300
 
273
301
  return p;
@@ -275,13 +303,6 @@ export let app = {
275
303
 
276
304
  submitFormAndUpdateView: (controller, form, url, method) => {
277
305
  let formData = new FormData(form);
278
- const redirector = (a) => {
279
- if (a["url-link"]) {
280
- trigger("onNavLink", a["url-link"]);
281
- } else {
282
- window.location.href = a["url"];
283
- }
284
- };
285
306
 
286
307
  const p = new Promise((resolve, reject) => {
287
308
  uploadFileFormData(formData, url || form.action, method || form.method)
@@ -1,4 +1,4 @@
1
- import { getValueFromField, setValueInField, uuidv4 } from "./sdc_utils.js";
1
+ import { formatDate, getValueFromField, setValueInField, toDate, uuidv4 } from "./sdc_utils.js";
2
2
  import { getModel } from "./sdc_socket.js"
3
3
  import { app } from "./sdc_main.js";
4
4
  import { trigger } from "./sdc_events.js";
@@ -6,11 +6,15 @@ import { trigger } from "./sdc_events.js";
6
6
  const MAX_FILE_UPLOAD = 25000;
7
7
  const CONNECTING_REQUEST_ID = "_connecting_process";
8
8
 
9
- const cloneObject = (original) => {
10
- return Object.assign(
11
- Object.create(Object.getPrototypeOf(original)),
12
- structuredClone(original)
13
- );
9
+ /**
10
+ * Copy a model instance for another queryset. A new instance of the same class is
11
+ * created from the serialized field values (structuredClone cannot copy the
12
+ * WeakRef/Proxy members of a model).
13
+ */
14
+ const cloneModel = (original) => {
15
+ const copy = new original.constructor({});
16
+ copy.setValues(original.serialize());
17
+ return copy;
14
18
  }
15
19
 
16
20
  class SdcModelError extends Error {
@@ -82,6 +86,29 @@ function normalizePk(pk) {
82
86
  return Number.isNaN(normalizedPk) ? -1 : normalizedPk;
83
87
  }
84
88
 
89
+ /**
90
+ * Decide whether an argument of `create()` is an options object
91
+ * (`{elem, data}`) or the data itself. It is treated as options if it is
92
+ * a non-empty plain object whose keys are only `elem` and/or `data`, and
93
+ * `elem` (if present) is an SdcModel.
94
+ *
95
+ * @param {*} arg
96
+ * @returns {boolean}
97
+ */
98
+ function isCreateOptions(arg) {
99
+ if (!arg || typeof arg !== "object" || arg instanceof SdcModel) {
100
+ return false;
101
+ }
102
+ const keys = Object.keys(arg);
103
+ if (keys.length === 0 || !keys.every((k) => k === "elem" || k === "data")) {
104
+ return false;
105
+ }
106
+ if ("elem" in arg && arg.elem !== null && !(arg.elem instanceof SdcModel)) {
107
+ return false;
108
+ }
109
+ return !("data" in arg) || arg.data === null || typeof arg.data === "object";
110
+ }
111
+
85
112
  export class SdcQuerySet {
86
113
  /**
87
114
  * Lightweight client-side collection wrapper around the SDC model websocket.
@@ -109,6 +136,9 @@ export class SdcQuerySet {
109
136
  };
110
137
  this.onCreate = () => {
111
138
  };
139
+ // Called with the removed models when rows of this queryset are deleted on the
140
+ // server. If not set, onUpdate is called instead.
141
+ this.onDelete = null;
112
142
 
113
143
  return new Proxy(this, {
114
144
  get(target, prop) {
@@ -150,6 +180,22 @@ export class SdcQuerySet {
150
180
  this.onUpdate = handler;
151
181
  }
152
182
 
183
+ set on_delete(handler) {
184
+ this.onDelete = handler;
185
+ }
186
+
187
+ /**
188
+ * Remove the models with the given ids from the local list.
189
+ *
190
+ * @param {Array<number>} ids
191
+ * @returns {Array<SdcModel>} the removed models
192
+ */
193
+ _removeIds(ids) {
194
+ const removed = this.valuesList.filter((item) => ids.includes(item.id));
195
+ this.valuesList = this.valuesList.filter((item) => !ids.includes(item.id));
196
+ return removed;
197
+ }
198
+
153
199
  /**
154
200
  *
155
201
  *
@@ -169,19 +215,25 @@ export class SdcQuerySet {
169
215
  *
170
216
  *
171
217
  * @param {Array<integr|string>|integer|SdcModel|SdcQuerySet|string} ids
172
- */cloneObject
218
+ */
173
219
  _setIds(ids) {
174
220
 
175
221
  if (ids === null || ids === "" || Array.isArray(ids) && ids.length === 0) {
176
222
  this.valuesList = [];
177
223
  return this.valuesList;
178
- } else if (ids instanceof SdcQuerySet) {
179
- this.valuesList = ids.valuesList.map(cloneObject);
180
- this.valuesList.forEach(value => value._setQuerySet(this, true));
181
- return this.valuesList;
182
- } else if (ids instanceof SdcModel) {
183
- this.valuesList = [cloneObject(ids)];
184
- this.valuesList.forEach(value => value._setQuerySet(this, true));
224
+ } else if (ids instanceof SdcQuerySet || ids instanceof SdcModel) {
225
+ // Models already in this queryset are kept (so references to them stay valid);
226
+ // other models are copied into it.
227
+ const sources = ids instanceof SdcModel ? [ids] : ids.valuesList;
228
+ this.valuesList = sources.map((source) => {
229
+ const existing = source.id !== null ? this.byId(source.id) : null;
230
+ if (existing) {
231
+ return existing;
232
+ }
233
+ const copy = cloneModel(source);
234
+ copy._setQuerySet(this, true);
235
+ return copy;
236
+ });
185
237
  return this.valuesList;
186
238
  }
187
239
 
@@ -327,7 +379,10 @@ export class SdcQuerySet {
327
379
  }),
328
380
  );
329
381
 
330
- this.openRequest[event_id] = [resolve, reject];
382
+ this.openRequest[event_id] = [(res) => {
383
+ this._removeIds([normalizePk(pk)]);
384
+ resolve(res);
385
+ }, reject];
331
386
  });
332
387
  });
333
388
  }
@@ -638,12 +693,24 @@ export class SdcQuerySet {
638
693
  }
639
694
 
640
695
  /**
696
+ * Create a new instance in the DB.
641
697
  *
642
- * @param elem {?SdcModel}
643
- * @param data {?object}
698
+ * @param {{elem?: SdcModel, data?: object} | object} arg
699
+ * Either an options object containing `elem` and/or `data`,
700
+ * or `data` directly.
701
+ *
702
+ * @example
703
+ * create({ elem, data });
704
+ * create({ elem });
705
+ * create(data);
644
706
  * @returns {Promise<unknown>}
645
707
  */
646
- create({ elem = null, data = null } = {}) {
708
+ create(arg = null) {
709
+ let { elem = null, data = null } = isCreateOptions(arg) ? arg : { data: arg };
710
+ if (data && typeof data === "object" && Object.keys(data).length === 0) {
711
+ data = null;
712
+ }
713
+
647
714
  const event_id = uuidv4();
648
715
  if (!elem) {
649
716
  elem = this.new(data);
@@ -758,43 +825,47 @@ export class SdcQuerySet {
758
825
  const value = elem[key];
759
826
  if (value instanceof File) {
760
827
  toSolve.push(
761
- new Promise(async (resolve, reject) => {
828
+ new Promise((resolve, reject) => {
762
829
  const event_id = uuidv4();
763
830
  this.openRequest[event_id] = [resolve, reject];
764
831
 
765
- const buffer = await value.arrayBuffer();
766
- let result = new Uint8Array(buffer);
767
- let numberOfChunks = Math.ceil(result.length / MAX_FILE_UPLOAD);
768
- files[key] = {
769
- id: event_id,
770
- file_name: value.name,
771
- field_name: key,
772
- content_length: value.size,
773
- };
774
- for (let i = 0; i < numberOfChunks; ++i) {
775
- const chunk = Array.from(result.slice(
776
- MAX_FILE_UPLOAD * i,
777
- MAX_FILE_UPLOAD * (i + 1),
778
- ));
779
- this.socket.send(
780
- JSON.stringify({
781
- event: "model",
782
- event_type: "upload",
783
- event_id,
784
- args: {
785
- chunk,
786
- idx: i,
787
- number_of_chunks: numberOfChunks,
788
- file_name: value.name,
789
- field_name: key,
790
- content_length: value.size,
791
- content_type: value.type,
792
- model_name: this.modelName,
793
- model_query: this.modelQuery,
794
- },
795
- }),
796
- );
797
- }
832
+ value.arrayBuffer().then((buffer) => {
833
+ let result = new Uint8Array(buffer);
834
+ let numberOfChunks = Math.ceil(result.length / MAX_FILE_UPLOAD);
835
+ files[key] = {
836
+ id: event_id,
837
+ file_name: value.name,
838
+ field_name: key,
839
+ content_length: value.size,
840
+ };
841
+ for (let i = 0; i < numberOfChunks; ++i) {
842
+ const chunk = Array.from(result.slice(
843
+ MAX_FILE_UPLOAD * i,
844
+ MAX_FILE_UPLOAD * (i + 1),
845
+ ));
846
+ this.socket.send(
847
+ JSON.stringify({
848
+ event: "model",
849
+ event_type: "upload",
850
+ event_id,
851
+ args: {
852
+ chunk,
853
+ idx: i,
854
+ number_of_chunks: numberOfChunks,
855
+ file_name: value.name,
856
+ field_name: key,
857
+ content_length: value.size,
858
+ content_type: value.type,
859
+ model_name: this.modelName,
860
+ model_query: this.modelQuery,
861
+ },
862
+ }),
863
+ );
864
+ }
865
+ }).catch((err) => {
866
+ this._closeOpenRequest(event_id);
867
+ reject(err);
868
+ });
798
869
  }),
799
870
  );
800
871
  }
@@ -814,7 +885,7 @@ export class SdcQuerySet {
814
885
  async _onMessage(e) {
815
886
  let data = JSON.parse(e.data);
816
887
  if (data.is_error) {
817
- if (this.openRequest.hasOwnProperty(data.event_id)) {
888
+ if (Object.hasOwn(this.openRequest, data.event_id)) {
818
889
  this.openRequest[data.event_id][1](new SdcModelError(data));
819
890
  this._closeOpenRequest(data.event_id);
820
891
  }
@@ -838,7 +909,7 @@ export class SdcQuerySet {
838
909
  this._isConnectingProcess = false;
839
910
  this.openRequest[CONNECTING_REQUEST_ID][0](data);
840
911
  this._closeOpenRequest(CONNECTING_REQUEST_ID);
841
- } else if (["load", "named_view", "detail_view"].includes(data.type)) {
912
+ } else if (["load", "list_view", "named_view", "detail_view"].includes(data.type)) {
842
913
  const jsonRes = JSON.parse(data.args.data);
843
914
  data.args.data = await this._parseServerRes(jsonRes);
844
915
  } else if (data.type === "on_update" || data.type === "on_create") {
@@ -855,6 +926,11 @@ export class SdcQuerySet {
855
926
 
856
927
  cb(obj);
857
928
  data.args.data = obj;
929
+ } else if (data.type === "on_delete") {
930
+ const ids = JSON.parse(data.args.data).map((x) => normalizePk(x.id ?? x.pk));
931
+ const removed = this._removeIds(ids);
932
+ (this.onDelete ?? this.onUpdate)(removed);
933
+ data.args.data = removed;
858
934
  }
859
935
 
860
936
  let instance = data.data?.instance;
@@ -862,7 +938,7 @@ export class SdcQuerySet {
862
938
  data.data.instance = JSON.parse(data.data.instance);
863
939
  }
864
940
 
865
- if (this.openRequest.hasOwnProperty(data.event_id)) {
941
+ if (Object.hasOwn(this.openRequest, data.event_id)) {
866
942
  this.openRequest[data.event_id][0](data);
867
943
  this._closeOpenRequest(data.event_id);
868
944
  }
@@ -924,14 +1000,17 @@ export class SdcQuerySet {
924
1000
  `SDC Model (${this.modelName}, ${this.modelId}) Socket closed unexpectedly`,
925
1001
  );
926
1002
  this._isConnected = false;
927
- for (const [_key, value] of Object.entries(this.openRequest)) {
1003
+ for (const value of Object.values(this.openRequest)) {
928
1004
  value[1](e);
929
1005
  }
930
1006
  this.openRequest = {};
931
1007
 
1008
+ this._isConnectingProcess = false;
932
1009
  setTimeout(() => {
933
- if (this._autoReconnect) {
934
- this._connectToServer().then(() => {
1010
+ if (this._autoReconnect && !this._isConnected) {
1011
+ // isConnected() opens the socket and sends the connect handshake, so
1012
+ // live updates work again and later requests reuse this connection.
1013
+ this.isConnected().catch(() => {
935
1014
  });
936
1015
  }
937
1016
  }, 1000);
@@ -1028,11 +1107,12 @@ export default class SdcModel {
1028
1107
  * @returns {SdcQuerySet}
1029
1108
  */
1030
1109
  static querySet(modelQuerySet = null, parent = null) {
1110
+ const { modelName } = new this({});
1031
1111
  if (!parent) {
1032
- return new SdcQuerySet(this.modeName, modelQuerySet);
1112
+ return new SdcQuerySet(modelName, modelQuerySet);
1033
1113
  }
1034
1114
 
1035
- return parent.querySet(this.modeName, modelQuerySet);
1115
+ return parent.querySet(modelName, modelQuerySet);
1036
1116
  }
1037
1117
 
1038
1118
  addForm($form) {
@@ -1054,15 +1134,25 @@ export default class SdcModel {
1054
1134
  _onChange(event) {
1055
1135
  const { name } = event.target;
1056
1136
  if (this.constructor.fields[name]) {
1057
- this[`set${name}`](getValueFromField(event.target));
1137
+ try {
1138
+ this[`set${name}`](getValueFromField(event.target));
1139
+ } catch {
1140
+ // Invalid input (e.g. an empty required field) is kept in the form and
1141
+ // validated by the server on submit.
1142
+ return;
1143
+ }
1144
+ // Show the new value in the other forms of this model, not in the input being edited.
1145
+ this._updateForm(name, event.target);
1058
1146
  }
1059
1147
  }
1060
1148
 
1061
- _updateForm(fieldName) {
1149
+ _updateForm(fieldName, skipElement = null) {
1062
1150
  const self = this;
1063
1151
  this._forms.forEach(($form) => {
1064
1152
  $form.find(`[name="${fieldName}"]`).each(function () {
1065
- setValueInField(this, self[fieldName])
1153
+ if (this !== skipElement) {
1154
+ setValueInField(this, self[fieldName]);
1155
+ }
1066
1156
  });
1067
1157
  });
1068
1158
  }
@@ -1090,7 +1180,15 @@ export default class SdcModel {
1090
1180
  return this._querySet.deref().save({ pk: this.id, formName, data });
1091
1181
  }
1092
1182
 
1093
- create({ data = null } = {}) {
1183
+ /**
1184
+ * Create this instance in the DB.
1185
+ *
1186
+ * @param {{data?: object} | object | null} values
1187
+ * Either `{ data }` or `data` directly.
1188
+ * @returns {Promise<unknown>}
1189
+ */
1190
+ create(values = null) {
1191
+ const data = isCreateOptions(values) ? (values.data ?? null) : values;
1094
1192
  return this._querySet.deref().create({ elem: this, data });
1095
1193
  }
1096
1194
 
@@ -1159,6 +1257,9 @@ export default class SdcModel {
1159
1257
  }
1160
1258
  } else if (val.is_relation && value instanceof SdcModel) {
1161
1259
  acc[key] = value.id ?? null;
1260
+ } else if (value instanceof Date) {
1261
+ // Django's DateField does not accept a full ISO datetime.
1262
+ acc[key] = val.type === "DateField" ? formatDate(value) : value.toISOString();
1162
1263
  } else {
1163
1264
  acc[key] = value;
1164
1265
  }
@@ -1219,15 +1320,16 @@ export default class SdcModel {
1219
1320
  $forms = this._resolveForms($forms);
1220
1321
 
1221
1322
  const self = this;
1222
- const fields = this.constructor.fields;
1323
+ const { fields } = this.constructor;
1223
1324
  $forms.each(function () {
1224
1325
  const pk = normalizePk($(this).data("model_pk"));
1225
- if (self.id !== pk) {
1326
+ // Create forms carry model_pk -1 while the new model's id is still null.
1327
+ if (normalizePk(self.id) !== pk) {
1226
1328
  return;
1227
1329
  }
1228
1330
 
1229
1331
  for (let formItem of this.elements) {
1230
- let name = formItem.name;
1332
+ let { name } = formItem;
1231
1333
  if (name && name !== "" && !!fields[name]) {
1232
1334
  setValueInField(formItem, self[name])
1233
1335
  }
@@ -1247,12 +1349,17 @@ export default class SdcModel {
1247
1349
  $forms = this._resolveForms($forms);
1248
1350
 
1249
1351
  const self = this;
1250
- const fields = this.constructor.fields;
1352
+ const { fields } = this.constructor;
1251
1353
  const returnValue = {}
1252
1354
 
1253
1355
  function setValueInForm(name, value) {
1254
- if (!!fields[name]) {
1255
- self[name] = value;
1356
+ if (fields[name]) {
1357
+ try {
1358
+ self[name] = value;
1359
+ } catch {
1360
+ // Invalid values are still sent to the server, which returns the form
1361
+ // errors; the model keeps its last valid value.
1362
+ }
1256
1363
  }
1257
1364
  returnValue[name] = value;
1258
1365
  }
@@ -1387,14 +1494,21 @@ export default class SdcModel {
1387
1494
 
1388
1495
  switch (type) {
1389
1496
  case "CharField":
1390
- case "TeextField":
1497
+ case "TextField":
1498
+ case "SlugField":
1391
1499
  case "UUIDField":
1392
1500
  case "EmailField":
1393
1501
  return `${value}`;
1394
1502
 
1395
1503
  case "IntegerField":
1396
- case "AutoField":
1504
+ case "SmallIntegerField":
1397
1505
  case "BigIntegerField":
1506
+ case "PositiveIntegerField":
1507
+ case "PositiveSmallIntegerField":
1508
+ case "PositiveBigIntegerField":
1509
+ case "AutoField":
1510
+ case "SmallAutoField":
1511
+ case "BigAutoField":
1398
1512
  return parseInt(value, 10);
1399
1513
 
1400
1514
  case "FloatField":
@@ -1406,7 +1520,7 @@ export default class SdcModel {
1406
1520
 
1407
1521
  case "DateField":
1408
1522
  case "DateTimeField":
1409
- return Date.parse(value);
1523
+ return toDate(value);
1410
1524
 
1411
1525
  case "URLField":
1412
1526
  return new URL(value);
@@ -1429,14 +1543,7 @@ export default class SdcModel {
1429
1543
  }
1430
1544
 
1431
1545
  if (typeof File !== "undefined" && value instanceof File) {
1432
- if (config.max_size && value.size > config.max_size) {
1433
- return `File too large (max ${config.max_size} bytes)`;
1434
- }
1435
-
1436
- if (config.allowed_types && !config.allowed_types.includes(value.type)) {
1437
- return `Invalid file type (${value.type})`;
1438
- }
1439
-
1546
+ // Size and type are checked in validate().
1440
1547
  return value;
1441
1548
  }
1442
1549
  break;
@@ -1518,8 +1625,14 @@ function validateField(value, config) {
1518
1625
  break;
1519
1626
 
1520
1627
  case "IntegerField":
1521
- case "AutoField":
1628
+ case "SmallIntegerField":
1522
1629
  case "BigIntegerField":
1630
+ case "PositiveIntegerField":
1631
+ case "PositiveSmallIntegerField":
1632
+ case "PositiveBigIntegerField":
1633
+ case "AutoField":
1634
+ case "SmallAutoField":
1635
+ case "BigAutoField":
1523
1636
  if (!isFinite(parseInt(value))) {
1524
1637
  return "Must be an integer";
1525
1638
  }
@@ -1566,14 +1679,26 @@ function validateField(value, config) {
1566
1679
  break;
1567
1680
 
1568
1681
  case "JSONField":
1569
- if (typeof value !== "object") {
1570
- return "Must be JSON object";
1682
+ if (typeof value === "string") {
1683
+ try {
1684
+ JSON.parse(value);
1685
+ } catch {
1686
+ return "Must be valid JSON";
1687
+ }
1571
1688
  }
1572
1689
  break;
1573
1690
  case "FileField":
1574
1691
  if (!FileLoaded.isValid(value) && !(value instanceof File) && !(value instanceof FileLoaded)) {
1575
1692
  return "Must be a valid file";
1576
1693
  }
1694
+ if (typeof File !== "undefined" && value instanceof File) {
1695
+ if (config.max_size && value.size > config.max_size) {
1696
+ return `File too large (max ${config.max_size} bytes)`;
1697
+ }
1698
+ if (config.allowed_types && !config.allowed_types.includes(value.type)) {
1699
+ return `Invalid file type (${value.type})`;
1700
+ }
1701
+ }
1577
1702
  break;
1578
1703
  default:
1579
1704
  break;