From 4d83c50034ad6ef3b2793f695c9f02a295ebedd6 Mon Sep 17 00:00:00 2001 From: LucasLvy Date: Tue, 15 Oct 2024 16:42:29 +0200 Subject: [PATCH] feat(backend): get events with pagination --- backend/lib/peach/event.ex | 24 ++++- backend/lib/peach/events.ex | 36 +++++-- backend/lib/peach/tickets.ex | 18 ++-- .../peach_web/controllers/event_controller.ex | 79 ++++++++++++++- .../controllers/ticket_controller.ex | 5 +- backend/lib/peach_web/router.ex | 3 + .../20241009135655_create_events.exs | 3 +- .../peach/events/event_db_setters_test.exs | 23 +++-- .../controllers/create_event_test.exs | 7 +- .../peach_web/controllers/get_events_test.exs | 98 +++++++++++++++++++ .../controllers/ticket_controller_test.exs | 6 +- .../controllers/update_event_test.exs | 9 +- 12 files changed, 272 insertions(+), 39 deletions(-) create mode 100644 backend/test/peach_web/controllers/get_events_test.exs diff --git a/backend/lib/peach/event.ex b/backend/lib/peach/event.ex index 2ea84f4..fdd5ca9 100644 --- a/backend/lib/peach/event.ex +++ b/backend/lib/peach/event.ex @@ -8,7 +8,8 @@ defmodule Peach.Event do @derive Jason.Encoder schema "events" do field :name, :string - field :date, :naive_datetime + field :start, :naive_datetime + field :end, :naive_datetime field :description, :string field :location, :string field :cover, :string @@ -23,16 +24,29 @@ defmodule Peach.Event do @doc false def changeset(event, attrs) do event - |> cast(attrs, [:name, :description, :location, :date, :cover, :treasury]) + |> cast(attrs, [:name, :description, :location, :start, :end, :cover, :treasury]) |> cast_assoc(:ticket_tiers, with: &Peach.TicketTier.changeset/2, required: true) - |> validate_required([:name, :description, :location, :date, :cover, :treasury]) + |> validate_required([:name, :description, :location, :start, :end, :cover, :treasury]) |> validate_format(:treasury, ~r/^0x[0-9a-fA-F]{1,64}$/) + |> validate_end_after_start() end def update_changeset(event, attrs) do event - |> cast(attrs, [:name, :description, :location, :date, :cover, :treasury]) - |> validate_required([:name, :description, :location, :date, :cover, :treasury]) + |> cast(attrs, [:name, :description, :location, :start, :end, :cover, :treasury]) + |> validate_required([:name, :description, :location, :start, :end, :cover, :treasury]) |> validate_format(:treasury, ~r/^0x[0-9a-fA-F]{1,64}$/) + |> validate_end_after_start() + end + + defp validate_end_after_start(changeset) do + start_time = get_field(changeset, :start) + end_time = get_field(changeset, :end) + + if start_time && end_time && NaiveDateTime.compare(end_time, start_time) != :gt do + add_error(changeset, :end, "must be after the start date and time") + else + changeset + end end end diff --git a/backend/lib/peach/events.ex b/backend/lib/peach/events.ex index 38c3245..a1e8a6b 100644 --- a/backend/lib/peach/events.ex +++ b/backend/lib/peach/events.ex @@ -4,6 +4,7 @@ defmodule Peach.Events do """ alias Peach.Event alias Peach.Repo + import Ecto.Query @doc """ Creates an event with the given attributes. @@ -14,6 +15,18 @@ defmodule Peach.Events do |> Repo.insert() end + @doc """ + Returns the `first` events that end after `after_time` and their id is after `after_event_id` + """ + def get_events(after_datetime, after_event_id, first) do + Repo.all( + from e in Event, + where: e.end >= ^after_datetime and e.id > ^after_event_id, + order_by: [asc: e.start, asc: e.id], + limit: ^first + ) + end + @doc """ Updates the `name` field """ @@ -26,24 +39,35 @@ defmodule Peach.Events do end @doc """ - Updates the `date` field + Updates the `description` field """ - def update_event_date(event_id, date) do + def update_event_description(event_id, description) do event = Repo.get!(Event, event_id) event - |> Event.update_changeset(%{date: date}) + |> Event.update_changeset(%{description: description}) |> Repo.update() end @doc """ - Updates the `description` field + Updates the `end` field """ - def update_event_description(event_id, description) do + def update_event_end(event_id, end_date) do event = Repo.get!(Event, event_id) event - |> Event.update_changeset(%{description: description}) + |> Event.update_changeset(%{end: end_date}) + |> Repo.update() + end + + @doc """ + Updates the `start` field + """ + def update_event_start(event_id, start) do + event = Repo.get!(Event, event_id) + + event + |> Event.update_changeset(%{start: start}) |> Repo.update() end diff --git a/backend/lib/peach/tickets.ex b/backend/lib/peach/tickets.ex index f8a4d03..cb4cc60 100644 --- a/backend/lib/peach/tickets.ex +++ b/backend/lib/peach/tickets.ex @@ -6,13 +6,13 @@ defmodule Peach.Tickets do alias Peach.Ticket import Ecto.Query - def list_tickets_with_event_by_owner(owner_address) do - Repo.all( - from t in Ticket, - where: t.owner == ^owner_address, - join: tier in assoc(t, :ticket_tier), - join: event in assoc(tier, :event), - preload: [ticket_tier: {tier, event: event}] - ) - end + def list_tickets_with_event_by_owner(owner_address), + do: + Repo.all( + from t in Ticket, + where: t.owner == ^owner_address, + join: tier in assoc(t, :ticket_tier), + join: event in assoc(tier, :event), + preload: [ticket_tier: {tier, event: event}] + ) end diff --git a/backend/lib/peach_web/controllers/event_controller.ex b/backend/lib/peach_web/controllers/event_controller.ex index 0d3ba7b..4775705 100644 --- a/backend/lib/peach_web/controllers/event_controller.ex +++ b/backend/lib/peach_web/controllers/event_controller.ex @@ -21,11 +21,82 @@ defmodule PeachWeb.EventController do end end + def events(conn, params) do + with {:ok, after_datetime} <- validate_datetime(Map.get(params, "after_datetime")), + {:ok, after_event_id} <- + validate_integer(Map.get(params, "after_event_id", 0), "after_event_id"), + {:ok, first} <- validate_integer(Map.get(params, "first", 50), "first") do + # Fetch events and map them to desired structure + events = + Events.get_events(after_datetime, after_event_id, first) + |> Enum.map(&format_event/1) + + conn + |> put_status(:ok) + |> json(%{events: events}) + else + {:error, error} -> + conn + |> put_status(:unprocessable_entity) + |> json(%{errors: error}) + end + end + + defp validate_datetime(nil), do: {:error, %{after_datetime: "Can't be blank"}} + + defp validate_datetime(datetime_str) do + case NaiveDateTime.from_iso8601(datetime_str) do + {:ok, datetime} -> {:ok, datetime} + {:error, reason} -> {:error, %{after_datetime: reason}} + end + end + + defp validate_integer(value, _field) when is_integer(value), do: {:ok, value} + + defp validate_integer(value, field) do + case Integer.parse(value) do + {int, ""} -> {:ok, int} + _ -> {:error, %{field => "invalid_type"}} + end + end + + defp format_event(event), + do: %{ + "id" => event.id, + "name" => event.name, + "description" => event.description, + "start" => event.start, + "end" => event.end, + "location" => event.location, + "cover" => event.cover + } + @doc """ Updates the name of an event. """ def update_event_name(conn, %{"id" => id, "name" => name}) do - case Peach.Events.update_event_name(id, name) do + case Events.update_event_name(id, name) do + {:ok, _event} -> + conn + |> put_status(:no_content) + + {:error, changeset} -> + errors = + Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> + Phoenix.Naming.humanize(msg) + end) + + conn + |> put_status(:bad_request) + |> json(%{errors: errors}) + end + end + + @doc """ + Updates the starting datetime of an event. + """ + def update_event_start(conn, %{"id" => id, "start" => start}) do + case Events.update_event_start(id, start) do {:ok, _event} -> conn |> put_status(:no_content) @@ -43,10 +114,10 @@ defmodule PeachWeb.EventController do end @doc """ - Updates the date of an event. + Updates the ending datetime of an event. """ - def update_event_date(conn, %{"id" => id, "date" => date}) do - case Events.update_event_cover(id, date) do + def update_event_end(conn, %{"id" => id, "end" => end_date}) do + case Events.update_event_end(id, end_date) do {:ok, _event} -> conn |> put_status(:no_content) diff --git a/backend/lib/peach_web/controllers/ticket_controller.ex b/backend/lib/peach_web/controllers/ticket_controller.ex index 6070749..adfbbd2 100644 --- a/backend/lib/peach_web/controllers/ticket_controller.ex +++ b/backend/lib/peach_web/controllers/ticket_controller.ex @@ -9,7 +9,7 @@ defmodule PeachWeb.TicketController do # Group tickets by event and then by tier_id within each event events_with_tickets = tickets - |> Enum.group_by(fn ticket -> ticket.ticket_tier.event end) + |> Enum.group_by(& &1.ticket_tier.event) |> Enum.map(fn {event, tickets} -> # Group tickets by tier within each event tickets_by_tier = @@ -27,7 +27,8 @@ defmodule PeachWeb.TicketController do %{ "name" => event.name, "location" => event.location, - "date" => event.date, + "start" => event.start, + "end" => event.end, "cover" => event.cover, "tickets" => tickets_by_tier } diff --git a/backend/lib/peach_web/router.ex b/backend/lib/peach_web/router.ex index 71651a7..883ac48 100644 --- a/backend/lib/peach_web/router.ex +++ b/backend/lib/peach_web/router.ex @@ -13,6 +13,9 @@ defmodule PeachWeb.Router do patch "/events/:id/location", EventController, :update_event_location patch "/events/:id/cover", EventController, :update_event_cover patch "/events/:id/treasury", EventController, :update_event_treasury + patch "/events/:id/start", EventController, :update_event_start + patch "/events/:id/end", EventController, :update_event_end + get "/events", EventController, :events get "/tickets/:address", TicketController, :get_tickets_with_event_by_address end diff --git a/backend/priv/repo/migrations/20241009135655_create_events.exs b/backend/priv/repo/migrations/20241009135655_create_events.exs index 8585dd3..82bf253 100644 --- a/backend/priv/repo/migrations/20241009135655_create_events.exs +++ b/backend/priv/repo/migrations/20241009135655_create_events.exs @@ -6,7 +6,8 @@ defmodule Peach.Repo.Migrations.CreateEvents do add :name, :string add :description, :string add :location, :string - add :date, :utc_datetime + add :start, :utc_datetime + add :end, :utc_datetime add :cover, :string add :onchain, :boolean, default: false add :treasury, :string diff --git a/backend/test/peach/events/event_db_setters_test.exs b/backend/test/peach/events/event_db_setters_test.exs index 0a3fe87..9eec683 100644 --- a/backend/test/peach/events/event_db_setters_test.exs +++ b/backend/test/peach/events/event_db_setters_test.exs @@ -10,7 +10,8 @@ defmodule Peach.Events.EventDBSettersTest do {:ok, event} = Repo.insert(%Event{ name: "Original Name", - date: ~N[2024-01-01 10:00:00], + start: ~N[2024-01-01 10:00:00], + end: ~N[2024-01-01 15:00:00], description: "Original description", location: "Original location", cover: "https://example.com/original_cover.jpg", @@ -31,14 +32,24 @@ defmodule Peach.Events.EventDBSettersTest do assert updated_event.name == updated_name end - test "updates date in the database", %{event: event} do + test "updates end date in the database", %{event: event} do new_date = ~N[2025-12-31 23:59:59] - Events.update_event_date(event.id, new_date) + Events.update_event_end(event.id, new_date) updated_event = Repo.get!(Event, event.id) - assert updated_event.date == new_date - Events.update_event_date(event.id, 1) + assert updated_event.end == new_date + Events.update_event_end(event.id, 1) updated_event = Repo.get!(Event, event.id) - assert updated_event.date == new_date + assert updated_event.end == new_date + end + + test "updates start date in the database", %{event: event} do + new_date = ~N[2024-01-01 11:00:00] + Events.update_event_start(event.id, new_date) + updated_event = Repo.get!(Event, event.id) + assert updated_event.start == new_date + Events.update_event_start(event.id, 1) + updated_event = Repo.get!(Event, event.id) + assert updated_event.start == new_date end test "updates description in the database", %{event: event} do diff --git a/backend/test/peach_web/controllers/create_event_test.exs b/backend/test/peach_web/controllers/create_event_test.exs index ebc769e..91cd8fe 100644 --- a/backend/test/peach_web/controllers/create_event_test.exs +++ b/backend/test/peach_web/controllers/create_event_test.exs @@ -9,7 +9,8 @@ defmodule PeachWeb.EventCreateControllerTest do @valid_event_attrs %{ "name" => "Blockchain Conference", - "date" => "2024-11-10T10:00:00", + "start" => "2024-11-10T10:00:00", + "end" => "2024-11-10T13:00:00", "description" => "A conference about blockchain technology.", "location" => "San Francisco, CA", "cover" => "https://example.com/cover.jpg", @@ -51,7 +52,7 @@ defmodule PeachWeb.EventCreateControllerTest do end test "returns error when required fields are missing", %{conn: conn} do - required_fields = ["name", "date", "description", "location", "cover", "ticket_tiers"] + required_fields = ["name", "start", "end", "description", "location", "cover", "ticket_tiers"] Enum.each(required_fields, fn field -> # Remove one required field at a time @@ -70,7 +71,7 @@ defmodule PeachWeb.EventCreateControllerTest do end test "returns error when fields are in the wrong format", %{conn: conn} do - required_fields = ["name", "date", "description", "location", "cover", "ticket_tiers"] + required_fields = ["name", "start", "end", "description", "location", "cover", "ticket_tiers"] Enum.each(required_fields, fn field -> invalid_attrs = diff --git a/backend/test/peach_web/controllers/get_events_test.exs b/backend/test/peach_web/controllers/get_events_test.exs new file mode 100644 index 0000000..c4def76 --- /dev/null +++ b/backend/test/peach_web/controllers/get_events_test.exs @@ -0,0 +1,98 @@ +defmodule PeachWeb.GetEventControllerTest do + use PeachWeb.ConnCase, async: true + alias Peach.Event + alias Peach.Repo + + setup do + # Insert sample events + event1 = + Repo.insert!(%Event{ + name: "Past Event", + start: ~N[2024-11-05 09:00:00], + end: ~N[2024-11-06 17:00:00] + }) + + event2 = + Repo.insert!(%Event{ + name: "Current Event", + start: ~N[2024-11-10 09:00:00], + end: ~N[2024-11-12 17:00:00] + }) + + event3 = + Repo.insert!(%Event{ + name: "Future Event", + start: ~N[2024-11-15 09:00:00], + end: ~N[2024-11-17 17:00:00] + }) + + {:ok, event1: event1, event2: event2, event3: event3} + end + + test "returns events after specified time and id", %{conn: conn, event2: event2, event3: event3} do + after_datetime = "2024-11-08T00:00:00" + after_event_id = "#{event2.id}" + first = "10" + + conn = + get( + conn, + "/api/events?after_datetime=#{after_datetime}&after_event_id=#{after_event_id}&first=#{first}" + ) + + assert json_response(conn, 200)["events"] == [ + %{ + "id" => event3.id, + "name" => event3.name, + "description" => event3.description, + "start" => NaiveDateTime.to_iso8601(event3.start), + "end" => NaiveDateTime.to_iso8601(event3.end), + "location" => event3.location, + "cover" => event3.cover + } + ] + end + + test "returns events without optional params", %{conn: conn} do + conn = get(conn, "/api/events") + response = json_response(conn, 422) + assert response["errors"]["after_datetime"] == "Can't be blank" + end + + test "returns validation error for invalid after_datetime", %{conn: conn} do + after_datetime = "invalid-time-format" + conn = get(conn, "/api/events?after_datetime=#{after_datetime}") + + response = json_response(conn, 422) + assert response["errors"]["after_datetime"] == "invalid_format" + end + + test "returns validation error for non-integer after_event_id", %{conn: conn} do + after_datetime = "2024-11-08T00:00:00" + after_event_id = "not-an-integer" + + conn = + get(conn, "/api/events?after_datetime=#{after_datetime}&after_event_id=#{after_event_id}") + + response = json_response(conn, 422) + assert response["errors"]["after_event_id"] == "invalid_type" + end + + test "limits the number of events returned with first param", %{conn: conn, event2: event2} do + after_datetime = "2024-11-08T00:00:00" + first = 1 + conn = get(conn, "/api/events?after_datetime=#{after_datetime}&first=#{first}") + + assert json_response(conn, 200)["events"] == [ + %{ + "id" => event2.id, + "name" => event2.name, + "description" => event2.description, + "start" => NaiveDateTime.to_iso8601(event2.start), + "end" => NaiveDateTime.to_iso8601(event2.end), + "location" => event2.location, + "cover" => event2.cover + } + ] + end +end diff --git a/backend/test/peach_web/controllers/ticket_controller_test.exs b/backend/test/peach_web/controllers/ticket_controller_test.exs index 8260544..843d0d7 100644 --- a/backend/test/peach_web/controllers/ticket_controller_test.exs +++ b/backend/test/peach_web/controllers/ticket_controller_test.exs @@ -7,7 +7,8 @@ defmodule PeachWeb.TicketControllerTest do event = Repo.insert!(%Event{ name: "Blockchain Conference", - date: ~N[2024-11-10 00:00:00], + start: ~N[2024-11-10 00:00:00], + end: ~N[2024-11-10 08:00:00], description: "A blockchain event", location: "San Francisco, CA", cover: "https://example.com/cover.jpg", @@ -77,7 +78,8 @@ defmodule PeachWeb.TicketControllerTest do %{ "name" => "Blockchain Conference", "location" => "San Francisco, CA", - "date" => "2024-11-10T00:00:00", + "start" => "2024-11-10T00:00:00", + "end" => "2024-11-10T08:00:00", "cover" => "https://example.com/cover.jpg", "tickets" => [ %{ diff --git a/backend/test/peach_web/controllers/update_event_test.exs b/backend/test/peach_web/controllers/update_event_test.exs index f447264..9a0eb82 100644 --- a/backend/test/peach_web/controllers/update_event_test.exs +++ b/backend/test/peach_web/controllers/update_event_test.exs @@ -6,7 +6,8 @@ defmodule PeachWeb.EventUpdateControllertest do @original_event %Event{ name: "Original Name", - date: ~N[2024-01-01 10:00:00], + start: ~N[2024-01-01 10:00:00], + end: ~N[2024-01-01 16:00:00], description: "Original description", location: "Original location", cover: "https://example.com/original_cover.jpg", @@ -28,6 +29,8 @@ defmodule PeachWeb.EventUpdateControllertest do {"description", "Updated description"}, {"location", "Updated location"}, {"treasury", "0xbeef"}, + {"start", "2024-01-01T11:00:00"}, + {"end", "2024-01-01T13:00:00"}, {"cover", "https://example.com/updated_cover.jpg"} ] @@ -56,6 +59,10 @@ defmodule PeachWeb.EventUpdateControllertest do {"description", 2}, {"location", 3}, {"treasury", "wrong treasury format"}, + {"treasury", 1}, + {"start", "~N[2024-01-01 14:00:00]"}, + {"start", "2024-01-02T10:00:00"}, + {"end", "wrong type"}, {"cover", 4} ]