Skip to content

Commit

Permalink
City and Region models
Browse files Browse the repository at this point in the history
  • Loading branch information
anmarchenko committed Aug 17, 2024
1 parent 2bc47e6 commit c718c14
Show file tree
Hide file tree
Showing 7 changed files with 326 additions and 7 deletions.
51 changes: 51 additions & 0 deletions lib/hamster_travel/geo/city.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
defmodule HamsterTravel.Geo.City do
use Ecto.Schema
import Ecto.Changeset

schema "cities" do
field :name, :string
field :name_ru, :string
field :geonames_id, :string
field :lat, :float
field :lon, :float
field :population, :integer

belongs_to :country, HamsterTravel.Geo.Country,
foreign_key: :country_code,
references: :iso,
type: :string

# TODO: will this work????
belongs_to :region, HamsterTravel.Geo.Region,
foreign_key: :region_code,
references: :region_code,
type: :string

timestamps()
end

@doc false
def changeset(city, attrs) do
city
|> cast(attrs, [
:name,
:name_ru,
:region_code,
:geonames_id,
:country_code,
:lat,
:lon,
:population
])
|> validate_required([
:name,
:name_ru,
:region_code,
:geonames_id,
:country_code,
:lat,
:lon,
:population
])
end
end
70 changes: 69 additions & 1 deletion lib/hamster_travel/geo/geo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule HamsterTravel.Geo do
import Ecto.Query, warn: false
alias HamsterTravel.Repo

alias HamsterTravel.Geo.Country
alias HamsterTravel.Geo.{City, Country, Region}

