fix(calendar): persist settings on PUT/POST
Accept calendar.settings (e.g. week_patterns) in validate_update and convert_field; apply on create like tags. Unblocks Front#42 pattern save which previously got 200 with empty settings.
This commit is contained in:
@@ -85,6 +85,7 @@ calendar_schema() ->
|
|||||||
type => #{type => string, enum => [<<"personal">>, <<"commercial">>]},
|
type => #{type => string, enum => [<<"personal">>, <<"commercial">>]},
|
||||||
confirmation => #{type => string, enum => [<<"auto">>, <<"manual">>]},
|
confirmation => #{type => string, enum => [<<"auto">>, <<"manual">>]},
|
||||||
tags => #{type => array, items => #{type => string}},
|
tags => #{type => array, items => #{type => string}},
|
||||||
|
settings => #{type => object},
|
||||||
rating_avg => #{type => number, format => float},
|
rating_avg => #{type => number, format => float},
|
||||||
rating_count => #{type => integer},
|
rating_count => #{type => integer},
|
||||||
status => #{type => string},
|
status => #{type => string},
|
||||||
@@ -102,7 +103,12 @@ calendar_update_schema() ->
|
|||||||
description => #{type => string},
|
description => #{type => string},
|
||||||
type => #{type => string},
|
type => #{type => string},
|
||||||
confirmation => #{type => string},
|
confirmation => #{type => string},
|
||||||
tags => #{type => array, items => #{type => string}}
|
tags => #{type => array, items => #{type => string}},
|
||||||
|
short_name => #{type => string},
|
||||||
|
category => #{type => string},
|
||||||
|
color => #{type => string},
|
||||||
|
image_url => #{type => string},
|
||||||
|
settings => #{type => object, nullable => true}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
@@ -195,4 +201,9 @@ convert_field({<<"confirmation">>, #{<<"timeout">> := N}}) when is_integer(N), N
|
|||||||
{confirmation, {timeout, N}};
|
{confirmation, {timeout, N}};
|
||||||
convert_field({<<"confirmation">>, Val}) -> {confirmation, Val};
|
convert_field({<<"confirmation">>, Val}) -> {confirmation, Val};
|
||||||
convert_field({<<"tags">>, Val}) -> {tags, Val};
|
convert_field({<<"tags">>, Val}) -> {tags, Val};
|
||||||
|
convert_field({<<"short_name">>, Val}) -> {short_name, Val};
|
||||||
|
convert_field({<<"category">>, Val}) -> {category, Val};
|
||||||
|
convert_field({<<"color">>, Val}) -> {color, Val};
|
||||||
|
convert_field({<<"image_url">>, Val}) -> {image_url, Val};
|
||||||
|
convert_field({<<"settings">>, Val}) when is_map(Val) -> {settings, Val};
|
||||||
convert_field(Other) -> Other.
|
convert_field(Other) -> Other.
|
||||||
@@ -117,12 +117,21 @@ create_calendar(Req) ->
|
|||||||
Confirmation = parse_confirmation(maps:get(<<"confirmation">>, Decoded, <<"manual">>)),
|
Confirmation = parse_confirmation(maps:get(<<"confirmation">>, Decoded, <<"manual">>)),
|
||||||
Tags = maps:get(<<"tags">>, Decoded, []),
|
Tags = maps:get(<<"tags">>, Decoded, []),
|
||||||
Type = parse_type(maps:get(<<"type">>, Decoded, <<"personal">>)),
|
Type = parse_type(maps:get(<<"type">>, Decoded, <<"personal">>)),
|
||||||
|
Settings = maps:get(<<"settings">>, Decoded, undefined),
|
||||||
case logic_calendar:create_calendar(UserId, Title, Description, Confirmation, Type) of
|
case logic_calendar:create_calendar(UserId, Title, Description, Confirmation, Type) of
|
||||||
{ok, Calendar} ->
|
{ok, Calendar} ->
|
||||||
Updated = case Tags of
|
Extra0 = case Tags of
|
||||||
|
[] -> [];
|
||||||
|
_ -> [{tags, Tags}]
|
||||||
|
end,
|
||||||
|
Extra = case Settings of
|
||||||
|
S when is_map(S) -> [{settings, S} | Extra0];
|
||||||
|
_ -> Extra0
|
||||||
|
end,
|
||||||
|
Updated = case Extra of
|
||||||
[] -> Calendar;
|
[] -> Calendar;
|
||||||
_ ->
|
_ ->
|
||||||
case logic_calendar:update_calendar(UserId, Calendar#calendar.id, [{tags, Tags}]) of
|
case logic_calendar:update_calendar(UserId, Calendar#calendar.id, Extra) of
|
||||||
{ok, C2} -> C2;
|
{ok, C2} -> C2;
|
||||||
_ -> Calendar
|
_ -> Calendar
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -242,6 +242,14 @@ normalize_update({type, <<"personal">>}) -> {type, personal};
|
|||||||
normalize_update({type, <<"commercial">>}) -> {type, commercial};
|
normalize_update({type, <<"commercial">>}) -> {type, commercial};
|
||||||
normalize_update({type, personal}) -> {type, personal};
|
normalize_update({type, personal}) -> {type, personal};
|
||||||
normalize_update({type, commercial}) -> {type, commercial};
|
normalize_update({type, commercial}) -> {type, commercial};
|
||||||
|
normalize_update({<<"settings">>, Value}) when is_map(Value) -> {settings, Value};
|
||||||
|
normalize_update({<<"title">>, Value}) when is_binary(Value) -> {title, Value};
|
||||||
|
normalize_update({<<"description">>, Value}) when is_binary(Value) -> {description, Value};
|
||||||
|
normalize_update({<<"tags">>, Value}) when is_list(Value) -> {tags, Value};
|
||||||
|
normalize_update({<<"short_name">>, Value}) when is_binary(Value) -> {short_name, Value};
|
||||||
|
normalize_update({<<"category">>, Value}) when is_binary(Value) -> {category, Value};
|
||||||
|
normalize_update({<<"color">>, Value}) when is_binary(Value) -> {color, Value};
|
||||||
|
normalize_update({<<"image_url">>, Value}) when is_binary(Value) -> {image_url, Value};
|
||||||
normalize_update(Other) -> Other.
|
normalize_update(Other) -> Other.
|
||||||
|
|
||||||
validate_update({title, Value}) when is_binary(Value) -> true;
|
validate_update({title, Value}) when is_binary(Value) -> true;
|
||||||
@@ -249,6 +257,12 @@ validate_update({description, Value}) when is_binary(Value) -> true;
|
|||||||
validate_update({tags, Value}) when is_list(Value) -> true;
|
validate_update({tags, Value}) when is_list(Value) -> true;
|
||||||
validate_update({type, personal}) -> true;
|
validate_update({type, personal}) -> true;
|
||||||
validate_update({type, commercial}) -> true;
|
validate_update({type, commercial}) -> true;
|
||||||
|
validate_update({short_name, Value}) when is_binary(Value) -> true;
|
||||||
|
validate_update({category, Value}) when is_binary(Value) -> true;
|
||||||
|
validate_update({color, Value}) when is_binary(Value) -> true;
|
||||||
|
validate_update({image_url, Value}) when is_binary(Value) -> true;
|
||||||
|
%% Opaque JSON object for client prefs (e.g. week_patterns for schedule fill).
|
||||||
|
validate_update({settings, Value}) when is_map(Value) -> true;
|
||||||
validate_update({confirmation, Value}) ->
|
validate_update({confirmation, Value}) ->
|
||||||
case Value of
|
case Value of
|
||||||
auto -> true;
|
auto -> true;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ test() ->
|
|||||||
test_get_calendar_unauthorized(CalId),
|
test_get_calendar_unauthorized(CalId),
|
||||||
test_get_calendar_not_found(Token),
|
test_get_calendar_not_found(Token),
|
||||||
test_update_calendar(Token, CalId),
|
test_update_calendar(Token, CalId),
|
||||||
|
test_update_calendar_settings(Token, CalId),
|
||||||
test_update_calendar_forbidden(OtherToken, CalId),
|
test_update_calendar_forbidden(OtherToken, CalId),
|
||||||
test_delete_calendar(Token, CalId),
|
test_delete_calendar(Token, CalId),
|
||||||
test_delete_calendar_forbidden(OtherToken, CalId),
|
test_delete_calendar_forbidden(OtherToken, CalId),
|
||||||
@@ -60,6 +61,26 @@ test_update_calendar(Token, CalId) ->
|
|||||||
?assertEqual(<<"New desc">>, maps:get(<<"description">>, Updated)),
|
?assertEqual(<<"New desc">>, maps:get(<<"description">>, Updated)),
|
||||||
ct:pal(" OK").
|
ct:pal(" OK").
|
||||||
|
|
||||||
|
test_update_calendar_settings(Token, CalId) ->
|
||||||
|
ct:pal(" TEST: Persist calendar.settings.week_patterns"),
|
||||||
|
Path = <<"/v1/calendars/", CalId/binary>>,
|
||||||
|
Settings = #{
|
||||||
|
<<"week_patterns">> => [
|
||||||
|
#{
|
||||||
|
<<"id">> => <<"pat1">>,
|
||||||
|
<<"name">> => <<"Mon-Fri">>,
|
||||||
|
<<"slots">> => [
|
||||||
|
#{<<"weekday">> => 1, <<"time">> => <<"10:00">>, <<"duration">> => 60, <<"title">> => <<"Open">>}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
Updated = api_test_runner:client_put(Path, Token, #{settings => Settings}),
|
||||||
|
?assertEqual(Settings, maps:get(<<"settings">>, Updated)),
|
||||||
|
Got = api_test_runner:client_get(Path, Token),
|
||||||
|
?assertEqual(Settings, maps:get(<<"settings">>, Got)),
|
||||||
|
ct:pal(" OK").
|
||||||
|
|
||||||
test_update_calendar_forbidden(OtherToken, CalId) ->
|
test_update_calendar_forbidden(OtherToken, CalId) ->
|
||||||
ct:pal(" TEST: Update calendar by non-owner (403)"),
|
ct:pal(" TEST: Update calendar by non-owner (403)"),
|
||||||
Path = <<"/v1/calendars/", CalId/binary>>,
|
Path = <<"/v1/calendars/", CalId/binary>>,
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ logic_calendar_test_() ->
|
|||||||
{"Get calendar test", fun test_get_calendar/0},
|
{"Get calendar test", fun test_get_calendar/0},
|
||||||
{"List calendars test", fun test_list_calendars/0},
|
{"List calendars test", fun test_list_calendars/0},
|
||||||
{"Update calendar test", fun test_update_calendar/0},
|
{"Update calendar test", fun test_update_calendar/0},
|
||||||
|
{"Persist calendar settings (week_patterns)", fun test_update_settings/0},
|
||||||
|
{"Reject non-map settings", fun test_update_settings_invalid/0},
|
||||||
{"Delete calendar test", fun test_delete_calendar/0},
|
{"Delete calendar test", fun test_delete_calendar/0},
|
||||||
{"Access control test", fun test_access_control/0},
|
{"Access control test", fun test_access_control/0},
|
||||||
{"Ensure default calendar test", fun test_ensure_default_calendar/0}
|
{"Ensure default calendar test", fun test_ensure_default_calendar/0}
|
||||||
@@ -117,6 +119,39 @@ test_update_calendar() ->
|
|||||||
?assertMatch({error, access_denied},
|
?assertMatch({error, access_denied},
|
||||||
logic_calendar:update_calendar(OtherUserId, Calendar#calendar.id, Updates)).
|
logic_calendar:update_calendar(OtherUserId, Calendar#calendar.id, Updates)).
|
||||||
|
|
||||||
|
test_update_settings() ->
|
||||||
|
UserId = create_test_user(),
|
||||||
|
{ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Studio">>, <<"">>, manual),
|
||||||
|
Patterns = [
|
||||||
|
#{
|
||||||
|
<<"id">> => <<"p1">>,
|
||||||
|
<<"name">> => <<"Weekdays">>,
|
||||||
|
<<"updated_at">> => <<"2026-07-29T12:00:00Z">>,
|
||||||
|
<<"slots">> => [
|
||||||
|
#{<<"weekday">> => 1, <<"time">> => <<"10:00">>, <<"duration">> => 60, <<"title">> => <<"Open">>}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
Settings = #{<<"week_patterns">> => Patterns},
|
||||||
|
{ok, Updated} = logic_calendar:update_calendar(
|
||||||
|
UserId, Calendar#calendar.id, [{settings, Settings}]),
|
||||||
|
?assertEqual(Settings, Updated#calendar.settings),
|
||||||
|
{ok, Reloaded} = logic_calendar:get_calendar(UserId, Calendar#calendar.id),
|
||||||
|
?assertEqual(Settings, Reloaded#calendar.settings),
|
||||||
|
%% binary key path (as from raw JSON list before convert_field)
|
||||||
|
Settings2 = Settings#{<<"foo">> => <<"bar">>},
|
||||||
|
{ok, Updated2} = logic_calendar:update_calendar(
|
||||||
|
UserId, Calendar#calendar.id, [{<<"settings">>, Settings2}]),
|
||||||
|
?assertEqual(Settings2, Updated2#calendar.settings).
|
||||||
|
|
||||||
|
test_update_settings_invalid() ->
|
||||||
|
UserId = create_test_user(),
|
||||||
|
{ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Studio">>, <<"">>, manual),
|
||||||
|
%% non-map settings silently dropped by validate_updates — calendar unchanged
|
||||||
|
{ok, Same} = logic_calendar:update_calendar(
|
||||||
|
UserId, Calendar#calendar.id, [{settings, [<<"not-a-map">>]}]),
|
||||||
|
?assertEqual(#{}, Same#calendar.settings).
|
||||||
|
|
||||||
test_delete_calendar() ->
|
test_delete_calendar() ->
|
||||||
UserId = create_test_user(),
|
UserId = create_test_user(),
|
||||||
{ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Test">>, <<"">>, manual),
|
{ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Test">>, <<"">>, manual),
|
||||||
|
|||||||
Reference in New Issue
Block a user