Compare commits

...

15 Commits

Author SHA1 Message Date
alvaro 74a086c855 update champion card
ci / docker (push) Successful in 4m17s
2024-06-09 02:48:45 +02:00
alvaro 192748bb1d create loader component and load champs asynchronously 2024-06-09 02:48:29 +02:00
alvaro d9dee3af7b refactor champions, position filters, champion card...
ci / docker (push) Successful in 3m41s
2024-06-08 23:16:53 +02:00
alvaro 2b51aace6e fix ChampionPickedSummonerSpell.Repo insert
ci / docker (push) Successful in 5m20s
2024-06-07 02:04:28 +02:00
alvaro 3ef7861f22 insert_or_update for champion picked summoner spell fact
ci / docker (push) Successful in 5m7s
2024-06-07 01:57:34 +02:00
alvaro 968c2634b7 add metadata to summoner spell 2024-06-07 01:42:45 +02:00
alvaro 6dd8eea3d3 Fix warnings, remove analyzers and create fact for champion played game
ci / docker (push) Successful in 7m38s
2024-06-06 20:20:45 +02:00
Álvaro Girona Arias fe3d040978 wip champion picked summoner spell
ci / docker (push) Successful in 3m45s
2024-06-06 19:03:30 +02:00
alvaro 9b955641d0 remote inspects
ci / docker (push) Successful in 3m43s
2024-05-31 02:40:46 +02:00
alvaro 77b292e47e filtering by roles
ci / docker (push) Waiting to run
2024-05-31 02:40:03 +02:00
alvaro 22b79f5376 Rendering champions with win rates
ci / docker (push) Waiting to run
2024-05-31 00:45:22 +02:00
Álvaro Girona Arias 520c234a94 created schema, repo... for analytic tables
ci / docker (push) Waiting to run
2024-05-30 17:48:35 +02:00
Álvaro Girona Arias dacb9ad8fc add docker-compose 2024-05-30 17:47:33 +02:00
alvaro 6053cfcdac wip analytics tables
ci / docker (push) Waiting to run
2024-05-29 15:34:27 +02:00
alvaro 5ce7ce0542 classify matches with stream
ci / docker (push) Waiting to run
2024-05-28 19:29:45 +02:00
79 changed files with 1596 additions and 148 deletions
@@ -1,3 +0,0 @@
defmodule LolAnalytics.Analyzer do
@callback analyze(:url, path :: String.t()) :: :ok
end
@@ -1,50 +0,0 @@
defmodule LolAnalytics.Analyzer.ChampionAnalyzer do
alias Hex.HTTP
@behaviour LolAnalytics.Analyzer
def analyze_all_matches do
Storage.MatchStorage.S3MatchStorage.list_files("ranked")
|> Enum.map(& &1.key)
|> Enum.each(fn path ->
LolAnalytics.Analyzer.ChampionAnalyzer.analyze(:url, "http://localhost:9000/ranked/#{path}")
end)
end
@doc """
iex> LolAnalytics.Analyzer.ChampionAnalyzer.analyze(:url, "http://localhost:9000/ranked/14.9.580.2108/EUW1_6923309745.json")
"""
@impl true
@spec analyze(atom(), String.t()) :: :ok
def analyze(:url, path) do
data = HTTPoison.get!(path)
analyze(:data, data.body)
:ok
end
@impl true
@spec analyze(atom(), any()) :: list(LoLAPI.Model.Participant.t())
def analyze(:data, data) do
decoded_match = Poison.decode!(data, as: %LoLAPI.Model.MatchResponse{})
participants = decoded_match.info.participants
version = extract_game_version(decoded_match)
participants
|> Enum.each(fn participant = %LoLAPI.Model.Participant{} ->
if participant.teamPosition != "" do
LolAnalytics.ChampionWinRate.ChampionWinRateRepo.add_champion_win_rate(
participant.championId,
version,
participant.teamPosition,
participant.win
)
end
end)
end
defp extract_game_version(game_data) do
game_data.info.gameVersion
|> String.split(".")
|> Enum.take(2)
|> Enum.join(".")
end
end
@@ -46,7 +46,7 @@ defmodule LolAnalytics.ChampionWinRate.ChampionWinRateRepo do
Repo.all(ChampionWinRateSchema)
end
def get_champion_win_rate(champion_id, patch) do
def get_champion_win_rate(champion_id, _patch) do
champion_query =
from cwr in LolAnalytics.ChampionWinRate.ChampionWinRateSchema,
where: cwr.champion_id == ^champion_id
@@ -0,0 +1,41 @@
defmodule LolAnalytics.Dimensions.Champion.ChampionMetadata do
alias LolAnalytics.Dimensions.Champion.ChampionRepo
@champions_data_url "https://ddragon.leagueoflegends.com/cdn/14.11.1/data/en_US/champion.json"
def update_metadata() do
{:ok, %{"data" => data}} = get_champions()
data
|> Enum.each(&save_metadata/1)
end
defp get_champions() do
with {:ok, resp} <- HTTPoison.get(@champions_data_url),
data <- Poison.decode(resp.body) do
data
else
{:error, reason} -> {:error, reason}
end
end
defp save_metadata({_champion, info}) do
%{
"image" => %{
"full" => full_image
},
"name" => name,
"key" => key_string
} = info
attrs = %{
image: full_image,
name: name
}
{champion_id, _} = Integer.parse(key_string)
ChampionRepo.update(champion_id, attrs)
info
end
end
@@ -0,0 +1,29 @@
defmodule LolAnalytics.Dimensions.Champion.ChampionRepo do
alias LoLAnalytics.Repo
alias LolAnalytics.Dimensions.Champion.ChampionSchema
@spec get_or_create(String.t()) :: struct()
def get_or_create(champion_id) do
champion = Repo.get_by(ChampionSchema, champion_id: champion_id)
case champion do
nil ->
changeset = ChampionSchema.changeset(%ChampionSchema{}, %{champion_id: champion_id})
Repo.insert(changeset)
champion ->
champion
end
end
def update(champion_id, attrs) do
get_or_create(champion_id)
|> ChampionSchema.changeset(attrs)
|> Repo.update()
end
@spec list_champions() :: any()
def list_champions() do
Repo.all(ChampionSchema)
end
end
@@ -0,0 +1,17 @@
defmodule LolAnalytics.Dimensions.Champion.ChampionSchema do
use Ecto.Schema
import Ecto.Changeset
schema "dim_champion" do
field :champion_id, :integer
field :name, :string
field :image, :string
timestamps()
end
def changeset(champion = %__MODULE__{}, attrs \\ %{}) do
champion
|> cast(attrs, [:champion_id, :name, :image])
|> validate_required([:champion_id])
end
end
@@ -0,0 +1,21 @@
defmodule LolAnalytics.Dimensions.Item.ItemRepo do
alias LolAnalytics.Dimensions.Item.ItemSchema
alias LoLAnalytics.Repo
def get_or_create(item_id) do
item = Repo.get(ItemSchema, item_id: item_id)
case item do
nil ->
item_changeset = ItemSchema.changeset(%ItemSchema{}, %{item_id: item_id})
Repo.insert(item_changeset)
item ->
item
end
end
def list_items() do
Repo.all(ItemSchema)
end
end
@@ -0,0 +1,15 @@
defmodule LolAnalytics.Dimensions.Item.ItemSchema do
use Ecto.Schema
import Ecto.Changeset
schema "dim_item" do
field :item_id, :integer
timestamps()
end
def changeset(item = %__MODULE__{}, attrs \\ %{}) do
item
|> cast(attrs, [:item_id])
|> validate_required([:item_id])
end
end
@@ -0,0 +1,30 @@
defmodule LolAnalytics.Dimensions.Match.MatchRepo do
alias LolAnalytics.Dimensions.Match.MatchSchema
alias LoLAnalytics.Repo
import Ecto.Query
@spec get_or_create(String.t()) :: %MatchSchema{}
def get_or_create(match_id) do
query = from m in MatchSchema, where: m.match_id == ^match_id
match = Repo.one(query)
case match do
nil ->
match_changeset =
MatchSchema.changeset(
%MatchSchema{},
%{match_id: match_id}
)
Repo.insert(match_changeset)
match ->
match
end
end
def list_matches() do
Repo.all(MatchSchema)
end
end
@@ -0,0 +1,15 @@
defmodule LolAnalytics.Dimensions.Match.MatchSchema do
use Ecto.Schema
import Ecto.Changeset
schema "dim_match" do
field :match_id, :string
timestamps()
end
def changeset(match = %__MODULE__{}, attrs \\ %{}) do
match
|> cast(attrs, [:match_id])
|> validate_required([:match_id])
end
end
@@ -0,0 +1,26 @@
defmodule LolAnalytics.Dimensions.Patch.PatchRepo do
alias LolAnalytics.Dimensions.Patch.PatchSchema
alias LoLAnalytics.Repo
def get_or_create(patch_number) do
patch = Repo.get(PatchSchema, patch_number: patch_number)
case patch do
nil ->
patch_changeset =
PatchSchema.changeset(
%PatchSchema{},
%{patch_number: patch_number}
)
Repo.insert(patch_changeset)
patch ->
patch
end
end
def list_patches() do
Repo.all(PatchSchema)
end
end
@@ -0,0 +1,15 @@
defmodule LolAnalytics.Dimensions.Patch.PatchSchema do
use Ecto.Schema
import Ecto.Changeset
schema "dim_patch" do
field :patch_number, :string
timestamps()
end
def changeset(patch = %__MODULE__{}, attrs \\ %{}) do
patch
|> cast(attrs, [:patch_number])
|> validate_required([:patch_number])
end
end
@@ -0,0 +1,28 @@
defmodule LolAnalytics.Dimensions.Player.PlayerRepo do
import Ecto.Query
alias LolAnalytics.Dimensions.Player.PlayerSchema
alias LoLAnalytics.Repo
def get_or_create(puuid) do
query = from p in PlayerSchema, where: p.puuid == ^puuid
player = Repo.one(query)
case player do
nil ->
player_changeset =
PlayerSchema.changeset(
%PlayerSchema{},
%{puuid: puuid}
)
Repo.insert(player_changeset)
player ->
player
end
end
def list_players() do
Repo.all(PlayerSchema)
end
end
@@ -0,0 +1,15 @@
defmodule LolAnalytics.Dimensions.Player.PlayerSchema do
use Ecto.Schema
import Ecto.Changeset
schema "dim_player" do
field :puuid, :string
timestamps()
end
def changeset(player = %__MODULE__{}, attrs \\ %{}) do
player
|> cast(attrs, [:puuid])
|> validate_required([:puuid])
end
end
@@ -0,0 +1,22 @@
defmodule LolAnalytics.Dimensions.SummonerSpell.SummonerSpellMetadata do
alias LolAnalytics.Dimensions.SummonerSpell.SummonerSpellRepo
@spells_url "https://ddragon.leagueoflegends.com/cdn/14.11.1/data/en_US/summoner.json"
def update_metadata() do
get_spells()
|> Enum.each(&save_metadata/1)
end
defp get_spells() do
case HTTPoison.get(@spells_url) do
{:ok, resp} ->
Poison.decode!(resp.body)["data"]
end
end
defp save_metadata({name, metadata}) do
%{"key" => spell_id} = metadata
SummonerSpellRepo.update(spell_id, %{metadata: metadata})
end
end
@@ -0,0 +1,37 @@
defmodule LolAnalytics.Dimensions.SummonerSpell.SummonerSpellRepo do
import Ecto.Query
alias LolAnalytics.Dimensions.SummonerSpell.SummonerSpellSchema
alias LoLAnalytics.Repo
@spec get_or_create(String.t()) :: any()
def get_or_create(spell_id) do
query = from s in SummonerSpellSchema, where: s.spell_id == ^spell_id
spell = Repo.one(query)
case spell do
nil ->
spell_changeset =
SummonerSpellSchema.changeset(
%SummonerSpellSchema{},
%{spell_id: spell_id}
)
Repo.insert(spell_changeset)
spell ->
spell
end
end
@spec update(spell_id :: String.t(), attrs :: map()) :: any()
def update(spell_id, attrs) do
get_or_create(spell_id)
|> SummonerSpellSchema.changeset(attrs)
|> Repo.update()
end
def list_spells() do
Repo.all(SummonerSpellSchema)
end
end
@@ -0,0 +1,16 @@
defmodule LolAnalytics.Dimensions.SummonerSpell.SummonerSpellSchema do
use Ecto.Schema
import Ecto.Changeset
schema "dim_summoner_spell" do
field :spell_id, :integer
field :metadata, :map
timestamps()
end
def changeset(summoner_spell = %__MODULE__{}, attrs) do
summoner_spell
|> cast(attrs, [:spell_id, :metadata])
|> validate_required([:spell_id])
end
end
@@ -0,0 +1,52 @@
defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.FactProcessor do
alias LolAnalytics.Facts.ChampionPickedSummonerSpell
def process_game_at_url(url) do
data = HTTPoison.get!(url)
process_game_data(data.body)
end
defp process_game_data(data) do
decoded_match = Poison.decode!(data, as: %LoLAPI.Model.MatchResponse{})
participants = decoded_match.info.participants
version = extract_game_version(decoded_match)
participants
|> Enum.each(fn participant = %LoLAPI.Model.Participant{} ->
if participant.teamPosition != "" do
attrs_spell_1 = %{
champion_id: participant.championId,
match_id: decoded_match.metadata.matchId,
is_win: participant.win,
summoner_spell_id: participant.summoner1Id,
game_length_seconds: decoded_match.info.gameDuration,
queue_id: decoded_match.info.queueId,
puuid: participant.puuid,
team_position: participant.teamPosition,
patch_number: version
}
attrs_spell_2 = %{
champion_id: participant.championId,
match_id: decoded_match.metadata.matchId,
is_win: participant.win,
summoner_spell_id: participant.summoner2Id,
game_length_seconds: decoded_match.info.gameDuration,
queue_id: decoded_match.info.queueId,
puuid: participant.puuid,
team_position: participant.teamPosition,
patch_number: version
}
ChampionPickedSummonerSpell.Repo.insert(attrs_spell_1)
ChampionPickedSummonerSpell.Repo.insert(attrs_spell_2)
end
end)
end
defp extract_game_version(game_data) do
game_data.info.gameVersion
|> String.split(".")
|> Enum.take(2)
|> Enum.join(".")
end
end
@@ -0,0 +1,89 @@
defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo do
import Ecto.Query
alias LolAnalytics.Dimensions.SummonerSpell.SummonerSpellSchema
alias LolAnalytics.Dimensions.SummonerSpell.SummonerSpellRepo
alias LolAnalytics.Dimensions.Champion.ChampionSchema
alias LolAnalytics.Facts.ChampionPickedSummonerSpell.Schema
alias LolAnalytics.Dimensions.Player.PlayerRepo
alias LolAnalytics.Dimensions.Champion.ChampionRepo
alias LolAnalytics.Dimensions.Match.MatchRepo
alias LoLAnalytics.Repo
@type insert_attrs :: %{
match_id: String.t(),
champion_id: String.t(),
puuid: String.t(),
summoner_spell_id: :integer
}
@spec insert(insert_attrs()) :: any()
def insert(attrs) do
_match = MatchRepo.get_or_create(attrs.match_id)
_champion = ChampionRepo.get_or_create(attrs.champion_id)
_player = PlayerRepo.get_or_create(attrs.puuid)
_spell = SummonerSpellRepo.get_or_create(attrs.summoner_spell_id)
prev =
from(f in Schema,
where:
f.match_id == ^attrs.match_id and
f.champion_id == ^attrs.champion_id and
f.summoner_spell_id ==
^attrs.summoner_spell_id
)
|> Repo.one()
changeset = Schema.changeset(prev || %Schema{}, attrs)
IO.inspect(attrs)
Repo.insert_or_update(changeset)
|> IO.inspect()
end
def get_champion_picked_summoners(championId, team_position \\ "MIDDLE") do
query =
from f in Schema,
where: f.champion_id == ^championId and f.team_position == ^team_position,
join: c in ChampionSchema,
on: c.champion_id == f.champion_id,
join: s in SummonerSpellSchema,
on: s.spell_id == f.summoner_spell_id,
select: %{
wins: fragment("count(CASE WHEN ? THEN 1 END)", f.is_win),
win_rate:
fragment(
"
((cast(count(CASE WHEN ? THEN 1 END) as float) / cast(count(*) as float)) * 100.0
)",
f.is_win
),
id: f.champion_id,
spell_id: f.summoner_spell_id,
metadata: s.metadata,
champion_name: c.name,
champion_id: c.champion_id,
team_position: f.team_position,
total_games: count("*")
},
group_by: [
f.champion_id,
f.summoner_spell_id,
s.metadata,
c.image,
c.name,
c.champion_id,
f.team_position
]
Repo.all(query)
end
def list_facts() do
Repo.all(Schema)
end
end
@@ -0,0 +1,34 @@
defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Schema do
use Ecto.Schema
import Ecto.Changeset
@params [
:champion_id,
:summoner_spell_id,
:match_id,
:is_win,
:game_length_seconds,
:queue_id,
:team_position,
:puuid
]
schema "fact_champion_picked_summoner_spell" do
field :champion_id, :integer
field :summoner_spell_id, :integer
field :match_id, :string
field :is_win, :boolean
field :game_length_seconds, :integer
field :queue_id, :integer
field :team_position, :string
field :puuid, :string
end
def changeset(fact = %__MODULE__{}, attrs \\ %{}) do
fact
|> cast(attrs, @params)
|> validate_required(@params)
|> unique_constraint([:puuid, :match_id, :summoner_spell_id])
end
end
@@ -0,0 +1,50 @@
defmodule LolAnalytics.Facts.ChampionPlayedGame.Repo do
import Ecto.Query
alias LolAnalytics.Dimensions.Champion.ChampionSchema
alias LolAnalytics.Dimensions.Player.PlayerRepo
alias LolAnalytics.Dimensions.Champion.ChampionRepo
alias LolAnalytics.Dimensions.Match.MatchRepo
alias LolAnalytics.Facts.ChampionPlayedGame.Schema
alias LoLAnalytics.Repo
def insert(attrs) do
_match = MatchRepo.get_or_create(attrs.match_id)
_champion = ChampionRepo.get_or_create(attrs.champion_id)
_player = PlayerRepo.get_or_create(attrs.puuid)
changeset = Schema.changeset(%Schema{}, attrs)
Repo.insert(changeset)
end
def list_played_matches() do
Repo.all(Schema)
end
def get_win_rates do
query =
from m in Schema,
join: c in ChampionSchema,
on: c.champion_id == m.champion_id,
select: %{
wins: fragment("count(CASE WHEN ? THEN 1 END)", m.is_win),
win_rate:
fragment(
"
((cast(count(CASE WHEN ? THEN 1 END) as float) / cast(count(*) as float)) * 100.0
)",
m.is_win
),
id: m.champion_id,
name: c.name,
image: c.image,
team_position: m.team_position,
total_games: count("*")
},
group_by: [m.champion_id, c.image, c.name, m.team_position]
Repo.all(query)
end
def get_win_rates_by_roles() do
end
end
@@ -0,0 +1,33 @@
defmodule LolAnalytics.Facts.ChampionPlayedGame.Schema do
use Ecto.Schema
import Ecto.Changeset
@casting_attrs [
:champion_id,
:match_id,
:is_win,
:game_length_seconds,
:team_position,
:puuid,
:queue_id
]
schema "fact_champion_played_game" do
field :champion_id, :integer
field :match_id, :string
field :is_win, :boolean
field :game_length_seconds, :integer
field :team_position, :string
field :puuid, :string
field :queue_id, :integer
timestamps()
end
def changeset(fact = %__MODULE__{}, attrs \\ %{}) do
fact
|> cast(attrs, @casting_attrs)
|> validate_required(@casting_attrs)
|> unique_constraint([:id, :champion_id, :queue_id])
end
end
@@ -0,0 +1,37 @@
defmodule LolAnalytics.Facts.ChampionPlayedGame.FactProcessor do
def process_game_at_url(path) do
data = HTTPoison.get!(path)
process_game_data(data.body)
end
def process_game_data(data) do
decoded_match = Poison.decode!(data, as: %LoLAPI.Model.MatchResponse{})
participants = decoded_match.info.participants
version = extract_game_version(decoded_match)
participants
|> Enum.each(fn participant = %LoLAPI.Model.Participant{} ->
if participant.teamPosition != "" do
attrs = %{
champion_id: participant.championId,
match_id: decoded_match.metadata.matchId,
is_win: participant.win,
game_length_seconds: decoded_match.info.gameDuration,
queue_id: decoded_match.info.queueId,
puuid: participant.puuid,
team_position: participant.teamPosition,
patch_number: version
}
LolAnalytics.Facts.ChampionPlayedGame.Repo.insert(attrs)
end
end)
end
defp extract_game_version(game_data) do
game_data.info.gameVersion
|> String.split(".")
|> Enum.take(2)
|> Enum.join(".")
end
end
@@ -0,0 +1,22 @@
defmodule LolAnalytics.Facts.FactsRunner do
alias LolAnalytics.Facts
def analyze_all_matches do
Storage.MatchStorage.S3MatchStorage.stream_files("ranked")
|> Enum.each(fn %{key: path} ->
get_facts()
|> Enum.each(fn fact_runner ->
apply(fact_runner, ["http://192.168.1.55:9000/ranked/#{path}"])
end)
end)
end
def analyze_match() do
end
def get_facts() do
[
&Facts.ChampionPickedSummonerSpell.FactProcessor.process_game_at_url/1
]
end
end
@@ -13,7 +13,7 @@ defmodule LolAnalytics.Match.MatchRepo do
LoLAnalytics.Repo.one(query)
end
@spec get_match(String.t()) :: %LolAnalytics.Match.MatchSchema{}
@spec get_match(String.t()) :: %LolAnalytics.Match.MatchSchema{} | nil
def get_match(match_id) do
query = from m in MatchSchema, where: m.match_id == ^match_id
+1
View File
@@ -42,6 +42,7 @@ defmodule LoLAnalytics.MixProject do
{:postgrex, ">= 0.0.0"},
{:jason, "~> 1.2"},
{:lol_api, in_umbrella: true},
{:storage, in_umbrella: true},
{:httpoison, "~> 2.2"},
{:poison, "~> 5.0"}
]
@@ -0,0 +1,62 @@
defmodule LoLAnalytics.Repo.Migrations.AnalyticsTables do
use Ecto.Migration
def change do
create table("dim_champion") do
add :champion_id, :integer, primary_key: true, null: false
timestamps()
end
create index("dim_champion", [:champion_id], unique: true)
create table("dim_item") do
add :item_id, :integer, primary_key: true, null: false
timestamps()
end
create index("dim_item", [:item_id], unique: true)
create table("dim_match") do
add :match_id, :string, primary_key: true, null: false
timestamps()
end
create index("dim_match", [:match_id], unique: true)
create table("dim_patch") do
add :patch_number, :string, primary_key: true, null: false
timestamps()
end
create index("dim_patch", [:patch_number], unique: true)
create table("dim_player") do
add :puuid, :string, primary_key: true, null: false
timestamps()
end
create index("dim_player", [:puuid], unique: true)
create table("dim_summoner_spell") do
add :spell_id, :integer, primary_key: true, null: false
timestamps()
end
create index("dim_summoner_spell", [:spell_id], unique: true)
create table("fact_champion_played_game") do
add :champion_id, references("dim_champion", column: :champion_id, type: :integer)
add :match_id, references("dim_match", column: :match_id, type: :string)
add :is_win, :boolean
add :game_length_seconds, :integer
add :queue_id, :integer
add :patch_number, references("dim_patch", column: :patch_number, type: :string)
add :team_position, :string
add :puuid, references("dim_player", column: :puuid, type: :string)
timestamps()
end
create index("fact_champion_played_game", [:id, :champion_id, :queue_id])
create index("fact_champion_played_game", [:puuid, :match_id], unique: true)
end
end
@@ -0,0 +1,10 @@
defmodule LoLAnalytics.Repo.Migrations.ChampionDimName do
use Ecto.Migration
def change do
alter table("dim_champion") do
add :name, :string
add :image, :string
end
end
end
@@ -0,0 +1,31 @@
defmodule LoLAnalytics.Repo.Migrations.SummonerSpellWinRate do
use Ecto.Migration
def change do
create table("fact_champion_picked_summoner_spell") do
add :champion_id, references("dim_champion", column: :champion_id, type: :integer)
add :summoner_spell_id, references("dim_summoner_spell", column: :spell_id, type: :integer)
add :match_id, references("dim_match", column: :match_id, type: :string)
add :is_win, :boolean
add :game_length_seconds, :integer
add :queue_id, :integer
add :team_position, :string
add :puuid, references("dim_player", column: :puuid, type: :string)
add :patch_number, references("dim_patch", column: :patch_number, type: :string)
end
create index(
"fact_champion_picked_summoner_spell",
[:puuid, :match_id, :summoner_spell_id],
unique: true
)
create index("fact_champion_picked_summoner_spell", [
:is_win,
:team_position,
:queue_id,
:summoner_spell_id,
:patch_number
])
end
end
@@ -0,0 +1,9 @@
defmodule LoLAnalytics.Repo.Migrations.SummonerSpellMetadata do
use Ecto.Migration
def change do
alter table("dim_summoner_spell") do
add :metadata, :map
end
end
end
@@ -0,0 +1,11 @@
defmodule LoLAnalytics.Repo.Migrations.SummonerSpellMetadataIndex do
use Ecto.Migration
def up do
execute("CREATE INDEX dim_summoner_spell_metadata ON dim_summoner_spell USING GIN(metadata)")
end
def down do
execute("DROP INDEX dim_summoner_spell_metadata")
end
end
@@ -0,0 +1,16 @@
defmodule LolAnalyticsWeb.ChampionComponents.ChampionAvatar do
use Phoenix.Component
attr :id, :integer, required: true
attr :image, :string, required: true
attr :name, :string, required: true
def champion_avatar(assigns) do
~H"""
<div class="flex flex-col w-40">
<img src={@image} alt="champion-icon" />
<p class="w-full text-center"><%= @name %></p>
</div>
"""
end
end
@@ -0,0 +1,42 @@
defmodule LolAnalyticsWeb.ChampionComponents.ChampionCard.Props do
defstruct [:id, :win_rate, :image, :name, :team_position, :wins, :total_games]
end
defmodule LolAnalyticsWeb.ChampionComponents.ChampionCard do
use Phoenix.Component
alias LolAnalyticsWeb.ChampionComponents.ChampionCard.Props
attr :props, Props, default: %Props{}
def champion_card(assigns) do
~H"""
<.link patch={"/champions/#{@props.id}"}>
<div class="flex flex-col rounded-xl bg-clip-border overflow-hidden bg-gray-200">
<div class="flex flex-column overflow-hidden">
<div class=" px-4 py-1 opacity-80 gap-2 absolute z-10 align-bottom flex mx-auto bg-black w-max">
<img src={team_position_image(@props.team_position)} class="w-5 h-5" />
<h3 class="text-white"><%= @props.name %></h3>
</div>
<img
class="static w-40 h-40"
src={"https://ddragon.leagueoflegends.com/cdn/14.11.1/img/champion/#{@props.image}"}
/>
</div>
<div class="py-1" />
<div class="pl-2">
<h3>Win rate: <%= @props.win_rate %></h3>
<h3>Wins: <%= @props.wins %> / <%= @props.total_games %></h3>
</div>
<div class="py-1" />
</div>
</.link>
"""
end
defp team_position_image("BOTTOM"), do: "/images/lanes/bot.png"
defp team_position_image("MIDDLE"), do: "/images/lanes/mid.png"
defp team_position_image("TOP"), do: "/images/lanes/top.png"
defp team_position_image("JUNGLE"), do: "/images/lanes/jungle.png"
defp team_position_image("UTILITY"), do: "/images/lanes/utility.png"
end
@@ -0,0 +1,42 @@
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props.SummonerSpell do
defstruct [:id, :name, :win_rate, :wins, :total_games, :image]
@type t :: %{
id: integer(),
win_rate: float(),
wins: integer(),
total_games: integer(),
image: String.t(),
name: String.t()
}
end
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props do
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props.SummonerSpell
defstruct spell: %SummonerSpell{}
@type t :: %{spell: SummonerSpell.t()}
end
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpell do
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props
use Phoenix.Component
attr :spells, Props, default: %Props{}
def summoner_spells(assigns) do
~H"""
<div class="flex flex-wrap flex-wrap gap-4">
<%= for spell <- assigns.spells.summoner_spells do %>
<div clas="flex flex-col gap-1">
<img src={spell.spell.image} />
<p><%= spell.spell.name %></p>
<p><%= spell.spell.win_rate %>%</p>
<p><%= spell.spell.wins %>/<%= spell.spell.total_games %></p>
</div>
<% end %>
</div>
"""
end
end
@@ -302,6 +302,9 @@ defmodule LoLAnalyticsWeb.CoreComponents do
end
def input(%{type: "checkbox"} = assigns) do
IO.puts(">>>>")
IO.inspect(assigns)
assigns =
assign_new(assigns, :checked, fn ->
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
@@ -315,8 +318,8 @@ defmodule LoLAnalyticsWeb.CoreComponents do
type="checkbox"
id={@id}
name={@name}
value="true"
checked={@checked}
value="false"
checked={false}
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
{@rest}
/>
@@ -2,25 +2,16 @@
<div class="flex items-center justify-between border-b border-zinc-100 py-3 text-sm">
<div class="flex items-center gap-4">
<a href="/">
<img src={~p"/images/logo.svg"} width="36" />
LoL Analytics
</a>
<p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6">
v<%= Application.spec(:phoenix, :vsn) %>
</p>
</div>
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900">
<a href="https://twitter.com/elixirphoenix" class="hover:text-zinc-700">
@elixirphoenix
</a>
<a href="https://github.com/phoenixframework/phoenix" class="hover:text-zinc-700">
GitHub
</a>
<a
href="https://hexdocs.pm/phoenix/overview.html"
class="rounded-lg bg-zinc-100 px-2 py-1 hover:bg-zinc-200/80"
>
Get Started <span aria-hidden="true">&rarr;</span>
</a>
<.link patch="/champions">
Champions
</.link>
</div>
</div>
</header>
@@ -29,4 +20,4 @@
<.flash_group flash={@flash} />
<%= @inner_content %>
</div>
</main>
</main>
@@ -1,17 +1,20 @@
<!DOCTYPE html>
<html lang="en" class="[scrollbar-gutter:stable]">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
<.live_title suffix=" · Phoenix Framework">
<%= assigns[:page_title] || "LoLAnalytics" %>
</.live_title>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script>
</head>
<body class="bg-white antialiased">
<%= @inner_content %>
</body>
</html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
<.live_title suffix=" · Phoenix Framework">
<%= assigns[:page_title] || "LoLAnalytics" %>
</.live_title>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script>
</head>
<body class="bg-white antialiased">
<%= @inner_content %>
</body>
</html>
@@ -0,0 +1,28 @@
defmodule LolAnalyticsWeb.Loader do
use Phoenix.Component
def loader(assigns) do
~H"""
<div role="status" class="flex flex-col mx-auto w-max justify-center items-center gap-4">
<span class="text-3xl">Loading</span>
<svg
aria-hidden="true"
class="w-16 h-16 text-gray-300 animate-spin dark:text-gray-300 fill-orange-600"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
<span class="sr-only">Loading...</span>
</div>
"""
end
end
@@ -0,0 +1,3 @@
defmodule LolAnalyticsWeb.ChampionLive.ChampionSummary do
defstruct [:id, :win_rate, :image, :name, :team_position, :wins, :total_games]
end
@@ -0,0 +1,58 @@
defmodule LolAnalyticsWeb.ChampionLive.Components.ChampionFilters do
use LoLAnalyticsWeb, :live_component
@roles_definition [
%{title: "All", value: "all"},
%{title: "Top", value: "TOP"},
%{title: "Jungle", value: "JUNGLE"},
%{title: "Mid", value: "MIDDLE"},
%{title: "Bot", value: "BOTTOM"},
%{title: "Support", value: "UTILITY"}
]
def on_role_selected(role) do
send_update(LolAnalyticsWeb.ChampionLive, role)
end
def mount(socket) do
socket =
assign(socket, :roles, @roles_definition)
{:ok, socket}
end
attr :selectedrole, :string, required: true
attr :roles, :list, default: []
def render(assigns) do
selected_class =
"px-8 py-2 flex flex-row gap-2 align-middle rounded-full border-gray-200 border border-orange-600 cursor-pointer"
~H"""
<div>
<div class="flex flex-row justify-between p-10">
<%= for role <- @roles do %>
<%!-- <%= IO.inspect(role) %> --%>
<%= if (assigns.selectedrole == role.value) do %>
<div phx-click="filter" phx-value-role={role.title} class={selected_class}>
<p><%= role.title %></p>
</div>
<% else %>
<div
phx-click="filter"
phx-value-role={role.value}
class="px-8 py-2 flex flex-row gap-2 align-middle cursor-pointer"
>
<p><%= role.title %></p>
</div>
<% end %>
<% end %>
</div>
</div>
"""
end
end
defmodule LolAnalyticsWeb.ChampionLive.Components.ChampionFilters.EventHandler do
@callback role_selected() :: none()
end
@@ -0,0 +1,98 @@
defmodule LoLAnalyticsWeb.ChampionLive.Index do
use LoLAnalyticsWeb, :live_view
import LolAnalyticsWeb.ChampionComponents.ChampionCard
import LolAnalyticsWeb.Loader
import Phoenix.VerifiedRoutes
alias LolAnalyticsWeb.ChampionLive.Mapper
alias LolAnalyticsWeb.ChampionLive.Components.ChampionFilters
@behaviour LolAnalyticsWeb.ChampionFilters.EventHandler
@impl true
def mount(_params, _session, socket) do
socket =
socket
|> assign(:champions, %{status: :loading})
|> load_champs("all")
|> assign(:selected_role, "all")
{:ok, socket}
end
def handle_params(params, _uri, socket) do
socket =
case params["role"] do
role -> {:noreply, assign(socket, selected_role: role)}
_ -> {:noreply, socket}
end
end
@impl true
def handle_event("filter", %{"role" => selected_role} = params, socket) do
{:reply, %{},
socket
|> push_navigate(to: ~p"/champions?#{params}")
|> assign(:champions, %{status: :loading})
|> load_champs(selected_role)
|> assign(:selected_role, selected_role)}
end
defp load_champs(socket, selected_role) do
socket
|> start_async(
:get_champs,
fn ->
LolAnalytics.Facts.ChampionPlayedGame.Repo.get_win_rates()
|> filter_champs(selected_role)
end
)
end
def handle_async(:get_champs, {:ok, fetched_events} = params, socket) do
{:noreply, assign(socket, :champions, %{status: :data, data: fetched_events})}
end
def render_champions(assigns) do
case assigns.champions do
%{status: :loading} ->
~H"""
<.loader />
"""
%{status: :data, data: champions} ->
~H"""
<div id="champions" class="grid grid-cols-4 gap-4">
<%= for champion <- champions do %>
<.champion_card props={champion} />
<% end %>
</div>
"""
end
end
defp filter_champs(champs, selected_role) do
champs =
LolAnalytics.Facts.ChampionPlayedGame.Repo.get_win_rates()
|> Enum.filter((&filter_role/1).(selected_role))
|> Mapper.map_champs()
|> Enum.sort(&(&1.win_rate >= &2.win_rate))
end
defp filter_role(role) do
fn champ ->
champ.team_position == role || role == "all"
end
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Champions")
end
end
@@ -0,0 +1,16 @@
<.header>
Champions
</.header>
<h1>Filters</h1>
<.live_component module={ChampionFilters} id="role-filters" selectedrole={@selected_role} />
<.render_champions champions={@champions} />
<.modal :if={@live_action in [:new, :edit]} id="champion-modal" show on_cancel={JS.patch(~p"/champions")}>
<.live_component module={LoLAnalyticsWeb.ChampionLive.FormComponent} id={@champion.champion_id || :new}
title={@page_title} action={@live_action} champion={@champion} patch={~p"/champions"} />
</.modal>
@@ -0,0 +1,13 @@
defmodule LolAnalyticsWeb.ChampionLive.Mapper do
alias LolAnalyticsWeb.ChampionLive.ChampionSummary
def map_champs(champs) do
champs
|> Enum.map(fn champ ->
%{
champ
| win_rate: :erlang.float_to_binary(champ.win_rate, decimals: 2)
}
end)
end
end
@@ -0,0 +1,39 @@
defmodule LoLAnalyticsWeb.ChampionLive.Show do
use LoLAnalyticsWeb, :live_view
import LolAnalyticsWeb.ChampionComponents.SummonerSpell
import LolAnalyticsWeb.ChampionComponents.ChampionAvatar
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.ShowMapper
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(%{"id" => id}, _, socket) do
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:champion, load_champion_info(id) |> ShowMapper.map_champion())
|> assign(:summoner_spells, %{
summoner_spells: load_summoner_spells(id) |> ShowMapper.map_spells()
})}
end
defp load_summoner_spells(champion_id) do
LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo.get_champion_picked_summoners(champion_id)
end
defp load_champion_info(champion_id) do
champion = LolAnalytics.Dimensions.Champion.ChampionRepo.get_or_create(champion_id)
IO.inspect(champion)
champion
end
defp page_title(:show), do: "Show Champion"
defp page_title(:edit), do: "Edit Champion"
end
@@ -0,0 +1,11 @@
<.header>
Champion <%= @champion.id %>
<:actions></:actions>
</.header>
<.champion_avatar id={@champion.id} name={@champion.name}
image={"https://ddragon.leagueoflegends.com/cdn/14.11.1/img/champion/#{@champion.image}"} />
<div class="my-4" />
<.summoner_spells spells={@summoner_spells} />
@@ -0,0 +1,33 @@
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpells.ShowMapper do
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.SummonerSpell
@spec map_champion(%LolAnalytics.Dimensions.Champion.ChampionSchema{}) :: map()
def map_champion(champion) do
%{
id: champion.champion_id,
name: champion.name,
image: champion.image
}
end
def map_spells(items) do
items
|> Enum.map(&map_spell/1)
|> Enum.sort(&(&1.spell.total_games > &2.spell.total_games))
end
def map_spell(spell) do
image = spell.metadata["image"]["full"]
%LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props{
spell: %LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props.SummonerSpell{
id: spell.id,
win_rate: :erlang.float_to_binary(spell.win_rate, decimals: 2),
total_games: spell.total_games,
image: "https://ddragon.leagueoflegends.com/cdn/14.11.1/img/spell/#{image}",
name: spell.metadata["name"],
wins: spell.wins
}
}
end
end
@@ -0,0 +1,27 @@
defmodule LoLAnalyticsWeb.RoleLive.FormComponent do
use LoLAnalyticsWeb, :live_component
@impl true
def render(assigns) do
~H"""
<div>
<.header>
<%= @title %>
<:subtitle>Use this form to manage role records in your database.</:subtitle>
</.header>
<.simple_form
for={@form}
id="role-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
>
<:actions>
<.button phx-disable-with="Saving...">Save Role</.button>
</:actions>
</.simple_form>
</div>
"""
end
end
@@ -0,0 +1,34 @@
defmodule LoLAnalyticsWeb.RoleLive.Index do
use LoLAnalyticsWeb, :live_view
@roles ["ALL", "TOP", "MIDDLE", "JUNGLE", "UTILITY", "BOTTOM"]
@impl true
def mount(_params, _session, socket) do
{:ok, stream(socket, :roles, @roles)}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Roles")
|> assign(:role, nil)
end
@impl true
def handle_info({LoLAnalyticsWeb.RoleLive.FormComponent, {:saved, role}}, socket) do
{:noreply, stream_insert(socket, :roles, role)}
end
# @impl true
# def handle_event("delete", %{"id" => id}, socket) do
# role = Accounts.get_role!(id)
# {:ok, _} = Accounts.delete_role(role)
# {:noreply, stream_delete(socket, :roles, role)}
# end
end
@@ -0,0 +1,4 @@
<.header>
Listing Roles
<:actions></:actions>
</.header>
@@ -0,0 +1,18 @@
defmodule LoLAnalyticsWeb.RoleLive.Show do
use LoLAnalyticsWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(%{"id" => _id}, _, socket) do
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))}
end
defp page_title(:show), do: "Show Role"
defp page_title(:edit), do: "Edit Role"
end
@@ -0,0 +1,5 @@
<.header>
Role <%= @role.id %>
<:subtitle>This is a role record from your database.</:subtitle>
<:actions></:actions>
</.header>
@@ -18,6 +18,12 @@ defmodule LoLAnalyticsWeb.Router do
pipe_through :browser
get "/", PageController, :home
live "/champions", ChampionLive.Index, :index
live "/champions/new", ChampionLive.Index, :new
live "/champions/:id/edit", ChampionLive.Index, :edit
live "/champions/:id", ChampionLive.Show, :show
live "/champions/:id/show/edit", ChampionLive.Show, :edit
end
# Other scopes may use custom stacks.
Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

