Fixed timeline creation, API logical fixes.

* Resolved issues:

    * #0006: Fix timeline menu UI.
    * #0015: Create new timeline button in timeline menu.
    * #0017: Implement timeline creation.

* Removed ts_api:post_timeline/3, corresponded to `POST` to
  `/ts_api/timelines/<user-id>`, which makes no sense as there is no way to
  auto-generate new timeline-ids.
* Changed ts_api:put_timeline/3 to use ts_timeline:write. This resolved #0017.
* Changed ts_api:put_entry/4 to use ts_entry:write.
* Implemented ts_entry:write/1 and ts_timeline:write/1. These functions are
  simple passthroughs to mnesia:dirty_write/1.
* Added clarifing documentation for ts_json:record_to_ejson JSON formats.
* Renamed ts_json:ejson_to_record/2 to ts_json:ejson_to_record_strict/2 and
  added ts_json:ejson_to_record/2 as a lax version of the same.
* Updated all ts_api functions to use updated ts_json methods.
This commit is contained in:
Jonathan Bernard 2011-06-07 20:47:23 -05:00
parent bc364b2ebd
commit b5eadd6fc4
8 changed files with 98 additions and 96 deletions

View File

@ -5,5 +5,5 @@ UI for timeline menu does not work.
========= ========== ========= ==========
Created: 2011-05-15 Created: 2011-05-15
Resolved: YYYY-MM-DD Resolved: 2011-06-07
========= ========== ========= ==========

View File

@ -5,5 +5,5 @@ Need a button to trigger the new timeline dialog.
========= ========== ========= ==========
Created: 2011-06-01 Created: 2011-06-01
Resolved: YYYY-MM-DD Resolved: 2011-06-07
========= ========== ========= ==========

View File

@ -2,6 +2,6 @@ Implement timeline creation.
============================ ============================
========= ========== ========= ==========
Created: 2011-05-15 Created: 2011-06-01
Resolved: YYYY-MM-DD Resolved: 2011-06-07
========= ========== ========= ==========

View File

@ -0,0 +1,9 @@
Error in UI when creating or selecting a timeline.
==================================================
Brief description.
========= ==========
Created: 2011-06-07
Resolved: YYYY-MM-DD
========= ==========

View File

