При массовом изменении статуса отзывов причина теперь обязательное поле #22
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%%% @doc Административный обработчик отзывов.
|
%%% @doc Административный обработчик отзывов.
|
||||||
%%% GET – список отзывов с пагинацией, фильтрацией и сортировкой.
|
%%% GET – список отзывов с пагинацией, фильтрацией и сортировкой.
|
||||||
%%% PATCH – массовое обновление статусов отзывов.
|
%%% PATCH – массовое обновление статусов отзывов с единой причиной.
|
||||||
%%% @end
|
%%% @end
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-module(admin_handler_reviews).
|
-module(admin_handler_reviews).
|
||||||
@@ -43,14 +43,31 @@ trails() ->
|
|||||||
#{ % PATCH bulk update
|
#{ % PATCH bulk update
|
||||||
path => <<"/v1/admin/reviews">>,
|
path => <<"/v1/admin/reviews">>,
|
||||||
method => <<"PATCH">>,
|
method => <<"PATCH">>,
|
||||||
description => <<"Bulk update review statuses">>,
|
description => <<"Bulk update review statuses with a single reason">>,
|
||||||
tags => [<<"Reviews">>],
|
tags => [<<"Reviews">>],
|
||||||
requestBody => #{
|
requestBody => #{
|
||||||
required => true,
|
required => true,
|
||||||
content => #{<<"application/json">> => #{schema => #{ type => array, items => #{ type => object, properties => #{ id => #{type => string}, status => #{type => string, enum => [<<"visible">>, <<"hidden">>, <<"deleted">>]} } }}} }
|
content => #{<<"application/json">> => #{schema => #{
|
||||||
|
type => object,
|
||||||
|
required => [<<"operations">>, <<"reason">>],
|
||||||
|
properties => #{
|
||||||
|
<<"operations">> => #{
|
||||||
|
type => array,
|
||||||
|
items => #{
|
||||||
|
type => object,
|
||||||
|
properties => #{
|
||||||
|
<<"id">> => #{type => string},
|
||||||
|
<<"status">> => #{type => string, enum => [<<"visible">>, <<"hidden">>, <<"deleted">>]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
<<"reason">> => #{type => string, description => <<"Reason applied to all updates">>}
|
||||||
|
}
|
||||||
|
}}}
|
||||||
},
|
},
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => #{description => <<"Number of updated reviews">>}
|
200 => #{description => <<"Number of updated reviews">>},
|
||||||
|
400 => #{description => <<"Invalid request body or missing reason">>}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
].
|
].
|
||||||
@@ -111,16 +128,18 @@ bulk_update_reviews(Req) ->
|
|||||||
{ok, AdminId, Req1} ->
|
{ok, AdminId, Req1} ->
|
||||||
try
|
try
|
||||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||||
Operations = jsx:decode(Body, [return_maps]),
|
#{<<"operations">> := Operations, <<"reason">> := Reason} = jsx:decode(Body, [return_maps]),
|
||||||
true = is_list(Operations),
|
true = is_list(Operations),
|
||||||
case logic_review:bulk_update_status(Operations) of
|
case logic_review:bulk_update_status(Operations, Reason) of
|
||||||
{ok, UpdatedCount} ->
|
{ok, UpdatedCount} ->
|
||||||
admin_utils:log_admin_action(AdminId, <<"bulk_update_reviews">>, <<"review">>, UpdatedCount, Req2),
|
admin_utils:log_admin_action(AdminId, <<"bulk_update_reviews">>, <<"review">>, UpdatedCount, Req2),
|
||||||
handler_utils:send_json(Req2, 200, #{updated_count => UpdatedCount});
|
handler_utils:send_json(Req2, 200, #{updated_count => UpdatedCount});
|
||||||
{error, Reason} ->
|
{error, ReasonErr} ->
|
||||||
handler_utils:send_error(Req2, 400, Reason)
|
handler_utils:send_error(Req2, 400, ReasonErr)
|
||||||
end
|
end
|
||||||
catch
|
catch
|
||||||
|
error:{badmatch, _} ->
|
||||||
|
handler_utils:send_error(Req1, 400, <<"Missing 'reason' field">>);
|
||||||
_:_ -> handler_utils:send_error(Req1, 400, <<"Invalid JSON body">>)
|
_:_ -> handler_utils:send_error(Req1, 400, <<"Invalid JSON body">>)
|
||||||
end;
|
end;
|
||||||
{error, Code, Msg, Req1} ->
|
{error, Code, Msg, Req1} ->
|
||||||
|
|||||||
+33
-12
@@ -4,7 +4,7 @@
|
|||||||
-export([create_review/5, get_review/2, list_reviews/3, list_user_reviews/1,
|
-export([create_review/5, get_review/2, list_reviews/3, list_user_reviews/1,
|
||||||
update_review/3, delete_review/2, hide_review/2, hide_review/3, unhide_review/2, unhide_review/3]).
|
update_review/3, delete_review/2, hide_review/2, hide_review/3, unhide_review/2, unhide_review/3]).
|
||||||
-export([can_review/3, update_target_rating/2, can_moderate_review/2]).
|
-export([can_review/3, update_target_rating/2, can_moderate_review/2]).
|
||||||
-export([list_admin_reviews/1, bulk_update_status/1]).
|
-export([list_admin_reviews/1, bulk_update_status/2]).
|
||||||
-export([list_admin_reviews/2, get_review_admin/1, update_review_admin/2]).
|
-export([list_admin_reviews/2, get_review_admin/1, update_review_admin/2]).
|
||||||
|
|
||||||
%% Создание отзыва
|
%% Создание отзыва
|
||||||
@@ -233,27 +233,48 @@ apply_filters(Reviews, [_ | Rest]) ->
|
|||||||
|
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%%% @doc Массово обновить статусы отзывов.
|
%%% @doc Массово обновить статусы отзывов.
|
||||||
%%% Operations: [#{id => ReviewId, status => visible | hidden}, ...]
|
%%%
|
||||||
%%% Все изменения выполняются в одной Mnesia-транзакции.
|
%%% Принимает список операций и одну причину, которая будет
|
||||||
|
%%% записана во все изменённые отзывы. Все изменения выполняются
|
||||||
|
%%% в одной Mnesia‑транзакции.
|
||||||
|
%%%
|
||||||
|
%%% Параметры:
|
||||||
|
%%% Operations :: [#{ id := binary(), status := binary() }]
|
||||||
|
%%% Каждый элемент обязан содержать ключи `id` и `status`.
|
||||||
|
%%% Допустимые значения `status`: `<<"visible">>`, `<<"hidden">>`,
|
||||||
|
%%% `<<"deleted">>`.
|
||||||
|
%%% Reason :: binary()
|
||||||
|
%%% Причина, которая будет записана в поле `reason` каждого
|
||||||
|
%%% обновлённого отзыва.
|
||||||
|
%%%
|
||||||
|
%%% Возвращает:
|
||||||
|
%%% {ok, UpdatedCount :: non_neg_integer()} в случае успеха,
|
||||||
|
%%% {error, Reason :: binary()} при ошибке.
|
||||||
%%% @end
|
%%% @end
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
bulk_update_status(Operations) when is_list(Operations) ->
|
-spec bulk_update_status(Operations :: [#{binary() => binary()}], Reason :: binary()) ->
|
||||||
|
{ok, non_neg_integer()} | {error, binary()}.
|
||||||
|
bulk_update_status(Operations, Reason) when is_list(Operations) ->
|
||||||
Fun = fun() ->
|
Fun = fun() ->
|
||||||
lists:foreach(fun do_update_status/1, Operations)
|
lists:foreach(fun(Op) -> do_update_status(Op, Reason) end, Operations)
|
||||||
end,
|
end,
|
||||||
case mnesia:transaction(Fun) of
|
case mnesia:transaction(Fun) of
|
||||||
{atomic, ok} ->
|
{atomic, ok} ->
|
||||||
{ok, length(Operations)};
|
{ok, length(Operations)};
|
||||||
{aborted, Reason} ->
|
{aborted, ReasonErr} ->
|
||||||
{error, Reason}
|
{error, ReasonErr}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_update_status(#{<<"id">> := Id, <<"status">> := NewStatus}) ->
|
%% @private Выполняет обновление одного отзыва внутри транзакции.
|
||||||
|
-spec do_update_status(map(), binary()) -> ok | no_return().
|
||||||
|
do_update_status(#{<<"id">> := Id, <<"status">> := NewStatus}, Reason) ->
|
||||||
case core_review:get_by_id(Id) of
|
case core_review:get_by_id(Id) of
|
||||||
{ok, Review} ->
|
{ok, _Review} ->
|
||||||
IdReview = Review#review{id = Id},
|
Updates = [{status, NewStatus}, {reason, Reason}],
|
||||||
UpdatedReview = Review#review{status = NewStatus},
|
case core_review:update(Id, Updates) of
|
||||||
core_review:update(IdReview, UpdatedReview);
|
{ok, _} -> ok;
|
||||||
|
{error, _} -> mnesia:abort(<<"review_update_failed">>)
|
||||||
|
end;
|
||||||
not_found ->
|
not_found ->
|
||||||
mnesia:abort(<<"review_not_found">>)
|
mnesia:abort(<<"review_not_found">>)
|
||||||
end.
|
end.
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ test() ->
|
|||||||
|
|
||||||
% Сначала меняем статусы, чтобы потом проверить фильтрацию по статусам
|
% Сначала меняем статусы, чтобы потом проверить фильтрацию по статусам
|
||||||
test_bulk_update_status(Token, Review1Id, Review2Id),
|
test_bulk_update_status(Token, Review1Id, Review2Id),
|
||||||
|
test_bulk_update_missing_reason(Token, Review1Id),
|
||||||
|
|
||||||
test_filter_by_status(Token),
|
test_filter_by_status(Token),
|
||||||
test_filter_combined_status_target(Token, EventId),
|
test_filter_combined_status_target(Token, EventId),
|
||||||
@@ -118,17 +119,32 @@ test_filter_by_user(Token, UserId) ->
|
|||||||
[?assertEqual(UserId, maps:get(<<"user_id">>, R)) || R <- Reviews],
|
[?assertEqual(UserId, maps:get(<<"user_id">>, R)) || R <- Reviews],
|
||||||
ct:pal(" OK: ~p reviews by user", [length(Reviews)]).
|
ct:pal(" OK: ~p reviews by user", [length(Reviews)]).
|
||||||
|
|
||||||
%% @doc PATCH /v1/admin/reviews – массовое обновление статусов.
|
%% @doc PATCH /v1/admin/reviews – массовое обновление статусов с причиной.
|
||||||
test_bulk_update_status(Token, Review1Id, Review2Id) ->
|
test_bulk_update_status(Token, Review1Id, Review2Id) ->
|
||||||
ct:pal(" TEST: Bulk update review statuses"),
|
ct:pal(" TEST: Bulk update review statuses"),
|
||||||
Body = [
|
BodyMap = #{
|
||||||
#{<<"id">> => Review1Id, <<"status">> => <<"visible">>},
|
<<"operations">> => [
|
||||||
#{<<"id">> => Review2Id, <<"status">> => <<"hidden">>}
|
#{<<"id">> => Review1Id, <<"status">> => <<"visible">>},
|
||||||
],
|
#{<<"id">> => Review2Id, <<"status">> => <<"hidden">>}
|
||||||
#{<<"updated_count">> := Count} = api_test_runner:admin_patch(
|
],
|
||||||
<<"/v1/admin/reviews">>, Token, Body),
|
<<"reason">> => <<"Moderation action">>
|
||||||
?assertEqual(2, Count),
|
},
|
||||||
ct:pal(" OK: updated ~p reviews", [Count]).
|
Body = jsx:encode(BodyMap),
|
||||||
|
{ok, 200, _, RespBody} = api_test_runner:admin_request(patch, <<"/v1/admin/reviews">>, Token, Body),
|
||||||
|
Result = jsx:decode(list_to_binary(RespBody), [return_maps]),
|
||||||
|
?assertEqual(2, maps:get(<<"updated_count">>, Result)),
|
||||||
|
ct:pal(" OK: updated ~p reviews", [maps:get(<<"updated_count">>, Result)]).
|
||||||
|
|
||||||
|
%% @doc PATCH /v1/admin/reviews – отсутствует reason → 400.
|
||||||
|
test_bulk_update_missing_reason(Token, ReviewId) ->
|
||||||
|
ct:pal(" TEST: Bulk update missing reason (400)"),
|
||||||
|
BodyMap = #{
|
||||||
|
<<"operations">> => [#{<<"id">> => ReviewId, <<"status">> => <<"hidden">>}]
|
||||||
|
% reason отсутствует
|
||||||
|
},
|
||||||
|
Body = jsx:encode(BodyMap),
|
||||||
|
{ok, 400, _, _} = api_test_runner:admin_request(patch, <<"/v1/admin/reviews">>, Token, Body),
|
||||||
|
ct:pal(" OK: got 400").
|
||||||
|
|
||||||
%% @doc GET /v1/admin/reviews?status=visible – фильтрация по статусу.
|
%% @doc GET /v1/admin/reviews?status=visible – фильтрация по статусу.
|
||||||
test_filter_by_status(Token) ->
|
test_filter_by_status(Token) ->
|
||||||
|
|||||||
Reference in New Issue
Block a user