From b706a8e4f5c991af7ff6a0f9c117c82f264ad02e Mon Sep 17 00:00:00 2001 From: Skander Mzali Date: Thu, 20 Jul 2023 16:40:22 -0700 Subject: [PATCH 1/4] Rename Radio context to RadioLegacy --- server/lib/orcasite/radio/candidate.ex | 4 +- server/lib/orcasite/radio/detection.ex | 4 +- server/lib/orcasite/radio/feed.ex | 2 +- .../radio/{radio.ex => radio_legacy.ex} | 6 +-- .../graphql/resolvers/detection.ex | 14 ++--- .../orcasite_web/graphql/resolvers/feed.ex | 8 +-- server/priv/repo/seeds.exs | 2 +- server/test/orcasite/radio/radio_test.exs | 52 +++++++++---------- 8 files changed, 46 insertions(+), 46 deletions(-) rename server/lib/orcasite/radio/{radio.ex => radio_legacy.ex} (97%) diff --git a/server/lib/orcasite/radio/candidate.ex b/server/lib/orcasite/radio/candidate.ex index c39d89ef..6fe8c74b 100644 --- a/server/lib/orcasite/radio/candidate.ex +++ b/server/lib/orcasite/radio/candidate.ex @@ -1,8 +1,8 @@ -defmodule Orcasite.Radio.Candidate do +defmodule Orcasite.RadioLegacy.Candidate do use Ecto.Schema import Ecto.Changeset - alias Orcasite.Radio.{Detection, Feed} + alias Orcasite.RadioLegacy.{Detection, Feed} schema "candidates" do field(:detection_count, :integer) diff --git a/server/lib/orcasite/radio/detection.ex b/server/lib/orcasite/radio/detection.ex index 30f8b713..69e77d67 100644 --- a/server/lib/orcasite/radio/detection.ex +++ b/server/lib/orcasite/radio/detection.ex @@ -1,8 +1,8 @@ -defmodule Orcasite.Radio.Detection do +defmodule Orcasite.RadioLegacy.Detection do use Ecto.Schema import Ecto.Changeset - alias Orcasite.Radio.{Feed, Candidate} + alias Orcasite.RadioLegacy.{Feed, Candidate} alias __MODULE__ schema "detections" do diff --git a/server/lib/orcasite/radio/feed.ex b/server/lib/orcasite/radio/feed.ex index 4854a6cf..013954e2 100644 --- a/server/lib/orcasite/radio/feed.ex +++ b/server/lib/orcasite/radio/feed.ex @@ -1,4 +1,4 @@ -defmodule Orcasite.Radio.Feed do +defmodule Orcasite.RadioLegacy.Feed do use Ecto.Schema import Ecto.Changeset diff --git a/server/lib/orcasite/radio/radio.ex b/server/lib/orcasite/radio/radio_legacy.ex similarity index 97% rename from server/lib/orcasite/radio/radio.ex rename to server/lib/orcasite/radio/radio_legacy.ex index 68839065..b513fd73 100644 --- a/server/lib/orcasite/radio/radio.ex +++ b/server/lib/orcasite/radio/radio_legacy.ex @@ -1,11 +1,11 @@ -defmodule Orcasite.Radio do +defmodule Orcasite.RadioLegacy do @moduledoc """ - The Radio context. + The RadioLegacy context. """ import Ecto.Query, warn: false alias Orcasite.Repo - alias Orcasite.Radio.{Feed, Detection, Candidate} + alias Orcasite.RadioLegacy.{Feed, Detection, Candidate} def list_feeds do Repo.all(Feed) diff --git a/server/lib/orcasite_web/graphql/resolvers/detection.ex b/server/lib/orcasite_web/graphql/resolvers/detection.ex index 5459e417..db59e27f 100644 --- a/server/lib/orcasite_web/graphql/resolvers/detection.ex +++ b/server/lib/orcasite_web/graphql/resolvers/detection.ex @@ -1,17 +1,17 @@ defmodule OrcasiteWeb.Resolvers.Detection do - alias Orcasite.Radio + alias Orcasite.RadioLegacy alias OrcasiteWeb.Paginated def index(_, _) do - {:ok, Radio.list_all_detections()} + {:ok, RadioLegacy.list_all_detections()} end def list_candidates(args, _) do - {:ok, Paginated.format(Radio.list_candidates(args))} + {:ok, Paginated.format(RadioLegacy.list_candidates(args))} end def list_detections(args, _) do - {:ok, Paginated.format(Radio.list_detections(args))} + {:ok, Paginated.format(RadioLegacy.list_detections(args))} end def create( @@ -28,15 +28,15 @@ defmodule OrcasiteWeb.Resolvers.Detection do |> :inet_parse.ntoa() |> to_string() - with :ok <- Radio.verify_can_submit_detection(feed_id, source_ip, lockout_seconds()) do + with :ok <- RadioLegacy.verify_can_submit_detection(feed_id, source_ip, lockout_seconds()) do detection_attrs |> Map.put(:source_ip, source_ip) - |> Radio.create_detection_with_candidate() + |> RadioLegacy.create_detection_with_candidate() |> case do {:ok, detection} -> # Send notification for new detection Task.Supervisor.async_nolink(Orcasite.TaskSupervisor, fn -> - %{slug: node} = Radio.get_feed!(feed_id) + %{slug: node} = RadioLegacy.get_feed!(feed_id) Orcasite.Notifications.Notification.notify_new_detection(detection.id, node) end) diff --git a/server/lib/orcasite_web/graphql/resolvers/feed.ex b/server/lib/orcasite_web/graphql/resolvers/feed.ex index f2caaed8..060f9eba 100644 --- a/server/lib/orcasite_web/graphql/resolvers/feed.ex +++ b/server/lib/orcasite_web/graphql/resolvers/feed.ex @@ -1,14 +1,14 @@ defmodule OrcasiteWeb.Resolvers.Feed do - alias Orcasite.Radio + alias Orcasite.RadioLegacy def index(_, _) do - {:ok, Radio.list_feeds()} + {:ok, RadioLegacy.list_feeds()} end def show(%{slug: slug}, _) do - {:ok, Radio.get_feed_by_slug(slug)} + {:ok, RadioLegacy.get_feed_by_slug(slug)} end def show(%{id: id}, _) do - {:ok, Radio.get_feed!(id)} + {:ok, RadioLegacy.get_feed!(id)} end end diff --git a/server/priv/repo/seeds.exs b/server/priv/repo/seeds.exs index c526d826..6f2f2370 100644 --- a/server/priv/repo/seeds.exs +++ b/server/priv/repo/seeds.exs @@ -11,7 +11,7 @@ # and so on) as they will fail if something goes wrong. # import Ecto.Query -alias Orcasite.Radio.Feed +alias Orcasite.RadioLegacy.Feed feeds = [ %{ diff --git a/server/test/orcasite/radio/radio_test.exs b/server/test/orcasite/radio/radio_test.exs index b01e6343..e5ecd323 100644 --- a/server/test/orcasite/radio/radio_test.exs +++ b/server/test/orcasite/radio/radio_test.exs @@ -1,10 +1,10 @@ -defmodule Orcasite.RadioTest do +defmodule Orcasite.RadioLegacyTest do use Orcasite.DataCase - alias Orcasite.Radio + alias Orcasite.RadioLegacy describe "feeds" do - alias Orcasite.Radio.Feed + alias Orcasite.RadioLegacy.Feed @valid_attrs %{slug: "some slug", name: "some name", node_name: "some node_name"} @update_attrs %{slug: "some updated slug", name: "some updated name", node_name: "some updated node_name"} @@ -14,35 +14,35 @@ defmodule Orcasite.RadioTest do {:ok, feed} = attrs |> Map.merge(@valid_attrs) - |> Radio.create_feed() + |> RadioLegacy.create_feed() feed end test "list_feeds/0 returns all feeds" do feed = feed_fixture() - assert Radio.list_feeds() == [feed] + assert RadioLegacy.list_feeds() == [feed] end test "get_feed!/1 returns the feed with given id" do feed = feed_fixture() - assert Radio.get_feed!(feed.id) == feed + assert RadioLegacy.get_feed!(feed.id) == feed end test "create_feed/1 with valid data creates a feed" do - assert {:ok, %Feed{} = feed} = Radio.create_feed(@valid_attrs) + assert {:ok, %Feed{} = feed} = RadioLegacy.create_feed(@valid_attrs) assert feed.slug == "some slug" assert feed.name == "some name" assert feed.node_name == "some node_name" end test "create_feed/1 with invalid data returns error changeset" do - assert {:error, %Ecto.Changeset{}} = Radio.create_feed(@invalid_attrs) + assert {:error, %Ecto.Changeset{}} = RadioLegacy.create_feed(@invalid_attrs) end test "update_feed/2 with valid data updates the feed" do feed = feed_fixture() - assert {:ok, feed} = Radio.update_feed(feed, @update_attrs) + assert {:ok, feed} = RadioLegacy.update_feed(feed, @update_attrs) assert %Feed{} = feed assert feed.slug == "some updated slug" assert feed.name == "some updated name" @@ -51,24 +51,24 @@ defmodule Orcasite.RadioTest do test "update_feed/2 with invalid data returns error changeset" do feed = feed_fixture() - assert {:error, %Ecto.Changeset{}} = Radio.update_feed(feed, @invalid_attrs) - assert feed == Radio.get_feed!(feed.id) + assert {:error, %Ecto.Changeset{}} = RadioLegacy.update_feed(feed, @invalid_attrs) + assert feed == RadioLegacy.get_feed!(feed.id) end test "delete_feed/1 deletes the feed" do feed = feed_fixture() - assert {:ok, %Feed{}} = Radio.delete_feed(feed) - assert_raise Ecto.NoResultsError, fn -> Radio.get_feed!(feed.id) end + assert {:ok, %Feed{}} = RadioLegacy.delete_feed(feed) + assert_raise Ecto.NoResultsError, fn -> RadioLegacy.get_feed!(feed.id) end end test "change_feed/1 returns a feed changeset" do feed = feed_fixture() - assert %Ecto.Changeset{} = Radio.change_feed(feed) + assert %Ecto.Changeset{} = RadioLegacy.change_feed(feed) end end describe "detections" do - alias Orcasite.Radio.Detection + alias Orcasite.RadioLegacy.Detection @valid_attrs %{source_ip: "some source_ip", playlist_timestamp: 42, time: "2010-04-17 14:00:00.000000Z"} @update_attrs %{source_ip: "some updated source_ip", playlist_timestamp: 43, time: "2011-05-18 15:01:01.000000Z"} @@ -78,35 +78,35 @@ defmodule Orcasite.RadioTest do {:ok, detection} = attrs |> Map.merge(@valid_attrs) - |> Radio.create_detection() + |> RadioLegacy.create_detection() detection end test "list_detections/0 returns all detections" do detection = detection_fixture() - assert Radio.list_detections() == [detection] + assert RadioLegacy.list_detections() == [detection] end test "get_detection!/1 returns the detection with given id" do detection = detection_fixture() - assert Radio.get_detection!(detection.id) == detection + assert RadioLegacy.get_detection!(detection.id) == detection end test "create_detection/1 with valid data creates a detection" do - assert {:ok, %Detection{} = detection} = Radio.create_detection(@valid_attrs) + assert {:ok, %Detection{} = detection} = RadioLegacy.create_detection(@valid_attrs) assert detection.source_ip == "some source_ip" assert detection.playlist_timestamp == 42 assert detection.time == DateTime.from_naive!(~N[2010-04-17 14:00:00.000000Z], "Etc/UTC") end test "create_detection/1 with invalid data returns error changeset" do - assert {:error, %Ecto.Changeset{}} = Radio.create_detection(@invalid_attrs) + assert {:error, %Ecto.Changeset{}} = RadioLegacy.create_detection(@invalid_attrs) end test "update_detection/2 with valid data updates the detection" do detection = detection_fixture() - assert {:ok, detection} = Radio.update_detection(detection, @update_attrs) + assert {:ok, detection} = RadioLegacy.update_detection(detection, @update_attrs) assert %Detection{} = detection assert detection.source_ip == "some updated source_ip" assert detection.playlist_timestamp == 43 @@ -115,19 +115,19 @@ defmodule Orcasite.RadioTest do test "update_detection/2 with invalid data returns error changeset" do detection = detection_fixture() - assert {:error, %Ecto.Changeset{}} = Radio.update_detection(detection, @invalid_attrs) - assert detection == Radio.get_detection!(detection.id) + assert {:error, %Ecto.Changeset{}} = RadioLegacy.update_detection(detection, @invalid_attrs) + assert detection == RadioLegacy.get_detection!(detection.id) end test "delete_detection/1 deletes the detection" do detection = detection_fixture() - assert {:ok, %Detection{}} = Radio.delete_detection(detection) - assert_raise Ecto.NoResultsError, fn -> Radio.get_detection!(detection.id) end + assert {:ok, %Detection{}} = RadioLegacy.delete_detection(detection) + assert_raise Ecto.NoResultsError, fn -> RadioLegacy.get_detection!(detection.id) end end test "change_detection/1 returns a detection changeset" do detection = detection_fixture() - assert %Ecto.Changeset{} = Radio.change_detection(detection) + assert %Ecto.Changeset{} = RadioLegacy.change_detection(detection) end end end From b44139bfd0a3567349fba60a2226fd663cd67172 Mon Sep 17 00:00:00 2001 From: Skander Mzali Date: Tue, 25 Jul 2023 14:22:52 -0700 Subject: [PATCH 2/4] Create Radio Api with Ash, move old Radio context to RadioLegacy. Use new queries for gql Feed resolvers --- server/config/config.exs | 3 +- .../lib/orcasite/{accounts => }/accounts.ex | 0 server/lib/orcasite/radio.ex | 11 ++ server/lib/orcasite/radio/feed.ex | 58 ++++---- server/lib/orcasite/radio/registry.ex | 8 ++ .../{radio => radio_legacy}/candidate.ex | 0 .../{radio => radio_legacy}/detection.ex | 0 .../lib/orcasite/radio_legacy/feed_legacy.ex | 40 ++++++ .../{radio => radio_legacy}/radio_legacy.ex | 25 ---- server/lib/orcasite/types/geometry.ex | 43 ++++++ .../graphql/resolvers/detection.ex | 2 +- .../orcasite_web/graphql/resolvers/feed.ex | 7 +- ...20230725205820_add_slug_index_to_feeds.exs | 25 ++++ .../repo/feeds/20230725205820.json | 98 ++++++++++++++ server/test/orcasite/radio/radio_test.exs | 126 +----------------- 15 files changed, 262 insertions(+), 184 deletions(-) rename server/lib/orcasite/{accounts => }/accounts.ex (100%) create mode 100644 server/lib/orcasite/radio.ex create mode 100644 server/lib/orcasite/radio/registry.ex rename server/lib/orcasite/{radio => radio_legacy}/candidate.ex (100%) rename server/lib/orcasite/{radio => radio_legacy}/detection.ex (100%) create mode 100644 server/lib/orcasite/radio_legacy/feed_legacy.ex rename server/lib/orcasite/{radio => radio_legacy}/radio_legacy.ex (92%) create mode 100644 server/lib/orcasite/types/geometry.ex create mode 100644 server/priv/repo/migrations/20230725205820_add_slug_index_to_feeds.exs create mode 100644 server/priv/resource_snapshots/repo/feeds/20230725205820.json diff --git a/server/config/config.exs b/server/config/config.exs index 62a5a2ed..8e9f8534 100644 --- a/server/config/config.exs +++ b/server/config/config.exs @@ -73,8 +73,9 @@ config :orcasite, OrcasiteWeb.Auth.AuthAccessPipeline, error_handler: OrcasiteWeb.Auth.AuthErrorHandler config :ash, :use_all_identities_in_manage_relationship?, false -config :orcasite, :ash_apis, [Orcasite.Notifications, Orcasite.Accounts] +config :orcasite, :ash_apis, [Orcasite.Notifications, Orcasite.Accounts, Orcasite.Radio] config :orcasite, :ecto_repos, [Orcasite.Repo] +config :ash, :custom_types, [geometry: Orcasite.Types.Geometry] config :orcasite, Oban, repo: Orcasite.Repo, diff --git a/server/lib/orcasite/accounts/accounts.ex b/server/lib/orcasite/accounts.ex similarity index 100% rename from server/lib/orcasite/accounts/accounts.ex rename to server/lib/orcasite/accounts.ex diff --git a/server/lib/orcasite/radio.ex b/server/lib/orcasite/radio.ex new file mode 100644 index 00000000..d6aaebf1 --- /dev/null +++ b/server/lib/orcasite/radio.ex @@ -0,0 +1,11 @@ +defmodule Orcasite.Radio do + use Ash.Api, extensions: [AshAdmin.Api] + + resources do + registry Orcasite.Radio.Registry + end + + admin do + show? true + end +end diff --git a/server/lib/orcasite/radio/feed.ex b/server/lib/orcasite/radio/feed.ex index 013954e2..7a4cbad3 100644 --- a/server/lib/orcasite/radio/feed.ex +++ b/server/lib/orcasite/radio/feed.ex @@ -1,40 +1,40 @@ -defmodule Orcasite.RadioLegacy.Feed do - use Ecto.Schema - import Ecto.Changeset +defmodule Orcasite.Radio.Feed do + use Ash.Resource, + data_layer: AshPostgres.DataLayer - alias __MODULE__ - schema "feeds" do - field(:name, :string) - field(:slug, :string) - field(:node_name, :string) - field(:location_point, Geo.PostGIS.Geometry) + attributes do + integer_primary_key :id - timestamps() + attribute :name, :string + attribute :node_name, :string + attribute :slug, :string + attribute :location_point, :geometry + + create_timestamp :inserted_at + update_timestamp :updated_at end - @doc false - def changeset(feed, attrs) do - feed - |> cast(attrs, [:name, :node_name, :slug, :location_point]) - |> validate_required([:name, :node_name, :slug]) + postgres do + table "feeds" + repo Orcasite.Repo end - def latlong_to_geo(lat, long) when is_float(lat) and is_float(long), - do: Geo.WKT.decode!("SRID=4326;POINT(#{lat} #{long})") - - # TODO: Find the actual json -> schema function - def from_json(attrs) do - %Feed{} - |> cast( - decode_location_point(attrs), - Map.keys(Orcasite.Utils.atomize_keys(attrs)) - ) - |> apply_changes() + identities do + identity :unique_slug, [:slug] end - def decode_location_point(%{"location_point" => point} = attrs) when is_binary(point), - do: %{attrs | "location_point" => Geo.WKB.decode!(attrs["location_point"])} + actions do + defaults [:read, :create, :update, :destroy] + + read :get_by_slug do + get_by :slug + end + end - def decode_location_point(attrs), do: attrs + code_interface do + define_for Orcasite.Radio + + define :get_feed_by_slug, action: :get_by_slug, args: [:slug], get?: true + end end diff --git a/server/lib/orcasite/radio/registry.ex b/server/lib/orcasite/radio/registry.ex new file mode 100644 index 00000000..bdf92660 --- /dev/null +++ b/server/lib/orcasite/radio/registry.ex @@ -0,0 +1,8 @@ +defmodule Orcasite.Radio.Registry do + use Ash.Registry, extensions: [Ash.Registry.ResourceValidations] + + entries do + entry Orcasite.Radio.Feed + end + +end diff --git a/server/lib/orcasite/radio/candidate.ex b/server/lib/orcasite/radio_legacy/candidate.ex similarity index 100% rename from server/lib/orcasite/radio/candidate.ex rename to server/lib/orcasite/radio_legacy/candidate.ex diff --git a/server/lib/orcasite/radio/detection.ex b/server/lib/orcasite/radio_legacy/detection.ex similarity index 100% rename from server/lib/orcasite/radio/detection.ex rename to server/lib/orcasite/radio_legacy/detection.ex diff --git a/server/lib/orcasite/radio_legacy/feed_legacy.ex b/server/lib/orcasite/radio_legacy/feed_legacy.ex new file mode 100644 index 00000000..013954e2 --- /dev/null +++ b/server/lib/orcasite/radio_legacy/feed_legacy.ex @@ -0,0 +1,40 @@ +defmodule Orcasite.RadioLegacy.Feed do + use Ecto.Schema + import Ecto.Changeset + + alias __MODULE__ + + schema "feeds" do + field(:name, :string) + field(:slug, :string) + field(:node_name, :string) + field(:location_point, Geo.PostGIS.Geometry) + + timestamps() + end + + @doc false + def changeset(feed, attrs) do + feed + |> cast(attrs, [:name, :node_name, :slug, :location_point]) + |> validate_required([:name, :node_name, :slug]) + end + + def latlong_to_geo(lat, long) when is_float(lat) and is_float(long), + do: Geo.WKT.decode!("SRID=4326;POINT(#{lat} #{long})") + + # TODO: Find the actual json -> schema function + def from_json(attrs) do + %Feed{} + |> cast( + decode_location_point(attrs), + Map.keys(Orcasite.Utils.atomize_keys(attrs)) + ) + |> apply_changes() + end + + def decode_location_point(%{"location_point" => point} = attrs) when is_binary(point), + do: %{attrs | "location_point" => Geo.WKB.decode!(attrs["location_point"])} + + def decode_location_point(attrs), do: attrs +end diff --git a/server/lib/orcasite/radio/radio_legacy.ex b/server/lib/orcasite/radio_legacy/radio_legacy.ex similarity index 92% rename from server/lib/orcasite/radio/radio_legacy.ex rename to server/lib/orcasite/radio_legacy/radio_legacy.ex index b513fd73..d593cf60 100644 --- a/server/lib/orcasite/radio/radio_legacy.ex +++ b/server/lib/orcasite/radio_legacy/radio_legacy.ex @@ -7,31 +7,6 @@ defmodule Orcasite.RadioLegacy do alias Orcasite.Repo alias Orcasite.RadioLegacy.{Feed, Detection, Candidate} - def list_feeds do - Repo.all(Feed) - end - - def get_feed!(id), do: Repo.get!(Feed, id) - - def get_feed_by_slug(slug) do - Feed - |> where(slug: ^slug) - |> limit(1) - |> Repo.one() - end - - def create_feed(attrs \\ %{}) do - %Feed{} - |> Feed.changeset(attrs) - |> Repo.insert() - end - - def update_feed(%Feed{} = feed, attrs) do - feed - |> Feed.changeset(attrs) - |> Repo.update() - end - def verify_can_submit_detection( feed_id, source_ip, diff --git a/server/lib/orcasite/types/geometry.ex b/server/lib/orcasite/types/geometry.ex new file mode 100644 index 00000000..d3d1ae7d --- /dev/null +++ b/server/lib/orcasite/types/geometry.ex @@ -0,0 +1,43 @@ +defmodule Orcasite.Types.Geometry do + @moduledoc false + + use Ash.Type + + @impl true + def storage_type, do: :geometry + + @impl true + def cast_input(nil, _), do: {:ok, nil} + + def cast_input(value, _) do + Geo.PostGIS.Geometry.cast(value) + end + + @impl true + def cast_stored(nil, _), do: {:ok, nil} + + def cast_stored(value, _) do + Geo.PostGIS.Geometry.load(value) + end + + @impl true + def dump_to_native(nil, _), do: {:ok, nil} + + def dump_to_native(value, _) do + Geo.PostGIS.Geometry.dump(value) + end +end + +if Code.ensure_loaded?(Ecto.DevLogger) do + defimpl Ecto.DevLogger.PrintableParameter, for: Geo.Point do + def to_expression(point) do + point + |> to_string_literal() + |> Ecto.DevLogger.Utils.in_string_quotes() + end + + def to_string_literal(point) do + Geo.WKT.Encoder.encode!(point) + end + end +end diff --git a/server/lib/orcasite_web/graphql/resolvers/detection.ex b/server/lib/orcasite_web/graphql/resolvers/detection.ex index db59e27f..85d33474 100644 --- a/server/lib/orcasite_web/graphql/resolvers/detection.ex +++ b/server/lib/orcasite_web/graphql/resolvers/detection.ex @@ -36,7 +36,7 @@ defmodule OrcasiteWeb.Resolvers.Detection do {:ok, detection} -> # Send notification for new detection Task.Supervisor.async_nolink(Orcasite.TaskSupervisor, fn -> - %{slug: node} = RadioLegacy.get_feed!(feed_id) + %{slug: node} = Orcasite.Radio.get!(Orcasite.Radio.Feed, feed_id) Orcasite.Notifications.Notification.notify_new_detection(detection.id, node) end) diff --git a/server/lib/orcasite_web/graphql/resolvers/feed.ex b/server/lib/orcasite_web/graphql/resolvers/feed.ex index 060f9eba..f6115b58 100644 --- a/server/lib/orcasite_web/graphql/resolvers/feed.ex +++ b/server/lib/orcasite_web/graphql/resolvers/feed.ex @@ -1,14 +1,13 @@ defmodule OrcasiteWeb.Resolvers.Feed do - alias Orcasite.RadioLegacy def index(_, _) do - {:ok, RadioLegacy.list_feeds()} + {:ok, Orcasite.Radio.read!(Orcasite.Radio.Feed)} end def show(%{slug: slug}, _) do - {:ok, RadioLegacy.get_feed_by_slug(slug)} + {:ok, Orcasite.Radio.Feed.get_feed_by_slug!(slug)} end def show(%{id: id}, _) do - {:ok, RadioLegacy.get_feed!(id)} + {:ok, Orcasite.Radio.get!(Orcasite.Radio.Feed, id)} end end diff --git a/server/priv/repo/migrations/20230725205820_add_slug_index_to_feeds.exs b/server/priv/repo/migrations/20230725205820_add_slug_index_to_feeds.exs new file mode 100644 index 00000000..3c6572e3 --- /dev/null +++ b/server/priv/repo/migrations/20230725205820_add_slug_index_to_feeds.exs @@ -0,0 +1,25 @@ +defmodule Orcasite.Repo.Migrations.AddSlugIndexToFeeds do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + alter table(:feeds) do + modify :name, :text + modify :node_name, :text + modify :slug, :text + modify :inserted_at, :utc_datetime_usec, null: false, default: fragment("now()") + modify :updated_at, :utc_datetime_usec, null: false, default: fragment("now()") + end + + create unique_index(:feeds, [:slug], name: "feeds_unique_slug_index") + end + + def down do + drop_if_exists unique_index(:feeds, [:slug], name: "feeds_unique_slug_index") + end +end diff --git a/server/priv/resource_snapshots/repo/feeds/20230725205820.json b/server/priv/resource_snapshots/repo/feeds/20230725205820.json new file mode 100644 index 00000000..9a52d4b0 --- /dev/null +++ b/server/priv/resource_snapshots/repo/feeds/20230725205820.json @@ -0,0 +1,98 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "nil", + "generated?": true, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "name", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "node_name", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "slug", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "location_point", + "type": "geometry" + }, + { + "allow_nil?": false, + "default": "fragment(\"now()\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"now()\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "B7E810FC144D0F20081905C08E9F22784DCCA284F539A988E342E7F348159968", + "identities": [ + { + "base_filter": null, + "index_name": "feeds_unique_slug_index", + "keys": [ + "slug" + ], + "name": "unique_slug" + } + ], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.Orcasite.Repo", + "schema": null, + "table": "feeds" +} \ No newline at end of file diff --git a/server/test/orcasite/radio/radio_test.exs b/server/test/orcasite/radio/radio_test.exs index e5ecd323..2311de9a 100644 --- a/server/test/orcasite/radio/radio_test.exs +++ b/server/test/orcasite/radio/radio_test.exs @@ -4,130 +4,8 @@ defmodule Orcasite.RadioLegacyTest do alias Orcasite.RadioLegacy describe "feeds" do - alias Orcasite.RadioLegacy.Feed - - @valid_attrs %{slug: "some slug", name: "some name", node_name: "some node_name"} - @update_attrs %{slug: "some updated slug", name: "some updated name", node_name: "some updated node_name"} - @invalid_attrs %{slug: nil, name: nil, node_name: nil} - - def feed_fixture(attrs \\ %{}) do - {:ok, feed} = - attrs - |> Map.merge(@valid_attrs) - |> RadioLegacy.create_feed() - - feed - end - - test "list_feeds/0 returns all feeds" do - feed = feed_fixture() - assert RadioLegacy.list_feeds() == [feed] - end - - test "get_feed!/1 returns the feed with given id" do - feed = feed_fixture() - assert RadioLegacy.get_feed!(feed.id) == feed - end - - test "create_feed/1 with valid data creates a feed" do - assert {:ok, %Feed{} = feed} = RadioLegacy.create_feed(@valid_attrs) - assert feed.slug == "some slug" - assert feed.name == "some name" - assert feed.node_name == "some node_name" - end - - test "create_feed/1 with invalid data returns error changeset" do - assert {:error, %Ecto.Changeset{}} = RadioLegacy.create_feed(@invalid_attrs) - end - - test "update_feed/2 with valid data updates the feed" do - feed = feed_fixture() - assert {:ok, feed} = RadioLegacy.update_feed(feed, @update_attrs) - assert %Feed{} = feed - assert feed.slug == "some updated slug" - assert feed.name == "some updated name" - assert feed.node_name == "some updated node_name" - end - - test "update_feed/2 with invalid data returns error changeset" do - feed = feed_fixture() - assert {:error, %Ecto.Changeset{}} = RadioLegacy.update_feed(feed, @invalid_attrs) - assert feed == RadioLegacy.get_feed!(feed.id) - end - - test "delete_feed/1 deletes the feed" do - feed = feed_fixture() - assert {:ok, %Feed{}} = RadioLegacy.delete_feed(feed) - assert_raise Ecto.NoResultsError, fn -> RadioLegacy.get_feed!(feed.id) end - end - - test "change_feed/1 returns a feed changeset" do - feed = feed_fixture() - assert %Ecto.Changeset{} = RadioLegacy.change_feed(feed) - end - end - - describe "detections" do - alias Orcasite.RadioLegacy.Detection - - @valid_attrs %{source_ip: "some source_ip", playlist_timestamp: 42, time: "2010-04-17 14:00:00.000000Z"} - @update_attrs %{source_ip: "some updated source_ip", playlist_timestamp: 43, time: "2011-05-18 15:01:01.000000Z"} - @invalid_attrs %{source_ip: nil, playlist_timestamp: nil, time: nil} - - def detection_fixture(attrs \\ %{}) do - {:ok, detection} = - attrs - |> Map.merge(@valid_attrs) - |> RadioLegacy.create_detection() - - detection - end - - test "list_detections/0 returns all detections" do - detection = detection_fixture() - assert RadioLegacy.list_detections() == [detection] - end - - test "get_detection!/1 returns the detection with given id" do - detection = detection_fixture() - assert RadioLegacy.get_detection!(detection.id) == detection - end - - test "create_detection/1 with valid data creates a detection" do - assert {:ok, %Detection{} = detection} = RadioLegacy.create_detection(@valid_attrs) - assert detection.source_ip == "some source_ip" - assert detection.playlist_timestamp == 42 - assert detection.time == DateTime.from_naive!(~N[2010-04-17 14:00:00.000000Z], "Etc/UTC") - end - - test "create_detection/1 with invalid data returns error changeset" do - assert {:error, %Ecto.Changeset{}} = RadioLegacy.create_detection(@invalid_attrs) - end - - test "update_detection/2 with valid data updates the detection" do - detection = detection_fixture() - assert {:ok, detection} = RadioLegacy.update_detection(detection, @update_attrs) - assert %Detection{} = detection - assert detection.source_ip == "some updated source_ip" - assert detection.playlist_timestamp == 43 - assert detection.time == DateTime.from_naive!(~N[2011-05-18 15:01:01.000000Z], "Etc/UTC") - end - - test "update_detection/2 with invalid data returns error changeset" do - detection = detection_fixture() - assert {:error, %Ecto.Changeset{}} = RadioLegacy.update_detection(detection, @invalid_attrs) - assert detection == RadioLegacy.get_detection!(detection.id) - end - - test "delete_detection/1 deletes the detection" do - detection = detection_fixture() - assert {:ok, %Detection{}} = RadioLegacy.delete_detection(detection) - assert_raise Ecto.NoResultsError, fn -> RadioLegacy.get_detection!(detection.id) end - end - - test "change_detection/1 returns a detection changeset" do - detection = detection_fixture() - assert %Ecto.Changeset{} = RadioLegacy.change_detection(detection) + it "works" do + assert true end end end From e867706dd8e72ff82280323f1cf0f59adc805ec0 Mon Sep 17 00:00:00 2001 From: Skander Mzali Date: Tue, 25 Jul 2023 14:44:19 -0700 Subject: [PATCH 3/4] Add calculation for long, lat --- .../radio/calculations/longitude_latitude.ex | 15 ++++ server/lib/orcasite/radio/feed.ex | 71 ++++++++++++++++++- 2 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 server/lib/orcasite/radio/calculations/longitude_latitude.ex diff --git a/server/lib/orcasite/radio/calculations/longitude_latitude.ex b/server/lib/orcasite/radio/calculations/longitude_latitude.ex new file mode 100644 index 00000000..7f6abec2 --- /dev/null +++ b/server/lib/orcasite/radio/calculations/longitude_latitude.ex @@ -0,0 +1,15 @@ +defmodule Orcasite.Radio.Calculations.LongitudeLatitude do + use Ash.Calculation + + @impl true + def load(_query, _opts, _context) do + [:location_point] + end + + @impl true + def calculate(records, _opts, _arguments) do + Enum.map(records, fn %{location_point: %{coordinates: {lng, lat}}} -> + "#{lng},#{lat}" + end) + end +end diff --git a/server/lib/orcasite/radio/feed.ex b/server/lib/orcasite/radio/feed.ex index 7a4cbad3..8d02002f 100644 --- a/server/lib/orcasite/radio/feed.ex +++ b/server/lib/orcasite/radio/feed.ex @@ -2,7 +2,6 @@ defmodule Orcasite.Radio.Feed do use Ash.Resource, data_layer: AshPostgres.DataLayer - attributes do integer_primary_key :id @@ -25,11 +24,42 @@ defmodule Orcasite.Radio.Feed do end actions do - defaults [:read, :create, :update, :destroy] + defaults [:destroy] + + read :read do + primary? true + + prepare fn query, _context -> + query + |> Ash.Query.load(:longitude_latitude) + end + end read :get_by_slug do get_by :slug end + + create :create do + primary? true + reject [:location_point] + + argument :longitude_latitude, :string do + description "A comma-separated string of longitude and latitude" + end + + change &change_longitude_latitude/2 + end + + update :update do + primary? true + reject [:location_point] + + argument :longitude_latitude, :string do + description "A comma-separated string of longitude and latitude" + end + + change &change_longitude_latitude/2 + end end code_interface do @@ -37,4 +67,41 @@ defmodule Orcasite.Radio.Feed do define :get_feed_by_slug, action: :get_by_slug, args: [:slug], get?: true end + + calculations do + calculate :longitude_latitude, + :string, + {Orcasite.Radio.Calculations.LongitudeLatitude, + keys: [:location_point], select: [:location_point]} + end + + defp change_longitude_latitude(changeset, _context) do + with {:is_string, lng_lat} when is_binary(lng_lat) <- + {:is_string, Ash.Changeset.get_argument(changeset, :longitude_latitude)}, + {:two_els, [lng, lat]} <- + {:two_els, lng_lat |> String.split(",") |> Enum.map(&String.trim/1)}, + {:two_floats, [{longitude, _}, {latitude, _}]} <- + {:two_floats, [lng, lat] |> Enum.map(&Float.parse/1)} do + changeset + |> Ash.Changeset.change_attribute(:location_point, %Geo.Point{ + coordinates: {longitude, latitude}, + srid: 4326 + }) + else + {:is_string, _} -> + changeset + + {:two_els, _} -> + changeset + |> Ash.Changeset.add_error( + field: :longitude_latitude, + message: "must be a comma-separated string" + ) + + {:two_floats, _} -> + changeset + |> Ash.Changeset.add_error(field: :longitude_latitude, message: "must be two floats") + end + |> IO.inspect(label: "changing? (server/lib/orcasite/radio/feed.ex:#{__ENV__.line})") + end end From e1af7ef66ac5187405aa9a0a22cd07462cdd7642 Mon Sep 17 00:00:00 2001 From: Skander Mzali Date: Tue, 25 Jul 2023 14:58:48 -0700 Subject: [PATCH 4/4] Update admin display --- server/lib/orcasite/accounts/user.ex | 6 +++++- .../orcasite/notifications/resources/notification.ex | 4 ++++ .../lib/orcasite/notifications/resources/subscriber.ex | 10 ++++++++++ .../orcasite/notifications/resources/subscription.ex | 6 ++++++ server/lib/orcasite/radio/feed.ex | 8 +++++++- 5 files changed, 32 insertions(+), 2 deletions(-) diff --git a/server/lib/orcasite/accounts/user.ex b/server/lib/orcasite/accounts/user.ex index 49c58b7e..8cffa33a 100644 --- a/server/lib/orcasite/accounts/user.ex +++ b/server/lib/orcasite/accounts/user.ex @@ -1,7 +1,7 @@ defmodule Orcasite.Accounts.User do use Ash.Resource, data_layer: AshPostgres.DataLayer, - extensions: [AshAuthentication] + extensions: [AshAuthentication, AshAdmin.Resource] attributes do @@ -46,4 +46,8 @@ defmodule Orcasite.Accounts.User do actions do defaults [:read, :create, :update, :destroy] end + + admin do + table_columns [:id, :email, :first_name, :last_name, :admin, :inserted_at] + end end diff --git a/server/lib/orcasite/notifications/resources/notification.ex b/server/lib/orcasite/notifications/resources/notification.ex index a4547cb4..022cee32 100644 --- a/server/lib/orcasite/notifications/resources/notification.ex +++ b/server/lib/orcasite/notifications/resources/notification.ex @@ -102,6 +102,10 @@ defmodule Orcasite.Notifications.Notification do end admin do + table_columns [:id, :meta, :event_type, :inserted_at] + + format_fields meta: {Jason, :encode!, []} + form do field :event_type, type: :default end diff --git a/server/lib/orcasite/notifications/resources/subscriber.ex b/server/lib/orcasite/notifications/resources/subscriber.ex index b7474165..9f857215 100644 --- a/server/lib/orcasite/notifications/resources/subscriber.ex +++ b/server/lib/orcasite/notifications/resources/subscriber.ex @@ -92,4 +92,14 @@ defmodule Orcasite.Notifications.Subscriber do relationships do has_many :subscriptions, Subscription end + + admin do + table_columns [:id, :name, :meta, :inserted_at] + + format_fields meta: {Jason, :encode!, []} + + form do + field :event_type, type: :default + end + end end diff --git a/server/lib/orcasite/notifications/resources/subscription.ex b/server/lib/orcasite/notifications/resources/subscription.ex index 2ae7461a..85be6ac7 100644 --- a/server/lib/orcasite/notifications/resources/subscription.ex +++ b/server/lib/orcasite/notifications/resources/subscription.ex @@ -172,4 +172,10 @@ defmodule Orcasite.Notifications.Subscription do {:ok, token} = AshAuthentication.Strategy.MagicLink.request_token_for(strategy, subscription) token end + + admin do + table_columns [:id, :name, :meta, :active, :event_type, :subscriber_id, :inserted_at] + + format_fields meta: {Jason, :encode!, []} + end end diff --git a/server/lib/orcasite/radio/feed.ex b/server/lib/orcasite/radio/feed.ex index 8d02002f..9d8ae11f 100644 --- a/server/lib/orcasite/radio/feed.ex +++ b/server/lib/orcasite/radio/feed.ex @@ -1,5 +1,6 @@ defmodule Orcasite.Radio.Feed do use Ash.Resource, + extensions: [AshAdmin.Resource], data_layer: AshPostgres.DataLayer attributes do @@ -102,6 +103,11 @@ defmodule Orcasite.Radio.Feed do changeset |> Ash.Changeset.add_error(field: :longitude_latitude, message: "must be two floats") end - |> IO.inspect(label: "changing? (server/lib/orcasite/radio/feed.ex:#{__ENV__.line})") + end + + admin do + table_columns [:id, :name, :slug, :node_name, :location_point] + + format_fields location_point: {Jason, :encode!, []} end end