Skip to content

Commit

Permalink
Ash Radio API, Feed with admin panel (#150)
Browse files Browse the repository at this point in the history
* Rename Radio context to RadioLegacy

* Create Radio Api with Ash, move old Radio context to RadioLegacy. Use new queries for gql Feed resolvers

* Add calculation for long, lat

* Update admin display
  • Loading branch information
skanderm authored Jul 26, 2023
1 parent 6cc5918 commit 156e596
Show file tree
Hide file tree
Showing 21 changed files with 389 additions and 199 deletions.
3 changes: 2 additions & 1 deletion server/config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
File renamed without changes.
6 changes: 5 additions & 1 deletion server/lib/orcasite/accounts/user.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Orcasite.Accounts.User do
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication]
extensions: [AshAuthentication, AshAdmin.Resource]


attributes do
Expand Down Expand Up @@ -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
4 changes: 4 additions & 0 deletions server/lib/orcasite/notifications/resources/notification.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions server/lib/orcasite/notifications/resources/subscriber.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions server/lib/orcasite/notifications/resources/subscription.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 11 additions & 0 deletions server/lib/orcasite/radio.ex
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions server/lib/orcasite/radio/calculations/longitude_latitude.ex
Original file line number Diff line number Diff line change
@@ -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
127 changes: 100 additions & 27 deletions server/lib/orcasite/radio/feed.ex
Original file line number Diff line number Diff line change
@@ -1,40 +1,113 @@
defmodule Orcasite.Radio.Feed do
use Ecto.Schema
import Ecto.Changeset
use Ash.Resource,
extensions: [AshAdmin.Resource],
data_layer: AshPostgres.DataLayer

alias __MODULE__
attributes do
integer_primary_key :id

schema "feeds" do
field(:name, :string)
field(:slug, :string)
field(:node_name, :string)
field(:location_point, Geo.PostGIS.Geometry)
attribute :name, :string
attribute :node_name, :string
attribute :slug, :string
attribute :location_point, :geometry

timestamps()
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})")
identities do
identity :unique_slug, [:slug]
end

actions do
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

# 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()
change &change_longitude_latitude/2
end
end

def decode_location_point(%{"location_point" => point} = attrs) when is_binary(point),
do: %{attrs | "location_point" => Geo.WKB.decode!(attrs["location_point"])}
code_interface do
define_for Orcasite.Radio

def decode_location_point(attrs), do: attrs
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
end

admin do
table_columns [:id, :name, :slug, :node_name, :location_point]

format_fields location_point: {Jason, :encode!, []}
end
end
8 changes: 8 additions & 0 deletions server/lib/orcasite/radio/registry.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule Orcasite.Radio.Registry do
use Ash.Registry, extensions: [Ash.Registry.ResourceValidations]

entries do
entry Orcasite.Radio.Feed
end

end
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
40 changes: 40 additions & 0 deletions server/lib/orcasite/radio_legacy/feed_legacy.ex
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -1,36 +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}

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
alias Orcasite.RadioLegacy.{Feed, Detection, Candidate}

def verify_can_submit_detection(
feed_id,
Expand Down
43 changes: 43 additions & 0 deletions server/lib/orcasite/types/geometry.ex
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 156e596

Please sign in to comment.