Skip to content

Commit e638edf

Browse files
authored
Merge pull request #50 from jeremiaheb/autosave-prototype
Autosave/draft prototype
2 parents 3f9dc45 + 181bc7f commit e638edf

39 files changed

+2598
-75160
lines changed

app/assets/javascripts/application.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,16 @@
2121
//= require bootstrap
2222
//= require jquery-ui/datepicker
2323
//= require jquery_nested_form
24+
//= require underscore-umd
2425
//= require_tree ../../../vendor/assets/javascripts
25-
//= require_tree .
2626
//
27+
//= require ./benthic_covers
28+
//= require ./boat_logs
29+
//= require ./coral_demographics
30+
//= require ./samples
31+
//= require ./static_pages
32+
//= require ./drafts
33+
//= require_self
2734

2835
// Set up our EA namespace for our functions
2936
var EA = {};

app/assets/javascripts/drafts.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
$(function() {
2+
// The main function that serializes the form and sends it to the save draft endpoint.
3+
//
4+
// It is wrapped in _.throttle so it is called on the trailing edge at most
5+
// once in a 3 second window. More casually this means no matter how many
6+
// times the function is called within a 3 second window, the code will only
7+
// actually be executed once at the end of the window.
8+
//
9+
// The draft is therefore saved periodically and after things have "settled,"
10+
// but not so often as to flood the server with requests.
11+
var currentDraftRequest;
12+
const saveDraftThrottled = _.throttle(function(e) {
13+
let $target = $(e.target);
14+
let $form = $target.closest("form");
15+
let draftsURL = $form.data("drafts-url");
16+
17+
if (!draftsURL) {
18+
return;
19+
}
20+
21+
if (currentDraftRequest) {
22+
currentDraftRequest.abort();
23+
currentDraftRequest = null;
24+
}
25+
26+
let params = $form.serializeArray();
27+
params.push({ name: "sequence", value: (new Date()).getTime() });
28+
params.push({ name: "focused_dom_id", value: $(document.activeElement).attr("id") });
29+
30+
currentDraftRequest = $.ajax({
31+
type: "PUT",
32+
url: draftsURL,
33+
data: $.param(params),
34+
headers: {
35+
"X-CSRF-Token": $("meta[name='csrf-token']").attr("content"),
36+
},
37+
withCredentials: true,
38+
success: function(data, textStatus, jqXHR) {
39+
$form.trigger("draft:success", [data, textStatus, jqXHR]);
40+
},
41+
error: function(jqXHR, textStatus, errorThrown) {
42+
if (textStatus === "abort") {
43+
return;
44+
}
45+
46+
$form.trigger("draft:error", [jqXHR, textStatus, errorThrown]);
47+
48+
// Retry. Rely on throttle to coalesce multiple calls into one retry.
49+
saveDraftThrottled(e);
50+
},
51+
});
52+
}, 3000, { leading: false });
53+
54+
// Save drafts for forms where data-drafts-url is present as an attribute.
55+
const $formsWithDraftsURLs = $("form[data-drafts-url]");
56+
$formsWithDraftsURLs.on("blur change", ":input", saveDraftThrottled)
57+
58+
// Trigger validation and cursor restoration for draft-enabled forms
59+
$formsWithDraftsURLs.each(function() {
60+
const $form = $(this);
61+
if ($form.data("draft-restored")) {
62+
$form.validate().form(); // trigger validation
63+
}
64+
if ($form.data("draft-focused-dom-id")) {
65+
$("#" + $form.data("draft-focused-dom-id")).focus();
66+
}
67+
});
68+
69+
// After the draft is discarded, reload the page to empty out all fields and
70+
// start from scratch cleanly.
71+
$("a.discard-draft-link").on("ajax:complete", function(e) {
72+
location.reload(true);
73+
});
74+
});

app/controllers/benthic_covers_controller.rb

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,17 @@ def show
4949
# GET /benthic_covers/new
5050
# GET /benthic_covers/new.json
5151
def new
52-
@benthic_cover = BenthicCover.new
53-
54-
@benthic_cover.build_invert_belt
55-
@benthic_cover.build_presence_belt
56-
@benthic_cover.point_intercepts.build
57-
@benthic_cover.build_rugosity_measure
52+
@draft = Draft.latest_for(diver_id: current_diver.id, model_klass: BenthicCover, model_id: nil)
53+
if @draft
54+
@benthic_cover = BenthicCover.new(@draft.model_attributes)
55+
else
56+
@benthic_cover = BenthicCover.new.tap do |b|
57+
b.build_invert_belt
58+
b.build_presence_belt
59+
b.point_intercepts.build
60+
b.build_rugosity_measure
61+
end
62+
end
5863

5964
respond_to do |format|
6065
format.html # new.html.erb
@@ -64,7 +69,12 @@ def new
6469

