Bugfixes: session management, new entry creation.

* Changed the cookie Path value to allow the cookie to be reused for the
  domain, not just ths `/ts_api` path. This allows the user to refresh the page
  and reuse their existing session as long as it is not stale.
* Fixed a bug in the `ts_json:ejson_to_record_strict/2` function. It was
  expecting a record out of `ts_json:ejson_to_record/2` but that function
  returns a tuple with the record and extended data. Because of the way
  `ejson_to_record_strict` uses case statements to check for specific values it
  was still passing the parsed record and data through, but all the checks were
  being bypassed.
* Fixed bugs in the `index.yaws` bootstrap code for the case where the user
  already has a valid session.
* Added `urlRoot` functions to the Backbone model definitions.
* Changed the behavior of the new entry creation method. We were trying to
  fetch just updated attributes from the server for the model we created, but
  we were pulling all the entries due to the URL backbone was using. This led
  to the new client-side model having all the previous entry models as
  attributes. Ideally we would fix the fetch so that only the one model is
  requested from the server, but we run into a catch-22 because the lookup id
  is not know by the client as it is generated on the server-side. For now I
  have changed this behavior so that we still pull all entries, but we pull
  them into the collection. The collection is then smart enough to update the
  entries that have changed (namely the new one). The server returns the newly
  created entry attributes in response to the POST request that the client
  makes initially, so when I have more time to work on this I plan to do away
  with the fetch after create, and just pull in the data from the server's
  response.
* Changed formatting.
This commit is contained in:
Jonathan Bernard 2013-10-22 15:32:22 +00:00
parent 0278179452
commit a3c55e918e
4 changed files with 145 additions and 215 deletions

View File

