Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autosave drafts #207

Open
jace opened this issue Jan 28, 2019 · 0 comments
Open

Autosave drafts #207

jace opened this issue Jan 28, 2019 · 0 comments

Comments

@jace
Copy link
Member

jace commented Jan 28, 2019

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:

  1. 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).

  2. 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:

  1. Dirty fields must be tracked for auto-save.
  2. Saves are triggered when a dirty field loses focus, or the user stops typing for one second.
  3. When a save is in progress, new save triggers are ignored.
  4. When a save succeeds, if there are any dirty fields, a new save is triggered.
  5. If a save operation fails due to a revision mismatch, the user is shown an error and asked to reload the page.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant