Раздел api Календари #22
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
-module(admin_handler_calendar_by_id).
|
||||
-behaviour(cowboy_handler).
|
||||
-export([init/2]).
|
||||
-export([trails/0]).
|
||||
|
||||
-include("records.hrl").
|
||||
|
||||
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
|
||||
init(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"GET">> -> get_calendar(Req);
|
||||
<<"PUT">> -> update_calendar(Req);
|
||||
<<"DELETE">> -> delete_calendar(Req);
|
||||
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
%%% Swagger
|
||||
-spec trails() -> [map()].
|
||||
trails() ->
|
||||
Path = <<"/v1/admin/calendars/{id}">>,
|
||||
[
|
||||
#{
|
||||
path => Path,
|
||||
method => <<"GET">>,
|
||||
description => <<"Get calendar by ID (admin)">>,
|
||||
tags => [<<"Admin Calendars">>],
|
||||
parameters => [id_param()],
|
||||
responses => #{
|
||||
200 => #{description => <<"Calendar object">>, content => calendar_content()},
|
||||
404 => #{description => <<"Not found">>}
|
||||
}
|
||||
},
|
||||
#{
|
||||
path => Path,
|
||||
method => <<"PUT">>,
|
||||
description => <<"Update calendar (admin)">>,
|
||||
tags => [<<"Admin Calendars">>],
|
||||
parameters => [id_param()],
|
||||
requestBody => update_body(),
|
||||
responses => #{
|
||||
200 => #{description => <<"Updated calendar">>, content => calendar_content()},
|
||||
400 => #{description => <<"Bad request">>},
|
||||
404 => #{description => <<"Not found">>}
|
||||
}
|
||||
},
|
||||
#{
|
||||
path => Path,
|
||||
method => <<"DELETE">>,
|
||||
description => <<"Delete calendar (admin)">>,
|
||||
tags => [<<"Admin Calendars">>],
|
||||
parameters => [id_param()],
|
||||
responses => #{
|
||||
200 => #{description => <<"Deleted">>},
|
||||
404 => #{description => <<"Not found">>}
|
||||
}
|
||||
}
|
||||
].
|
||||
|
||||
id_param() -> #{name => id, in => path, required => true, schema => #{type => string}}.
|
||||
calendar_content() -> #{<<"application/json">> => #{schema => admin_handler_calendars:calendar_schema()}}.
|
||||
update_body() ->
|
||||
#{
|
||||
required => true,
|
||||
content => #{
|
||||
<<"application/json">> => #{
|
||||
schema => #{
|
||||
type => object,
|
||||
properties => #{
|
||||
title => #{type => string},
|
||||
description => #{type => string},
|
||||
is_public => #{type => boolean}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% HTTP-методы
|
||||
%%%===================================================================
|
||||
|
||||
-spec get_calendar(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
|
||||
get_calendar(Req) ->
|
||||
case handler_utils:auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
Id = cowboy_req:binding(id, Req1),
|
||||
case logic_calendar:admin_get_by_id(Id) of
|
||||
{ok, Calendar} ->
|
||||
Json = handler_utils:calendar_to_json(Calendar),
|
||||
handler_utils:send_json(Req1, 200, Json);
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req1, 404, <<"Calendar not found">>)
|
||||
end;
|
||||
{error, Code, Msg, Req1} ->
|
||||
handler_utils:send_error(Req1, Code, Msg)
|
||||
end.
|
||||
|
||||
-spec update_calendar(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
|
||||
update_calendar(Req) ->
|
||||
case handler_utils:auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
Id = cowboy_req:binding(id, Req1),
|
||||
case cowboy_req:read_body(Req1) of
|
||||
{ok, Body, Req2} ->
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
Updates ->
|
||||
case logic_calendar:admin_update(Id, Updates) of
|
||||
{ok, Updated} ->
|
||||
Json = handler_utils:calendar_to_json(Updated),
|
||||
handler_utils:send_json(Req2, 200, Json);
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req2, 404, <<"Calendar not found">>);
|
||||
{error, _} ->
|
||||
handler_utils:send_error(Req2, 400, <<"Update failed">>)
|
||||
end
|
||||
catch
|
||||
_:_ -> handler_utils:send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
end;
|
||||
{error, _} ->
|
||||
handler_utils:send_error(Req1, 400, <<"Missing body">>)
|
||||
end;
|
||||
{error, Code, Msg, Req1} ->
|
||||
handler_utils:send_error(Req1, Code, Msg)
|
||||
end.
|
||||
|
||||
-spec delete_calendar(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
|
||||
delete_calendar(Req) ->
|
||||
case handler_utils:auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
Id = cowboy_req:binding(id, Req1),
|
||||
case logic_calendar:admin_delete(Id) of
|
||||
ok ->
|
||||
handler_utils:send_json(Req1, 200, #{});
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req1, 404, <<"Calendar not found">>)
|
||||
end;
|
||||
{error, Code, Msg, Req1} ->
|
||||
handler_utils:send_error(Req1, Code, Msg)
|
||||
end.
|
||||
@@ -0,0 +1,193 @@
|
||||
-module(admin_handler_calendars).
|
||||
-behaviour(cowboy_handler).
|
||||
-export([init/2]).
|
||||
-export([trails/0]).
|
||||
-export([calendar_schema/0]). % для использования в admin_handler_calendar_by_id
|
||||
|
||||
-include("records.hrl").
|
||||
|
||||
init(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"GET">> -> list_calendars(Req);
|
||||
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
trails() ->
|
||||
[
|
||||
#{
|
||||
path => <<"/v1/admin/calendars">>,
|
||||
method => <<"GET">>,
|
||||
description => <<"List all calendars with filtering, sorting, pagination (admin)">>,
|
||||
tags => [<<"Admin Calendars">>],
|
||||
parameters => [
|
||||
#{name => limit, in => query, schema => #{type => integer}, description => <<"Limit">>},
|
||||
#{name => offset, in => query, schema => #{type => integer}, description => <<"Offset">>},
|
||||
#{name => status, in => query, schema => #{type => string}, description => <<"Filter by status (active, frozen, deleted)">>},
|
||||
#{name => type, in => query, schema => #{type => string}, description => <<"Filter by type (personal, commercial)">>},
|
||||
#{name => q, in => query, schema => #{type => string}, description => <<"Search query">>},
|
||||
#{name => sort_by, in => query, schema => #{type => string, enum => [<<"title">>, <<"created_at">>, <<"updated_at">>]}, description => <<"Sort field">>},
|
||||
#{name => sort_order, in => query, schema => #{type => string, enum => [<<"asc">>, <<"desc">>]}, description => <<"Sort direction">>}
|
||||
],
|
||||
responses => #{
|
||||
200 => #{
|
||||
description => <<"Array of calendars">>,
|
||||
content => #{
|
||||
<<"application/json">> => #{
|
||||
schema => #{
|
||||
type => array,
|
||||
items => calendar_schema()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
403 => #{description => <<"Forbidden">>}
|
||||
}
|
||||
}
|
||||
].
|
||||
|
||||
calendar_schema() ->
|
||||
#{
|
||||
type => object,
|
||||
properties => #{
|
||||
id => #{type => string},
|
||||
owner_id => #{type => string},
|
||||
title => #{type => string},
|
||||
description => #{type => string},
|
||||
short_name => #{type => string},
|
||||
category => #{type => string},
|
||||
color => #{type => string},
|
||||
image_url => #{type => string},
|
||||
settings => #{type => object},
|
||||
tags => #{type => array, items => #{type => string}},
|
||||
type => #{type => string, enum => [<<"personal">>, <<"commercial">>]},
|
||||
confirmation => #{type => string},
|
||||
rating_avg => #{type => number},
|
||||
rating_count => #{type => integer},
|
||||
status => #{type => string, enum => [<<"active">>, <<"frozen">>, <<"deleted">>]},
|
||||
reason => #{type => string},
|
||||
created_at => #{type => string, format => date_time},
|
||||
updated_at => #{type => string, format => date_time}
|
||||
}
|
||||
}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% HTTP-метод
|
||||
%%%===================================================================
|
||||
|
||||
list_calendars(Req) ->
|
||||
case handler_utils:auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
% Парсим параметры
|
||||
Qs = cowboy_req:parse_qs(Req1),
|
||||
Filters = parse_filters(Qs),
|
||||
Sort = parse_sort(Qs),
|
||||
Pagination = handler_utils:parse_pagination_params(Req1),
|
||||
|
||||
% Получаем все календари
|
||||
{ok, AllCalendars} = logic_calendar:admin_list_all(),
|
||||
|
||||
% Применяем фильтры
|
||||
Filtered = apply_filters(AllCalendars, Filters),
|
||||
|
||||
% Сортируем
|
||||
Sorted = apply_sort(Filtered, Sort),
|
||||
|
||||
% Пагинация
|
||||
Total = length(Sorted),
|
||||
Start = maps:get(offset, Pagination) + 1,
|
||||
Limit = maps:get(limit, Pagination),
|
||||
Page = lists:sublist(Sorted, Start, Limit),
|
||||
|
||||
Json = [handler_utils:calendar_to_json(C) || C <- Page],
|
||||
ExtraHeaders = handler_utils:pagination_headers(Pagination, Total),
|
||||
handler_utils:send_json(Req1, 200, Json, ExtraHeaders);
|
||||
{error, Code, Msg, Req1} ->
|
||||
handler_utils:send_error(Req1, Code, Msg)
|
||||
end.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Парсинг параметров
|
||||
%%%===================================================================
|
||||
|
||||
parse_filters(Qs) ->
|
||||
Status = proplists:get_value(<<"status">>, Qs, undefined),
|
||||
Type = proplists:get_value(<<"type">>, Qs, undefined),
|
||||
Query = proplists:get_value(<<"q">>, Qs, undefined),
|
||||
#{
|
||||
status => safe_to_atom(Status),
|
||||
type => safe_to_atom(Type),
|
||||
q => Query
|
||||
}.
|
||||
|
||||
safe_to_atom(undefined) -> undefined;
|
||||
safe_to_atom(Bin) when is_binary(Bin) ->
|
||||
try binary_to_existing_atom(Bin, utf8)
|
||||
catch error:badarg -> undefined
|
||||
end.
|
||||
|
||||
parse_sort(Qs) ->
|
||||
SortBy = proplists:get_value(<<"sort_by">>, Qs, <<"created_at">>),
|
||||
SortOrder = proplists:get_value(<<"sort_order">>, Qs, <<"desc">>),
|
||||
#{
|
||||
sort_by => SortBy,
|
||||
sort_order => SortOrder
|
||||
}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Фильтрация
|
||||
%%%===================================================================
|
||||
|
||||
apply_filters(Calendars, Filters) ->
|
||||
StatusFilter = maps:get(status, Filters, undefined),
|
||||
TypeFilter = maps:get(type, Filters, undefined),
|
||||
QueryFilter = maps:get(q, Filters, undefined),
|
||||
|
||||
F1 = case StatusFilter of
|
||||
undefined -> Calendars;
|
||||
Status -> [C || C <- Calendars, C#calendar.status =:= Status]
|
||||
end,
|
||||
|
||||
F2 = case TypeFilter of
|
||||
undefined -> F1;
|
||||
Type -> [C || C <- F1, C#calendar.type =:= Type]
|
||||
end,
|
||||
|
||||
case QueryFilter of
|
||||
undefined -> F2;
|
||||
Q ->
|
||||
LowerQ = string:lowercase(binary_to_list(Q)),
|
||||
[C || C <- F2,
|
||||
string:str(string:lowercase(binary_to_list(C#calendar.title)), LowerQ) > 0 orelse
|
||||
string:str(string:lowercase(binary_to_list(C#calendar.description)), LowerQ) > 0]
|
||||
end.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Сортировка
|
||||
%%%===================================================================
|
||||
|
||||
apply_sort(Calendars, #{sort_by := SortBy, sort_order := SortOrder}) ->
|
||||
Field = binary_to_sort_field(SortBy),
|
||||
Order = case SortOrder of
|
||||
<<"asc">> -> fun(A, B) -> A < B end;
|
||||
<<"desc">> -> fun(A, B) -> A > B end;
|
||||
_ -> fun(A, B) -> A >= B end % default desc
|
||||
end,
|
||||
lists:sort(fun(A, B) ->
|
||||
ValA = get_field(A, Field),
|
||||
ValB = get_field(B, Field),
|
||||
case {ValA, ValB} of
|
||||
{undefined, _} -> false;
|
||||
{_, undefined} -> true;
|
||||
_ -> Order(ValA, ValB)
|
||||
end
|
||||
end, Calendars).
|
||||
|
||||
binary_to_sort_field(<<"title">>) -> title;
|
||||
binary_to_sort_field(<<"created_at">>) -> created_at;
|
||||
binary_to_sort_field(<<"updated_at">>) -> updated_at;
|
||||
binary_to_sort_field(_) -> created_at.
|
||||
|
||||
get_field(#calendar{title = Title}, title) -> Title;
|
||||
get_field(#calendar{created_at = CreatedAt}, created_at) -> CreatedAt;
|
||||
get_field(#calendar{updated_at = UpdatedAt}, updated_at) -> UpdatedAt;
|
||||
get_field(_, _) -> undefined.
|
||||
@@ -26,7 +26,9 @@
|
||||
calendar_to_json/1,
|
||||
subscription_to_json/1,
|
||||
trails_for_crud/4,
|
||||
is_superadmin/1]).
|
||||
is_superadmin/1,
|
||||
pagination_headers/2
|
||||
]).
|
||||
|
||||
-include("records.hrl").
|
||||
|
||||
@@ -118,6 +120,18 @@ parse_int_qs(undefined, Default) -> Default;
|
||||
parse_int_qs(Bin, Default) ->
|
||||
try binary_to_integer(Bin) catch _:_ -> Default end.
|
||||
|
||||
-spec pagination_headers(map(), non_neg_integer()) -> map().
|
||||
pagination_headers(#{limit := Limit, offset := Offset}, Total) ->
|
||||
NextOffset = Offset + Limit,
|
||||
HasMore = NextOffset < Total,
|
||||
#{
|
||||
<<"x-total-count">> => integer_to_binary(Total),
|
||||
<<"x-next-offset">> => case HasMore of
|
||||
true -> integer_to_binary(NextOffset);
|
||||
false -> <<"">>
|
||||
end
|
||||
}.
|
||||
|
||||
%% @doc Преобразует бинарный ISO8601 параметр в datetime().
|
||||
-spec parse_datetime_qs(binary() | undefined) -> calendar:datetime() | undefined.
|
||||
parse_datetime_qs(undefined) -> undefined;
|
||||
|
||||
Reference in New Issue
Block a user