@ -190,7 +190,7 @@ do_login(YArg) ->
{CookieVal, _Session} = ts_api_session:new(Username), {CookieVal, _Session} = ts_api_session:new(Username),
[{header, {set_cookie, io_lib:format( [{header, {set_cookie, io_lib:format(
"ts_api_session=~s; Path=/ts_api", "ts_api_session=~s; Path=/",
[CookieVal])}}, [CookieVal])}},
{header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]}, {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
{header, ["Access-Control-Allow-Credentials: ", "true"]}, {header, ["Access-Control-Allow-Credentials: ", "true"]},
@ -269,7 +269,7 @@ put_user(YArg, Username) ->
throw(make_json_400(YArg, [{request_error, InputError}])) throw(make_json_400(YArg, [{request_error, InputError}]))
end, end,
% update the record (we do not support creating users via the API right now % update the record (we do not support creating users via the API right now)
{ok, UpdatedRec} = ts_user:update(UR, ExtData), {ok, UpdatedRec} = ts_user:update(UR, ExtData),
% return a 200 % return a 200

View File

@ -81,31 +81,31 @@ ejson_to_record(Empty, {struct, EJSONFields}) ->
construct_record(Empty, EJSONFields, []). construct_record(Empty, EJSONFields, []).
ejson_to_record(Empty, Ref, EJSON) -> ejson_to_record(Empty, Ref, EJSON) ->
Constructed = ejson_to_record(Empty, EJSON), {Constructed, ExtData} = ejson_to_record(Empty, EJSON),
setelement(2, Constructed, Ref). {setelement(2, Constructed, Ref), ExtData}.
ejson_to_record_strict(Empty=#ts_user{}, EJSON) -> ejson_to_record_strict(Empty=#ts_user{}, EJSON) ->
Constructed = ejson_to_record(Empty, EJSON), {Constructed, ExtData} = ejson_to_record(Empty, EJSON),
case Constructed of case Constructed of
#ts_user{name = undefined} -> throw("Missing user 'name' field."); #ts_user{name = undefined} -> throw("Missing user 'name' field.");
#ts_user{email = undefined} -> throw("Missing user 'email' field."); #ts_user{email = undefined} -> throw("Missing user 'email' field.");
#ts_user{join_date = undefined} -> #ts_user{join_date = undefined} ->
throw("Missing user 'join_date' field."); throw("Missing user 'join_date' field.");
_Other -> Constructed _Other -> {Constructed, ExtData}
end; end;
ejson_to_record_strict(Empty=#ts_timeline{}, EJSON) -> ejson_to_record_strict(Empty=#ts_timeline{}, EJSON) ->
Constructed = ejson_to_record(Empty, EJSON), {Constructed, ExtData} = ejson_to_record(Empty, EJSON),
case Constructed of case Constructed of
#ts_timeline{created = undefined} -> #ts_timeline{created = undefined} ->
throw("Missing timeline 'created' field."); throw("Missing timeline 'created' field.");
#ts_timeline{desc = undefined} -> #ts_timeline{desc = undefined} ->
throw("Missing timeline 'description' field."); throw("Missing timeline 'description' field.");
_Other -> Constructed _Other -> {Constructed, ExtData}
end; end;
ejson_to_record_strict(Empty=#ts_entry{}, EJSON) -> ejson_to_record_strict(Empty=#ts_entry{}, EJSON) ->
Constructed = ejson_to_record(Empty, EJSON), {Constructed, ExtData} = ejson_to_record(Empty, EJSON),
case Constructed of case Constructed of
#ts_entry{timestamp = undefined} -> #ts_entry{timestamp = undefined} ->
throw("Missing timelne 'timestamp' field."); throw("Missing timelne 'timestamp' field.");
@ -113,12 +113,12 @@ ejson_to_record_strict(Empty=#ts_entry{}, EJSON) ->
throw("Missing timeline 'mark' field."); throw("Missing timeline 'mark' field.");
#ts_entry{notes = undefined} -> #ts_entry{notes = undefined} ->
throw("Missing timeline 'notes' field/"); throw("Missing timeline 'notes' field/");
_Other -> Constructed _Other -> {Constructed, ExtData}
end. end.
ejson_to_record_strict(Empty, Ref, EJSON) -> ejson_to_record_strict(Empty, Ref, EJSON) ->
Constructed = ejson_to_record_strict(Empty, EJSON), {Constructed, ExtData} = ejson_to_record_strict(Empty, EJSON),
setelement(2, Constructed, Ref). {setelement(2, Constructed, Ref), ExtData}.
construct_record(User=#ts_user{}, [{Key, Value}|Fields], ExtData) -> construct_record(User=#ts_user{}, [{Key, Value}|Fields], ExtData) ->
case Key of case Key of

View File

@ -28,33 +28,38 @@ out(YArg) ->
Username = element(2, Session), Username = element(2, Session),
% get the user % get the user
{content, _, UserJSON} = ts_api:get_user(YArg, Username), UserResp = ts_api:get_user(YArg, Username),
{content, _, UserJSON} = lists:keyfind(content, 1, UserResp),
UserRecord = ts_user:lookup(Username), UserRecord = ts_user:lookup(Username),
% get the timelines % get the timelines
{content, _, TimelineListJSON} = ts_api:list_timelines(YArg, Username), TimelineResp = ts_api:list_timelines(YArg, Username),
{content, _, TimelineListJSON} = lists:keyfind(content, 1, TimelineResp),
% get the selected timeline % get the last used timeline if there is one.
SelectedTimeline = case lists:keyfind( SelectedTimeline = case ts_ext_data:get_property(Username, last_timeline) of
selected_timeline, 1, element(8, UserRecord)) of
false -> ts_timeline:list(Username, 0, 1); not_set -> ts_timeline:list(Username, 0, 1);
T -> T T -> T
end, end,
% get entries for this timeline % get entries for this timeline
{content, _, EntryListJSON} = EntriesResp =
ts_api:list_entries(YArg, Username, SelectedTimeline), ts_api:list_entries(YArg, Username, SelectedTimeline),
{content, _, EntryListJSON} = lists:keyfind(content, 1, EntriesResp),
{html, f( {html, f(
"function bootstrap() {~n" "function bootstrap() {~n"
" var data = {};~n" " var data = {};~n"
" data.user = ~p;~n" " data.user = ~s;~n"
" data.timelines = ~p;~n" " data.timelines = ~s;~n"
" data.initialTimelineId = ~p;~n" " data.initialTimelineId = ~p;~n"
" data.entries = ~p;~n" " data.entries = ~s;~n"
" return data;~n" " return data;~n"
"};", "};",
[UserJSON, TimelineListJSON, SelectedTimeline, EntryListJSON])} [lists:flatten(UserJSON), lists:flatten(TimelineListJSON),
SelectedTimeline, lists:flatten(EntryListJSON)])}
end. end.
</erl> </erl>
</script> </script>

View File

@ -1,3 +1,8 @@
/**
* # `ts.js`: TimeStamper Web UI Client-side
* @author Jonathan Bernard <jdb@jdb-labs.com>
**/
// TimeStamper namespace // TimeStamper namespace
var TS = {}; var TS = {};
@ -8,24 +13,27 @@ $(document).ready(function(){
/* Entry model. /* Entry model.
* Attributes * Attributes
* - user_id
* - timeline_id
* - id * - id
* - mark * - mark
* - uuid
* - notes * - notes
* - timestamp * - timestamp
*/ */
TS.EntryModel = Backbone.Model.extend({ TS.EntryModel = Backbone.Model.extend({
initialize: function(attrs, options) {
_.bindAll(this, 'get', 'set', 'urlRoot'); },
get: function(attribute) { get: function(attribute) {
if (attribute == "timestamp") { if (attribute == "timestamp") {
if (!this.timestampDate) { if (!this.timestampDate) {
this.timestampDate = new Date( this.timestampDate = new Date(
Backbone.Model.prototype.get.call(this, attribute)); Backbone.Model.prototype.get.call(this, attribute)); }
} return this.timestampDate; }
return this.timestampDate; else {
} else { return Backbone.Model.prototype.get.call(this, attribute); } },
return Backbone.Model.prototype.get.call(this, attribute);
}
},
set: function(attributes, options) { set: function(attributes, options) {
var attrsToSet = {} var attrsToSet = {}
@ -33,44 +41,42 @@ $(document).ready(function(){
if (key == "timestamp") { if (key == "timestamp") {
if (val instanceof Date) { if (val instanceof Date) {
this.timestampDate = val; this.timestampDate = val;
attrsToSet.timestamp = dateToJSON(val); attrsToSet.timestamp = dateToJSON(val); }
} else { else {
this.timestampDate = new Date(val); this.timestampDate = new Date(val);
attrsToSet.timestamp = dateToJSON(this.timestampDate); attrsToSet.timestamp = dateToJSON(this.timestampDate); } }
} else { attrsToSet[key] = val; } });
} else {
attrsToSet[key] = val;
}
});
return Backbone.Model.prototype.set.call(this, attrsToSet, options); return Backbone.Model.prototype.set.call(
} this, attrsToSet, options); },
});
urlRoot: function() { return '/ts_api/entries/' + this.get('user_id') +
'/' + this.get('timeline_id') + '/'; } });
/* Timeline model. /* Timeline model.
* Attributes: * Attributes:
* - user_id
* - id * - id
* - description * - description
* - created * - created
*/ */
TS.TimelineModel = Backbone.Model.extend({ TS.TimelineModel = Backbone.Model.extend({
urlRoot: function() {
return '/ts_api/timelines/' + this.get('user_id') + '/'; },
}); initialze: function(attrs, options) { _.bindAll(this, 'urlRoot'); } });
/* User model. /* User model.
* Attributes: * Attributes:
* - username * - id
* - fullname * - name
* - email * - email
* - join_date * - join_date
*/ */
TS.UserModel = Backbone.Model.extend({ TS.UserModel = Backbone.Model.extend({
url: function() { return '/ts_api/users/' + this.get('id'); }, url: function() { return '/ts_api/users/' + this.get('id'); },
initialize: function(attrs, options) { initialize: function(attrs, options) { _.bindAll(this, 'url'); } });
_.bind(this, 'url');
}
});
TS.EntryList = Backbone.Collection.extend({ TS.EntryList = Backbone.Collection.extend({
model: TS.EntryModel, model: TS.EntryModel,
@ -79,17 +85,23 @@ $(document).ready(function(){
initialize: function(model, options) { initialize: function(model, options) {
if (options.timelineModel == undefined) { if (options.timelineModel == undefined) {
throw "Cannot create an EntryList without a TimelineModel reference." throw "Cannot create an EntryList without a " +
} else { this.timelineModel = options.timelineModel; } "TimelineModel reference." }
else { this.timelineModel = options.timelineModel; }
_.bindAll(this, "url"); _.bindAll(this, "url", "create"); },
},
/*create: function(model, options) {
if (!(model instanceof Backbone.Model)) {
model.user_id = this.timelineModel.get('user_id');
model.timeline_id = this.timelineModel.get('timeline_id'); }
else {
model.set('user_id') = this.timelineModel.get('user_id')
},*/
url: function() { url: function() {
return "/ts_api/entries/" + this.timelineModel.get('user_id') + "/" return "/ts_api/entries/" + this.timelineModel.get('user_id') + "/"
+ this.timelineModel.get('id'); + this.timelineModel.get('id'); } });
}
});
TS.TimelineList = Backbone.Collection.extend({ TS.TimelineList = Backbone.Collection.extend({
model: TS.TimelineModel, model: TS.TimelineModel,
@ -99,17 +111,12 @@ $(document).ready(function(){
throw "Cannot create a TimelineList without a UserModel reference."; throw "Cannot create a TimelineList without a UserModel reference.";
} else { this.user = options.user; } } else { this.user = options.user; }
_.bindAll(this, 'url'); _.bindAll(this, 'url'); },
},
comparator: function(timeline) { comparator: function(timeline) { return timeline.get('id'); },
return timeline.get('id');
},
url: function() { url: function() {
return "/ts_api/timelines/" + this.user.get('id'); return "/ts_api/timelines/" + this.user.get('id'); } });
}
});
// ======== DEFINE VIEWS ========// // ======== DEFINE VIEWS ========//
@ -135,8 +142,7 @@ $(document).ready(function(){
"keypress .notes-input" : "saveOnCtrlEnter", "keypress .notes-input" : "saveOnCtrlEnter",
"blur .mark-input" : "save", "blur .mark-input" : "save",
"blur .timestamp-input" : "save", "blur .timestamp-input" : "save",
"blur .notes-input" : "save" "blur .notes-input" : "save" },
},
initialize: function(options) { initialize: function(options) {
_.bindAll(this, 'render', 'editTImestamp', 'editMark', 'update', _.bindAll(this, 'render', 'editTImestamp', 'editMark', 'update',
@ -148,8 +154,7 @@ $(document).ready(function(){
this.model.bind('change', this.update); this.model.bind('change', this.update);
this.model.view = this; this.model.view = this;
this.nextModel = options.nextModel; this.nextModel = options.nextModel; },
},
/** /**
* Refresh the display based on the model replacing the existing * Refresh the display based on the model replacing the existing
@ -162,8 +167,7 @@ $(document).ready(function(){
// invalidate the notes display cache // invalidate the notes display cache
this.notesCache = false; this.notesCache = false;
return this; return this; },
},
/** /**
* Refresh the display based on the model using the existing DOM * Refresh the display based on the model using the existing DOM
@ -178,27 +182,23 @@ $(document).ready(function(){
this.$('.duration').text(data.duration); this.$('.duration').text(data.duration);
this.$('.notes-text').html(this.renderNotes(data.notes)); this.$('.notes-text').html(this.renderNotes(data.notes));
this.$('.notes-input').val(data.notes); this.$('.notes-input').val(data.notes);
return this; return this; },
},
renderNotes: function(source) { renderNotes: function(source) {
if (!this.notesCache) { if (!this.notesCache) {
this.notesCache = this.markdownConverter.makeHtml(source); } this.notesCache = this.markdownConverter.makeHtml(source); }
return this.notesCache return this.notesCache },
},
editMark: function() { editMark: function() {
$(this.el).addClass('edit-mark'); $(this.el).addClass('edit-mark');
this.$('.mark-input').focus(); this.$('.mark-input').focus();
return this; return this; },
},
editTimestamp: function() { editTimestamp: function() {
$(this.el).addClass('edit-timestamp'); $(this.el).addClass('edit-timestamp');
this.$('timestamp-input').focus(); this.$('timestamp-input').focus();
return this; return this; },
},
editNotes: function() { editNotes: function() {
// invalidate notes HTML cache // invalidate notes HTML cache
@ -209,8 +209,7 @@ $(document).ready(function(){
// focus input // focus input
this.$('.notes-input').focus(); this.$('.notes-input').focus();
return this; return this; },
},
/** /**
* Translate the model data into a form suitable to be displayed. * Translate the model data into a form suitable to be displayed.
@ -224,8 +223,7 @@ $(document).ready(function(){
data.start = this.formatStart(tsDate); data.start = this.formatStart(tsDate);
data.duration = this.formatDuration(this.model, this.nextModel); data.duration = this.formatDuration(this.model, this.nextModel);
data.notes = data.notes ? data.notes : '*No notes for this entry.*'; data.notes = data.notes ? data.notes : '*No notes for this entry.*';
return data; return data; },
},
/** Save and close editable fields. */ /** Save and close editable fields. */
save: function() { save: function() {
@ -235,17 +233,12 @@ $(document).ready(function(){
notes: this.$('.notes-input').val()}); notes: this.$('.notes-input').val()});
this.$('.notes-text').html(this.renderNotes(this.model.get('notes'))); this.$('.notes-text').html(this.renderNotes(this.model.get('notes')));
$(this.el).removeClass('edit-mark edit-timestamp edit-notes'); $(this.el).removeClass('edit-mark edit-timestamp edit-notes'); },
},
/** Event handler for keypresses on entry input fields. */ /** Event handler for keypresses on entry input fields. */
saveOnEnter: function(e) { saveOnEnter: function(e) { if(e.keyCode == 13) { this.save(); } },
if(e.keyCode == 13) { this.save(); }
},
saveOnCtrlEnter: function(e) { saveOnCtrlEnter: function(e) { if (e.keyCode == 10) { this.save(); } },
if (e.keyCode == 10) { this.save(); }
},
/** /**
* Get the display-able start time from the entry timestamp. * Get the display-able start time from the entry timestamp.
@ -263,8 +256,7 @@ $(document).ready(function(){
// 12 hour // 12 hour
var hour = startDate.getHours() % 12; var hour = startDate.getHours() % 12;
return (hour == 0 ? 12 : hour) + ":" + startDate.getMinutes() + return (hour == 0 ? 12 : hour) + ":" + startDate.getMinutes() +
" " + (startDate.getHours() > 11 ? "pm" : "am"); " " + (startDate.getHours() > 11 ? "pm" : "am"); },
},
/** /**
* Get the duration of the entry based on this entry's timestamp and * Get the duration of the entry based on this entry's timestamp and
@ -296,8 +288,7 @@ $(document).ready(function(){
return (day > 0 ? day + "d " : "") + return (day > 0 ? day + "d " : "") +
(hr > 0 ? hr + "hr " : "") + (hr > 0 ? hr + "hr " : "") +
min + "m "; min + "m "; },
},
showNotes: function() { showNotes: function() {
if (!this.notesCache) { if (!this.notesCache) {
@ -305,22 +296,17 @@ $(document).ready(function(){
this.renderNotes(this.model.get('notes'))); } this.renderNotes(this.model.get('notes'))); }
this.$('.notes').slideDown(); this.$('.notes').slideDown();
$(this.el).addClass('show-notes'); $(this.el).addClass('show-notes'); },
},
hideNotes: function() { hideNotes: function() {
this.$('.notes').slideUp(); this.$('.notes').slideUp();
$(this.el).removeClass('show-notes'); $(this.el).removeClass('show-notes'); } });
}
});
TS.EntryListView = Backbone.View.extend({ TS.EntryListView = Backbone.View.extend({
el: $("#entry-list"), el: $("#entry-list"),
events: { events: { "keypress #new-entry-input" : "createNewEntryOnEnter" },
"keypress #new-entry-input" : "createNewEntryOnEnter"
},
initialize: function() { initialize: function() {
_.bindAll(this, 'addOne', 'createNewEntry', 'render', 'renderOne'); _.bindAll(this, 'addOne', 'createNewEntry', 'render', 'renderOne');
@ -328,15 +314,13 @@ $(document).ready(function(){
this.collection.bind('refresh', this.render); this.collection.bind('refresh', this.render);
this.collection.view = this; this.collection.view = this;
this.entryContainer = this.$("#entries") this.entryContainer = this.$("#entries")
this.markdownConverter = new Showdown.converter(); this.markdownConverter = new Showdown.converter(); },
},
addOne: function(entry) { addOne: function(entry) {
var lastEntry = this.collection.at(this.collection.length - 2); var lastEntry = this.collection.at(this.collection.length - 2);
lastEntry.view.nextModel = entry; lastEntry.view.nextModel = entry;
lastEntry.view.update(); lastEntry.view.update();
this.renderOne(entry, null); this.renderOne(entry, null); },
},
renderOne: function(entry, nextEntry) { renderOne: function(entry, nextEntry) {
// exclude if any exclusion RegExps match // exclude if any exclusion RegExps match
@ -357,9 +341,7 @@ $(document).ready(function(){
// hide it if excluded // hide it if excluded
if (excluded) { if (excluded) {
$(el).fadeOut('slow'); $(el).fadeOut('slow');
$(el).addClass('excluded'); $(el).addClass('excluded'); } },
}
},
createNewEntryOnEnter: function(e) { createNewEntryOnEnter: function(e) {
@ -371,13 +353,12 @@ $(document).ready(function(){
// create the mark. Immediately fetch to get server-side timestamp // create the mark. Immediately fetch to get server-side timestamp
this.collection.create({mark: entryMark, this.collection.create({mark: entryMark,
notes: '', notes: '',
timestamp: new Date()}).fetch(); timestamp: new Date()});
this.collection.fetch();
// clear the input for the next entry // clear the input for the next entry
this.$("#new-entry-input").val(""); this.$("#new-entry-input").val(""); } },
}
},
render: function() { render: function() {
@ -416,12 +397,9 @@ $(document).ready(function(){
if (currentDay.getDate() != entry.get("timestamp").getDate()) { if (currentDay.getDate() != entry.get("timestamp").getDate()) {
this.topSeparator.after(ich.daySeparatorTemplate( this.topSeparator.after(ich.daySeparatorTemplate(
{separatorLabel: this.formatDaySeparator(today, currentDay)})); {separatorLabel: this.formatDaySeparator(today, currentDay)}));
currentDay = entry.get('timestamp'); currentDay = entry.get('timestamp'); }
}
this.renderOne(entry, nextEntry); this.renderOne(entry, nextEntry); } },
}
},
formatDaySeparator: function(today, labelDay) { formatDaySeparator: function(today, labelDay) {
@ -456,10 +434,7 @@ $(document).ready(function(){
else if (dayDiff == 1) { return "Yesterday"; } else if (dayDiff == 1) { return "Yesterday"; }
// today // today
else if (dayDiff == 0) { return "Today"; } else if (dayDiff == 0) { return "Today"; } } });
}
});
TS.TimelineListView = Backbone.View.extend({ TS.TimelineListView = Backbone.View.extend({
el: $("#timeline"), el: $("#timeline"),
@ -471,65 +446,53 @@ $(document).ready(function(){
"dblclick .timeline-desc" : "editDesc", "dblclick .timeline-desc" : "editDesc",
"keypress .timeline-id-input" : "saveOnEnter", "keypress .timeline-id-input" : "saveOnEnter",
"keypress .timeline-desc-input" : "saveOnEnter", "keypress .timeline-desc-input" : "saveOnEnter",
"click .new-timeline-link" : "showNewTimelineDialog" "click .new-timeline-link" : "showNewTimelineDialog" },
},
initialize: function(options) { initialize: function(options) {
_.bindAll(this, 'render', 'renderOne', 'editId', _.bindAll(this, 'render', 'renderOne', 'editId',
'editDesc', 'saveOnEnter', 'save'); 'editDesc', 'saveOnEnter', 'save');
if (options.initialTimelineId == undefined) { if (options.initialTimelineId == undefined) {
throw "Can not create a TimelineListView without an initial timeline." throw "Can not create a TimelineListView without an " +
} else { "initial timeline." }
this.selectedModel = this.collection.get(options.initialTimelineId); else {
} this.selectedModel =
this.collection.get(options.initialTimelineId); }
this.collection.bind('add', this.renderOne); this.collection.bind('add', this.renderOne);
this.collection.bind('refresh', this.render); this.collection.bind('refresh', this.render); },
},
renderOne: function(timeline) { renderOne: function(timeline) {
this.$('.drop-menu-items').append( this.$('.drop-menu-items').append(
ich.timelineLinkTemplate(timeline.toJSON())); ich.timelineLinkTemplate(timeline.toJSON())); },
},
render: function() { render: function() {
// render the basic template // render the basic template
$(this.el).html(ich.timelineTemplate(this.selectedModel.toJSON())); $(this.el).html(ich.timelineTemplate(this.selectedModel.toJSON()));
// render the selection list // render the selection list
_.each(this.collection.without([this.selectedModel]), this.renderOne); _.each(this.collection.without([this.selectedModel]), this.renderOne); },
},
editId: function() { editId: function() {
$(this.el).addClass('edit-id'); $(this.el).addClass('edit-id');
this.$('.timeline-id-input').focus(); this.$('.timeline-id-input').focus();
return this; return this; },
},
editDesc: function() { editDesc: function() {
$(this.el).addClass('edit-desc'); $(this.el).addClass('edit-desc');
this.$('.timeline-desc-input').focus(); this.$('.timeline-desc-input').focus();
return this; return this; },
},
save: function() { save: function() {
this.selectedModel.save({ this.selectedModel.save({
id: this.$('.timeline-id-input').val(), id: this.$('.timeline-id-input').val(),
description: this.$('.timeline-desc-input').val()}); description: this.$('.timeline-desc-input').val()});
$(this.el).removeClass('edit-id edit-desc'); $(this.el).removeClass('edit-id edit-desc');
this.render(); this.render(); },
},
showNewTimelineDialog: function() { showNewTimelineDialog: function() { TS.app.newTimelineDialog.show(); },
TS.app.newTimelineDialog.show();
},
saveOnEnter: function(e) { saveOnEnter: function(e) { if (e.keyCode == 13) { this.save(); } } });
if (e.keyCode == 13) { this.save(); }
}
});
TS.UserView = Backbone.View.extend({ TS.UserView = Backbone.View.extend({
@ -539,44 +502,34 @@ $(document).ready(function(){
events: { events: {
'dblclick .fullname': 'editFullname', 'dblclick .fullname': 'editFullname',
'keypress .fullname-input': 'saveOnEnter' 'keypress .fullname-input': 'saveOnEnter' },
},
initialize: function() { initialize: function() {
_.bindAll(this, 'render', 'save', 'editFullname', 'saveOnEnter'); _.bindAll(this, 'render', 'save', 'editFullname', 'saveOnEnter');
this.model.bind('change', this.render); this.model.bind('change', this.render);
this.model.view = this; this.model.view = this; },
},
render: function() { render: function() {
$(this.el).html(ich.userTemplate(this.model.toJSON())); $(this.el).html(ich.userTemplate(this.model.toJSON()));
return this; return this; },
},
editFullname: function() { editFullname: function() {
$(this.el).addClass('edit-fullname'); $(this.el).addClass('edit-fullname');
this.$('.fullname-input').focus(); this.$('.fullname-input').focus();
return this; return this; },
},
save: function() { save: function() {
this.model.set({name: this.$('fullname-input').val()}); this.model.set({name: this.$('fullname-input').val()});
this.model.save(); this.model.save();
$(this.el).removeClass('edit-fullname'); $(this.el).removeClass('edit-fullname'); },
},
saveOnEnter: function(e) { saveOnEnter: function(e) { if (e.keyCode == 13) this.save(); } });
if (e.keyCode == 13) this.save();
}
});
TS.AppView = Backbone.View.extend({ TS.AppView = Backbone.View.extend({
el: $("body"), el: $("body"),
events: { events: { 'click #timeline .drop-menu-items a': 'selectTimeline' },
'click #timeline .drop-menu-items a': 'selectTimeline'
},
initialize: function() { initialize: function() {
@ -593,12 +546,12 @@ $(document).ready(function(){
else { else {
// this is async (waiting for user input) // this is async (waiting for user input)
this.loginDialog.authenticate(function() { this.loginDialog.authenticate(function() {
appThis.initializeData(appThis.loadInitialData())}); appThis.initializeData(appThis.loadInitialData())}); } },
}
},
initializeData: function(data) { initializeData: function(data) {
TS.app = this;
// create user data // create user data
this.user = {}; this.user = {};
this.user.model = new TS.UserModel(data.user); this.user.model = new TS.UserModel(data.user);
@ -606,8 +559,7 @@ $(document).ready(function(){
// create timeline models from the bootstrapped data // create timeline models from the bootstrapped data
var tlModels = _.map(data.timelines, function(timeline) { var tlModels = _.map(data.timelines, function(timeline) {
return new TS.TimelineModel(timeline); return new TS.TimelineModel(timeline); });
});
// create the timeline list collection // create the timeline list collection
this.timelines = {}; this.timelines = {};
@ -623,8 +575,7 @@ $(document).ready(function(){
// create entry models from the bootstrapped data // create entry models from the bootstrapped data
var entryModels = _.map(data.entries, function(entry) { var entryModels = _.map(data.entries, function(entry) {
return new TS.EntryModel(entry); return new TS.EntryModel(entry); });
});
// create the entry collection // create the entry collection
this.entries = {}; this.entries = {};
@ -636,10 +587,7 @@ $(document).ready(function(){
// render views // render views
this.user.view.render(); this.user.view.render();
this.timelines.view.render(); this.timelines.view.render();
this.entries.view.render(); this.entries.view.render(); },
},
loadInitialData: function() { loadInitialData: function() {
// assume we are authenticated // assume we are authenticated
@ -658,8 +606,7 @@ $(document).ready(function(){
data.initialTimelineId, data.initialTimelineId,
async: false}).responseText); async: false}).responseText);
return data; return data; },
},
selectTimeline: function(e) { selectTimeline: function(e) {
if (e) { if (e) {
@ -680,32 +627,24 @@ $(document).ready(function(){
this.timelines.view.render(); this.timelines.view.render();
// refresh EntryList records // refresh EntryList records
this.entries.collection.fetch() this.entries.collection.fetch() } } });
}
}
});
TS.LoginView = Backbone.View.extend({ TS.LoginView = Backbone.View.extend({
el: $("#login"), el: $("#login"),
events: { events: {
"keypress #password-input" : "loginOnEnter", "keypress #password-input" : "loginOnEnter",
"click #login-button a" : "doLogin" "click #login-button a" : "doLogin" },
},
initialize: function() { initialize: function() {
_.bindAll(this, 'authenticate', 'doLogin', 'hide', 'loginOnEnter', _.bindAll(this, 'authenticate', 'doLogin', 'hide', 'loginOnEnter',
'show'); 'show'); },
},
action: function() {}, action: function() {},
authenticate: function(nextAction) { authenticate: function(nextAction) {
this.action = nextAction; this.action = nextAction;
this.show(); this.show(); },
},
doLogin: function(){ doLogin: function(){
var viewThis = this; var viewThis = this;
@ -725,46 +664,35 @@ $(document).ready(function(){
// we should check that, FIXME // we should check that, FIXME
var tips = $(".validate-tips"); var tips = $(".validate-tips");
tips.text("Incorrect username/password combination."); tips.text("Incorrect username/password combination.");
tips.slideDown(); tips.slideDown(); },
},
success: function(data, textStatus, jqXHR) { success: function(data, textStatus, jqXHR) {
viewThis.hide(); viewThis.hide();
viewThis.action(); viewThis.action(); } }); },
}
});
},
hide: function() { $(this.el).addClass('hidden'); }, hide: function() { $(this.el).addClass('hidden'); },
show: function() { show: function() {
$(this.el).removeClass('hidden'); $(this.el).removeClass('hidden');
this.$("#username-input").focus(); this.$("#username-input").focus(); },
},
loginOnEnter: function(e) { loginOnEnter: function(e) {
if (e.keyCode == 13) { this.doLogin(); } if (e.keyCode == 13) { this.doLogin(); } } });
}
});
TS.NewTimelineView = Backbone.View.extend({ TS.NewTimelineView = Backbone.View.extend({
el: $("#new-timeline"), el: $("#new-timeline"),
events: { events: {
"click #new-timeline-create a" : "createTimeline", "click #new-timeline-create a" : "createTimeline",
"click #new-timeline-cancel a" : "hide" "click #new-timeline-cancel a" : "hide" },
},
initialize: function(options) { initialize: function(options) {
_.bindAll(this, 'createTimeline', 'hide', 'show'); _.bindAll(this, 'createTimeline', 'hide', 'show');
if (options.timelineCollection == undefined) { if (options.timelineCollection == undefined) {
throw "Can not create the NewTimelineView without the timeline collection." throw "Can not create the NewTimelineView without the " +
} else { "timeline collection." }
this.timelineCollection = options.timelineCollection; else { this.timelineCollection = options.timelineCollection; } },
}
},
createTimeline: function() { createTimeline: function() {
var timelineId = this.$("#new-timeline-id").val(); var timelineId = this.$("#new-timeline-id").val();
@ -772,8 +700,7 @@ $(document).ready(function(){
this.timelineCollection.create( this.timelineCollection.create(
{id: timelineId, description: timelineDesc, {id: timelineId, description: timelineDesc,
created: dateToJSON(new Date())}); created: dateToJSON(new Date())});
this.hide(); this.hide(); },
},
hide: function() { $(this.el).addClass('hidden'); }, hide: function() { $(this.el).addClass('hidden'); },
@ -781,11 +708,9 @@ $(document).ready(function(){
this.$("#new-timeline-id").val(""); this.$("#new-timeline-id").val("");
this.$("#new-timeline-desc").val(""); this.$("#new-timeline-desc").val("");
$(this.el).removeClass('hidden'); $(this.el).removeClass('hidden');
this.$("#new-timeline-id").focus(); this.$("#new-timeline-id").focus(); } });
}
});
TS.app = new TS.AppView; new TS.AppView();
}) })