Пагинация, фильтры и сортировки Часть 1 #20
This commit is contained in:
@@ -113,12 +113,14 @@ convert_field({<<"location">>, Val}) -> {location, Val};
|
|||||||
convert_field({<<"tags">>, Val}) -> {tags, Val};
|
convert_field({<<"tags">>, Val}) -> {tags, Val};
|
||||||
convert_field({<<"capacity">>, Val}) -> {capacity, Val};
|
convert_field({<<"capacity">>, Val}) -> {capacity, Val};
|
||||||
convert_field({<<"online_link">>, Val}) -> {online_link, Val};
|
convert_field({<<"online_link">>, Val}) -> {online_link, Val};
|
||||||
convert_field({<<"status">>, Val}) -> {status, Val};
|
convert_field({<<"status">>, Val}) ->
|
||||||
|
try binary_to_existing_atom(Val, utf8) of
|
||||||
|
Atom -> {status, Atom}
|
||||||
|
catch
|
||||||
|
error:badarg -> {status, Val} % fallback, но лучше залогировать
|
||||||
|
end;
|
||||||
convert_field(Other) -> Other.
|
convert_field(Other) -> Other.
|
||||||
|
|
||||||
%% event_to_json, datetime_to_iso8601, parse_datetime, parse_datetime_binary
|
|
||||||
%% берутся те же, что и в admin_handler_events.erl (можно вынести в общий модуль,
|
|
||||||
%% но для простоты дублируем).
|
|
||||||
event_to_json(Event) ->
|
event_to_json(Event) ->
|
||||||
LocationJson = case Event#event.location of
|
LocationJson = case Event#event.location of
|
||||||
undefined -> null;
|
undefined -> null;
|
||||||
|
|||||||
@@ -6,37 +6,51 @@
|
|||||||
|
|
||||||
init(Req, _Opts) ->
|
init(Req, _Opts) ->
|
||||||
case cowboy_req:method(Req) of
|
case cowboy_req:method(Req) of
|
||||||
<<"GET">> ->
|
<<"GET">> -> list_all_events(Req);
|
||||||
list_all_events(Req);
|
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||||
_ ->
|
|
||||||
send_error(Req, 405, <<"Method not allowed">>)
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% GET /v1/admin/events
|
|
||||||
list_all_events(Req) ->
|
list_all_events(Req) ->
|
||||||
case auth_admin(Req) of
|
case auth_admin(Req) of
|
||||||
{ok, _AdminId, Req1} ->
|
{ok, _AdminId, Req1} ->
|
||||||
Filters = parse_filters(Req1),
|
Params = parse_admin_event_search(Req1),
|
||||||
{ok, Events} = logic_event:list_all_events(Filters),
|
{ok, Total, Events} = logic_event:search_events(Params),
|
||||||
Json = [event_to_json(E) || E <- Events],
|
Json = [event_to_json(E) || E <- Events],
|
||||||
send_json(Req1, 200, Json);
|
Limit = maps:get(limit, Params, 50),
|
||||||
|
Offset = maps:get(offset, Params, 0),
|
||||||
|
RangeEnd = min(Offset + Limit - 1, Total - 1),
|
||||||
|
Headers = #{
|
||||||
|
<<"content-type">> => <<"application/json">>,
|
||||||
|
<<"content-range">> => iolist_to_binary(
|
||||||
|
io_lib:format("items ~B-~B/~B", [Offset, RangeEnd, Total])),
|
||||||
|
<<"x-total-count">> => integer_to_binary(Total),
|
||||||
|
<<"access-control-expose-headers">> => <<"Content-Range, X-Total-Count">>
|
||||||
|
},
|
||||||
|
Body = jsx:encode(Json),
|
||||||
|
cowboy_req:reply(200, Headers, Body, Req1),
|
||||||
|
{ok, Body, []};
|
||||||
{error, Code, Msg, Req1} ->
|
{error, Code, Msg, Req1} ->
|
||||||
send_error(Req1, Code, Msg)
|
send_error(Req1, Code, Msg)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% --- Вспомогательные функции ---
|
parse_admin_event_search(Req) ->
|
||||||
|
|
||||||
parse_filters(Req) ->
|
|
||||||
Qs = cowboy_req:parse_qs(Req),
|
Qs = cowboy_req:parse_qs(Req),
|
||||||
lists:filtermap(
|
#{
|
||||||
fun
|
from => parse_datetime_qs(proplists:get_value(<<"from">>, Qs)),
|
||||||
({<<"from">>, Val}) -> {true, {from, parse_datetime_binary(Val)}};
|
to => parse_datetime_qs(proplists:get_value(<<"to">>, Qs)),
|
||||||
({<<"to">>, Val}) -> {true, {to, parse_datetime_binary(Val)}};
|
status => proplists:get_value(<<"status">>, Qs, undefined),
|
||||||
(_) -> false
|
calendar_id => proplists:get_value(<<"calendar_id">>, Qs, undefined),
|
||||||
end,
|
title => proplists:get_value(<<"title">>, Qs, undefined),
|
||||||
Qs
|
q => proplists:get_value(<<"q">>, Qs, undefined),
|
||||||
).
|
limit => parse_int_qs(proplists:get_value(<<"limit">>, Qs), 50),
|
||||||
|
offset => parse_int_qs(proplists:get_value(<<"offset">>, Qs), 0),
|
||||||
|
sort => proplists:get_value(<<"sort">>, Qs, <<"created_at">>),
|
||||||
|
order => proplists:get_value(<<"order">>, Qs, <<"desc">>)
|
||||||
|
}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Вспомогательные функции
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
auth_admin(Req) ->
|
auth_admin(Req) ->
|
||||||
case handler_auth:authenticate(Req) of
|
case handler_auth:authenticate(Req) of
|
||||||
{ok, AdminId, Req1} ->
|
{ok, AdminId, Req1} ->
|
||||||
@@ -48,7 +62,14 @@ auth_admin(Req) ->
|
|||||||
{error, Code, Msg, Req1}
|
{error, Code, Msg, Req1}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Сериализация события (полностью скопирована из handler_event_by_id.erl)
|
parse_int_qs(undefined, Default) -> Default;
|
||||||
|
parse_int_qs(Bin, Default) ->
|
||||||
|
try binary_to_integer(Bin) catch _:_ -> Default end.
|
||||||
|
|
||||||
|
parse_datetime_qs(undefined) -> undefined;
|
||||||
|
parse_datetime_qs(Bin) ->
|
||||||
|
case parse_datetime(Bin) of {ok, Dt} -> Dt; _ -> undefined end.
|
||||||
|
|
||||||
event_to_json(Event) ->
|
event_to_json(Event) ->
|
||||||
LocationJson = case Event#event.location of
|
LocationJson = case Event#event.location of
|
||||||
undefined -> null;
|
undefined -> null;
|
||||||
@@ -97,12 +118,6 @@ datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
|||||||
datetime_to_iso8601(undefined) ->
|
datetime_to_iso8601(undefined) ->
|
||||||
undefined.
|
undefined.
|
||||||
|
|
||||||
parse_datetime_binary(Str) ->
|
|
||||||
case parse_datetime(Str) of
|
|
||||||
{ok, Dt} -> Dt;
|
|
||||||
_ -> undefined
|
|
||||||
end.
|
|
||||||
|
|
||||||
parse_datetime(Str) ->
|
parse_datetime(Str) ->
|
||||||
try
|
try
|
||||||
[DateStr, TimeStr] = string:split(Str, "T"),
|
[DateStr, TimeStr] = string:split(Str, "T"),
|
||||||
@@ -119,12 +134,6 @@ parse_datetime(Str) ->
|
|||||||
catch _:_ -> {error, invalid_format}
|
catch _:_ -> {error, invalid_format}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
send_json(Req, Status, Data) ->
|
|
||||||
Body = jsx:encode(Data),
|
|
||||||
Headers = #{<<"content-type">> => <<"application/json">>},
|
|
||||||
cowboy_req:reply(Status, Headers, Body, Req),
|
|
||||||
{ok, Body, []}.
|
|
||||||
|
|
||||||
send_error(Req, Status, Message) ->
|
send_error(Req, Status, Message) ->
|
||||||
Body = jsx:encode(#{error => Message}),
|
Body = jsx:encode(#{error => Message}),
|
||||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||||
|
|||||||
@@ -227,6 +227,8 @@ table_opts(admin_session) -> [{ram_copies, [node()]}, {attributes, record_info(f
|
|||||||
|
|
||||||
create_indices() ->
|
create_indices() ->
|
||||||
mnesia:add_table_index(event, calendar_id),
|
mnesia:add_table_index(event, calendar_id),
|
||||||
|
mnesia:add_table_index(event, title),
|
||||||
|
mnesia:add_table_index(event, created_at),
|
||||||
mnesia:add_table_index(event, start_time),
|
mnesia:add_table_index(event, start_time),
|
||||||
mnesia:add_table_index(event, event_type),
|
mnesia:add_table_index(event, event_type),
|
||||||
mnesia:add_table_index(event, master_id),
|
mnesia:add_table_index(event, master_id),
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
-export([validate_event_time/1, validate_event_time/2, get_occurrences/3, cancel_occurrence/3]).
|
-export([validate_event_time/1, validate_event_time/2, get_occurrences/3, cancel_occurrence/3]).
|
||||||
-export([materialize_for_booking/3]).
|
-export([materialize_for_booking/3]).
|
||||||
-export([list_all_events/1, get_event_admin/1, update_event_admin/2, delete_event_admin/1]).
|
-export([list_all_events/1, get_event_admin/1, update_event_admin/2, delete_event_admin/1]).
|
||||||
|
-export([search_events/1]).
|
||||||
|
|
||||||
|
-define(DEFAULT_SEARCH_DAYS, 30).
|
||||||
|
|
||||||
%% Создание одиночного события
|
%% Создание одиночного события
|
||||||
create_event(UserId, CalendarId, Title, StartTime, Duration) ->
|
create_event(UserId, CalendarId, Title, StartTime, Duration) ->
|
||||||
@@ -168,6 +171,112 @@ delete_event(UserId, EventId) ->
|
|||||||
Error
|
Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%% @doc Поиск событий с пагинацией, фильтрацией и сортировкой
|
||||||
|
search_events(Params) ->
|
||||||
|
#{
|
||||||
|
from := FromIn, to := ToIn,
|
||||||
|
status := StatusFilter,
|
||||||
|
calendar_id := CalId,
|
||||||
|
title := TitleExact,
|
||||||
|
q := Query,
|
||||||
|
limit := Limit,
|
||||||
|
offset := Offset,
|
||||||
|
sort := SortField,
|
||||||
|
order := Order
|
||||||
|
} = Params,
|
||||||
|
{From, To} = ensure_time_range(FromIn, ToIn),
|
||||||
|
%% 1. Получаем исходный список событий
|
||||||
|
AllEvents = case CalId of
|
||||||
|
undefined ->
|
||||||
|
% Без календаря — только активные мастер-события
|
||||||
|
core_event:list_all();
|
||||||
|
_ ->
|
||||||
|
% Для конкретного календаря загружаем все события (любой статус)
|
||||||
|
mnesia:dirty_index_match_object(
|
||||||
|
event,
|
||||||
|
#event{calendar_id = CalId, _ = '_'},
|
||||||
|
calendar_id
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
%% 2. Фильтрация по дате (игнорируем undefined и instance)
|
||||||
|
TimeFiltered = [E || E <- AllEvents,
|
||||||
|
E#event.is_instance =:= false,
|
||||||
|
E#event.start_time =/= undefined,
|
||||||
|
E#event.start_time >= From,
|
||||||
|
E#event.start_time =< To],
|
||||||
|
%% 3. Фильтр по точному названию
|
||||||
|
TitleFiltered = if TitleExact /= undefined ->
|
||||||
|
[E || E <- TimeFiltered, E#event.title =:= TitleExact];
|
||||||
|
true -> TimeFiltered
|
||||||
|
end,
|
||||||
|
%% 4. Пост‑фильтры по статусу и поисковой строке
|
||||||
|
%% Определяем эффективный фильтр статуса
|
||||||
|
EffectiveStatus = case {CalId, StatusFilter} of
|
||||||
|
{undefined, undefined} -> undefined; % без календаря → только active
|
||||||
|
{_, undefined} -> <<"all">>; % конкретный календарь → все статусы
|
||||||
|
_ -> StatusFilter
|
||||||
|
end,
|
||||||
|
FinalFiltered = apply_post_filters(TitleFiltered, EffectiveStatus, Query),
|
||||||
|
%% 5. Сортировка и пагинация
|
||||||
|
SortedEvents = sort_events(FinalFiltered, SortField, Order),
|
||||||
|
Total = length(SortedEvents),
|
||||||
|
Page = lists:sublist(SortedEvents, Offset + 1, Limit),
|
||||||
|
{ok, Total, Page}.
|
||||||
|
|
||||||
|
%% Дополнительная фильтрация по статусу и подстроке
|
||||||
|
apply_post_filters(Events, StatusFilter, Query) ->
|
||||||
|
E1 = case StatusFilter of
|
||||||
|
<<"all">> -> Events;
|
||||||
|
undefined -> [E || E <- Events, E#event.status =:= active];
|
||||||
|
_ ->
|
||||||
|
try binary_to_existing_atom(StatusFilter, utf8) of
|
||||||
|
Atom -> [E || E <- Events, E#event.status =:= Atom]
|
||||||
|
catch
|
||||||
|
error:badarg -> Events
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case Query of
|
||||||
|
undefined -> E1;
|
||||||
|
_ -> [E || E <- E1,
|
||||||
|
string:str(binary_to_list(E#event.title), binary_to_list(Query)) > 0 orelse
|
||||||
|
string:str(binary_to_list(E#event.description), binary_to_list(Query)) > 0]
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Внутренние функции для search_events
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
ensure_time_range(undefined, undefined) ->
|
||||||
|
NowSec = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
|
||||||
|
FromSec = NowSec - ?DEFAULT_SEARCH_DAYS * 86400,
|
||||||
|
ToSec = NowSec + ?DEFAULT_SEARCH_DAYS * 86400,
|
||||||
|
{calendar:gregorian_seconds_to_datetime(FromSec),
|
||||||
|
calendar:gregorian_seconds_to_datetime(ToSec)};
|
||||||
|
ensure_time_range(From, undefined) ->
|
||||||
|
{From, calendar:gregorian_seconds_to_datetime(
|
||||||
|
calendar:datetime_to_gregorian_seconds(From) + ?DEFAULT_SEARCH_DAYS * 2 * 86400)};
|
||||||
|
ensure_time_range(undefined, To) ->
|
||||||
|
{calendar:gregorian_seconds_to_datetime(
|
||||||
|
calendar:datetime_to_gregorian_seconds(To) - ?DEFAULT_SEARCH_DAYS * 2 * 86400), To};
|
||||||
|
ensure_time_range(From, To) -> {From, To}.
|
||||||
|
|
||||||
|
sort_events(Events, SortField, Order) ->
|
||||||
|
Field = binary_to_existing_atom(SortField, utf8),
|
||||||
|
Sorted = lists:sort(
|
||||||
|
fun(A, B) ->
|
||||||
|
ValA = event_field(A, Field),
|
||||||
|
ValB = event_field(B, Field),
|
||||||
|
if Order == <<"asc">> -> ValA =< ValB;
|
||||||
|
true -> ValA >= ValB
|
||||||
|
end
|
||||||
|
end, Events),
|
||||||
|
Sorted.
|
||||||
|
|
||||||
|
event_field(Event, created_at) -> Event#event.created_at;
|
||||||
|
event_field(Event, start_time) -> Event#event.start_time;
|
||||||
|
event_field(Event, title) -> Event#event.title;
|
||||||
|
event_field(Event, status) -> Event#event.status;
|
||||||
|
event_field(_, _) -> undefined.
|
||||||
|
|
||||||
%% Валидация времени события (без учёта пользователя)
|
%% Валидация времени события (без учёта пользователя)
|
||||||
validate_event_time(StartTime) ->
|
validate_event_time(StartTime) ->
|
||||||
validate_event_time(StartTime, undefined).
|
validate_event_time(StartTime, undefined).
|
||||||
@@ -214,6 +323,7 @@ validate_update({location, Value}, _) ->
|
|||||||
validate_update({tags, Value}, _) when is_list(Value) -> true;
|
validate_update({tags, Value}, _) when is_list(Value) -> true;
|
||||||
validate_update({capacity, Value}, _) when is_integer(Value), Value > 0 -> true;
|
validate_update({capacity, Value}, _) when is_integer(Value), Value > 0 -> true;
|
||||||
validate_update({online_link, Value}, _) when is_binary(Value) -> true;
|
validate_update({online_link, Value}, _) when is_binary(Value) -> true;
|
||||||
|
validate_update({status, Value}, _) when is_atom(Value) -> true;
|
||||||
validate_update(_, _) -> false.
|
validate_update(_, _) -> false.
|
||||||
|
|
||||||
get_exceptions(MasterId) ->
|
get_exceptions(MasterId) ->
|
||||||
|
|||||||
@@ -43,25 +43,25 @@ test() ->
|
|||||||
{ok, {{_, 200, _}, _, StatsResp1}} = httpc:request(get, {AdminURL ++ "/v1/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(SuperadminToken)}]}, [], []),
|
{ok, {{_, 200, _}, _, StatsResp1}} = httpc:request(get, {AdminURL ++ "/v1/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(SuperadminToken)}]}, [], []),
|
||||||
Stats1 = jsx:decode(list_to_binary(StatsResp1), [return_maps]),
|
Stats1 = jsx:decode(list_to_binary(StatsResp1), [return_maps]),
|
||||||
ct:pal(" OK (Stats 1: ~p)~n", [Stats1]),
|
ct:pal(" OK (Stats 1: ~p)~n", [Stats1]),
|
||||||
map_size(Stats1) > 0,
|
true = map_size(Stats1) > 0,
|
||||||
|
|
||||||
ct:pal(" Admin stats (admin)... "),
|
ct:pal(" Admin stats (admin)... "),
|
||||||
{ok, {{_, 200, _}, _, StatsResp2}} = httpc:request(get, {AdminURL ++ "/v1/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
{ok, {{_, 200, _}, _, StatsResp2}} = httpc:request(get, {AdminURL ++ "/v1/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||||
Stats2 = jsx:decode(list_to_binary(StatsResp2), [return_maps]),
|
Stats2 = jsx:decode(list_to_binary(StatsResp2), [return_maps]),
|
||||||
ct:pal(" OK (Stats 1: ~p)~n", [Stats2]),
|
ct:pal(" OK (Stats 1: ~p)~n", [Stats2]),
|
||||||
map_size(Stats2) > 0,
|
true = map_size(Stats2) > 0,
|
||||||
|
|
||||||
ct:pal(" Admin stats (moderator)... "),
|
ct:pal(" Admin stats (moderator)... "),
|
||||||
{ok, {{_, 200, _}, _, StatsResp3}} = httpc:request(get, {AdminURL ++ "/v1/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(ModeratorToken)}]}, [], []),
|
{ok, {{_, 200, _}, _, StatsResp3}} = httpc:request(get, {AdminURL ++ "/v1/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(ModeratorToken)}]}, [], []),
|
||||||
Stats3 = jsx:decode(list_to_binary(StatsResp3), [return_maps]),
|
Stats3 = jsx:decode(list_to_binary(StatsResp3), [return_maps]),
|
||||||
ct:pal(" OK (Stats 1: ~p)~n", [Stats3]),
|
ct:pal(" OK (Stats 1: ~p)~n", [Stats3]),
|
||||||
map_size(Stats3) > 0,
|
true = map_size(Stats3) > 0,
|
||||||
|
|
||||||
ct:pal(" Admin stats (support)... "),
|
ct:pal(" Admin stats (support)... "),
|
||||||
{ok, {{_, 200, _}, _, StatsResp4}} = httpc:request(get, {AdminURL ++ "/v1/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(SupportToken)}]}, [], []),
|
{ok, {{_, 200, _}, _, StatsResp4}} = httpc:request(get, {AdminURL ++ "/v1/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(SupportToken)}]}, [], []),
|
||||||
Stats4 = jsx:decode(list_to_binary(StatsResp4), [return_maps]),
|
Stats4 = jsx:decode(list_to_binary(StatsResp4), [return_maps]),
|
||||||
ct:pal(" OK (Stats 1: ~p)~n", [Stats4]),
|
ct:pal(" OK (Stats 1: ~p)~n", [Stats4]),
|
||||||
map_size(Stats4) > 0,
|
true = map_size(Stats4) > 0,
|
||||||
|
|
||||||
%% TEST 4: List users
|
%% TEST 4: List users
|
||||||
ct:pal(" TEST 4: List users... "),
|
ct:pal(" TEST 4: List users... "),
|
||||||
@@ -356,5 +356,158 @@ test() ->
|
|||||||
[], []),
|
[], []),
|
||||||
ct:pal("OK~n"),
|
ct:pal("OK~n"),
|
||||||
|
|
||||||
|
%% ========================================================
|
||||||
|
%% Extended Admin Events Search & Filter Tests
|
||||||
|
%% ========================================================
|
||||||
|
|
||||||
|
%% ── Подготовка изолированных данных ──
|
||||||
|
ct:pal(" Preparing isolated search test data... "),
|
||||||
|
UserToken = api_test_runner:get_user_token(),
|
||||||
|
SearchCalId = api_test_runner:create_calendar(UserToken, #{title => <<"SearchTestCal">>}),
|
||||||
|
SearchCalIdStr = binary_to_list(SearchCalId),
|
||||||
|
AlphaId = api_test_runner:create_event(UserToken, SearchCalId, #{
|
||||||
|
title => <<"Test Event Alpha">>,
|
||||||
|
start_time => api_SUITE:future_date(),
|
||||||
|
duration => 60
|
||||||
|
}),
|
||||||
|
BetaId = api_test_runner:create_event(UserToken, SearchCalId, #{
|
||||||
|
title => <<"Beta Event">>,
|
||||||
|
start_time => api_SUITE:future_date(),
|
||||||
|
duration => 60
|
||||||
|
}),
|
||||||
|
_AlphaConfId = api_test_runner:create_event(UserToken, SearchCalId, #{
|
||||||
|
title => <<"Alpha Conference">>,
|
||||||
|
start_time => api_SUITE:future_date(),
|
||||||
|
duration => 60
|
||||||
|
}),
|
||||||
|
% Отменяем BetaId через административный эндпоинт (PUT /v1/admin/events/:id)
|
||||||
|
ct:pal(" Cancelling Beta Event (admin)... "),
|
||||||
|
{ok, {{_, 200, _}, _, _}} =
|
||||||
|
httpc:request(put, {AdminURL ++ "/v1/admin/events/" ++ binary_to_list(BetaId),
|
||||||
|
[{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}],
|
||||||
|
"application/json", jsx:encode(#{<<"status">> => <<"cancelled">>})}, [], []),
|
||||||
|
ct:pal("OK~n"),
|
||||||
|
|
||||||
|
%% ── TEST 34: Filter by status=active ──
|
||||||
|
ct:pal(" TEST 34: Filter events by status=active... "),
|
||||||
|
{ok, {{_, 200, _}, _, Body34}} =
|
||||||
|
httpc:request(get, {AdminURL ++ "/v1/admin/events?calendar_id=" ++ SearchCalIdStr ++ "&status=active",
|
||||||
|
[{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]},
|
||||||
|
[], []),
|
||||||
|
Events34 = jsx:decode(list_to_binary(Body34), [return_maps]),
|
||||||
|
ct:pal("DEBUG: events34 count = ~p", [length(Events34)]),
|
||||||
|
ct:pal("DEBUG: events34 = ~p", [Events34]),
|
||||||
|
true = (length(Events34) >= 2),
|
||||||
|
Ids34 = [maps:get(<<"id">>, E) || E <- Events34],
|
||||||
|
ct:pal("OK (count: ~p, ids: ~p)~n", [length(Events34), Ids34]),
|
||||||
|
|
||||||
|
%% ── TEST 35: Filter by status=cancelled ──
|
||||||
|
ct:pal(" TEST 35: Filter events by status=cancelled... "),
|
||||||
|
{ok, {{_, 200, _}, _, Body35}} =
|
||||||
|
httpc:request(get, {AdminURL ++ "/v1/admin/events?calendar_id=" ++ SearchCalIdStr ++ "&status=cancelled",
|
||||||
|
[{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]},
|
||||||
|
[], []),
|
||||||
|
Events35 = jsx:decode(list_to_binary(Body35), [return_maps]),
|
||||||
|
ct:pal("DEBUG: Events35 count = ~p", [length(Events35)]),
|
||||||
|
ct:pal("DEBUG: Events35 = ~p", [Events35]),
|
||||||
|
true = (length(Events35) >= 1),
|
||||||
|
ct:pal("OK (count: ~p)~n", [length(Events35)]),
|
||||||
|
|
||||||
|
%% ── TEST 36: Filter by status=all ──
|
||||||
|
ct:pal(" TEST 36: Filter events by status=all... "),
|
||||||
|
{ok, {{_, 200, _}, _, Body36}} =
|
||||||
|
httpc:request(get, {AdminURL ++ "/v1/admin/events?calendar_id=" ++ SearchCalIdStr ++ "&status=all",
|
||||||
|
[{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]},
|
||||||
|
[], []),
|
||||||
|
Events36 = jsx:decode(list_to_binary(Body36), [return_maps]),
|
||||||
|
ct:pal("DEBUG: Events36 count = ~p", [length(Events36)]),
|
||||||
|
ct:pal("DEBUG: Events36 = ~p", [Events36]),
|
||||||
|
true = (length(Events36) >= 3),
|
||||||
|
ct:pal("OK (count: ~p)~n", [length(Events36)]),
|
||||||
|
|
||||||
|
%% ── TEST 37: Filter by calendar_id ──
|
||||||
|
ct:pal(" TEST 37: Filter by calendar_id... "),
|
||||||
|
{ok, {{_, 200, _}, _, Body37}} =
|
||||||
|
httpc:request(get, {AdminURL ++ "/v1/admin/events?calendar_id=" ++ SearchCalIdStr,
|
||||||
|
[{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]},
|
||||||
|
[], []),
|
||||||
|
Events37 = jsx:decode(list_to_binary(Body37), [return_maps]),
|
||||||
|
ct:pal("DEBUG: Events37 count = ~p", [length(Events37)]),
|
||||||
|
ct:pal("DEBUG: Events37 = ~p", [Events37]),
|
||||||
|
true = (length(Events37) >= 3),
|
||||||
|
ct:pal("OK (count: ~p)~n", [length(Events37)]),
|
||||||
|
|
||||||
|
%% ── TEST 38: Exact title match ──
|
||||||
|
ct:pal(" TEST 38: Exact title match... "),
|
||||||
|
{ok, {{_, 200, _}, _, Body38}} =
|
||||||
|
httpc:request(get, {AdminURL ++ "/v1/admin/events?calendar_id=" ++ SearchCalIdStr ++ "&title=Test%20Event%20Alpha",
|
||||||
|
[{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]},
|
||||||
|
[], []),
|
||||||
|
Events38 = jsx:decode(list_to_binary(Body38), [return_maps]),
|
||||||
|
ct:pal("DEBUG: Events38 count = ~p", [length(Events38)]),
|
||||||
|
ct:pal("DEBUG: Events38 = ~p", [Events38]),
|
||||||
|
1 = length(Events38),
|
||||||
|
#{<<"id">> := AlphaId} = hd(Events38),
|
||||||
|
ct:pal("OK~n"),
|
||||||
|
|
||||||
|
%% ── TEST 39: Substring search (q) ──
|
||||||
|
ct:pal(" TEST 39: Substring search (q=Alpha)... "),
|
||||||
|
{ok, {{_, 200, _}, _, Body39}} =
|
||||||
|
httpc:request(get, {AdminURL ++ "/v1/admin/events?calendar_id=" ++ SearchCalIdStr ++ "&q=Alpha",
|
||||||
|
[{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]},
|
||||||
|
[], []),
|
||||||
|
Events39 = jsx:decode(list_to_binary(Body39), [return_maps]),
|
||||||
|
ct:pal("DEBUG: Events39 count = ~p", [length(Events39)]),
|
||||||
|
ct:pal("DEBUG: Events39 = ~p", [Events39]),
|
||||||
|
true = (length(Events39) >= 2),
|
||||||
|
Titles39 = [maps:get(<<"title">>, E) || E <- Events39],
|
||||||
|
ct:pal("OK (count: ~p, titles: ~p)~n", [length(Events39), Titles39]),
|
||||||
|
|
||||||
|
%% ── TEST 40: Combined filters (calendar_id + status) ──
|
||||||
|
ct:pal(" TEST 40: Combined filters (calendar+status)... "),
|
||||||
|
{ok, {{_, 200, _}, _, Body40}} =
|
||||||
|
httpc:request(get, {AdminURL ++ "/v1/admin/events?calendar_id=" ++ SearchCalIdStr ++ "&status=active",
|
||||||
|
[{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]},
|
||||||
|
[], []),
|
||||||
|
Events40 = jsx:decode(list_to_binary(Body40), [return_maps]),
|
||||||
|
ct:pal("DEBUG: Events40 count = ~p", [length(Events40)]),
|
||||||
|
ct:pal("DEBUG: Events40 = ~p", [Events40]),
|
||||||
|
true = (length(Events40) >= 2),
|
||||||
|
ct:pal("OK (count: ~p)~n", [length(Events40)]),
|
||||||
|
|
||||||
|
%% ── TEST 41: Pagination (limit & offset) ──
|
||||||
|
ct:pal(" TEST 41: Pagination... "),
|
||||||
|
{ok, {{_, 200, _}, Headers41a, Body41a}} =
|
||||||
|
httpc:request(get, {AdminURL ++ "/v1/admin/events?calendar_id=" ++ SearchCalIdStr ++ "&status=all&limit=2&offset=0&sort=title&order=asc",
|
||||||
|
[{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]},
|
||||||
|
[], []),
|
||||||
|
Events41a = jsx:decode(list_to_binary(Body41a), [return_maps]),
|
||||||
|
ct:pal("DEBUG: Events41a count = ~p", [length(Events41a)]),
|
||||||
|
ct:pal("DEBUG: Events41a = ~p", [Events41a]),
|
||||||
|
2 = length(Events41a),
|
||||||
|
{"content-range", ContentRange41a} = lists:keyfind("content-range", 1, Headers41a),
|
||||||
|
ct:pal("page1: ~s; ", [ContentRange41a]),
|
||||||
|
{ok, {{_, 200, _}, Headers41b, Body41b}} =
|
||||||
|
httpc:request(get, {AdminURL ++ "/v1/admin/events?calendar_id=" ++ SearchCalIdStr ++ "&status=all&limit=2&offset=2&sort=title&order=asc",
|
||||||
|
[{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]},
|
||||||
|
[], []),
|
||||||
|
Events41b = jsx:decode(list_to_binary(Body41b), [return_maps]),
|
||||||
|
ct:pal("DEBUG: Events41b count = ~p", [length(Events41b)]),
|
||||||
|
ct:pal("DEBUG: Events41b = ~p", [Events41b]),
|
||||||
|
1 = length(Events41b),
|
||||||
|
{"content-range", ContentRange41b} = lists:keyfind("content-range", 1, Headers41b),
|
||||||
|
ct:pal("page2: ~s~n", [ContentRange41b]),
|
||||||
|
|
||||||
|
%% ── TEST 42: Sorting (order=asc) ──
|
||||||
|
ct:pal(" TEST 42: Sorting by title ascending... "),
|
||||||
|
{ok, {{_, 200, _}, _, Body42}} =
|
||||||
|
httpc:request(get, {AdminURL ++ "/v1/admin/events?calendar_id=" ++ SearchCalIdStr ++ "&status=all&sort=title&order=asc",
|
||||||
|
[{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]},
|
||||||
|
[], []),
|
||||||
|
Events42 = jsx:decode(list_to_binary(Body42), [return_maps]),
|
||||||
|
SortedTitles = [maps:get(<<"title">>, E) || E <- Events42],
|
||||||
|
SortedTitles = lists:sort(SortedTitles),
|
||||||
|
ct:pal("OK (titles: ~p)~n", [SortedTitles]),
|
||||||
|
|
||||||
ct:pal("~n✅ Admin API tests passed!~n"),
|
ct:pal("~n✅ Admin API tests passed!~n"),
|
||||||
{?MODULE, ok}.
|
{?MODULE, ok}.
|
||||||
Reference in New Issue
Block a user