You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This ticket provides a foundation for #50 (realtime edit synchronization). We'd like to eliminate save buttons across our apps, creating clear distinctions between save and submit functionality. Baseframe (with Coaster) can provide a generic method to achieve this. We have two use cases to solve for:
When a new document is created, start saving it immediately as a draft. This requires the model to support a "draft" state, and views to provide access to drafts. Hasjob's JobPost and Funnel's Project and Proposal all have this state, but currently will not save a draft until form validations pass. This too should be allowed. Draft state objects do not need to pass form validations, merely database validations (for example, field value length should be within the size limit for the column). Form validations must pass only when the document is submitted (leaving the draft state).
When an existing document is edited, especially a published document, edits should be saved automatically, but should not be published, since we do not want a user's typing to show up in a published document while they are still typing. Users should be given the option to publish or discard their changes.
These use cases require the following pieces. For simplicity's sake, auto-save is only available for models that use UUIDs (in either a primary key or a supplementary unique key).
Empty documents
Models that support auto-save should allow for empty documents. This means all columns should have a default value or accept null values, without exception. Apps must make this change when implementing auto-save.
In addition, the name column in BaseNameMixin and BaseScopedNameMixin-derived models must get a default value as it's required for URLs (BaseIdNameMixin and BaseScopedIdNameMixin don't have this problem as they have a url_id). name can be set by default to the value of suuid for UuidMixin-derived models, with the caveat that when self.name == self.suuid, the name should be considered temporary and liable to change. A Coaster ticket is required for this change: the make_name method currently (a) only checks if name is None, not if it matches suuid, and (b) generates name from title (or short_title), not from suuid if title is missing.
Draft model
A generic Draft model (tablename draft) holds unvalidated form data that has been autosaved. It contains these fields:
id (uuid draft id)
table (table in which row is being edited)
table_id (uuid identifying row in table being edited)
body (json representation of form being edited, created from {'form': MultiDict.items(multi=True)})
revision (uuid or integer set randomly every time the draft is updated; PostgreSQL's internal xmin is not used as system columns can expose internal workings)
Drafts must always have a corresponding table and row, which is why empty documents are required for auto-saving new documents.
Views
Views will need distinct handling for GET and POST for both edit and new actions. POST operations will need to include an autosave flag (or distinct endpoint) to indicate that a form is being autosaved and not being submitted.
Edit GET:
Looks for a matching draft and initialises the form using ModelForm(obj=obj, formdata=MultiDict(draft.body['form'])) (or just ModelForm(obj=obj) if there's no draft).
Renders the form with a hidden 'draft.revision' field containing draft.revision, or blank value if there's no draft.
Edit POST (with autosave flag):
Looks for a matching draft and confirm request.form['draft.revision'] == draft.revision. If they don't match, abort the operation and let front-end render an error to the user, requesting a page reload.
If no existing draft is found, create one.
Save the draft with a new revision value and return it. Front-end updates 'draft.revision' input with this value.
Edit POST (no autosave flag):
Works as before, no changes, except...
If save is successful, finds existing draft(s) and deletes.
New GET:
Renders a blank form with hidden 'draft.revision' input set to blank value.
New POST (autosave):
Creates a new blank document (the document is expected to issue itself a temporary name if relevant).
Creates a draft for this new blank document and saves form to it.
Returns draft revision and URL to edit blank document
Front-end rewrites URL to this new URL. It is expected that the new and edit pages have the same UI. If this is not the case, a seamless page re-render will have to happen without interrupting the user's typing. We haven't specced how to do this, so this is a road-block for implementing auto-save in new documents.
Draft previews
The View GET action on a document will not load a draft. Passing in ?draft=<uuid> should load a matching draft where draft.id == <uuid>, draft.table == model.__tablename__, and draft.table_id == obj.uuid. However, it is dangerous to render such a draft as the form has not been validated.
Draft preview from the draft table should not be supported.
POST queue
When the front-end submits a revision, it may take a while to save. The front-end must do the following to avoid race conditions:
Dirty fields must be tracked for auto-save.
Saves are triggered when a dirty field loses focus, or the user stops typing for one second.
When a save is in progress, new save triggers are ignored.
When a save succeeds, if there are any dirty fields, a new save is triggered.
If a save operation fails due to a revision mismatch, the user is shown an error and asked to reload the page.
The text was updated successfully, but these errors were encountered:
This ticket provides a foundation for #50 (realtime edit synchronization). We'd like to eliminate save buttons across our apps, creating clear distinctions between save and submit functionality. Baseframe (with Coaster) can provide a generic method to achieve this. We have two use cases to solve for:
When a new document is created, start saving it immediately as a draft. This requires the model to support a "draft" state, and views to provide access to drafts. Hasjob's
JobPost
and Funnel'sProject
andProposal
all have this state, but currently will not save a draft until form validations pass. This too should be allowed. Draft state objects do not need to pass form validations, merely database validations (for example, field value length should be within the size limit for the column). Form validations must pass only when the document is submitted (leaving the draft state).When an existing document is edited, especially a published document, edits should be saved automatically, but should not be published, since we do not want a user's typing to show up in a published document while they are still typing. Users should be given the option to publish or discard their changes.
These use cases require the following pieces. For simplicity's sake, auto-save is only available for models that use UUIDs (in either a primary key or a supplementary unique key).
Empty documents
Models that support auto-save should allow for empty documents. This means all columns should have a default value or accept null values,
without exception
. Apps must make this change when implementing auto-save.In addition, the
name
column inBaseNameMixin
andBaseScopedNameMixin
-derived models must get a default value as it's required for URLs (BaseIdNameMixin
andBaseScopedIdNameMixin
don't have this problem as they have aurl_id
).name
can be set by default to the value ofsuuid
forUuidMixin
-derived models, with the caveat that whenself.name == self.suuid
, the name should be considered temporary and liable to change. A Coaster ticket is required for this change: themake_name
method currently (a) only checks ifname
is None, not if it matchessuuid
, and (b) generates name fromtitle
(orshort_title
), not fromsuuid
if title is missing.Draft model
A generic Draft model (tablename
draft
) holds unvalidated form data that has been autosaved. It contains these fields:id
(uuid
draft id)table
(table in which row is being edited)table_id
(uuid identifying row in table being edited)body
(json
representation of form being edited, created from{'form': MultiDict.items(multi=True)}
)revision
(uuid
orinteger
set randomly every time the draft is updated; PostgreSQL's internalxmin
is not used as system columns can expose internal workings)Drafts must always have a corresponding table and row, which is why empty documents are required for auto-saving new documents.
Views
Views will need distinct handling for GET and POST for both edit and new actions. POST operations will need to include an
autosave
flag (or distinct endpoint) to indicate that a form is being autosaved and not being submitted.Edit GET:
ModelForm(obj=obj, formdata=MultiDict(draft.body['form']))
(or justModelForm(obj=obj)
if there's no draft).'draft.revision'
field containingdraft.revision
, or blank value if there's no draft.Edit POST (with autosave flag):
request.form['draft.revision'] == draft.revision
. If they don't match, abort the operation and let front-end render an error to the user, requesting a page reload.revision
value and return it. Front-end updates'draft.revision'
input with this value.Edit POST (no autosave flag):
New GET:
'draft.revision'
input set to blank value.New POST (autosave):
name
if relevant).Draft previews
The View GET action on a document will not load a draft. Passing in
?draft=<uuid>
should load a matching draft wheredraft.id == <uuid>
,draft.table == model.__tablename__
, anddraft.table_id == obj.uuid
. However, it is dangerous to render such a draft as the form has not been validated.Draft preview from the
draft
table should not be supported.POST queue
When the front-end submits a revision, it may take a while to save. The front-end must do the following to avoid race conditions:
The text was updated successfully, but these errors were encountered: