From cd182d54c39714d9554359c4022b6a264981e488 Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Wed, 15 Jun 2011 16:50:38 -0500 Subject: [PATCH] Fixing problems introduced by the model change. * Many calls to ``ts_ext_data:get_properties/1`` from ``ts_api`` were passing the record reference, not the record itself. Fixed. * Created ``ts_api:put_user/2`` which updates a ``ts_user`` record. This is needed specifically for updating the ``liat_timeline`` extended data property. * Removed unreachable code in ``ts_api:post_entry/3`` * Fixed return value of some ``ts_user`` functions to use the ``{Status, Value}`` convention. TODO: make sure all calls are using the same convention. * Fixed error message formatting in ``ts_ext_data:set_property/3``. * Added clauses to ``ts_json:ejson_to_record/2``, ``ts_json:ejson_to_record_strict/2`` and ``ts_json:construct_record/3`` to handle the ``ts_user`` record type. * Added code to ``AppView.loadInitialData`` and ``AppView.selectTimeline`` to support the ``last_timeline`` extended data property. --- .../vim-views/{ts.js.view => index.yaws.view} | 51 +---- .ide/vim-views/ts_api.erl.view | 185 ++++++++++++++++++ db/test/DECISION_TAB.LOG | Bin 156 -> 156 bytes db/test/LATEST.LOG | Bin 92 -> 92 bytes db/test/id_counter.DCD | Bin 130 -> 130 bytes db/test/ts_entry.DCD | Bin 3440 -> 3325 bytes db/test/ts_ext_data.DCD | Bin 304 -> 310 bytes db/test/ts_ext_data.DCL | Bin 224 -> 0 bytes db/test/ts_timeline.DCD | Bin 385 -> 385 bytes src/ts_api.erl | 51 +++-- src/ts_common.erl | 3 +- src/ts_entry.erl | 2 +- src/ts_ext_data.erl | 4 +- src/ts_json.erl | 55 +++++- src/ts_user.erl | 16 +- www/js/ts.js | 9 +- 16 files changed, 297 insertions(+), 79 deletions(-) rename .ide/vim-views/{ts.js.view => index.yaws.view} (81%) create mode 100644 .ide/vim-views/ts_api.erl.view delete mode 100644 db/test/ts_ext_data.DCL diff --git a/.ide/vim-views/ts.js.view b/.ide/vim-views/index.yaws.view similarity index 81% rename from .ide/vim-views/ts.js.view rename to .ide/vim-views/index.yaws.view index 7c0774d..d626383 100644 --- a/.ide/vim-views/ts.js.view +++ b/.ide/vim-views/index.yaws.view @@ -1,6 +1,6 @@ let s:so_save = &so | let s:siso_save = &siso | set so=0 siso=0 argglobal -edit ~/projects/jdb-labs/timestamper/web-app/www/js/ts.js +edit /mnt/secure/projects/jdb-labs/timestamper/web-app/www/index.yaws setlocal keymap= setlocal noarabic setlocal autoindent @@ -26,8 +26,8 @@ setlocal nodiff setlocal equalprg= setlocal errorformat= setlocal expandtab -if &filetype != 'javascript' -setlocal filetype=javascript +if &filetype != 'erlang' +setlocal filetype=erlang endif setlocal foldcolumn=0 setlocal foldenable @@ -83,8 +83,8 @@ setlocal statusline= setlocal suffixesadd= setlocal swapfile setlocal synmaxcol=3000 -if &syntax != 'javascript' -setlocal syntax=javascript +if &syntax != 'erlang' +setlocal syntax=erlang endif setlocal tabstop=4 setlocal tags= @@ -95,48 +95,13 @@ setlocal nowinfixwidth setlocal wrap setlocal wrapmargin=0 silent! normal! zE -9,18fold -20,28fold -30,43fold -45,61fold -63,81fold -86,288fold -290,348fold -350,418fold -420,457fold -459,557fold -559,618fold -620,656fold -9 -normal zc -20 -normal zc -30 -normal zc -45 -normal zc -63 -normal zc -86 -normal zc -290 -normal zc -350 -normal zc -420 -normal zc -459 -normal zc -559 -normal zc -620 -normal zc -let s:l = 3 - ((2 * winheight(0) + 23) / 47) +let s:l = 1 - ((0 * winheight(0) + 36) / 72) if s:l < 1 | let s:l = 1 | endif exe s:l normal! zt -3 +1 normal! 0 +lcd /mnt/secure/projects/jdb-labs/timestamper/web-app/www let &so = s:so_save | let &siso = s:siso_save doautoall SessionLoadPost " vim: set ft=vim : diff --git a/.ide/vim-views/ts_api.erl.view b/.ide/vim-views/ts_api.erl.view new file mode 100644 index 0000000..0551c96 --- /dev/null +++ b/.ide/vim-views/ts_api.erl.view @@ -0,0 +1,185 @@ +let s:so_save = &so | let s:siso_save = &siso | set so=0 siso=0 +argglobal +edit /mnt/secure/projects/jdb-labs/timestamper/web-app/src/ts_api.erl +setlocal keymap= +setlocal noarabic +setlocal autoindent +setlocal balloonexpr= +setlocal nobinary +setlocal bufhidden= +setlocal buflisted +setlocal buftype= +setlocal nocindent +setlocal cinkeys=0{,0},0),:,0#,!^F,o,O,e +setlocal cinoptions= +setlocal cinwords=if,else,while,do,for,switch +setlocal comments=s1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:- +setlocal commentstring=/*%s*/ +setlocal complete=.,w,b,u,t,i +setlocal completefunc= +setlocal nocopyindent +setlocal nocursorcolumn +setlocal nocursorline +setlocal define= +setlocal dictionary= +setlocal nodiff +setlocal equalprg= +setlocal errorformat= +setlocal expandtab +if &filetype != 'erlang' +setlocal filetype=erlang +endif +setlocal foldcolumn=0 +setlocal foldenable +setlocal foldexpr=0 +setlocal foldignore=# +setlocal foldlevel=0 +setlocal foldmarker={{{,}}} +setlocal foldmethod=manual +setlocal foldminlines=1 +setlocal foldnestmax=20 +setlocal foldtext=foldtext() +setlocal formatexpr= +setlocal formatoptions=tcq +setlocal formatlistpat=^\\s*\\d\\+[\\]:.)}\\t\ ]\\s* +setlocal grepprg= +setlocal iminsert=2 +setlocal imsearch=2 +setlocal include= +setlocal includeexpr= +setlocal indentexpr= +setlocal indentkeys=0{,0},:,0#,!^F,o,O,e +setlocal noinfercase +setlocal iskeyword=@,48-57,_,192-255 +setlocal keywordprg= +setlocal nolinebreak +setlocal nolisp +setlocal nolist +setlocal makeprg= +setlocal matchpairs=(:),{:},[:] +setlocal nomodeline +setlocal modifiable +setlocal nrformats=octal,hex +setlocal number +setlocal numberwidth=4 +setlocal omnifunc= +setlocal path= +setlocal nopreserveindent +setlocal nopreviewwindow +setlocal quoteescape=\\ +setlocal noreadonly +setlocal norightleft +setlocal rightleftcmd=search +setlocal noscrollbind +setlocal shiftwidth=4 +setlocal noshortname +setlocal nosmartindent +setlocal softtabstop=0 +setlocal nospell +setlocal spellcapcheck=[.?!]\\_[\\])'\"\ \ ]\\+ +setlocal spellfile= +setlocal spelllang=en +setlocal statusline= +setlocal suffixesadd= +setlocal swapfile +setlocal synmaxcol=3000 +if &syntax != 'erlang' +setlocal syntax=erlang +endif +setlocal tabstop=4 +setlocal tags= +setlocal textwidth=80 +setlocal thesaurus= +setlocal nowinfixheight +setlocal nowinfixwidth +setlocal wrap +setlocal wrapmargin=0 +silent! normal! zE +7,28fold +35,51fold +55,73fold +77,92fold +96,126fold +130,161fold +167,196fold +198,202fold +204,239fold +241,248fold +250,267fold +269,301fold +303,310fold +312,331fold +335,421fold +423,429fold +431,453fold +455,469fold +471,486fold +492,499fold +502,506fold +508,515fold +517,525fold +528,539fold +541,552fold +554,563fold +7 +normal zc +35 +normal zc +55 +normal zc +77 +normal zc +96 +normal zc +130 +normal zc +167 +normal zc +198 +normal zc +204 +normal zo +241 +normal zc +250 +normal zc +269 +normal zc +303 +normal zc +312 +normal zo +335 +normal zc +423 +normal zc +431 +normal zc +455 +normal zc +471 +normal zc +492 +normal zc +502 +normal zc +508 +normal zc +517 +normal zc +528 +normal zc +541 +normal zc +554 +normal zc +let s:l = 250 - ((25 * winheight(0) + 35) / 71) +if s:l < 1 | let s:l = 1 | endif +exe s:l +normal! zt +250 +normal! 0 +let &so = s:so_save | let &siso = s:siso_save +doautoall SessionLoadPost +" vim: set ft=vim : +syntax on diff --git a/db/test/DECISION_TAB.LOG b/db/test/DECISION_TAB.LOG index c329c24fc948a2e9ca6445005adcfd51b00385f3..f15ab56697dd251fb738bafb507abb29f591aa5e 100644 GIT binary patch delta 22 dcmbQkIEQgUI0w`8*dzvCzO@tMt(ch|vj9q}1|Wp>(K`9W$z5D%kBZxREW)O`j9hNj7h+)5i8idh)5L9B@lTx?c(tPFQjCf{MUVa(e6 IfrXa?0Ma86%>V!Z diff --git a/db/test/ts_ext_data.DCD b/db/test/ts_ext_data.DCD index 3e52c5681e65e5a4ee3a3a2f6fedcf02a97c26c0..4d049a2f15c9328654d24a6250d109e5c4bf375b 100644 GIT binary patch delta 59 zcmdnMw2f&(5D!!7i6jQj=N}jt7`!KL6rWgX&FDOFqnT(9P=GO;fg>(DxHz^XzaR_9 NH=DRob+R<0C;%_Z5o!Pc delta 53 zcmdnSw1H_t5D(*v=}8RSJFYP>FnCYgC_b^&n$dRRMl=3w29CJs;NsYl`~skq*~E>i IlNA|70o!d5hX4Qo diff --git a/db/test/ts_ext_data.DCL b/db/test/ts_ext_data.DCL deleted file mode 100644 index 9ea29b70d9d1910f34e9974f81bdfa7d3847d0d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 224 zcmb7-OA5j;6h&_v>#qndz%>*rU4Tygj2tM4hNdr=en=#Vs1qmd!~MAn3$DQFJ)Coc zkaddvCI+w+yY=Fwx`bh4&Rofyq$TvIXm~r+U}-X~(an-9NoRzSHrkM!YomX%y@%bw<6%u0axV(E#xzANtV^EQhR_B%oW2wD;w76)lt~A;y_NUOaZ$XXz Q>9BQ2Z!Q%fy4ljpFV=rU^8f$< diff --git a/db/test/ts_timeline.DCD b/db/test/ts_timeline.DCD index 7722c9ea5a2c5d82814f70ff50c3d5294f4d4199..e2a70fa4480269495e2318d9948cf0cc4955b1f3 100644 GIT binary patch delta 29 kcmZo|{ko)rrmj0CDpOZ~y=R delta 29 lcmZo {'EXIT', Err} -> % TODO: log error internally error_logger:error_report("TimeStamper: ~p", [Err]), + io:format("Error: ~n~p", [Err]), make_json_500(YArg, Err); Other -> Other end. @@ -211,7 +212,7 @@ get_user_summary(YArg, Username) -> User -> % get user extended data properties - UserExtData = ts_ext_data:get_properties(Username), + UserExtData = ts_ext_data:get_properties(User), % convert to intermediate JSON form EJSONUser = ts_json:record_to_ejson(User, UserExtData), @@ -223,7 +224,7 @@ get_user_summary(YArg, Username) -> lists:map( fun(Timeline) -> ts_json:record_to_ejson(Timeline, - ts_ext_data:get_properties(Timeline#ts_timeline.ref)) + ts_ext_data:get_properties(Timeline)) end, Timelines)}, @@ -246,6 +247,25 @@ get_user(YArg, Username) -> User -> make_json_200(YArg, User) end. +put_user(YArg, Username) -> + + % parse the POST data + EJSON = parse_json_body(YArg), + + {UR, ExtData} = + try ts_user:ejson_to_record_strict(#ts_user{username=Username}, EJSON) + catch throw:{InputError, StackTrace} -> + error_logger:error_report("Bad input in put_user/2: ~p", + [InputError]), + throw(make_json_400(YArg, [{request_error, InputError}])) + end, + + % update the record (we do not support creating users via the API right now + {ok, UpdatedRec} = ts_user:update(UR, ExtData), + + % return a 200 + make_json_200(YArg, UpdatedRec). + list_timelines(YArg, Username) -> % pull out the POST data QueryData = yaws_api:parse_query(YArg), @@ -270,7 +290,7 @@ list_timelines(YArg, Username) -> EJSONTimelines = {array, lists:map( fun (Timeline) -> ts_json:record_to_ejson(Timeline, - ts_ext_data:get_properties(Timeline#ts_timeline.ref)) + ts_ext_data:get_properties(Timeline)) end, Timelines)}, @@ -301,7 +321,7 @@ put_timeline(YArg, Username, TimelineId) -> % we can not parse it, tell the user catch throw:{InputError, _StackTrace} -> error_logger:error_report("Bad input: ~p", [InputError]), - throw(make_json_400(YArg, {request_error, InputError})) + throw(make_json_400(YArg, [{request_error, InputError}])) end, % write the changes. @@ -355,7 +375,7 @@ list_entries(YArg, Username, TimelineId) -> EJSONEntries = {array, lists:map( fun (Entry) -> ts_json:record_to_ejson(Entry, - ts_ext_data:get_properties(Entry#ts_entry.ref)) + ts_ext_data:get_properties(Entry)) end, Entries)}, @@ -391,7 +411,7 @@ list_entries(YArg, Username, TimelineId) -> EJSONEntries = {array, lists:map( fun (Entry) -> ts_json:record_to_ejson(Entry, - ts_ext_data:get_properties(Entry#ts_entry.ref)) + ts_ext_data:get_properties(Entry)) end, Entries)}, @@ -418,7 +438,7 @@ post_entry(YArg, Username, TimelineId) -> #ts_entry{ref = {Username, TimelineId, undefined}}, EJSON) catch _:InputError -> error_logger:error_report("Bad input: ~p", [InputError]), - throw(make_json_400(YArg, {request_error, InputError})) + throw(make_json_400(YArg, [{request_error, InputError}])) end, case ts_entry:new(ER, ExtData) of @@ -427,14 +447,8 @@ post_entry(YArg, Username, TimelineId) -> [{status, 201}, make_json_200(YArg, CreatedRecord)]; - % will not create, record exists - {error, {record_exists, ExistingRecord}} -> - JSONResponse = json:encode(ts_json:record_to_ejson(ExistingRecord)), - - {content, "application/json", JSONResponse}; - OtherError -> - error_logger:error_report("TimeStamper: Could not create entry: ~p", [OtherError]), + error_logger:error_report("Could not create entry: ~p", [OtherError]), make_json_500(YArg, OtherError) end. @@ -486,8 +500,9 @@ parse_json_body(YArg) -> %% Create a JSON 200 response. make_json_200(_YArg, Record) -> - RecordExtData = ts_ext_data:get_properties(element(2, Record)), - JSONResponse = json:encode(ts_json:record_to_ejson(Record, RecordExtData)), + RecordExtData = ts_ext_data:get_properties(Record), + EJSON = ts_json:record_to_ejson(Record, RecordExtData), + JSONResponse = json:encode(EJSON), {content, "application/json", JSONResponse}. make_json_400(YArg) -> make_json_400(YArg, []). @@ -532,14 +547,14 @@ make_json_405(_YArg, Fields) -> end, % add the path they requested - % F2 = F1 ++ [{path, io_lib:format("~s", [(YArg#arg.req)#http_request.path])}], + % F2 = F1 ++ [{path, io_lib:format("~p", [(YArg#arg.req)#http_request.path])}], [{status, 405}, {content, "application/json", json:encode({struct, F1})}]. make_json_500(_YArg, Error) -> EJSON = {struct, [ {status, "internal server error"}, - {error, io_lib:format("~s", [Error])}]}, + {error, lists:flatten(io_lib:format("~p", [Error]))}]}, [{status, 500}, {content, "application/json", json:encode(EJSON)}]. make_json_500(_YArg) -> diff --git a/src/ts_common.erl b/src/ts_common.erl index 9feee2a..93c9a9e 100644 --- a/src/ts_common.erl +++ b/src/ts_common.erl @@ -100,10 +100,9 @@ list(Query, Start, Length) -> % should be wrapped in a transaction do_set_ext_data(Record, ExtData) when is_list(ExtData) -> - Ref = element(2, Record), lists:foreach( fun({Key, Val}) -> - {atomic, ok} = ts_ext_data:set_property(Ref, Key, Val) + {atomic, ok} = ts_ext_data:set_property(Record, Key, Val) end, ExtData), ok. diff --git a/src/ts_entry.erl b/src/ts_entry.erl index 266db8a..22dc088 100644 --- a/src/ts_entry.erl +++ b/src/ts_entry.erl @@ -19,7 +19,7 @@ new(ER = #ts_entry{}, ExtData) when is_list(ExtData) -> NewRow = do_new(ER), ts_common:do_set_ext_data(ER, ExtData), NewRow end), - NewRow. + {ok, NewRow}. do_new(ER = #ts_entry{}) -> {Username, TimelineId, _} = ER#ts_entry.ref, diff --git a/src/ts_ext_data.erl b/src/ts_ext_data.erl index 2cb8fcd..1fa967e 100644 --- a/src/ts_ext_data.erl +++ b/src/ts_ext_data.erl @@ -21,8 +21,8 @@ set_property(Rec=#ts_timeline{}, entry_exclusions, ExclusionList) -> do_set_property(Rec#ts_timeline.ref, entry_exclusions, ExclusionList); set_property(Rec, Key, _Value) -> - throw(io_lib:format("Property '~s' not available for a ~s record.", - [Key, element(1, Rec)])). + throw(lists:flatten(io_lib:format("Property '~s' not available for a ~s record.", + [Key, element(1, Rec)]))). get_property(Ref, PropKey) -> {atomic, Result} = mnesia:transaction(fun() -> diff --git a/src/ts_json.erl b/src/ts_json.erl index e310e74..56039c0 100644 --- a/src/ts_json.erl +++ b/src/ts_json.erl @@ -81,6 +81,16 @@ ejson_to_record(Empty, Ref, EJSON) -> Constructed = ejson_to_record(Empty, EJSON), setelement(2, Constructed, Ref). +ejson_to_record_strict(Empty=#ts_user{}, EJSON) -> + Constructed = 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 + end; + ejson_to_record_strict(Empty=#ts_timeline{}, EJSON) -> Constructed = ejson_to_record(Empty, EJSON), case Constructed of @@ -107,22 +117,55 @@ ejson_to_record_strict(Empty, Ref, EJSON) -> Constructed = ejson_to_record_strict(Empty, EJSON), setelement(2, Constructed, Ref). -construct_record(Timeline=#ts_timeline{}, [{Key, Val}|Fields], ExtData) -> +construct_record(User=#ts_user{}, [{Key, Value}|Fields], ExtData) -> case Key of - created -> construct_record( - Timeline#ts_timeline{created = decode_datetime(Val)}, + id -> construct_record(User#ts_user{username = Value}, + Fields, ExtData); + name -> construct_record(User#ts_user{name = Value}, Fields, ExtData); + join_date -> construct_record( + User#ts_user{join_date = decode_datetime(Value)}, + Fields, ExtData); + _Other -> + ExtDataProp = ejson_to_ext_data({Key, Value}), + construct_record(User, Fields, [ExtDataProp|ExtData]) + end; + +construct_record(Timeline=#ts_timeline{}, [{Key, Value}|Fields], ExtData) -> + case Key of + user_id -> + {_, TimelineId} = Timeline#ts_timeline.ref, + construct_record(Timeline#ts_timeline{ref = {Value, TimelineId}}, Fields, ExtData); - description -> construct_record(Timeline#ts_timeline{desc = Val}, + id -> + {Username, _} = Timeline#ts_timeline.ref, + construct_record(Timeline#ts_timeline{ref = {Username, Value}}, + Fields, ExtData); + created -> construct_record( + Timeline#ts_timeline{created = decode_datetime(Value)}, + Fields, ExtData); + description -> construct_record(Timeline#ts_timeline{desc = Value}, Fields, ExtData); _Other -> - ExtDataProp = ejson_to_ext_data({Key, Val}), + ExtDataProp = ejson_to_ext_data({Key, Value}), construct_record(Timeline, Fields, [ExtDataProp|ExtData]) end; construct_record(Entry=#ts_entry{}, [{Key, Value}|Fields], ExtData) -> case Key of + user_id -> + {_, TimelineId, EntryId} = Entry#ts_entry.ref, + construct_record(Entry#ts_entry{ref = {Value, TimelineId, EntryId}}, + Fields, ExtData); + timeline_id -> + {Username, _, EntryId} = Entry#ts_entry.ref, + construct_record(Entry#ts_entry{ref = {Username, Value, EntryId}}, + Fields, ExtData); + id -> + {Username, TimelineId, _} = Entry#ts_entry.ref, + construct_record(Entry#ts_entry{ref = {Username, TimelineId, Value}}, + Fields, ExtData); timestamp -> construct_record( - Entry#ts_entry{timestamp = calendar:datetime_to_gregoraian_seconds( + Entry#ts_entry{timestamp = calendar:datetime_to_gregorian_seconds( decode_datetime(Value))}, Fields, ExtData); mark -> construct_record(Entry#ts_entry{mark=Value}, Fields, ExtData); diff --git a/src/ts_user.erl b/src/ts_user.erl index e8242b6..1e4927c 100644 --- a/src/ts_user.erl +++ b/src/ts_user.erl @@ -13,31 +13,34 @@ create_table(TableOpts) -> % expects the password in clear new(UR = #ts_user{}) -> {atomic, Result} = mnesia:transaction(fun() -> do_new(UR) end), - Result. + {ok, Result}. new(UR = #ts_user{}, ExtData) -> {atomic, Result} = mnesia:transaction(fun() -> NewUser = do_new(UR), ts_common:do_set_ext_data(UR, ExtData), NewUser end), - Result. + {ok, Result}. do_new(UR = #ts_user{}) -> case mnesia:read(ts_user, UR#ts_user.username) of [ExistingRecord] -> {error, {record_exists, ExistingRecord}}; - [] -> mnesia:write(hash_input_record(UR)) + [] -> + NewRec = hash_input_record(UR), + mnesia:write(NewRec), + NewRec end. update(UR = #ts_user{}) -> {atomic, Result} = mnesia:transaction(fun() -> do_update(UR) end), - Result. + {ok, Result}. update(UR = #ts_user{}, ExtData) -> {atomic, Result} = mnesia:transaction(fun() -> UpdatedUser = do_update(UR), ts_common:do_set_ext_data(UR, ExtData), UpdatedUser end), - Result. + {ok, Result}. do_update(UR = #ts_user{}) -> case mnesia:read(ts_user, UR#ts_user.username) of @@ -48,7 +51,8 @@ do_update(UR = #ts_user{}) -> undefined -> UpdatedRecord; _Password -> hash_input_record(UpdatedRecord) end, - mnesia:write(HashedRecord) + mnesia:write(HashedRecord), + HashedRecord end. lookup(Username) -> diff --git a/www/js/ts.js b/www/js/ts.js index caa95a8..37fa7fb 100644 --- a/www/js/ts.js +++ b/www/js/ts.js @@ -535,7 +535,10 @@ $(document).ready(function(){ url: '/ts_api/app/user_summary/' + username, async: false}).responseText); - data.initialTimelineId = data.timelines[0].id; + // look for the last used timeline, default to first timeline + data.initialTimelineId = + data.user.last_timeline || data.timelines[0].id + data.entries = jQuery.parseJSON($.ajax({ url: '/ts_api/entries/' + username + '/' + data.initialTimelineId, @@ -555,6 +558,10 @@ $(document).ready(function(){ // set the timeline on the EntryList this.entries.collection.timeline = tl; + // update the last_timeline field of the user model + this.user.model.set('last_timeline', tl.get('id'); + this.user.model.save(); + // refresh TimelineListView this.timelines.view.render();