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:
parent
0278179452
commit
a3c55e918e
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
309
www/js/ts.js
309
www/js/ts.js
@ -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();
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user