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:
		| @@ -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 | ||||||
| =========  ========== | =========  ========== | ||||||
| @@ -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 | ||||||
| =========  ========== | =========  ========== | ||||||
| @@ -2,6 +2,6 @@ Implement timeline creation. | |||||||
| ============================ | ============================ | ||||||
| 
 | 
 | ||||||
| =========  ========== | =========  ========== | ||||||
| Created:   2011-05-15 | Created:   2011-06-01 | ||||||
| Resolved:  YYYY-MM-DD | Resolved:  2011-06-07 | ||||||
| =========  ========== | =========  ========== | ||||||
							
								
								
									
										9
									
								
								doc/issues/desktop/0018bn2.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								doc/issues/desktop/0018bn2.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | Error in UI when creating or selecting a timeline. | ||||||
|  | ================================================== | ||||||
|  |  | ||||||
|  | Brief description. | ||||||
|  |  | ||||||
|  | =========  ========== | ||||||
|  | Created:   2011-06-07 | ||||||
|  | Resolved:  YYYY-MM-DD | ||||||
|  | =========  ========== | ||||||
| @@ -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"}]; | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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)), | ||||||
|   | |||||||
| @@ -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"). | ||||||
| @@ -12,6 +12,8 @@ create_table(TableOpts) -> | |||||||
| new(TR = #ts_timeline{}) -> ts_common:new(TR). | 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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user