Рефакторинг админских обработчиков - пагинация, сортировка, фильтрация. Часть 1

This commit is contained in:
2026-05-26 13:04:31 +03:00
parent 01dbc6d6cb
commit ed5b275efb
24 changed files with 1009 additions and 1126 deletions
+58 -107
View File
@@ -2,7 +2,6 @@
-behaviour(cowboy_handler).
-export([init/2]).
-export([trails/0]).
-export([calendar_schema/0]). % для использования в admin_handler_calendar_by_id
-include("records.hrl").
@@ -17,28 +16,26 @@ trails() ->
#{
path => <<"/v1/admin/calendars">>,
method => <<"GET">>,
description => <<"List all calendars with filtering, sorting, pagination (admin)">>,
description => <<"List all calendars (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">>}
#{name => <<"status">>, in => <<"query">>, schema => #{type => string}, description => <<"Filter by status">>},
#{name => <<"type">>, in => <<"query">>, schema => #{type => string}, description => <<"Filter by type">>},
#{name => <<"q">>, in => <<"query">>, schema => #{type => string}, description => <<"Search in title/description">>},
#{name => <<"sort_by">>, in => <<"query">>, schema => #{type => string}, description => <<"Sort field (title, created_at, updated_at)">>},
#{name => <<"sort_order">>, in => <<"query">>, schema => #{type => string, enum => [<<"asc">>, <<"desc">>]}, description => <<"Sort direction">>},
#{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}, description => <<"Page size">>},
#{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}, description => <<"Offset">>}
],
responses => #{
200 => #{
description => <<"Array of calendars">>,
content => #{
<<"application/json">> => #{
schema => #{
type => array,
items => calendar_schema()
}
content => #{<<"application/json">> => #{
schema => #{
type => array,
items => calendar_schema()
}
}
}}
},
403 => #{description => <<"Forbidden">>}
}
@@ -70,34 +67,22 @@ calendar_schema() ->
}
}.
%%%===================================================================
%%% 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),
% Получаем все календари
Filters = parse_calendar_filters(Req1),
{ok, AllCalendars} = logic_calendar:admin_list_all(),
% Применяем фильтры
Filtered = apply_filters(AllCalendars, Filters),
% Сортируем
Sorted = apply_sort(Filtered, Sort),
% Пагинация
Filtered = apply_calendar_filters(AllCalendars, Filters),
% Формируем параметры сортировки из query-параметров
Qs = cowboy_req:parse_qs(Req1),
SortBy = proplists:get_value(<<"sort_by">>, Qs, <<"created_at">>),
SortOrder = proplists:get_value(<<"sort_order">>, Qs, <<"desc">>),
Sorted = sort_calendars(Filtered, SortBy, SortOrder),
Total = length(Sorted),
Start = maps:get(offset, Pagination) + 1,
Limit = maps:get(limit, Pagination),
Page = lists:sublist(Sorted, Start, Limit),
Page = lists:sublist(Sorted, maps:get(offset, Pagination) + 1, maps:get(limit, Pagination)),
Json = [handler_utils:calendar_to_json(C) || C <- Page],
ExtraHeaders = handler_utils:pagination_headers(Pagination, Total),
handler_utils:send_json(Req1, 200, Json, ExtraHeaders);
@@ -105,89 +90,55 @@ list_calendars(Req) ->
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),
parse_calendar_filters(Req) ->
Qs = cowboy_req:parse_qs(Req),
#{
status => proplists:get_value(<<"status">>, Qs),
type => proplists:get_value(<<"type">>, Qs),
q => proplists:get_value(<<"q">>, Qs)
}.
F1 = case StatusFilter of
apply_calendar_filters(Calendars, Filters) ->
StatusBin = maps:get(status, Filters),
TypeBin = maps:get(type, Filters),
Q = maps:get(q, Filters),
F1 = case StatusBin of
undefined -> Calendars;
Status -> [C || C <- Calendars, C#calendar.status =:= Status]
_ -> Status = binary_to_existing_atom(StatusBin, utf8),
[C || C <- Calendars, C#calendar.status =:= Status]
end,
F2 = case TypeFilter of
F2 = case TypeBin of
undefined -> F1;
Type -> [C || C <- F1, C#calendar.type =:= Type]
_ -> Type = binary_to_existing_atom(TypeBin, utf8),
[C || C <- F1, C#calendar.type =:= Type]
end,
case QueryFilter of
case Q of
undefined -> F2;
Q ->
LowerQ = string:lowercase(binary_to_list(Q)),
_ ->
QLower = 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]
string:str(string:lowercase(binary_to_list(C#calendar.title)), QLower) > 0 orelse
string:str(string:lowercase(binary_to_list(C#calendar.description)), QLower) > 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
sort_calendars(Calendars, SortBy, SortOrder) ->
Index = case SortBy of
<<"title">> -> #calendar.title; % элемент 3
<<"created_at">> -> #calendar.created_at; % элемент 17
<<"updated_at">> -> #calendar.updated_at; % элемент 18
_ -> #calendar.created_at
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)
Sorted = lists:sort(fun(A, B) ->
VA = element(Index, A),
VB = element(Index, B),
case SortOrder of
<<"desc">> -> VA > VB;
_ -> VA < VB
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.
end, Calendars),
Sorted.