e9d1a13900
Refs EventHub/EventHubBack#37 Refs EventHub/EventHubBack#38 Refs EventHub/EventHubBack#39 Refs EventHub/EventHubBack#40
332 lines
14 KiB
Erlang
332 lines
14 KiB
Erlang
%%%-------------------------------------------------------------------
|
|
%%% @doc Обработчик маршрута `/v1/calendars/:calendar_id/events`.
|
|
%%%
|
|
%%% POST – создание нового события (одиночного или повторяющегося).
|
|
%%% GET – получение списка событий календаря с возможностью фильтрации
|
|
%%% по диапазону дат и разворачиванием повторяющихся событий.
|
|
%%% @end
|
|
%%%-------------------------------------------------------------------
|
|
-module(handler_events).
|
|
-behaviour(cowboy_handler).
|
|
|
|
-export([init/2]).
|
|
-export([trails/0]).
|
|
|
|
-include("records.hrl").
|
|
|
|
%%% cowboy_handler callback
|
|
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
|
|
init(Req, Opts) ->
|
|
handle(Req, Opts).
|
|
|
|
%%% Swagger metadata
|
|
-spec trails() -> [map()].
|
|
trails() ->
|
|
[
|
|
#{ % POST create event
|
|
path => <<"/v1/calendars/:calendar_id/events">>,
|
|
method => <<"POST">>,
|
|
description => <<"Create a new event (single or recurring)">>,
|
|
tags => [<<"Events">>],
|
|
parameters => [
|
|
#{
|
|
name => <<"calendar_id">>,
|
|
in => <<"path">>,
|
|
description => <<"Calendar ID">>,
|
|
required => true,
|
|
schema => #{type => string}
|
|
}
|
|
],
|
|
requestBody => #{
|
|
required => true,
|
|
content => #{<<"application/json">> => #{schema => event_create_schema()}}
|
|
},
|
|
responses => #{
|
|
201 => #{description => <<"Event created">>},
|
|
400 => #{description => <<"Missing required fields or invalid JSON">>},
|
|
403 => #{description => <<"Access denied">>},
|
|
404 => #{description => <<"Calendar not found">>}
|
|
}
|
|
},
|
|
#{ % GET list events
|
|
path => <<"/v1/calendars/:calendar_id/events">>,
|
|
method => <<"GET">>,
|
|
description => <<"List events of a calendar with optional date range">>,
|
|
tags => [<<"Events">>],
|
|
parameters => [
|
|
#{
|
|
name => <<"calendar_id">>,
|
|
in => <<"path">>,
|
|
description => <<"Calendar ID">>,
|
|
required => true,
|
|
schema => #{type => string}
|
|
},
|
|
#{
|
|
name => <<"from">>,
|
|
in => <<"query">>,
|
|
description => <<"Start datetime (ISO8601)">>,
|
|
required => false,
|
|
schema => #{type => string, format => <<"date-time">>}
|
|
},
|
|
#{
|
|
name => <<"to">>,
|
|
in => <<"query">>,
|
|
description => <<"End datetime (ISO8601)">>,
|
|
required => false,
|
|
schema => #{type => string, format => <<"date-time">>}
|
|
}
|
|
],
|
|
responses => #{
|
|
200 => #{
|
|
description => <<"Array of events">>,
|
|
content => #{<<"application/json">> => #{schema => #{
|
|
type => array,
|
|
items => event_schema()
|
|
}}}
|
|
},
|
|
403 => #{description => <<"Access denied">>},
|
|
404 => #{description => <<"Calendar not found">>}
|
|
}
|
|
}
|
|
].
|
|
|
|
event_schema() ->
|
|
#{
|
|
type => object,
|
|
properties => #{
|
|
id => #{type => string},
|
|
calendar_id => #{type => string},
|
|
title => #{type => string},
|
|
description => #{type => string},
|
|
event_type => #{type => string, enum => [<<"single">>, <<"recurring">>]},
|
|
start_time => #{type => string, format => <<"date-time">>},
|
|
duration => #{type => integer},
|
|
recurrence => #{type => object, nullable => true},
|
|
master_id => #{type => string, nullable => true},
|
|
is_instance => #{type => boolean},
|
|
specialist_id => #{type => string, nullable => true},
|
|
location => #{type => object, nullable => true},
|
|
tags => #{type => array, items => #{type => string}},
|
|
capacity => #{type => integer, nullable => true},
|
|
online_link => #{type => string, nullable => true},
|
|
status => #{type => string, enum => [<<"active">>, <<"cancelled">>, <<"completed">>]},
|
|
reason => #{type => string, nullable => true},
|
|
rating_avg => #{type => number, format => float},
|
|
rating_count => #{type => integer},
|
|
attachments => #{type => array, items => #{type => string}, nullable => true},
|
|
edit_history => #{type => array, items => #{type => object}, nullable => true},
|
|
created_at => #{type => string, format => <<"date-time">>},
|
|
updated_at => #{type => string, format => <<"date-time">>}
|
|
}
|
|
}.
|
|
|
|
event_create_schema() ->
|
|
#{
|
|
type => object,
|
|
required => [<<"title">>, <<"start_time">>, <<"duration">>],
|
|
properties => #{
|
|
title => #{type => string},
|
|
start_time => #{type => string, format => <<"date-time">>},
|
|
duration => #{type => integer, description => <<"Duration in minutes">>},
|
|
description => #{type => string},
|
|
tags => #{type => array, items => #{type => string}},
|
|
capacity => #{type => integer},
|
|
online_link => #{type => string},
|
|
location => #{
|
|
type => object,
|
|
properties => #{
|
|
address => #{type => string},
|
|
lat => #{type => number, format => float},
|
|
lon => #{type => number, format => float}
|
|
}
|
|
},
|
|
recurrence => #{type => object, description => <<"Recurrence rule (RFC 5545)">>}
|
|
}
|
|
}.
|
|
|
|
%%%===================================================================
|
|
%%% HTTP-методы
|
|
%%%===================================================================
|
|
|
|
%% @private
|
|
-spec handle(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
|
|
handle(Req, _Opts) ->
|
|
case cowboy_req:method(Req) of
|
|
<<"POST">> -> create_event(Req);
|
|
<<"GET">> -> list_events(Req);
|
|
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
|
|
end.
|
|
|
|
%% @doc POST /v1/calendars/:calendar_id/events — создание события.
|
|
-spec create_event(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
|
|
create_event(Req) ->
|
|
case handler_utils:auth_user(Req) of
|
|
{ok, UserId, Req1} ->
|
|
CalendarId = cowboy_req:binding(calendar_id, Req1),
|
|
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
|
try jsx:decode(Body, [return_maps]) of
|
|
Decoded when is_map(Decoded) ->
|
|
case Decoded of
|
|
#{<<"title">> := Title, <<"start_time">> := StartTimeStr, <<"duration">> := Duration} ->
|
|
case handler_utils:parse_datetime(StartTimeStr) of
|
|
{ok, StartTime} ->
|
|
Location = parse_location(maps:get(<<"location">>, Decoded, undefined)),
|
|
Description = maps:get(<<"description">>, Decoded, <<>>),
|
|
case maps:get(<<"recurrence">>, Decoded, undefined) of
|
|
undefined ->
|
|
case logic_event:create_event(UserId, CalendarId, Title, StartTime, Duration, Description) of
|
|
{ok, Event} ->
|
|
update_event_fields(UserId, Event#event.id, Location, Decoded),
|
|
{ok, UpdatedEvent} = core_event:get_by_id(Event#event.id),
|
|
Response = handler_utils:event_to_json(UpdatedEvent),
|
|
handler_utils:send_json(Req2, 201, Response);
|
|
{error, access_denied} ->
|
|
handler_utils:send_error(Req2, 403, <<"Access denied">>);
|
|
{error, not_found} ->
|
|
handler_utils:send_error(Req2, 404, <<"Calendar not found">>);
|
|
{error, event_in_past} ->
|
|
handler_utils:send_error(Req2, 400, <<"Event cannot be in the past">>);
|
|
{error, {content_banned, _}} ->
|
|
handler_utils:send_error(Req2, 400, <<"Content contains banned words">>);
|
|
{error, _} ->
|
|
handler_utils:send_error(Req2, 500, <<"Internal server error">>)
|
|
end;
|
|
RRule ->
|
|
case logic_event:create_recurring_event(UserId, CalendarId, Title, StartTime, Duration, RRule, Description) of
|
|
{ok, Event} ->
|
|
update_event_fields(UserId, Event#event.id, Location, Decoded),
|
|
{ok, UpdatedEvent} = core_event:get_by_id(Event#event.id),
|
|
Response = handler_utils:event_to_json(UpdatedEvent),
|
|
handler_utils:send_json(Req2, 201, Response);
|
|
{error, invalid_rrule} ->
|
|
handler_utils:send_error(Req2, 400, <<"Invalid recurrence rule">>);
|
|
{error, access_denied} ->
|
|
handler_utils:send_error(Req2, 403, <<"Access denied">>);
|
|
{error, not_found} ->
|
|
handler_utils:send_error(Req2, 404, <<"Calendar not found">>);
|
|
{error, event_in_past} ->
|
|
handler_utils:send_error(Req2, 400, <<"Event cannot be in the past">>);
|
|
{error, {content_banned, _}} ->
|
|
handler_utils:send_error(Req2, 400, <<"Content contains banned words">>);
|
|
{error, _} ->
|
|
handler_utils:send_error(Req2, 500, <<"Internal server error">>)
|
|
end
|
|
end;
|
|
{error, _} ->
|
|
handler_utils:send_error(Req2, 400, <<"Invalid start_time format. Use ISO 8601">>)
|
|
end;
|
|
_ ->
|
|
handler_utils:send_error(Req2, 400, <<"Missing required fields: title, start_time, duration">>)
|
|
end;
|
|
_ ->
|
|
handler_utils:send_error(Req2, 400, <<"Invalid JSON">>)
|
|
catch
|
|
_:_ -> handler_utils:send_error(Req2, 400, <<"Invalid JSON format">>)
|
|
end;
|
|
{error, Code, Message, Req1} ->
|
|
handler_utils:send_error(Req1, Code, Message)
|
|
end.
|
|
|
|
%% @doc GET /v1/calendars/:calendar_id/events — список событий.
|
|
-spec list_events(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
|
|
list_events(Req) ->
|
|
case handler_utils:auth_user(Req) of
|
|
{ok, UserId, Req1} ->
|
|
CalendarId = cowboy_req:binding(calendar_id, Req1),
|
|
Qs = cowboy_req:parse_qs(Req1),
|
|
From = proplists:get_value(<<"from">>, Qs, undefined),
|
|
To = proplists:get_value(<<"to">>, Qs, undefined),
|
|
case logic_event:list_events(UserId, CalendarId) of
|
|
{ok, Events} ->
|
|
Response = case {From, To} of
|
|
{undefined, undefined} ->
|
|
[handler_utils:event_to_json(E) || E <- Events];
|
|
{FromStr, ToStr} ->
|
|
FromDt = parse_datetime_binary(FromStr),
|
|
ToDt = parse_datetime_binary(ToStr),
|
|
expand_recurring_events(UserId, Events, FromDt, ToDt)
|
|
end,
|
|
handler_utils:send_json(Req1, 200, Response);
|
|
{error, access_denied} ->
|
|
handler_utils:send_error(Req1, 403, <<"Access denied">>);
|
|
{error, not_found} ->
|
|
handler_utils:send_error(Req1, 404, <<"Calendar not found">>);
|
|
{error, _} ->
|
|
handler_utils:send_error(Req1, 500, <<"Internal server error">>)
|
|
end;
|
|
{error, Code, Message, Req1} ->
|
|
handler_utils:send_error(Req1, Code, Message)
|
|
end.
|
|
|
|
%%%===================================================================
|
|
%%% Вспомогательные функции
|
|
%%%===================================================================
|
|
|
|
update_event_fields(UserId, EventId, Location, Decoded) ->
|
|
Updates = [],
|
|
Updates1 = case Location of undefined -> Updates; _ -> [{location, Location} | Updates] end,
|
|
Updates2 = case maps:get(<<"capacity">>, Decoded, undefined) of undefined -> Updates1; Cap -> [{capacity, Cap} | Updates1] end,
|
|
Updates3 = case maps:get(<<"tags">>, Decoded, undefined) of undefined -> Updates2; Tags -> [{tags, Tags} | Updates2] end,
|
|
%% description already applied in create_event/6 when present
|
|
Updates4 = case maps:get(<<"online_link">>, Decoded, undefined) of undefined -> Updates3; Link -> [{online_link, Link} | Updates3] end,
|
|
case Updates4 of
|
|
[] -> ok;
|
|
_ ->
|
|
case logic_event:update_event(UserId, EventId, Updates4) of
|
|
{ok, _} -> ok;
|
|
_ -> ok
|
|
end
|
|
end.
|
|
|
|
parse_location(undefined) -> undefined;
|
|
parse_location(LocationMap) when is_map(LocationMap) ->
|
|
case LocationMap of
|
|
#{<<"lat">> := Lat, <<"lon">> := Lon} ->
|
|
Address = maps:get(<<"address">>, LocationMap, <<"">>),
|
|
#location{address = Address, lat = Lat, lon = Lon};
|
|
_ -> undefined
|
|
end;
|
|
parse_location(_) -> undefined.
|
|
|
|
expand_recurring_events(UserId, Events, From, To) ->
|
|
lists:flatmap(fun(Event) ->
|
|
case Event#event.event_type of
|
|
single ->
|
|
case is_in_range(Event#event.start_time, From, To) of
|
|
true -> [handler_utils:event_to_json(Event)];
|
|
false -> []
|
|
end;
|
|
recurring ->
|
|
case logic_event:get_occurrences(UserId, Event#event.id, To) of
|
|
{ok, Occurrences} ->
|
|
lists:filtermap(
|
|
fun({virtual, Occ}) ->
|
|
case is_in_range(Occ, From, To) of
|
|
true -> {true, occurrence_to_json(Event, Occ)};
|
|
false -> false
|
|
end;
|
|
({materialized, Instance}) ->
|
|
case is_in_range(Instance#event.start_time, From, To) of
|
|
true -> {true, handler_utils:event_to_json(Instance)};
|
|
false -> false
|
|
end
|
|
end, Occurrences);
|
|
_ -> []
|
|
end
|
|
end
|
|
end, Events).
|
|
|
|
is_in_range(Time, From, To) -> Time >= From andalso Time =< To.
|
|
|
|
parse_datetime_binary(Str) ->
|
|
{ok, Dt} = handler_utils:parse_datetime(Str),
|
|
Dt.
|
|
|
|
occurrence_to_json(Master, Occurrence) ->
|
|
#{
|
|
master_id => Master#event.id,
|
|
start_time => handler_utils:datetime_to_iso8601(Occurrence),
|
|
duration => Master#event.duration,
|
|
title => Master#event.title,
|
|
is_virtual => true
|
|
}. |