6570
# GET /benthic_covers/1/edit
6671
def edit
67-
@benthic_cover = BenthicCover.find(params[:id])
72+
@draft = Draft.latest_for(diver_id: current_diver.id, model_klass: BenthicCover, model_id: params[:id])
73+
if @draft
74+
@benthic_cover = BenthicCover.new(@draft.model_attributes.merge(id: params[:id]))
75+
else
76+
@benthic_cover = BenthicCover.find(params[:id])
77+
end
6878
end
6979

7080
# POST /benthic_covers
@@ -74,6 +84,8 @@ def create
7484

7585
respond_to do |format|
7686
if @benthic_cover.save
87+
Draft.destroy_for(diver_id: current_diver.id, model_klass: BenthicCover, model_id: nil)
88+
7789
format.html { redirect_to benthic_covers_path, notice: 'Benthic cover was successfully created.' }
7890
format.json { render json: @benthic_cover, status: :created, location: @benthic_cover }
7991
else
@@ -90,6 +102,8 @@ def update
90102

91103
respond_to do |format|
92104
if @benthic_cover.update(benthic_cover_params)
105+
Draft.destroy_for(diver_id: current_diver.id, model_klass: BenthicCover, model_id: @benthic_cover.id)
106+
93107
format.html { redirect_to benthic_covers_path, notice: 'Benthic cover was successfully updated.' }
94108
format.json { head :no_content }
95109
else
@@ -111,6 +125,31 @@ def destroy
111125
end
112126
end
113127

128+
# PUT /draft
129+
def draft
130+
# Handle intentional delete of draft data
131+
unless params[:benthic_cover].present?
132+
Draft.destroy_for(diver_id: current_diver.id, model_klass: BenthicCover, model_id: params[:id])
133+
134+
head :ok
135+
return
136+
end
137+
138+
draft = Draft.new(
139+
diver_id: current_diver.id,
140+
model_klass: BenthicCover,
141+
model_id: benthic_cover_params[:id],
142+
model_attributes: benthic_cover_params,
143+
sequence: params[:sequence],
144+
focused_dom_id: params[:focused_dom_id],
145+
)
146+
if draft.save
147+
render json: {}, status: :created
148+
else
149+
render json: draft.errors, status: :unprocessable_entity
150+
end
151+
end
152+
114153
private
115154

116155
def benthic_cover_params

app/controllers/coral_demographics_controller.rb

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,14 @@ def show
5050
# GET /coral_demographics/new
5151
# GET /coral_demographics/new.json
5252
def new
53-
@coral_demographic = CoralDemographic.new
54-
55-
@coral_demographic.demographic_corals.build
53+
@draft = Draft.latest_for(diver_id: current_diver.id, model_klass: CoralDemographic, model_id: nil)
54+
if @draft
55+
@coral_demographic = CoralDemographic.new(@draft.model_attributes)
56+
else
57+
@coral_demographic = CoralDemographic.new.tap do |c|
58+
c.demographic_corals.build
59+
end
60+
end
5661

5762
respond_to do |format|
5863
format.html # new.html.erb
@@ -62,7 +67,12 @@ def new
6267

6368
# GET /coral_demographics/1/edit
6469
def edit
65-
@coral_demographic = CoralDemographic.find(params[:id])
70+
@draft = Draft.latest_for(diver_id: current_diver.id, model_klass: CoralDemographic, model_id: params[:id])
71+
if @draft
72+
@coral_demographic = CoralDemographic.new(@draft.model_attributes.merge(id: params[:id]))
73+
else
74+
@coral_demographic = CoralDemographic.find(params[:id])
75+
end
6676
end
6777

6878
# POST /coral_demographics
@@ -72,6 +82,8 @@ def create
7282

7383
respond_to do |format|
7484
if @coral_demographic.save
85+
Draft.destroy_for(diver_id: current_diver.id, model_klass: CoralDemographic, model_id: nil)
86+
7587
format.html { redirect_to coral_demographics_path, notice: 'Coral demographic was successfully created.' }
7688
format.json { render json: @coral_demographic, status: :created, location: @coral_demographic }
7789
else
@@ -88,6 +100,8 @@ def update
88100

89101
respond_to do |format|
90102
if @coral_demographic.update(coral_demographic_params)
103+
Draft.destroy_for(diver_id: current_diver.id, model_klass: CoralDemographic, model_id: @coral_demographic.id)
104+
91105
format.html { redirect_to coral_demographics_path, notice: 'Coral demographic was successfully updated.' }
92106
format.json { head :no_content }
93107
else
@@ -109,6 +123,31 @@ def destroy
109123
end
110124
end
111125

