Refactoring of Adresses context

This commit is contained in:
miffy 2019-09-08 03:05:30 +02:00
parent 2a9605c66a
commit 3a4a006c44
3 changed files with 119 additions and 199 deletions

View file

@ -1,12 +1,31 @@
defmodule Mobilizon.Addresses.Address do
@moduledoc "An address for an event or a group"
@moduledoc """
Represents an address for an event or a group.
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Addresses.Address
alias Mobilizon.Events.Event
# alias Mobilizon.Actors.Actor
@attrs [
@type t :: %__MODULE__{
country: String.t(),
locality: String.t(),
region: String.t(),
description: String.t(),
floor: String.t(),
geom: Geo.PostGIS.Geometry.t(),
postal_code: String.t(),
street: String.t(),
url: String.t(),
origin_id: String.t(),
events: [Event.t()]
}
@required_attrs [:url]
@optional_attrs [
:description,
:floor,
:geom,
@ -15,12 +34,9 @@ defmodule Mobilizon.Addresses.Address do
:region,
:postal_code,
:street,
:url,
:origin_id
]
@required [
:url
]
@attrs @required_attrs ++ @optional_attrs
schema "addresses" do
field(:country, :string)
@ -33,22 +49,29 @@ defmodule Mobilizon.Addresses.Address do
field(:street, :string)
field(:url, :string)
field(:origin_id, :string)
has_many(:event, Event, foreign_key: :physical_address_id)
has_many(:events, Event, foreign_key: :physical_address_id)
timestamps()
end
@doc false
@spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
def changeset(%Address{} = address, attrs) do
address
|> cast(attrs, @attrs)
|> set_url()
|> validate_required(@required)
|> validate_required(@required_attrs)
end
@spec set_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp set_url(%Ecto.Changeset{changes: changes} = changeset) do
url =
Map.get(changes, :url, MobilizonWeb.Endpoint.url() <> "/address/#{Ecto.UUID.generate()}")
Map.get(
changes,
:url,
"#{MobilizonWeb.Endpoint.url()}/address/#{Ecto.UUID.generate()}"
)
put_change(changeset, :url, url)
end

View file

@ -3,83 +3,50 @@ defmodule Mobilizon.Addresses do
The Addresses context.
"""
import Ecto.Query, warn: false
import Ecto.Query
alias Mobilizon.Addresses.Address
alias Mobilizon.Storage.Repo
require Logger
@geom_types [:point]
@doc false
@spec data :: Dataloader.Ecto.t()
def data, do: Dataloader.Ecto.new(Repo, query: &query/2)
@doc false
def data() do
Dataloader.Ecto.new(Repo, query: &query/2)
end
@doc false
def query(queryable, _params) do
queryable
end
@spec query(Ecto.Query.t(), map) :: Ecto.Query.t()
def query(queryable, _params), do: queryable
@doc """
Returns the list of addresses.
## Examples
iex> list_addresses()
[%Address{}, ...]
"""
def list_addresses do
Repo.all(Address)
end
@spec list_addresses :: [Address.t()]
def list_addresses, do: Repo.all(Address)
@doc """
Gets a single address.
Raises `Ecto.NoResultsError` if the Address does not exist.
## Examples
iex> get_address!(123)
%Address{}
iex> get_address!(456)
** (Ecto.NoResultsError)
"""
def get_address!(id), do: Repo.get!(Address, id)
@spec get_address(integer | String.t()) :: Address.t() | nil
def get_address(id), do: Repo.get(Address, id)
@doc """
Gets a single address by it's url
## Examples
iex> get_address_by_url("https://mobilizon.social/addresses/4572")
%Address{}
iex> get_address_by_url("https://mobilizon.social/addresses/099")
nil
Gets a single address.
Raises `Ecto.NoResultsError` if the address does not exist.
"""
def get_address_by_url(url) do
Repo.get_by(Address, url: url)
end
@spec get_address!(integer | String.t()) :: Address.t()
def get_address!(id), do: Repo.get!(Address, id)
@doc """
Creates a address.
## Examples
iex> create_address(%{field: value})
{:ok, %Address{}}
iex> create_address(%{field: bad_value})
{:error, %Ecto.Changeset{}}
Gets a single address by its url.
"""
@spec get_address_by_url(String.t()) :: Address.t() | nil
def get_address_by_url(url), do: Repo.get_by(Address, url: url)
@doc """
Creates an address.
"""
@spec create_address(map) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()}
def create_address(attrs \\ %{}) do
%Address{}
|> Address.changeset(attrs)
@ -90,17 +57,9 @@ defmodule Mobilizon.Addresses do
end
@doc """
Updates a address.
## Examples
iex> update_address(address, %{field: new_value})
{:ok, %Address{}}
iex> update_address(address, %{field: bad_value})
{:error, %Ecto.Changeset{}}
Updates an address.
"""
@spec update_address(Address.t(), map) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()}
def update_address(%Address{} = address, attrs) do
address
|> Address.changeset(attrs)
@ -108,131 +67,87 @@ defmodule Mobilizon.Addresses do
end
@doc """
Deletes a Address.
## Examples
iex> delete_address(address)
{:ok, %Address{}}
iex> delete_address(address)
{:error, %Ecto.Changeset{}}
Deletes an address.
"""
def delete_address(%Address{} = address) do
Repo.delete(address)
end
@spec delete_address(Address.t()) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()}
def delete_address(%Address{} = address), do: Repo.delete(address)
@doc """
Returns an `%Ecto.Changeset{}` for tracking address changes.
## Examples
iex> change_address(address)
%Ecto.Changeset{source: %Address{}}
Searches addresses.
We only look at the description for now, and eventually order by object distance.
"""
def change_address(%Address{} = address) do
Address.changeset(address, %{})
end
@spec search_addresses(String.t(), keyword) :: [Address.t()]
def search_addresses(search, options \\ []) do
query =
search
|> search_addresses_query(Keyword.get(options, :limit, 5))
|> order_by_coords(Keyword.get(options, :coords))
|> filter_by_contry(Keyword.get(options, :country))
@doc """
Processes raw geo data informations and return a `Geo` geometry which can be one of `Geo.Point`.
"""
# TODO: Unused, remove me
def process_geom(%{"type" => type_input, "data" => data}) do
type =
if !is_atom(type_input) && type_input != nil do
try do
String.to_existing_atom(type_input)
rescue
e in ArgumentError ->
Logger.error("#{type_input} is not an existing atom : #{inspect(e)}")
:invalid_type
end
else
type_input
end
if Enum.member?(@geom_types, type) do
case type do
:point ->
process_point(data["latitude"], data["longitude"])
end
else
{:error, :invalid_type}
case Keyword.get(options, :single, false) do
true -> Repo.one(query)
false -> Repo.all(query)
end
end
@doc false
def process_geom(nil) do
{:error, nil}
end
@spec process_point(number(), number()) :: tuple()
defp process_point(latitude, longitude) when is_number(latitude) and is_number(longitude) do
{:ok, %Geo.Point{coordinates: {latitude, longitude}, srid: 4326}}
end
defp process_point(_, _) do
{:error, "Latitude and longitude must be numbers"}
end
@doc """
Search addresses in our database
Reverse geocode from coordinates.
We only look at the description for now, and eventually order by object distance
We only take addresses 50km around and sort them by distance.
"""
@spec search_addresses(String.t(), list()) :: list(Address.t())
def search_addresses(search, options \\ []) do
limit = Keyword.get(options, :limit, 5)
query = from(a in Address, where: ilike(a.description, ^"%#{search}%"), limit: ^limit)
query =
if coords = Keyword.get(options, :coords, false),
do:
from(a in query,
order_by: [fragment("? <-> ?", a.geom, ^"POINT(#{coords.lon} #{coords.lat})'")]
),
else: query
query =
if country = Keyword.get(options, :country, nil),
do: from(a in query, where: ilike(a.country, ^"%#{country}%")),
else: query
if Keyword.get(options, :single, false) == true, do: Repo.one(query), else: Repo.all(query)
end
@doc """
Reverse geocode from coordinates in our database
We only take addresses 50km around and sort them by distance
"""
@spec reverse_geocode(number(), number(), list()) :: list(Address.t())
@spec reverse_geocode(number, number, keyword) :: [Address.t()]
def reverse_geocode(lon, lat, options) do
limit = Keyword.get(options, :limit, 5)
radius = Keyword.get(options, :radius, 50_000)
country = Keyword.get(options, :country, nil)
country = Keyword.get(options, :country)
srid = Keyword.get(options, :srid, 4326)
import Geo.PostGIS
with {:ok, point} <- Geo.WKT.decode("SRID=#{srid};POINT(#{lon} #{lat})") do
query =
from(a in Address,
order_by: [fragment("? <-> ?", a.geom, ^point)],
limit: ^limit,
where: st_dwithin_in_meters(^point, a.geom, ^radius)
)
query =
if country,
do: from(a in query, where: ilike(a.country, ^"%#{country}%")),
else: query
Repo.all(query)
point
|> addresses_around_query(radius, limit)
|> filter_by_contry(country)
|> Repo.all()
end
end
@spec search_addresses_query(String.t(), integer) :: Ecto.Query.t()
defp search_addresses_query(search, limit) do
from(
a in Address,
where: ilike(a.description, ^"%#{search}%"),
limit: ^limit
)
end
@spec order_by_coords(Ecto.Query.t(), map | nil) :: Ecto.Query.t()
defp order_by_coords(query, nil), do: query
defp order_by_coords(query, coords) do
from(
a in query,
order_by: [fragment("? <-> ?", a.geom, ^"POINT(#{coords.lon} #{coords.lat})'")]
)
end
@spec filter_by_contry(Ecto.Query.t(), String.t() | nil) :: Ecto.Query.t()
defp filter_by_contry(query, nil), do: query
defp filter_by_contry(query, country) do
from(
a in query,
where: ilike(a.country, ^"%#{country}%")
)
end
@spec addresses_around_query(Geo.geometry(), integer, integer) :: Ecto.Query.t()
defp addresses_around_query(point, radius, limit) do
import Geo.PostGIS
from(a in Address,
where: st_dwithin_in_meters(^point, a.geom, ^radius),
order_by: [fragment("? <-> ?", a.geom, ^point)],
limit: ^limit
)
end
end

View file

@ -76,23 +76,5 @@ defmodule Mobilizon.AddressesTest do
assert {:ok, %Address{}} = Addresses.delete_address(address)
assert_raise Ecto.NoResultsError, fn -> Addresses.get_address!(address.id) end
end
test "change_address/1 returns a address changeset" do
address = insert(:address)
assert %Ecto.Changeset{} = Addresses.change_address(address)
end
test "process_geom/2 with valid data returns a Point element" do
attrs = %{"type" => "point", "data" => %{"latitude" => 10, "longitude" => -10}}
assert {:ok, %Geo.Point{}} = Addresses.process_geom(attrs)
end
test "process_geom/2 with invalid data returns nil" do
attrs = %{"type" => :point, "data" => %{"latitude" => nil, "longitude" => nil}}
assert {:error, "Latitude and longitude must be numbers"} = Addresses.process_geom(attrs)
attrs = %{"type" => :not_valid, "data" => %{"latitude" => nil, "longitude" => nil}}
assert {:error, :invalid_type} == Addresses.process_geom(attrs)
end
end
end