-
Notifications
You must be signed in to change notification settings - Fork 53
Draft model and autosave on Project edit form #352
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
Changes from 50 commits
fd29b97
5e0c866
29ad207
468cc92
e20f3f0
86b5449
6db29ca
45cc1ec
21c55fe
3404dc3
2ba252a
13bb27f
2009366
4e3f037
e32971e
cbae311
22cae87
7154dd2
acdb758
beaa65f
bc25e3e
f1fe677
94f25bb
07b1c5f
c2d1bc0
4d31f12
d25d236
51ac07f
daf4270
5df6d23
3a7b9f2
34a5295
7f3ae6a
51e6dfd
ae27841
059430d
35715f2
2e773ed
e8497a1
8013eb2
7d460d3
193416a
2df3296
e075640
2ec4fb8
3d2b35f
a9f1933
3bf1d4e
aadd9f8
a0b660e
90a12ff
03be511
49e91eb
dea2a95
96af04a
b703846
82f87da
e5050fd
dd6499c
7866c6e
2ab6a44
9f1c3f5
5bb58c6
91f3d93
30d0b2e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
from sqlalchemy_utils import UUIDType | ||
from . import db, JsonDict, NoIdMixin, TimestampMixin | ||
|
||
__all__ = ['Draft'] | ||
|
||
|
||
class Draft(NoIdMixin, TimestampMixin, db.Model): | ||
iambibhas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Store for autosaved, unvalidated drafts on behalf of other models""" | ||
__tablename__ = 'draft' | ||
__uuid_primary_key__ = True | ||
iambibhas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
table = db.Column(db.UnicodeText, primary_key=True) | ||
table_row_id = db.Column(UUIDType(binary=False), primary_key=True) | ||
body = db.Column(JsonDict, nullable=False, server_default='{}') | ||
revision = db.Column(UUIDType(binary=False)) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,18 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
import unicodecsv | ||
from uuid import uuid4 | ||
from cStringIO import StringIO | ||
from flask import g, flash, redirect, Response, request, abort, current_app | ||
from werkzeug.datastructures import MultiDict | ||
from baseframe import _, forms | ||
from baseframe.forms import render_form | ||
from coaster.auth import current_auth | ||
from coaster.utils import getbool | ||
from coaster.views import jsonp, route, render_with, requires_permission, UrlForView, ModelView | ||
|
||
from .. import app, funnelapp, lastuser | ||
from ..models import db, Project, Section, Proposal, Rsvp, RSVP_STATUS | ||
from ..models import db, Project, Section, Proposal, Rsvp, Draft, RSVP_STATUS | ||
from ..forms import ProjectForm, SubprojectForm, RsvpForm, ProjectTransitionForm, ProjectBoxofficeForm | ||
from ..jobs import tag_locations, import_tickets | ||
from .proposal import proposal_headers, proposal_data, proposal_data_flat | ||
|
@@ -129,23 +132,81 @@ def csv(self): | |
headers=[('Content-Disposition', 'attachment;filename="{project}.csv"'.format(project=self.obj.name))]) | ||
|
||
@route('edit', methods=['GET', 'POST']) | ||
@render_with(json=True) | ||
@lastuser.requires_login | ||
@requires_permission('edit_project') | ||
def edit(self): | ||
if self.obj.parent_project: | ||
form = SubprojectForm(obj=self.obj, model=Project) | ||
else: | ||
form = ProjectForm(obj=self.obj, parent=self.obj.profile, model=Project) | ||
form.parent_project.query = Project.query.filter(Project.profile == self.obj.profile, Project.id != self.obj.id, Project.parent_project == None) # NOQA | ||
if request.method == 'GET' and not self.obj.timezone: | ||
form.timezone.data = current_app.config.get('TIMEZONE') | ||
if form.validate_on_submit(): | ||
form.populate_obj(self.obj) | ||
db.session.commit() | ||
flash(_("Your changes have been saved"), 'info') | ||
tag_locations.queue(self.obj.id) | ||
return redirect(self.obj.url_for(), code=303) | ||
return render_form(form=form, title=_("Edit project"), submit=_("Save changes")) | ||
if request.method == 'GET': | ||
# find draft if it exists | ||
draft = Draft.query.filter_by(table=Project.__tablename__, table_row_id=self.obj.uuid).first() | ||
initial_formdata = MultiDict(draft.body['form']) if draft is not None else None | ||
|
||
# initialize forms with draft initial formdata. | ||
# if no draft exists, initial_formdata is None. wtforms ignore formdata if it's None. | ||
if self.obj.parent_project: | ||
form = SubprojectForm(obj=self.obj, model=Project, formdata=initial_formdata) | ||
else: | ||
form = ProjectForm(obj=self.obj, parent=self.obj.profile, model=Project, formdata=initial_formdata) | ||
|
||
if not self.obj.timezone: | ||
form.timezone.data = current_app.config.get('TIMEZONE') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unrelated fix, but this should default to the user's timezone, not the app's. Use |
||
|
||
# if draft exists, add latest revision ID to the form | ||
last_revision_id = draft.revision if draft is not None else None | ||
|
||
return render_form(form=form, title=_("Edit project"), submit=_("Save changes"), autosave=True, last_revision_id=last_revision_id) | ||
elif request.method == 'POST': | ||
if getbool(request.args.get('form.autosave')): | ||
if 'form.revision' not in request.form: | ||
# as form.autosave is true, the form should have `form.revision` field even if it's empty | ||
return {'error': _("Form must contain a valid revision ID.")}, 400 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You will not have a revision if there was no draft when the user submitted. This can happen if the user submits without editing, or JavaScript is disabled. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a comment. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The first autosave will not have a revision. There is nothing to revise. |
||
|
||
client_revision = request.form['form.revision'] | ||
|
||
# find the last draft | ||
draft = Draft.query.get((Project.__tablename__, self.obj.uuid)) | ||
|
||
if draft is not None: | ||
if client_revision is None or (client_revision is not None and str(draft.revision) != client_revision): | ||
# draft exists, but the form did not send a revision ID, | ||
# OR revision ID sent by client does not match the last revision ID | ||
return {'error': _("There has been changes to this draft since you last edited it. Please reload.")}, 400 | ||
elif client_revision is not None and str(draft.revision) == client_revision: | ||
# revision ID sent my client matches, save updated draft data and update revision ID | ||
existing = MultiDict(draft.body['form']) | ||
incoming = MultiDict(request.form.items(multi=True)) | ||
for key in incoming.keys(): | ||
if existing[key] != incoming[key]: | ||
existing[key] = incoming[key] | ||
draft.body = {'form': existing} | ||
draft.revision = uuid4() | ||
else: | ||
# no draft exists, create one | ||
draft = Draft( | ||
table=Project.__tablename__, table_row_id=self.obj.uuid, | ||
body={'form': request.form.items(multi=True)}, revision=uuid4() | ||
) | ||
db.session.add(draft) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This requires a CSRF check. Without the check, a malicious third party website can dump junk content into the Draft model by performing a cross-site POST (which is what CSRF protects from).
|
||
db.session.commit() | ||
return {'revision': draft.revision} | ||
else: | ||
if self.obj.parent_project: | ||
form = SubprojectForm(obj=self.obj, model=Project) | ||
else: | ||
form = ProjectForm(obj=self.obj, parent=self.obj.profile, model=Project) | ||
if form.validate_on_submit(): | ||
form.populate_obj(self.obj) | ||
db.session.commit() | ||
flash(_("Your changes have been saved"), 'info') | ||
tag_locations.queue(self.obj.id) | ||
|
||
# find and delete drafts | ||
tdraft = Draft.query.get((Project.__tablename__, self.obj.uuid)) | ||
db.session.delete(tdraft) | ||
db.session.commit() | ||
return redirect(self.obj.url_for(), code=303) | ||
else: | ||
return render_form(form=form, title=_("Edit project"), submit=_("Save changes"), autosave=True) | ||
|
||
@route('boxoffice_data', methods=['GET', 'POST']) | ||
@lastuser.requires_login | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
"""draft model | ||
|
||
Revision ID: 94ce3a9b7a3a | ||
Revises: c3069d33419a | ||
Create Date: 2019-02-06 20:48:34.700795 | ||
|
||
""" | ||
|
||
revision = '94ce3a9b7a3a' | ||
down_revision = 'c3069d33419a' | ||
|
||
from alembic import op | ||
import sqlalchemy as sa | ||
|
||
from sqlalchemy_utils.types.uuid import UUIDType | ||
from coaster.sqlalchemy.columns import JsonDict | ||
|
||
|
||
def upgrade(): | ||
op.create_table('draft', | ||
sa.Column('created_at', sa.DateTime(), nullable=False), | ||
sa.Column('updated_at', sa.DateTime(), nullable=False), | ||
sa.Column('table', sa.UnicodeText(), nullable=False), | ||
sa.Column('table_row_id', UUIDType(binary=False), nullable=False), | ||
sa.Column('body', JsonDict(), server_default='{}', nullable=False), | ||
sa.Column('revision', UUIDType(binary=False), nullable=True), | ||
sa.PrimaryKeyConstraint('table', 'table_row_id') | ||
) | ||
|
||
|
||
def downgrade(): | ||
op.drop_table('draft') |
Uh oh!
There was an error while loading. Please reload this page.