diff --git a/src/ts_api.erl b/src/ts_api.erl index 2f569af..4da4d1c 100644 --- a/src/ts_api.erl +++ b/src/ts_api.erl @@ -190,7 +190,7 @@ do_login(YArg) -> {CookieVal, _Session} = ts_api_session:new(Username), [{header, {set_cookie, io_lib:format( - "ts_api_session=~s; Path=/ts_api", + "ts_api_session=~s; Path=/", [CookieVal])}}, {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]}, {header, ["Access-Control-Allow-Credentials: ", "true"]}, @@ -269,7 +269,7 @@ put_user(YArg, Username) -> throw(make_json_400(YArg, [{request_error, InputError}])) 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), % return a 200 diff --git a/src/ts_json.erl b/src/ts_json.erl index f24b190..b7cd5bc 100644 --- a/src/ts_json.erl +++ b/src/ts_json.erl @@ -81,31 +81,31 @@ ejson_to_record(Empty, {struct, EJSONFields}) -> construct_record(Empty, EJSONFields, []). ejson_to_record(Empty, Ref, EJSON) -> - Constructed = ejson_to_record(Empty, EJSON), - setelement(2, Constructed, Ref). + {Constructed, ExtData} = ejson_to_record(Empty, EJSON), + {setelement(2, Constructed, Ref), ExtData}. ejson_to_record_strict(Empty=#ts_user{}, EJSON) -> - Constructed = ejson_to_record(Empty, EJSON), + {Constructed, ExtData} = ejson_to_record(Empty, EJSON), case Constructed of #ts_user{name = undefined} -> throw("Missing user 'name' field."); #ts_user{email = undefined} -> throw("Missing user 'email' field."); #ts_user{join_date = undefined} -> throw("Missing user 'join_date' field."); - _Other -> Constructed + _Other -> {Constructed, ExtData} end; ejson_to_record_strict(Empty=#ts_timeline{}, EJSON) -> - Constructed = ejson_to_record(Empty, EJSON), + {Constructed, ExtData} = ejson_to_record(Empty, EJSON), case Constructed of #ts_timeline{created = undefined} -> throw("Missing timeline 'created' field."); #ts_timeline{desc = undefined} -> throw("Missing timeline 'description' field."); - _Other -> Constructed + _Other -> {Constructed, ExtData} end; ejson_to_record_strict(Empty=#ts_entry{}, EJSON) -> - Constructed = ejson_to_record(Empty, EJSON), + {Constructed, ExtData} = ejson_to_record(Empty, EJSON), case Constructed of #ts_entry{timestamp = undefined} -> throw("Missing timelne 'timestamp' field."); @@ -113,12 +113,12 @@ ejson_to_record_strict(Empty=#ts_entry{}, EJSON) -> throw("Missing timeline 'mark' field."); #ts_entry{notes = undefined} -> throw("Missing timeline 'notes' field/"); - _Other -> Constructed + _Other -> {Constructed, ExtData} end. ejson_to_record_strict(Empty, Ref, EJSON) -> - Constructed = ejson_to_record_strict(Empty, EJSON), - setelement(2, Constructed, Ref). + {Constructed, ExtData} = ejson_to_record_strict(Empty, EJSON), + {setelement(2, Constructed, Ref), ExtData}. construct_record(User=#ts_user{}, [{Key, Value}|Fields], ExtData) -> case Key of diff --git a/www/index.yaws b/www/index.yaws index 256de7b..02ea0c4 100644 --- a/www/index.yaws +++ b/www/index.yaws @@ -28,33 +28,38 @@ out(YArg) -> Username = element(2, Session), % 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), % 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 - SelectedTimeline = case lists:keyfind( - selected_timeline, 1, element(8, UserRecord)) of - false -> ts_timeline:list(Username, 0, 1); + % get the last used timeline if there is one. + SelectedTimeline = case ts_ext_data:get_property(Username, last_timeline) of + + not_set -> ts_timeline:list(Username, 0, 1); T -> T end, % get entries for this timeline - {content, _, EntryListJSON} = + EntriesResp = ts_api:list_entries(YArg, Username, SelectedTimeline), + {content, _, EntryListJSON} = lists:keyfind(content, 1, EntriesResp), {html, f( "function bootstrap() {~n" " var data = {};~n" -" data.user = ~p;~n" -" data.timelines = ~p;~n" +" data.user = ~s;~n" +" data.timelines = ~s;~n" " data.initialTimelineId = ~p;~n" -" data.entries = ~p;~n" +" data.entries = ~s;~n" " return data;~n" "};", - [UserJSON, TimelineListJSON, SelectedTimeline, EntryListJSON])} + [lists:flatten(UserJSON), lists:flatten(TimelineListJSON), + SelectedTimeline, lists:flatten(EntryListJSON)])} end. diff --git a/www/js/ts.js b/www/js/ts.js index c4fbbe0..7e8aacb 100644 --- a/www/js/ts.js +++ b/www/js/ts.js @@ -1,3 +1,8 @@ +/** + * # `ts.js`: TimeStamper Web UI Client-side + * @author Jonathan Bernard + **/ + // TimeStamper namespace var TS = {}; @@ -8,24 +13,27 @@ $(document).ready(function(){ /* Entry model. * Attributes + * - user_id + * - timeline_id * - id * - mark + * - uuid * - notes * - timestamp */ TS.EntryModel = Backbone.Model.extend({ + initialize: function(attrs, options) { + _.bindAll(this, 'get', 'set', 'urlRoot'); }, + get: function(attribute) { if (attribute == "timestamp") { if (!this.timestampDate) { this.timestampDate = new Date( - Backbone.Model.prototype.get.call(this, attribute)); - } - return this.timestampDate; - } else { - return Backbone.Model.prototype.get.call(this, attribute); - } - }, + Backbone.Model.prototype.get.call(this, attribute)); } + return this.timestampDate; } + else { + return Backbone.Model.prototype.get.call(this, attribute); } }, set: function(attributes, options) { var attrsToSet = {} @@ -33,44 +41,42 @@ $(document).ready(function(){ if (key == "timestamp") { if (val instanceof Date) { this.timestampDate = val; - attrsToSet.timestamp = dateToJSON(val); - } else { + attrsToSet.timestamp = dateToJSON(val); } + else { this.timestampDate = new Date(val); - attrsToSet.timestamp = dateToJSON(this.timestampDate); - } - } else { - attrsToSet[key] = val; - } - }); + attrsToSet.timestamp = dateToJSON(this.timestampDate); } } + 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. * Attributes: + * - user_id * - id * - description * - created */ TS.TimelineModel = Backbone.Model.extend({ + urlRoot: function() { + return '/ts_api/timelines/' + this.get('user_id') + '/'; }, - }); + initialze: function(attrs, options) { _.bindAll(this, 'urlRoot'); } }); /* User model. * Attributes: - * - username - * - fullname + * - id + * - name * - email * - join_date */ TS.UserModel = Backbone.Model.extend({ url: function() { return '/ts_api/users/' + this.get('id'); }, - initialize: function(attrs, options) { - _.bind(this, 'url'); - } - }); + initialize: function(attrs, options) { _.bindAll(this, 'url'); } }); TS.EntryList = Backbone.Collection.extend({ model: TS.EntryModel, @@ -79,17 +85,23 @@ $(document).ready(function(){ initialize: function(model, options) { if (options.timelineModel == undefined) { - throw "Cannot create an EntryList without a TimelineModel reference." - } else { this.timelineModel = options.timelineModel; } + throw "Cannot create an EntryList without a " + + "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() { return "/ts_api/entries/" + this.timelineModel.get('user_id') + "/" - + this.timelineModel.get('id'); - } - }); + + this.timelineModel.get('id'); } }); TS.TimelineList = Backbone.Collection.extend({ model: TS.TimelineModel, @@ -99,17 +111,12 @@ $(document).ready(function(){ throw "Cannot create a TimelineList without a UserModel reference."; } else { this.user = options.user; } - _.bindAll(this, 'url'); - }, + _.bindAll(this, 'url'); }, - comparator: function(timeline) { - return timeline.get('id'); - }, + comparator: function(timeline) { return timeline.get('id'); }, url: function() { - return "/ts_api/timelines/" + this.user.get('id'); - } - }); + return "/ts_api/timelines/" + this.user.get('id'); } }); // ======== DEFINE VIEWS ========// @@ -135,8 +142,7 @@ $(document).ready(function(){ "keypress .notes-input" : "saveOnCtrlEnter", "blur .mark-input" : "save", "blur .timestamp-input" : "save", - "blur .notes-input" : "save" - }, + "blur .notes-input" : "save" }, initialize: function(options) { _.bindAll(this, 'render', 'editTImestamp', 'editMark', 'update', @@ -148,8 +154,7 @@ $(document).ready(function(){ this.model.bind('change', this.update); this.model.view = this; - this.nextModel = options.nextModel; - }, + this.nextModel = options.nextModel; }, /** * Refresh the display based on the model replacing the existing @@ -162,8 +167,7 @@ $(document).ready(function(){ // invalidate the notes display cache this.notesCache = false; - return this; - }, + return this; }, /** * Refresh the display based on the model using the existing DOM @@ -178,27 +182,23 @@ $(document).ready(function(){ this.$('.duration').text(data.duration); this.$('.notes-text').html(this.renderNotes(data.notes)); this.$('.notes-input').val(data.notes); - return this; - }, + return this; }, renderNotes: function(source) { if (!this.notesCache) { this.notesCache = this.markdownConverter.makeHtml(source); } - return this.notesCache - }, + return this.notesCache }, editMark: function() { $(this.el).addClass('edit-mark'); this.$('.mark-input').focus(); - return this; - }, + return this; }, editTimestamp: function() { $(this.el).addClass('edit-timestamp'); this.$('timestamp-input').focus(); - return this; - }, + return this; }, editNotes: function() { // invalidate notes HTML cache @@ -209,8 +209,7 @@ $(document).ready(function(){ // focus input this.$('.notes-input').focus(); - return this; - }, + return this; }, /** * Translate the model data into a form suitable to be displayed. @@ -224,8 +223,7 @@ $(document).ready(function(){ data.start = this.formatStart(tsDate); data.duration = this.formatDuration(this.model, this.nextModel); data.notes = data.notes ? data.notes : '*No notes for this entry.*'; - return data; - }, + return data; }, /** Save and close editable fields. */ save: function() { @@ -235,17 +233,12 @@ $(document).ready(function(){ notes: this.$('.notes-input').val()}); 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. */ - saveOnEnter: function(e) { - if(e.keyCode == 13) { this.save(); } - }, + saveOnEnter: function(e) { if(e.keyCode == 13) { this.save(); } }, - saveOnCtrlEnter: function(e) { - if (e.keyCode == 10) { this.save(); } - }, + saveOnCtrlEnter: function(e) { if (e.keyCode == 10) { this.save(); } }, /** * Get the display-able start time from the entry timestamp. @@ -263,8 +256,7 @@ $(document).ready(function(){ // 12 hour var hour = startDate.getHours() % 12; 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 @@ -296,8 +288,7 @@ $(document).ready(function(){ return (day > 0 ? day + "d " : "") + (hr > 0 ? hr + "hr " : "") + - min + "m "; - }, + min + "m "; }, showNotes: function() { if (!this.notesCache) { @@ -305,22 +296,17 @@ $(document).ready(function(){ this.renderNotes(this.model.get('notes'))); } this.$('.notes').slideDown(); - $(this.el).addClass('show-notes'); - }, + $(this.el).addClass('show-notes'); }, hideNotes: function() { this.$('.notes').slideUp(); - $(this.el).removeClass('show-notes'); - } - }); + $(this.el).removeClass('show-notes'); } }); TS.EntryListView = Backbone.View.extend({ el: $("#entry-list"), - events: { - "keypress #new-entry-input" : "createNewEntryOnEnter" - }, + events: { "keypress #new-entry-input" : "createNewEntryOnEnter" }, initialize: function() { _.bindAll(this, 'addOne', 'createNewEntry', 'render', 'renderOne'); @@ -328,15 +314,13 @@ $(document).ready(function(){ this.collection.bind('refresh', this.render); this.collection.view = this; this.entryContainer = this.$("#entries") - this.markdownConverter = new Showdown.converter(); - }, + this.markdownConverter = new Showdown.converter(); }, addOne: function(entry) { var lastEntry = this.collection.at(this.collection.length - 2); lastEntry.view.nextModel = entry; lastEntry.view.update(); - this.renderOne(entry, null); - }, + this.renderOne(entry, null); }, renderOne: function(entry, nextEntry) { // exclude if any exclusion RegExps match @@ -357,9 +341,7 @@ $(document).ready(function(){ // hide it if excluded if (excluded) { $(el).fadeOut('slow'); - $(el).addClass('excluded'); - } - }, + $(el).addClass('excluded'); } }, createNewEntryOnEnter: function(e) { @@ -371,13 +353,12 @@ $(document).ready(function(){ // create the mark. Immediately fetch to get server-side timestamp this.collection.create({mark: entryMark, notes: '', - timestamp: new Date()}).fetch(); + timestamp: new Date()}); + + this.collection.fetch(); // clear the input for the next entry - this.$("#new-entry-input").val(""); - - } - }, + this.$("#new-entry-input").val(""); } }, render: function() { @@ -416,12 +397,9 @@ $(document).ready(function(){ if (currentDay.getDate() != entry.get("timestamp").getDate()) { this.topSeparator.after(ich.daySeparatorTemplate( {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) { @@ -456,10 +434,7 @@ $(document).ready(function(){ else if (dayDiff == 1) { return "Yesterday"; } // today - else if (dayDiff == 0) { return "Today"; } - - } - }); + else if (dayDiff == 0) { return "Today"; } } }); TS.TimelineListView = Backbone.View.extend({ el: $("#timeline"), @@ -471,65 +446,53 @@ $(document).ready(function(){ "dblclick .timeline-desc" : "editDesc", "keypress .timeline-id-input" : "saveOnEnter", "keypress .timeline-desc-input" : "saveOnEnter", - "click .new-timeline-link" : "showNewTimelineDialog" - }, + "click .new-timeline-link" : "showNewTimelineDialog" }, initialize: function(options) { _.bindAll(this, 'render', 'renderOne', 'editId', 'editDesc', 'saveOnEnter', 'save'); if (options.initialTimelineId == undefined) { - throw "Can not create a TimelineListView without an initial timeline." - } else { - this.selectedModel = this.collection.get(options.initialTimelineId); - } + throw "Can not create a TimelineListView without an " + + "initial timeline." } + else { + this.selectedModel = + this.collection.get(options.initialTimelineId); } this.collection.bind('add', this.renderOne); - this.collection.bind('refresh', this.render); - }, + this.collection.bind('refresh', this.render); }, renderOne: function(timeline) { this.$('.drop-menu-items').append( - ich.timelineLinkTemplate(timeline.toJSON())); - }, + ich.timelineLinkTemplate(timeline.toJSON())); }, render: function() { // render the basic template $(this.el).html(ich.timelineTemplate(this.selectedModel.toJSON())); // render the selection list - _.each(this.collection.without([this.selectedModel]), this.renderOne); - }, + _.each(this.collection.without([this.selectedModel]), this.renderOne); }, editId: function() { $(this.el).addClass('edit-id'); this.$('.timeline-id-input').focus(); - return this; - }, + return this; }, editDesc: function() { $(this.el).addClass('edit-desc'); this.$('.timeline-desc-input').focus(); - return this; - }, + return this; }, save: function() { this.selectedModel.save({ id: this.$('.timeline-id-input').val(), description: this.$('.timeline-desc-input').val()}); $(this.el).removeClass('edit-id edit-desc'); - this.render(); - }, + this.render(); }, - showNewTimelineDialog: function() { - TS.app.newTimelineDialog.show(); - }, + showNewTimelineDialog: function() { TS.app.newTimelineDialog.show(); }, - saveOnEnter: function(e) { - if (e.keyCode == 13) { this.save(); } - } - - }); + saveOnEnter: function(e) { if (e.keyCode == 13) { this.save(); } } }); TS.UserView = Backbone.View.extend({ @@ -539,44 +502,34 @@ $(document).ready(function(){ events: { 'dblclick .fullname': 'editFullname', - 'keypress .fullname-input': 'saveOnEnter' - }, + 'keypress .fullname-input': 'saveOnEnter' }, initialize: function() { _.bindAll(this, 'render', 'save', 'editFullname', 'saveOnEnter'); this.model.bind('change', this.render); - this.model.view = this; - }, + this.model.view = this; }, render: function() { $(this.el).html(ich.userTemplate(this.model.toJSON())); - return this; - }, + return this; }, editFullname: function() { $(this.el).addClass('edit-fullname'); this.$('.fullname-input').focus(); - return this; - }, + return this; }, save: function() { this.model.set({name: this.$('fullname-input').val()}); this.model.save(); - $(this.el).removeClass('edit-fullname'); - }, + $(this.el).removeClass('edit-fullname'); }, - saveOnEnter: function(e) { - if (e.keyCode == 13) this.save(); - } - }); + saveOnEnter: function(e) { if (e.keyCode == 13) this.save(); } }); TS.AppView = Backbone.View.extend({ el: $("body"), - events: { - 'click #timeline .drop-menu-items a': 'selectTimeline' - }, + events: { 'click #timeline .drop-menu-items a': 'selectTimeline' }, initialize: function() { @@ -593,12 +546,12 @@ $(document).ready(function(){ else { // this is async (waiting for user input) this.loginDialog.authenticate(function() { - appThis.initializeData(appThis.loadInitialData())}); - } - }, + appThis.initializeData(appThis.loadInitialData())}); } }, initializeData: function(data) { + TS.app = this; + // create user data this.user = {}; this.user.model = new TS.UserModel(data.user); @@ -606,8 +559,7 @@ $(document).ready(function(){ // create timeline models from the bootstrapped data var tlModels = _.map(data.timelines, function(timeline) { - return new TS.TimelineModel(timeline); - }); + return new TS.TimelineModel(timeline); }); // create the timeline list collection this.timelines = {}; @@ -623,8 +575,7 @@ $(document).ready(function(){ // create entry models from the bootstrapped data var entryModels = _.map(data.entries, function(entry) { - return new TS.EntryModel(entry); - }); + return new TS.EntryModel(entry); }); // create the entry collection this.entries = {}; @@ -636,10 +587,7 @@ $(document).ready(function(){ // render views this.user.view.render(); this.timelines.view.render(); - this.entries.view.render(); - - - }, + this.entries.view.render(); }, loadInitialData: function() { // assume we are authenticated @@ -658,8 +606,7 @@ $(document).ready(function(){ data.initialTimelineId, async: false}).responseText); - return data; - }, + return data; }, selectTimeline: function(e) { if (e) { @@ -680,32 +627,24 @@ $(document).ready(function(){ this.timelines.view.render(); // refresh EntryList records - this.entries.collection.fetch() - } - } - - }); + this.entries.collection.fetch() } } }); TS.LoginView = Backbone.View.extend({ el: $("#login"), events: { "keypress #password-input" : "loginOnEnter", - "click #login-button a" : "doLogin" - }, + "click #login-button a" : "doLogin" }, initialize: function() { _.bindAll(this, 'authenticate', 'doLogin', 'hide', 'loginOnEnter', - 'show'); - - }, + 'show'); }, action: function() {}, authenticate: function(nextAction) { this.action = nextAction; - this.show(); - }, + this.show(); }, doLogin: function(){ var viewThis = this; @@ -725,46 +664,35 @@ $(document).ready(function(){ // we should check that, FIXME var tips = $(".validate-tips"); tips.text("Incorrect username/password combination."); - tips.slideDown(); - }, + tips.slideDown(); }, success: function(data, textStatus, jqXHR) { viewThis.hide(); - viewThis.action(); - } - }); - }, + viewThis.action(); } }); }, hide: function() { $(this.el).addClass('hidden'); }, show: function() { $(this.el).removeClass('hidden'); - this.$("#username-input").focus(); - }, + this.$("#username-input").focus(); }, loginOnEnter: function(e) { - if (e.keyCode == 13) { this.doLogin(); } - } - }); + if (e.keyCode == 13) { this.doLogin(); } } }); TS.NewTimelineView = Backbone.View.extend({ el: $("#new-timeline"), events: { "click #new-timeline-create a" : "createTimeline", - "click #new-timeline-cancel a" : "hide" - }, + "click #new-timeline-cancel a" : "hide" }, initialize: function(options) { _.bindAll(this, 'createTimeline', 'hide', 'show'); if (options.timelineCollection == undefined) { - throw "Can not create the NewTimelineView without the timeline collection." - } else { - this.timelineCollection = options.timelineCollection; - } - - }, + throw "Can not create the NewTimelineView without the " + + "timeline collection." } + else { this.timelineCollection = options.timelineCollection; } }, createTimeline: function() { var timelineId = this.$("#new-timeline-id").val(); @@ -772,8 +700,7 @@ $(document).ready(function(){ this.timelineCollection.create( {id: timelineId, description: timelineDesc, created: dateToJSON(new Date())}); - this.hide(); - }, + this.hide(); }, hide: function() { $(this.el).addClass('hidden'); }, @@ -781,11 +708,9 @@ $(document).ready(function(){ this.$("#new-timeline-id").val(""); this.$("#new-timeline-desc").val(""); $(this.el).removeClass('hidden'); - this.$("#new-timeline-id").focus(); - } - }); + this.$("#new-timeline-id").focus(); } }); - TS.app = new TS.AppView; + new TS.AppView(); })