126+
# PUT /draft
127+
def draft
128+
# Handle intentional delete of draft data
129+
unless params[:coral_demographic].present?
130+
Draft.destroy_for(diver_id: current_diver.id, model_klass: CoralDemographic, model_id: params[:id])
131+
132+
head :ok
133+
return
134+
end
135+
136+
draft = Draft.new(
137+
diver_id: current_diver.id,
138+
model_klass: CoralDemographic,
139+
model_id: coral_demographic_params[:id],
140+
model_attributes: coral_demographic_params,
141+
sequence: params[:sequence],
142+
focused_dom_id: params[:focused_dom_id],
143+
)
144+
if draft.save
145+
render json: {}, status: :created
146+
else
147+
render json: draft.errors, status: :unprocessable_entity
148+
end
149+
end
150+
112151
private
113152

114153
def coral_demographic_params

app/controllers/samples_controller.rb

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,14 @@ def show
5757
# GET /samples/new
5858
# GET /samples/new.json
5959
def new
60-
@sample = Sample.new
61-
62-
2.times { @sample.diver_samples.build }
63-
@sample.sample_animals.build
64-
60+
@draft = Draft.latest_for(diver_id: current_diver.id, model_klass: Sample, model_id: nil)
61+
if @draft
62+
@sample = Sample.new(@draft.model_attributes)
63+
else
64+
@sample = Sample.new.tap do |s|
65+
s.sample_animals.build
66+
end
67+
end
6568

6669
respond_to do |format|
6770
format.html # new.html.erb
@@ -71,8 +74,12 @@ def new
7174

7275
# GET /samples/1/edit
7376
def edit
74-
@sample = Sample.find(params[:id])
75-
77+
@draft = Draft.latest_for(diver_id: current_diver.id, model_klass: Sample, model_id: params[:id])
78+
if @draft
79+
@sample = Sample.new(@draft.model_attributes.merge(id: params[:id]))
80+
else
81+
@sample = Sample.find(params[:id])
82+
end
7683
end
7784

7885
# POST /samples
@@ -82,6 +89,8 @@ def create
8289

8390
respond_to do |format|
8491
if @sample.save
92+
Draft.destroy_for(diver_id: current_diver.id, model_klass: Sample, model_id: nil)
93+
8594
format.html { redirect_to samples_path, notice: 'Sample was successfully created.' }
8695
format.json { render json: @sample, status: :created, location: @sample }
8796
else
@@ -98,6 +107,8 @@ def update
98107

99108
respond_to do |format|
100109
if @sample.update(sample_params)
110+
Draft.destroy_for(diver_id: current_diver.id, model_klass: Sample, model_id: @sample.id)
111+
101112
format.html { redirect_to samples_path, notice: 'Sample was successfully updated.' }
102113
format.json { head :no_content }
103114
else
@@ -119,6 +130,31 @@ def destroy
119130
end
120131
end
121132

133+
# PUT /draft
134+
def draft
135+
# Handle intentional delete of draft data
136+
unless params[:sample].present?
137+
Draft.destroy_for(diver_id: current_diver.id, model_klass: Sample, model_id: params[:id])
138+
139+
head :ok
140+
return
141+
end
142+
143+
draft = Draft.new(
144+
diver_id: current_diver.id,
145+
model_klass: Sample,
146+
model_id: sample_params[:id],
147+
model_attributes: sample_params,
148+
sequence: params[:sequence],
149+
focused_dom_id: params[:focused_dom_id],
150+
)
151+
if draft.save
152+
render json: {}, status: :created
153+
else
154+
render json: draft.errors, status: :unprocessable_entity
155+
end
156+
end
157+
122158
def proofing_template
123159
@sample = Sample.find(params[:id])
124160

app/helpers/samples_helper.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
module SamplesHelper
2+
def diver_samples_primary_first(diver_samples)
3+
[
4+
diver_samples.find { |ds| ds.primary_diver? } || diver_samples.build(primary_diver: true, diver_id: current_diver.id),
5+
diver_samples.find { |ds| !ds.primary_diver? } || diver_samples.build(primary_diver: false),
6+
].compact
7+
end
28
end

app/models/ability.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def initialize(current_diver)
3535
elsif current_diver.role == 'manager'
3636
can :manage, [Sample, BenthicCover, CoralDemographic, BoatLog]
3737
elsif current_diver.role == 'diver'
38+
can :draft, [Sample, BenthicCover, CoralDemographic]
3839
can :create, [Sample, BenthicCover, CoralDemographic]
3940
can :read, [Sample, BenthicCover, CoralDemographic]
4041
can :destroy, [Sample, BenthicCover, CoralDemographic]
@@ -48,6 +49,5 @@ def initialize(current_diver)
4849
coral_demographic.try(:myId) == current_diver.id
4950
end
5051
end
51-
5252
end
5353
end

app/models/diver_sample.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ class DiverSample < ActiveRecord::Base
55
scope :primary, lambda { where(primary_diver: true) }
66
scope :secondary, lambda { where(primary_diver: false) }
77

8+
validates :primary_diver, inclusion: [true, false]
89
validates :diver_id, :presence => true
910
end

0 commit comments

Comments
 (0)