@@ -0,0 +1,5 @@
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
#
# To ban all spiders from the entire site uncomment the next two lines:
# User-agent: *
# Disallow: /
Binary file not shown.
@@ -0,0 +1,108 @@
defmodule LoLAnalyticsWeb.ChampionLiveTest do
use LoLAnalyticsWeb.ConnCase
import Phoenix.LiveViewTest
import LoLAnalytics.AccountsFixtures
@create_attrs %{}
@update_attrs %{}
@invalid_attrs %{}
defp create_champion(_) do
champion = champion_fixture()
%{champion: champion}
end
describe "Index" do
setup [:create_champion]
test "lists all champions", %{conn: conn} do
{:ok, _index_live, html} = live(conn, ~p"/champions")
assert html =~ "Listing Champions"
end
test "saves new champion", %{conn: conn} do
{:ok, index_live, _html} = live(conn, ~p"/champions")
assert index_live |> element("a", "New Champion") |> render_click() =~
"New Champion"
assert_patch(index_live, ~p"/champions/new")
assert index_live
|> form("#champion-form", champion: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#champion-form", champion: @create_attrs)
|> render_submit()
assert_patch(index_live, ~p"/champions")
html = render(index_live)
assert html =~ "Champion created successfully"
end
test "updates champion in listing", %{conn: conn, champion: champion} do
{:ok, index_live, _html} = live(conn, ~p"/champions")
assert index_live |> element("#champions-#{champion.id} a", "Edit") |> render_click() =~
"Edit Champion"
assert_patch(index_live, ~p"/champions/#{champion}/edit")
assert index_live
|> form("#champion-form", champion: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#champion-form", champion: @update_attrs)
|> render_submit()
assert_patch(index_live, ~p"/champions")
html = render(index_live)
assert html =~ "Champion updated successfully"
end
test "deletes champion in listing", %{conn: conn, champion: champion} do
{:ok, index_live, _html} = live(conn, ~p"/champions")
assert index_live |> element("#champions-#{champion.id} a", "Delete") |> render_click()
refute has_element?(index_live, "#champions-#{champion.id}")
end
end
describe "Show" do
setup [:create_champion]
test "displays champion", %{conn: conn, champion: champion} do
{:ok, _show_live, html} = live(conn, ~p"/champions/#{champion}")
assert html =~ "Show Champion"
end
test "updates champion within modal", %{conn: conn, champion: champion} do
{:ok, show_live, _html} = live(conn, ~p"/champions/#{champion}")
assert show_live |> element("a", "Edit") |> render_click() =~
"Edit Champion"
assert_patch(show_live, ~p"/champions/#{champion}/show/edit")
assert show_live
|> form("#champion-form", champion: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert show_live
|> form("#champion-form", champion: @update_attrs)
|> render_submit()
assert_patch(show_live, ~p"/champions/#{champion}")
html = render(show_live)
assert html =~ "Champion updated successfully"
end
end
end
@@ -0,0 +1,108 @@
defmodule LoLAnalyticsWeb.RoleLiveTest do
use LoLAnalyticsWeb.ConnCase
import Phoenix.LiveViewTest
import LoLAnalytics.AccountsFixtures
@create_attrs %{}
@update_attrs %{}
@invalid_attrs %{}
defp create_role(_) do
role = role_fixture()
%{role: role}
end
describe "Index" do
setup [:create_role]
test "lists all roles", %{conn: conn} do
{:ok, _index_live, html} = live(conn, ~p"/roles")
assert html =~ "Listing Roles"
end
test "saves new role", %{conn: conn} do
{:ok, index_live, _html} = live(conn, ~p"/roles")
assert index_live |> element("a", "New Role") |> render_click() =~
"New Role"
assert_patch(index_live, ~p"/roles/new")
assert index_live
|> form("#role-form", role: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#role-form", role: @create_attrs)
|> render_submit()
assert_patch(index_live, ~p"/roles")
html = render(index_live)
assert html =~ "Role created successfully"
end
test "updates role in listing", %{conn: conn, role: role} do
{:ok, index_live, _html} = live(conn, ~p"/roles")
assert index_live |> element("#roles-#{role.id} a", "Edit") |> render_click() =~
"Edit Role"
assert_patch(index_live, ~p"/roles/#{role}/edit")
assert index_live
|> form("#role-form", role: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#role-form", role: @update_attrs)
|> render_submit()
assert_patch(index_live, ~p"/roles")
html = render(index_live)
assert html =~ "Role updated successfully"
end
test "deletes role in listing", %{conn: conn, role: role} do
{:ok, index_live, _html} = live(conn, ~p"/roles")
assert index_live |> element("#roles-#{role.id} a", "Delete") |> render_click()
refute has_element?(index_live, "#roles-#{role.id}")
end
end
describe "Show" do
setup [:create_role]
test "displays role", %{conn: conn, role: role} do
{:ok, _show_live, html} = live(conn, ~p"/roles/#{role}")
assert html =~ "Show Role"
end
test "updates role within modal", %{conn: conn, role: role} do
{:ok, show_live, _html} = live(conn, ~p"/roles/#{role}")
assert show_live |> element("a", "Edit") |> render_click() =~
"Edit Role"
assert_patch(show_live, ~p"/roles/#{role}/show/edit")
assert show_live
|> form("#role-form", role: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert show_live
|> form("#role-form", role: @update_attrs)
|> render_submit()
assert_patch(show_live, ~p"/roles/#{role}")
html = render(show_live)
assert html =~ "Role updated successfully"
end
end
end
+1 -1
View File
@@ -18,7 +18,7 @@ defmodule LoLAPI.AccountApi do
200 ->
{:ok, Poison.decode(response.body)}
code ->
_code ->
Logger.error("Error getting puuid from player #{name} \##{tag}")
{:err, response.status_code}
end
+6 -19
View File
@@ -1,19 +1,9 @@
defmodule Scrapper.MatchClassifier do
require Logger
@spec classify_match(%LoLAPI.Model.MatchResponse{}) :: nil
def classify_match(match = %LoLAPI.Model.MatchResponse{}) do
classify_match_by_queue(match.info.queueId)
end
@spec classify_match_by_queue(String.t()) :: nil
def classify_match_by_queue("420") do
matches = Storage.MatchStorage.S3MatchStorage.list_files("matches")
total_matches = Enum.count(matches)
matches
|> Enum.with_index(fn match, index -> {match, index} end)
|> Scrapper.Parallel.peach(fn {match, index} ->
def stream_classify_matches_by_queue(queue \\ 420, bucket \\ "ranked") do
Storage.MatchStorage.S3MatchStorage.stream_files("matches")
|> Stream.each(fn match ->
%{key: json_file} = match
[key | _] = String.split(json_file, ".")
@@ -25,15 +15,12 @@ defmodule Scrapper.MatchClassifier do
%{"info" => %{"gameVersion" => gameVersion, "queueId" => queueId}} =
Poison.decode!(response.body)
if queueId == 420 do
Storage.MatchStorage.S3MatchStorage.store_match(key, response.body, "ranked", gameVersion)
Logger.info("Match at #{index} of #{total_matches} is classified")
if queueId == queue do
Storage.MatchStorage.S3MatchStorage.store_match(key, response.body, bucket, gameVersion)
Logger.info("Match #{key} processed")
end
match
end)
end
def classify_match_by_queue(_) do
end
end
@@ -66,6 +66,6 @@ defmodule Scrapper.Processor.MatchProcessor do
end)
end
def process_resp({:err, code}, match_id) do
def process_resp({:err, _code}, _match_id) do
end
end
@@ -1,5 +1,4 @@
defmodule Scrapper.Processor.PlayerProcessor do
alias Calendar.ISO
use Broadway
def start_link(_opts) do
@@ -19,7 +19,7 @@ defmodule Scrapper.Queue.MatchQueue do
LolAnalytics.Match.MatchRepo.get_match(match_id)
|> case do
nil -> GenServer.call(__MODULE__, {:queue_match, match_id})
match -> :already_processed
_match -> :already_processed
end
end
@@ -1,6 +1,13 @@
defmodule Storage.MatchStorage do
@callback stream_files(String.t()) :: Enumerable.t()
@callback get_match(String.t()) :: {:ok, Scrapper.Data.Match.t()} | {:error, :not_found}
@callback save_match(String.t(), Scrapper.Data.Match.t()) :: :ok
@callback list_matches() :: [map()]
@callback store_match(match_id :: String.t(), match :: map(), path :: String.t()) :: String.t()
@callback store_match(match_id :: String.t(), match_data :: String.t(), path :: String.t()) ::
String.t()
@callback store_match(
match_id :: String.t(),
match_data :: String.t(),
bucket :: String.t(),
path :: String.t()
) ::
String.t()
end
@@ -2,43 +2,14 @@ defmodule Storage.MatchStorage.S3MatchStorage do
require Logger
@behaviour Storage.MatchStorage
def get_match(match_id) do
""
end
@doc """
Lists all files at the given path.
iex > Storage.MatchStorage.S3MatchStorage.list_files("matches")
"""
@impl true
def list_files(path) do
{:ok, %{:body => %{:contents => contents, next_continuation_token: next_continuation_token}}} =
ExAws.S3.list_objects_v2(path)
|> ExAws.request()
if next_continuation_token do
list_files(path, contents, next_continuation_token)
# |> Enum.map(fn %{key: key} -> key end)
else
contents
# |> Enum.map(fn %{key: key} -> key end)
end
def get_match(_match_id) do
end
@spec list_files(String.t(), list(String.t()), String.t()) :: list(String.t())
defp list_files(path, acc, continuation_token) do
resp =
{:ok,
%{:body => %{:contents => contents, next_continuation_token: next_continuation_token}}} =
ExAws.S3.list_objects_v2(path, continuation_token: continuation_token)
|> ExAws.request()
if next_continuation_token == "" do
acc ++ contents
else
list_files(path, acc ++ contents, next_continuation_token)
end
@impl true
def stream_files(path) do
ExAws.S3.list_objects_v2(path)
|> ExAws.stream!()
end
@doc """
+1 -1
View File
@@ -8,7 +8,7 @@ import Config
config :lol_analytics_web, LoLAnalyticsWeb.Endpoint,
server: true,
http: [ip: {0, 0, 0, 0}, port: 4000],
url: [host: "lol-analytics.kaizer.cloud", port: 80],
url: [host: "localhost", port: 80],
cache_static_manifest: "priv/static/cache_manifest.json"
# Do not print debug messages in production
+1 -1
View File
@@ -6,7 +6,7 @@ import Config
# and secrets from environment variables or elsewhere. Do not define
# any compile-time configuration in here, as it won't be applied.
# The block below contains prod specific runtime configuration.
if config_env() == :prod do
if config_env() == :prod || true do
database_url =
System.get_env("DATABASE_URL") ||
raise """
+36
View File
@@ -0,0 +1,36 @@
version: '3.8'
services:
db:
image: postgres:latest
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: postgres
ports:
- "5432:5432"
rabbitmq:
image: rabbitmq:3-management-alpine
container_name: 'rabbitmq'
ports:
- "15671:15671"
- "15672:15672"
- "15691:15691"
- "15692:15692"
- "25672:25672"
- "4639:4639"
- "5671:5671"
- "5672:5672"
volumes:
- ~/.docker-conf/rabbitmq/data/:/var/lib/rabbitmq/
- ~/.docker-conf/rabbitmq/log/:/var/log/rabbitmq
networks:
- rabbitmq_go_net
volumes:
postgres-data:
networks:
rabbitmq_go_net:
driver: bridge
+22
View File
@@ -0,0 +1,22 @@
```
SELECT
(cast(count(CASE WHEN is_win THEN 1 END) as float) / cast(count(*) as float)) * 100.0 as win_rate,
count(CASE WHEN is_win THEN 1 END) as games_won,
count(*) as total_games,
champion_id
FROM fact_champion_played_game
GROUP BY champion_id
ORDER BY win_rate desc;
```
```
SELECT
(cast(count(CASE WHEN is_win THEN 1 END) as float) / cast(count(*) as float)) * 100.0 as win_rate,
count(CASE WHEN is_win THEN 1 END) as games_won,
count(*) as total_games,
champion_id,
team_position
FROM fact_champion_played_game
GROUP BY champion_id, team_position
ORDER BY win_rate desc;
```