@doc """
Returns the list of countries.
Expand Down Expand Up @@ -51,4 +51,72 @@ defmodule HamsterTravel.Geo do
def find_country_by_iso(iso) do
Repo.get_by(Country, iso: iso)
end

@doc """
Gets a single region.
Raises `Ecto.NoResultsError` if the Region does not exist.
## Examples
iex> get_region!(123)
%Region{}
iex> get_region!(456)
** (Ecto.NoResultsError)
"""
def get_region!(id), do: Repo.get!(Region, id)

@doc """
Creates a region.
## Examples
iex> create_region(%{field: value})
{:ok, %Region{}}
iex> create_region(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_region(attrs \\ %{}) do
%Region{}
|> Region.changeset(attrs)
|> Repo.insert()
end

@doc """
Gets a single city.
Raises `Ecto.NoResultsError` if the City does not exist.
## Examples
iex> get_city!(123)
%City{}
iex> get_city!(456)
** (Ecto.NoResultsError)
"""
def get_city!(id), do: Repo.get!(City, id)

@doc """
Creates a city.
## Examples
iex> create_city(%{field: value})
{:ok, %City{}}
iex> create_city(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_city(attrs \\ %{}) do
%City{}
|> City.changeset(attrs)
|> Repo.insert()
end
end
82 changes: 76 additions & 6 deletions lib/hamster_travel/geo/geonames.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,46 @@ defmodule HamsterTravel.Geo.Geonames do
@moduledoc """
Handles downloading, parsing, and importing geonames data.
"""
alias HamsterTravel.Geo
alias HamsterTravel.Geo.Country
alias HamsterTravel.Repo

require Logger

def import do
Logger.info("Importing geonames data...")
import_countries()

iso_codes = Geo.list_country_iso_codes()

iso_codes
|> Enum.each(&import_features/1)
end

def import_features(iso_code) do
{:ok, features} = download_features(iso_code)

features =
features
|> String.split("\n")
|> Enum.reject(&String.starts_with?(&1, "#"))
|> Enum.map(&String.split(String.trim(&1), "\t"))
# |> Enum.map(&parse_feature/1)
|> Enum.reject(fn feature_data ->
feature_data == nil
end)

{entries_count, _} =
Repo.insert_all(
Geo.Region,
features,
on_conflict: {:replace_all_except, [:id, :inserted_at, :name_ru]},
conflict_target: :geonames_id
)

Logger.info("Imported #{entries_count} features for #{iso_code}")
end

def import_countries do
{:ok, countries} = download_countries()

Expand All @@ -20,12 +55,15 @@ defmodule HamsterTravel.Geo.Geonames do
country_data == nil || country_data[:iso] == "CS" || country_data[:iso] == "AN"
end)

Repo.insert_all(
Country,
countries,
on_conflict: {:replace_all_except, [:id, :inserted_at, :name_ru]},
conflict_target: :geonames_id
)
{entries_count, _} =
Repo.insert_all(
Country,
countries,
on_conflict: {:replace_all_except, [:id, :inserted_at, :name_ru]},
conflict_target: :geonames_id
)

Logger.info("Imported #{entries_count} countries")
end

defp download_countries do
Expand Down Expand Up @@ -60,6 +98,38 @@ defmodule HamsterTravel.Geo.Geonames do
end
end

defp download_features(iso_code) do
case Req.get("https://download.geonames.org/export/dump/#{iso_code}.zip", options()) do
{:ok, %Req.Response{body: [{_, _}, {_, csv}]} = resp} ->
if resp.status < 400 do
{:ok, csv}
else
:telemetry.execute(
[:hamster_travel, :geonames, :download_features],
%{error: 1},
%{
reason: "status_#{resp.status}"
}
)

Logger.error("Failed to download features for #{iso_code}: #{inspect(resp)}")
{:error, "HTTP error: #{resp.status}"}
end

{:error, reason} = error_tuple ->
:telemetry.execute(
[:hamster_travel, :geonames, :download_features],
%{error: 1},
%{
reason: "network"
}
)

Logger.error("Failed to download features for #{iso_code}: #{inspect(reason)}")
error_tuple
end
end

defp parse_country([
iso,
iso3,
Expand Down
27 changes: 27 additions & 0 deletions lib/hamster_travel/geo/region.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
defmodule HamsterTravel.Geo.Region do
use Ecto.Schema
import Ecto.Changeset

schema "regions" do
field :name, :string
field :name_ru, :string
field :region_code, :string
field :geonames_id, :string
field :lat, :float
field :lon, :float

belongs_to :country, HamsterTravel.Geo.Country,
foreign_key: :country_code,
references: :iso,
type: :string

timestamps()
end

@doc false
def changeset(region, attrs) do
region
|> cast(attrs, [:name, :name_ru, :region_code, :geonames_id, :country_code, :lat, :lon])
|> validate_required([:name, :name_ru, :region_code, :geonames_id, :country_code, :lat, :lon])
end
end
20 changes: 20 additions & 0 deletions priv/repo/migrations/20240817144716_create_regions.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule HamsterTravel.Repo.Migrations.CreateRegions do
use Ecto.Migration

def change do
create table(:regions) do
add :name, :string, null: false
add :name_ru, :string
add :region_code, :string, null: false
add :geonames_id, :string, null: false
add :country_code, references("countries", column: :iso, type: :string), null: false
add :lat, :float
add :lon, :float

timestamps()
end

create unique_index(:regions, [:geonames_id])
create unique_index(:regions, [:country_code, :region_code])
end
end
42 changes: 42 additions & 0 deletions priv/repo/migrations/20240817150722_create_cities.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
defmodule HamsterTravel.Repo.Migrations.CreateCities do
use Ecto.Migration

def change do
create table(:cities) do
add :name, :string, null: false
add :name_ru, :string

add :region_code,
references("regions",
column: :region_code,
type: :string,
with: [country_code: :country_code]
),
null: false

add :geonames_id, :string, null: false
add :country_code, references("countries", column: :iso, type: :string), null: false
add :lat, :float
add :lon, :float
add :population, :integer, null: false

timestamps()
end

execute "CREATE EXTENSION IF NOT EXISTS pg_trgm;"

execute """
CREATE INDEX cities_name_gin_trgm_idx
ON cities
USING gin (name gin_trgm_ops);
"""

execute """
CREATE INDEX cities_name_ru_gin_trgm_idx
ON cities
USING gin (name_ru gin_trgm_ops);
"""

create unique_index(:cities, [:geonames_id])
end
end
41 changes: 41 additions & 0 deletions test/support/fixtures/geo_fixtures.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,45 @@ defmodule HamsterTravel.GeoFixtures do

Geonames.import_countries()
end

@doc """
Generate a region.
"""
def region_fixture(attrs \\ %{}) do
{:ok, region} =
attrs
|> Enum.into(%{
country_code: "some country_code",
geonames_id: "some geonames_id",
lat: 120.5,
lon: 120.5,
name: "some name",
name_ru: "some name_ru",
region_code: "some region_code"
})
|> HamsterTravel.Geo.create_region()

region
end

@doc """
Generate a city.
"""
def city_fixture(attrs \\ %{}) do
{:ok, city} =
attrs
|> Enum.into(%{
country_code: "some country_code",
geonames_id: "some geonames_id",
lat: 120.5,
lon: 120.5,
name: "some name",
name_ru: "some name_ru",
population: 42,
region_code: "some region_code"
})
|> HamsterTravel.Geo.create_city()

city
end
end

0 comments on commit c718c14

Please sign in to comment.