@ -116,7 +116,6 @@ dispatch_timeline(YArg, [Username, TimelineId]) ->
case HTTPMethod of case HTTPMethod of
'GET' -> get_timeline(YArg, Username, TimelineId); 'GET' -> get_timeline(YArg, Username, TimelineId);
'POST' -> post_timeline(YArg, Username, TimelineId);
'PUT' -> put_timeline(YArg, Username, TimelineId); 'PUT' -> put_timeline(YArg, Username, TimelineId);
'DELETE' -> delete_timeline(YArg, Username, TimelineId); 'DELETE' -> delete_timeline(YArg, Username, TimelineId);
_Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/timelines.html"}]) _Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/timelines.html"}])
@ -263,70 +262,21 @@ get_timeline(YArg, Username, TimelineId) ->
Timeline -> make_json_200(YArg, Timeline) Timeline -> make_json_200(YArg, Timeline)
end. end.
post_timeline(YArg, Username, TimelineId) ->
% parse the request body
EJSON = parse_json_body(YArg),
% parse into a Timeline record
TR = try ts_json:ejson_to_record(#ts_timeline{}, EJSON)
catch _:InputError ->
error_logger:error_report("Bad input: ~p", [InputError]),
throw(make_json_400(YArg))
end,
% set username and timeline id
NewRecord = TR#ts_timeline{ref = {Username, TimelineId}},
% insert into the database
case ts_timeline:new(NewRecord) of
% record created
ok -> [{status, 201}, make_json_200(YArg, NewRecord)];
% will not create, record exists
{error, {record_exists, ExistingRecord}} ->
JSONResponse = json:encode(ts_json:record_to_ejson(ExistingRecord)),
{content, "application/json", JSONResponse};
Error ->
error_logger:error_report("Unable to create a new timeline: ~p", [Error]),
make_json_500(YArg, Error)
end.
put_timeline(YArg, Username, TimelineId) -> put_timeline(YArg, Username, TimelineId) ->
% parse the POST data % parse the POST data
EJSON = parse_json_body(YArg), EJSON = parse_json_body(YArg),
%{struct, Fields} = EJSON, %{struct, Fields} = EJSON,
% parse into a timeline record % parse into a timeline record
TR = try ts_json:ejson_to_record(#ts_timeline{}, EJSON) TR = try ts_json:ejson_to_record_strict(
#ts_timeline{ref={Username, TimelineId}}, EJSON)
catch _:InputError -> catch _:InputError ->
error_logger:error_report("Bad input: ~p", [InputError]), error_logger:error_report("Bad input: ~p", [InputError]),
throw(make_json_400(YArg)) throw(make_json_400(YArg))
end, end,
% not supported right now, would require changing all the entry keys ts_timeline:write(TR),
% check to see if they are changing the timeline id make_json_200(YArg, TR).
%NewTimelineId = case lists:keyfind(1, timeline_id, Fields) of
% {timeline_id, Field} -> Field;
% false -> TimelineId end,
% set username and timeline id
NewRecord = TR#ts_timeline{ref = {Username, TimelineId}},
case ts_timeline:update(NewRecord) of
ok -> make_json_200(YArg, NewRecord);
no_record -> make_json_404(YArg,
[{error, "no such timeline"},
{see_docs, "/ts_api_doc/timelines.html#PUT"}]);
Error ->
error_logger:error_report("Unable update timeline: ~p", [Error]),
make_json_500(YArg, Error)
end.
delete_timeline(_YArg, _Username, _TimelineId) -> {status, 405}. delete_timeline(_YArg, _Username, _TimelineId) -> {status, 405}.
@ -424,16 +374,14 @@ post_entry(YArg, Username, TimelineId) ->
EJSON = parse_json_body(YArg), EJSON = parse_json_body(YArg),
% parse into ts_entry record % parse into ts_entry record
ER = try ts_json:ejson_to_record(#ts_entry{}, EJSON) ER = try ts_json:ejson_to_record_strict(
#ts_entry{ref = {Username, TimelineId, undefined}}, EJSON)
catch _:InputError -> catch _:InputError ->
error_logger:error_report("Bad input: ~p", [InputError]), error_logger:error_report("Bad input: ~p", [InputError]),
throw(make_json_400(YArg)) throw(make_json_400(YArg))
end, end,
% set username and timeline id case ts_entry:new(ER) of
NewRecord = ER#ts_entry{ref = {Username, TimelineId, undef}},
case ts_entry:new(NewRecord) of
% record created % record created
{ok, CreatedRecord} -> {ok, CreatedRecord} ->
[{status, 201}, make_json_200(YArg, CreatedRecord)]; [{status, 201}, make_json_200(YArg, CreatedRecord)];
@ -455,25 +403,15 @@ put_entry(YArg, Username, TimelineId, EntryId) ->
EJSON = parse_json_body(YArg), EJSON = parse_json_body(YArg),
% parse into ts_entry record % parse into ts_entry record
ER = try ts_json:ejson_to_record(#ts_entry{}, EJSON) ER = try ts_json:ejson_to_record_strict(
#ts_entry{ref={Username, TimelineId, EntryId}}, EJSON)
catch _:InputError -> catch _:InputError ->
error_logger:error_report("Bad input: ~p", [InputError]), error_logger:error_report("Bad input: ~p", [InputError]),
throw(make_json_400(YArg)) throw(make_json_400(YArg))
end, end,
% set uername, timeline id, and entry id ts_entry:write(ER),
NewRecord = ER#ts_entry{ref = {Username, TimelineId, EntryId}}, make_json_200(YArg, ER).
case ts_entry:update(NewRecord) of
ok -> make_json_200(YArg, NewRecord);
no_record -> make_json_404(YArg,
[{error, "no such entry"}, {see_docs, "/ts_api_doc/entries.html#POST"}]);
Error ->
error_logger:error_report("TimeStamper: Unable to update entry: ~p", [Error]),
make_json_500(YArg, Error)
end.
delete_entry(YArg, Username, TimelineId, EntryId) -> delete_entry(YArg, Username, TimelineId, EntryId) ->
@ -544,7 +482,7 @@ make_json_404(YArg, Fields) ->
[{status, 404}, {content, "application/json", json:encode({struct, F2})}]. [{status, 404}, {content, "application/json", json:encode({struct, F2})}].
make_json_405(YArg) -> make_json_405(YArg, []). make_json_405(YArg) -> make_json_405(YArg, []).
make_json_405(YArg, Fields) -> make_json_405(_YArg, Fields) ->
% add default status if not provided % add default status if not provided
F1 = case lists:keyfind(status, 1, Fields) of F1 = case lists:keyfind(status, 1, Fields) of
false -> Fields ++ [{status, "method not allowed"}]; false -> Fields ++ [{status, "method not allowed"}];

View File

@ -1,5 +1,5 @@
-module(ts_entry). -module(ts_entry).
-export([create_table/1, new/1, update/1, delete/1, lookup/3, list_asc/3, list_desc/3]). -export([create_table/1, new/1, update/1, write/1, delete/1, lookup/3, list_asc/3, list_desc/3]).
-include("ts_db_records.hrl"). -include("ts_db_records.hrl").
-include_lib("stdlib/include/qlc.hrl"). -include_lib("stdlib/include/qlc.hrl").
@ -18,16 +18,9 @@ new(ER = #ts_entry{}) ->
NewRow end), NewRow end),
{ok, NewRow}. {ok, NewRow}.
update(ER = #ts_entry{}) -> update(ER = #ts_entry{}) -> ts_common:update(ER).
% look for existing record write(ER = #ts_entry{}) -> mnesia:dirty_write(ER).
case mnesia:dirty_read(ts_entry, ER#ts_entry.ref) of
% record does not exist
[] -> no_record;
% record exists, update it
[ExistingRecord] ->
mnesia:dirty_write(ts_common:update_record(ExistingRecord, ER))
end.
lookup(Username, TimelineId, EntryId) -> lookup(Username, TimelineId, EntryId) ->
case mnesia:dirty_read(ts_entry, {Username, TimelineId, EntryId}) of case mnesia:dirty_read(ts_entry, {Username, TimelineId, EntryId}) of

View File

@ -1,10 +1,17 @@
-module(ts_json). -module(ts_json).
-export([encode_record/1, record_to_ejson/1, ejson_to_record/2]). -export([encode_record/1, record_to_ejson/1, ejson_to_record/2, ejson_to_record_strict/2]).
-include("ts_db_records.hrl"). -include("ts_db_records.hrl").
encode_record(Record) -> lists:flatten(json:encode(record_to_ejson(Record))). encode_record(Record) -> lists:flatten(json:encode(record_to_ejson(Record))).
% User JSON record structure:
% {"id": "john_doe",
% "name": "John Doe",
% "email": "john.doe@example.com",
% "join_date": "2011-01-01T12:00.000Z",
% "ext_data": {"last_timeline", "personal"}}
record_to_ejson(Record=#ts_user{}) -> record_to_ejson(Record=#ts_user{}) ->
{struct, [ {struct, [
{id, Record#ts_user.username}, {id, Record#ts_user.username},
@ -13,6 +20,12 @@ record_to_ejson(Record=#ts_user{}) ->
{join_date, encode_datetime(Record#ts_user.join_date)}, {join_date, encode_datetime(Record#ts_user.join_date)},
{ext_data, Record#ts_user.ext_data}]}; {ext_data, Record#ts_user.ext_data}]};
% Timeline JSON record stucture:
% {"user_id": "john_doe",
% "id": "personal",
% "created": "2011-01-01T14:00.000Z",
% "description:"Personal time-tracking."}
record_to_ejson(Record=#ts_timeline{}) -> record_to_ejson(Record=#ts_timeline{}) ->
% pull out the username and timeline id % pull out the username and timeline id
{Username, TimelineId} = Record#ts_timeline.ref, {Username, TimelineId} = Record#ts_timeline.ref,
@ -24,6 +37,14 @@ record_to_ejson(Record=#ts_timeline{}) ->
{created, encode_datetime(Record#ts_timeline.created)}, {created, encode_datetime(Record#ts_timeline.created)},
{description, Record#ts_timeline.desc}]}; {description, Record#ts_timeline.desc}]};
% Entry JSON record structure:
% {"user_id": "john_doe",
% "timeline_id": "personal",
% "id": "1",
% "timestamp": "2011-01-01T14:01.000Z",
% "mark": "Workout.",
% "notes": "First workout after a long break."}
record_to_ejson(Record=#ts_entry{}) -> record_to_ejson(Record=#ts_entry{}) ->
% pull out the username, timeline id, and entry id % pull out the username, timeline id, and entry id
{Username, TimelineId, EntryId} = Record#ts_entry.ref, {Username, TimelineId, EntryId} = Record#ts_entry.ref,
@ -44,19 +65,58 @@ encode_datetime({{Year, Month, Day}, {Hour, Minute, Second}}) ->
lists:flatten(io_lib:format("~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0B.~3.10.0BZ", lists:flatten(io_lib:format("~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0B.~3.10.0BZ",
[Year, Month, Day, Hour, Minute, Second, 000])). [Year, Month, Day, Hour, Minute, Second, 000])).
ejson_to_record(_Empty=#ts_timeline{}, EJSON) -> ejson_to_record(Rec=#ts_timeline{}, EJSON) ->
{struct, Fields} = EJSON, {struct, Fields} = EJSON,
#ts_timeline{ Created = case lists:keyfind(created, 1, Fields) of
ref = {undefined, undefined}, false -> undefined;
{created, CreatedVal} -> decode_datetime(CreatedVal)
end,
Desc = case lists:keyfind(description, 1, Fields) of
false -> undefined;
{description, DescVal} -> DescVal
end,
Rec#ts_timeline{
created = Created,
desc = Desc };
ejson_to_record(Rec=#ts_entry{}, EJSON) ->
{struct, Fields} = EJSON,
Timestamp = case lists:keyfind(timestamp, 1, Fields) of
false -> undefined;
{timestamp, TSVal} -> calendar:datetime_to_gregorian_seconds(
decode_datetime(TSVal))
end,
Mark = case lists:keyfind(mark, 1, Fields) of
false -> undefined;
{mark, MarkVal} -> MarkVal
end,
Notes = case lists:keyfind(notes, 1, Fields) of
false -> undefined;
{notes, NotesVal} -> NotesVal
end,
Rec#ts_entry{
timestamp = Timestamp,
mark = Mark,
notes = Notes}.
ejson_to_record_strict(Rec=#ts_timeline{}, EJSON) ->
{struct, Fields} = EJSON,
Rec#ts_timeline{
created = decode_datetime(element(2, lists:keyfind(created, 1, Fields))), created = decode_datetime(element(2, lists:keyfind(created, 1, Fields))),
desc = element(2, lists:keyfind(description, 1, Fields))}; desc = element(2, lists:keyfind(description, 1, Fields))};
ejson_to_record(_Empty=#ts_entry{}, EJSON) -> ejson_to_record_strict(Rec=#ts_entry{}, EJSON) ->
{struct, Fields} = EJSON, {struct, Fields} = EJSON,
#ts_entry{ Rec#ts_entry{
ref = {undefined, undefined, undefined},
timestamp = calendar:datetime_to_gregorian_seconds(decode_datetime( timestamp = calendar:datetime_to_gregorian_seconds(decode_datetime(
element(2, lists:keyfind(timestamp, 1, Fields)))), element(2, lists:keyfind(timestamp, 1, Fields)))),
mark = element(2, lists:keyfind(mark, 1, Fields)), mark = element(2, lists:keyfind(mark, 1, Fields)),

View File

@ -1,5 +1,5 @@
-module(ts_timeline). -module(ts_timeline).
-export([create_table/1, new/1, update/1, lookup/2, list/3]). -export([create_table/1, new/1, update/1, write/1, lookup/2, list/3]).
-include("ts_db_records.hrl"). -include("ts_db_records.hrl").
-include_lib("stdlib/include/qlc.hrl"). -include_lib("stdlib/include/qlc.hrl").
@ -13,6 +13,8 @@ new(TR = #ts_timeline{}) -> ts_common:new(TR).
update(TR = #ts_timeline{}) -> ts_common:update(TR). update(TR = #ts_timeline{}) -> ts_common:update(TR).
write(TR = #ts_timeline{}) -> mnesia:dirty_write(TR).
lookup(Username, TimelineId) -> lookup(Username, TimelineId) ->
case mnesia:dirty_read(ts_timeline, {Username, TimelineId}) of case mnesia:dirty_read(ts_timeline, {Username, TimelineId}) of
[] -> no_record; [] -> no_record;