diff --git a/lib/bike_brigade/locations.ex b/lib/bike_brigade/locations.ex index e4568a08..64985f4c 100644 --- a/lib/bike_brigade/locations.ex +++ b/lib/bike_brigade/locations.ex @@ -21,7 +21,10 @@ defmodule BikeBrigade.Locations do Returns the list of community_fridges. """ def list_community_fridges do - Repo.all(from cf in CommunityFridge, order_by: [desc: cf.active, asc: cf.name]) + CommunityFridge + |> from(order_by: [desc: :active, asc: :name]) + |> Repo.all() + |> Repo.preload(:location) end @doc """ @@ -29,13 +32,17 @@ defmodule BikeBrigade.Locations do Raises `Ecto.NoResultsError` if the Community fridge does not exist. """ - def get_community_fridge!(id), do: Repo.get!(CommunityFridge, id) + def get_community_fridge!(id) do + CommunityFridge + |> Repo.get!(id) + |> Repo.preload(:location) + end @doc """ Creates a community_fridge. """ def create_community_fridge(attrs \\ %{}) do - %CommunityFridge{} + %CommunityFridge{location: nil} |> CommunityFridge.changeset(attrs) |> Repo.insert() end @@ -45,6 +52,7 @@ defmodule BikeBrigade.Locations do """ def update_community_fridge(%CommunityFridge{} = community_fridge, attrs) do community_fridge + |> Repo.preload(:location) |> CommunityFridge.changeset(attrs) |> Repo.update() end diff --git a/lib/bike_brigade/locations/community_fridge.ex b/lib/bike_brigade/locations/community_fridge.ex index c801e49e..aa536b6a 100644 --- a/lib/bike_brigade/locations/community_fridge.ex +++ b/lib/bike_brigade/locations/community_fridge.ex @@ -19,16 +19,30 @@ defmodule BikeBrigade.Locations.CommunityFridge do field :active, :boolean, default: true field :pair_preferred, :boolean, default: false - belongs_to :location, Location + belongs_to :location, Location, on_replace: :update timestamps() end def changeset(community_fridge, params) do community_fridge - |> cast(params, @fields ++ [:location_id]) + |> cast(params, @fields) + |> maybe_cast_location(params) |> validate_required([:name]) |> unique_constraint(:location_id) - |> assoc_constraint(:location) + end + + # Only associate a location when an address is present in the params (new location), + # or the fridge already has a location that may need updating (edit scenario). + defp maybe_cast_location(changeset, params) do + location_params = Map.get(params, "location") || Map.get(params, :location, %{}) + address = Map.get(location_params, "address") || Map.get(location_params, :address) + has_existing = not is_nil(changeset.data.location_id) + + if address not in [nil, ""] || has_existing do + cast_assoc(changeset, :location) + else + changeset + end end end diff --git a/lib/bike_brigade_web/live/community_fridge_live/form_component.ex b/lib/bike_brigade_web/live/community_fridge_live/form_component.ex index 3f821db7..4d41ff3a 100644 --- a/lib/bike_brigade_web/live/community_fridge_live/form_component.ex +++ b/lib/bike_brigade_web/live/community_fridge_live/form_component.ex @@ -2,6 +2,7 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.FormComponent do use BikeBrigadeWeb, :live_component alias BikeBrigade.{Locations, MediaStorage} + alias BikeBrigadeWeb.Components.LiveLocation @impl true def mount(socket) do diff --git a/lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex b/lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex index 2c21fcfd..801babaa 100644 --- a/lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex +++ b/lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex @@ -12,6 +12,13 @@ <.input type="textarea" field={f[:description]} label="Description" /> + <.live_component + module={LiveLocation} + id="location-form" + field={f[:location]} + label="Location" + /> +
assign(:page, :community_fridges) |> assign(:page_title, "Community Fridges") |> assign(:community_fridge, nil) - |> assign(:community_fridges, Locations.list_community_fridges())} + |> assign(:community_fridges, Locations.list_community_fridges()) + |> assign(:map_layers, []) + |> assign(:map_coords, @default_coords)} end @impl Phoenix.LiveView @@ -23,6 +28,8 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.Index do socket |> assign(:page_title, "Community Fridges") |> assign(:community_fridge, nil) + |> assign(:map_layers, fridge_markers(socket.assigns.community_fridges)) + |> assign(:map_coords, map_center(socket.assigns.community_fridges)) end defp apply_action(socket, :new, _params) do @@ -36,4 +43,33 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.Index do |> assign(:page_title, "Edit Fridge") |> assign(:community_fridge, Locations.get_community_fridge!(id)) end + + defp fridge_markers(community_fridges) do + for %{id: id, name: name, location: location} <- community_fridges, + not is_nil(location), + not is_nil(location.coords) do + %{ + id: "fridge-#{id}", + type: :marker, + data: %{ + lat: lat(location), + lng: lng(location), + icon: "warehouse", + color: "#1c64f2", + tooltip: name + } + } + end + end + + defp map_center(community_fridges) do + community_fridges + |> Enum.find_value(fn %{location: location} -> + location && location.coords + end) + |> case do + nil -> @default_coords + coords -> coords + end + end end diff --git a/lib/bike_brigade_web/live/community_fridge_live/index.html.heex b/lib/bike_brigade_web/live/community_fridge_live/index.html.heex index dd8e8ee9..5986223a 100644 --- a/lib/bike_brigade_web/live/community_fridge_live/index.html.heex +++ b/lib/bike_brigade_web/live/community_fridge_live/index.html.heex @@ -4,6 +4,13 @@ Community fridges are neighbourhood food-sharing locations. Dispatchers can manage fridge details here. +<.map + id="community-fridges-map" + coords={@map_coords} + initial_layers={@map_layers} + class="w-full h-64 mb-4" +/> +
<.button patch={~p"/community_fridges/new"}> New Community Fridge @@ -13,6 +20,9 @@ <:col :let={community_fridge} label="Name"> {community_fridge.name} + <:col :let={community_fridge} label="Address"> + {community_fridge.location} + <:col :let={community_fridge} label="Description"> {community_fridge.description} diff --git a/test/bike_brigade/locations/community_fridge_test.exs b/test/bike_brigade/locations/community_fridge_test.exs index c8b3dc46..e2e4004c 100644 --- a/test/bike_brigade/locations/community_fridge_test.exs +++ b/test/bike_brigade/locations/community_fridge_test.exs @@ -1,69 +1,62 @@ defmodule BikeBrigade.Locations.CommunityFridgeTest do use BikeBrigade.DataCase - alias BikeBrigade.Locations.Location + alias BikeBrigade.Locations alias BikeBrigade.Locations.CommunityFridge - - setup do - location_data = fixture(:location) - - {:ok, location} = - %Location{} - |> Location.changeset(location_data) - |> Repo.insert() - - %{location: location} - end + alias BikeBrigade.Repo.Seeds.Toronto describe "CommunityFridge changeset" do - test "changeset with valid attributes", %{location: location} do + test "valid with nested location params" do attrs = %{ name: "Downtown Community Fridge", description: "A fridge for the community", - photo: "https://example.com/photo.jpg", active: true, pair_preferred: false, - location_id: location.id + location: Toronto.random_location() } - changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) + changeset = CommunityFridge.changeset(%CommunityFridge{location: nil}, attrs) assert changeset.valid? end - test "changeset with minimal required attributes", %{location: location} do - attrs = %{ - name: "Community Fridge", - location_id: location.id - } - - changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) + test "valid without a location" do + changeset = + CommunityFridge.changeset(%CommunityFridge{location: nil}, %{name: "Bare Fridge"}) assert changeset.valid? end - test "changeset requires name", %{location: location} do - attrs = %{ - description: "A fridge without a name", - location_id: location.id - } - - changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) + test "requires name" do + changeset = + CommunityFridge.changeset(%CommunityFridge{location: nil}, %{ + location: Toronto.random_location() + }) refute changeset.valid? assert "can't be blank" in errors_on(changeset).name end - test "changeset rejects empty name", %{location: location} do - attrs = %{ - name: "", - location_id: location.id - } - - changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) + test "rejects empty name" do + changeset = + CommunityFridge.changeset(%CommunityFridge{location: nil}, %{ + name: "", + location: Toronto.random_location() + }) refute changeset.valid? assert "can't be blank" in errors_on(changeset).name end + + test "updating location updates the associated record in place" do + fridge = fixture(:community_fridge) + original_location_id = fridge.location.id + + new_location = Toronto.random_location() + {:ok, updated} = Locations.update_community_fridge(fridge, %{location: new_location}) + + assert updated.location.id == original_location_id + assert updated.location.address == new_location.address + end end end diff --git a/test/bike_brigade_web/live/community_fridge_live_test.exs b/test/bike_brigade_web/live/community_fridge_live_test.exs index b9cc4e38..0cb5f01e 100644 --- a/test/bike_brigade_web/live/community_fridge_live_test.exs +++ b/test/bike_brigade_web/live/community_fridge_live_test.exs @@ -15,6 +15,26 @@ defmodule BikeBrigadeWeb.CommunityFridgeLiveTest do assert html =~ fridge.name end + test "shows address column for fridge with a location", ctx do + fridge = fixture(:community_fridge, %{name: "Located Fridge"}) + {:ok, _view, html} = live(ctx.conn, ~p"/community_fridges") + + assert html =~ fridge.location.address + end + + test "renders map element on index page", ctx do + {:ok, _view, html} = live(ctx.conn, ~p"/community_fridges") + + assert html =~ "community-fridges-map" + end + + test "map includes pin for fridge with coordinates", ctx do + fridge = fixture(:community_fridge, %{name: "Mapped Fridge"}) + {:ok, _view, html} = live(ctx.conn, ~p"/community_fridges") + + assert html =~ "fridge-#{fridge.id}" + end + test "can create a new community fridge", ctx do {:ok, view, _html} = live(ctx.conn, ~p"/community_fridges") diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex index e392f86e..d96147d3 100644 --- a/test/support/fixtures.ex +++ b/test/support/fixtures.ex @@ -193,15 +193,10 @@ defmodule BikeBrigade.Fixtures do end def fixture(:community_fridge, attrs) do - attrs = - attrs - |> Map.put_new_lazy(:location_id, fn -> fixture(:persisted_location, %{}).id end) + attrs = Map.put_new_lazy(attrs, :location, fn -> Toronto.random_location() end) {:ok, community_fridge} = - %{ - name: Faker.Company.name(), - active: true - } + %{name: Faker.Company.name(), active: true} |> Map.merge(attrs) |> Locations.create_community_fridge()