354 lines
9.2 KiB
Elixir
354 lines
9.2 KiB
Elixir
|
|
defmodule ShootingEventPhx.Domain.EventData do
|
||
|
|
@moduledoc false
|
||
|
|
import Ecto.Query
|
||
|
|
alias ShootingEventPhx.Repo
|
||
|
|
alias ShootingEventPhx.Domain.{Player, Score, ScoreAttachment, Stages}
|
||
|
|
|
||
|
|
def list_players do
|
||
|
|
Repo.all(from p in Player, order_by: [asc: p.id])
|
||
|
|
end
|
||
|
|
|
||
|
|
def ensure_score_rows(players) do
|
||
|
|
Repo.transaction(fn ->
|
||
|
|
for player <- players, stage <- Stages.score_stages() do
|
||
|
|
Repo.query!(
|
||
|
|
"""
|
||
|
|
INSERT OR IGNORE INTO scores(stage, player_id, score)
|
||
|
|
VALUES(?1, ?2, 0)
|
||
|
|
""",
|
||
|
|
[stage, player.id]
|
||
|
|
)
|
||
|
|
end
|
||
|
|
end)
|
||
|
|
end
|
||
|
|
|
||
|
|
def read_scores(players) do
|
||
|
|
base =
|
||
|
|
for stage <- Stages.score_stages(), into: %{} do
|
||
|
|
{stage, Map.new(players, &{&1.id, 0})}
|
||
|
|
end
|
||
|
|
|
||
|
|
rows =
|
||
|
|
Repo.all(
|
||
|
|
from s in Score,
|
||
|
|
select: {s.stage, s.player_id, s.score}
|
||
|
|
)
|
||
|
|
|
||
|
|
Enum.reduce(rows, base, fn {stage, player_id, score}, acc ->
|
||
|
|
if Map.has_key?(acc, stage) do
|
||
|
|
Map.update!(acc, stage, &Map.put(&1, player_id, score))
|
||
|
|
else
|
||
|
|
acc
|
||
|
|
end
|
||
|
|
end)
|
||
|
|
end
|
||
|
|
|
||
|
|
def read_score_proofs(players) do
|
||
|
|
base =
|
||
|
|
for stage <- Stages.score_stages(), into: %{} do
|
||
|
|
{stage, Map.new(players, &{&1.id, ""})}
|
||
|
|
end
|
||
|
|
|
||
|
|
rows =
|
||
|
|
Repo.all(
|
||
|
|
from sa in ScoreAttachment,
|
||
|
|
select: {sa.stage, sa.player_id, sa.image_data}
|
||
|
|
)
|
||
|
|
|
||
|
|
Enum.reduce(rows, base, fn {stage, player_id, image_data}, acc ->
|
||
|
|
if Map.has_key?(acc, stage) do
|
||
|
|
Map.update!(acc, stage, &Map.put(&1, player_id, image_data || ""))
|
||
|
|
else
|
||
|
|
acc
|
||
|
|
end
|
||
|
|
end)
|
||
|
|
end
|
||
|
|
|
||
|
|
def score_map_to_json(scores) do
|
||
|
|
for {stage, stage_map} <- scores, into: %{} do
|
||
|
|
{stage,
|
||
|
|
for({player_id, value} <- stage_map, into: %{}, do: {Integer.to_string(player_id), value})}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
def proof_map_to_json(proofs) do
|
||
|
|
for {stage, stage_map} <- proofs, into: %{} do
|
||
|
|
compact =
|
||
|
|
for {player_id, image_data} <- stage_map,
|
||
|
|
String.trim(to_string(image_data)) != "",
|
||
|
|
into: %{} do
|
||
|
|
{Integer.to_string(player_id), image_data}
|
||
|
|
end
|
||
|
|
|
||
|
|
{stage, compact}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
def create_player(attrs) do
|
||
|
|
name_ar = attrs["nameAr"] |> to_string() |> String.trim()
|
||
|
|
name_en = attrs["nameEn"] |> to_string() |> String.trim()
|
||
|
|
group_code = attrs["groupCode"] |> normalize_group()
|
||
|
|
|
||
|
|
cond do
|
||
|
|
name_ar == "" or name_en == "" ->
|
||
|
|
{:error, :bad_request, "both Arabic and English names are required"}
|
||
|
|
|
||
|
|
true ->
|
||
|
|
Repo.transaction(fn ->
|
||
|
|
{:ok, player} =
|
||
|
|
%Player{name_ar: name_ar, name_en: name_en, group_code: group_code, image_data: ""}
|
||
|
|
|> Repo.insert()
|
||
|
|
|
||
|
|
for stage <- Stages.score_stages() do
|
||
|
|
Repo.query!(
|
||
|
|
"""
|
||
|
|
INSERT OR IGNORE INTO scores(stage, player_id, score) VALUES(?1, ?2, 0)
|
||
|
|
""",
|
||
|
|
[stage, player.id]
|
||
|
|
)
|
||
|
|
end
|
||
|
|
end)
|
||
|
|
|> case do
|
||
|
|
{:ok, _} -> :ok
|
||
|
|
{:error, reason} -> {:error, :internal, inspect(reason)}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
def update_player(id, attrs) do
|
||
|
|
with %Player{} = player <- Repo.get(Player, id) do
|
||
|
|
updates = player_updates(attrs)
|
||
|
|
|
||
|
|
cond do
|
||
|
|
Map.has_key?(updates, :_error) ->
|
||
|
|
{:error, :bad_request, Map.get(updates, :_error)}
|
||
|
|
|
||
|
|
map_size(updates) == 0 ->
|
||
|
|
{:error, :bad_request, "no fields to update"}
|
||
|
|
|
||
|
|
true ->
|
||
|
|
case Repo.update(Ecto.Changeset.change(player, updates)) do
|
||
|
|
{:ok, _} -> :ok
|
||
|
|
{:error, changeset} -> {:error, :bad_request, format_changeset_errors(changeset)}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
else
|
||
|
|
nil -> {:error, :not_found, "player not found"}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
def delete_player(id) do
|
||
|
|
case Repo.get(Player, id) do
|
||
|
|
nil ->
|
||
|
|
{:error, :not_found, "player not found"}
|
||
|
|
|
||
|
|
player ->
|
||
|
|
case Repo.delete(player) do
|
||
|
|
{:ok, _} -> :ok
|
||
|
|
{:error, reason} -> {:error, :internal, inspect(reason)}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
def auto_group_players(groups) do
|
||
|
|
normalized_groups =
|
||
|
|
groups
|
||
|
|
|> List.wrap()
|
||
|
|
|> Enum.map(&normalize_group/1)
|
||
|
|
|> Enum.reject(&(&1 == ""))
|
||
|
|
|> Enum.uniq()
|
||
|
|
|
||
|
|
if normalized_groups == [] do
|
||
|
|
{:error, :bad_request, "at least one group is required"}
|
||
|
|
else
|
||
|
|
player_ids = Repo.all(from p in Player, select: p.id, order_by: [asc: p.id])
|
||
|
|
shuffled = Enum.shuffle(player_ids)
|
||
|
|
|
||
|
|
case Repo.transaction(fn ->
|
||
|
|
Enum.with_index(shuffled)
|
||
|
|
|> Enum.each(fn {player_id, idx} ->
|
||
|
|
group_code = Enum.at(normalized_groups, rem(idx, length(normalized_groups)))
|
||
|
|
|
||
|
|
Repo.query!(
|
||
|
|
"UPDATE players SET group_code = ?1, updated_at = CURRENT_TIMESTAMP WHERE id = ?2",
|
||
|
|
[group_code, player_id]
|
||
|
|
)
|
||
|
|
end)
|
||
|
|
end) do
|
||
|
|
{:ok, _} -> :ok
|
||
|
|
{:error, reason} -> {:error, :internal, inspect(reason)}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
def update_score(stage, player_id, score) when is_integer(score) do
|
||
|
|
if score < 0 or score > 9999 do
|
||
|
|
{:error, :bad_request, "score must be between 0 and 9999"}
|
||
|
|
else
|
||
|
|
Repo.query!(
|
||
|
|
"""
|
||
|
|
INSERT INTO scores(stage, player_id, score)
|
||
|
|
VALUES(?1, ?2, ?3)
|
||
|
|
ON CONFLICT(stage, player_id) DO UPDATE SET score = excluded.score, updated_at = CURRENT_TIMESTAMP
|
||
|
|
""",
|
||
|
|
[stage, player_id, score]
|
||
|
|
)
|
||
|
|
|
||
|
|
:ok
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
def update_score_proof(stage, player_id, image_data) do
|
||
|
|
payload = to_string(image_data || "") |> String.trim()
|
||
|
|
|
||
|
|
cond do
|
||
|
|
payload == "" ->
|
||
|
|
{:error, :bad_request, "imageData is required"}
|
||
|
|
|
||
|
|
byte_size(payload) > 5_000_000 ->
|
||
|
|
{:error, :bad_request, "image payload too large"}
|
||
|
|
|
||
|
|
true ->
|
||
|
|
Repo.query!(
|
||
|
|
"""
|
||
|
|
INSERT INTO score_attachments(stage, player_id, image_data)
|
||
|
|
VALUES(?1, ?2, ?3)
|
||
|
|
ON CONFLICT(stage, player_id) DO UPDATE SET image_data = excluded.image_data, updated_at = CURRENT_TIMESTAMP
|
||
|
|
""",
|
||
|
|
[stage, player_id, payload]
|
||
|
|
)
|
||
|
|
|
||
|
|
:ok
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
def delete_score_proof(stage, player_id) do
|
||
|
|
Repo.query!("DELETE FROM score_attachments WHERE stage = ?1 AND player_id = ?2", [
|
||
|
|
stage,
|
||
|
|
player_id
|
||
|
|
])
|
||
|
|
|
||
|
|
:ok
|
||
|
|
end
|
||
|
|
|
||
|
|
def reset_stage_scores(stages, reset_proofs?) do
|
||
|
|
case Repo.transaction(fn ->
|
||
|
|
Enum.each(stages, fn stage ->
|
||
|
|
Repo.query!(
|
||
|
|
"UPDATE scores SET score = 0, updated_at = CURRENT_TIMESTAMP WHERE stage = ?1",
|
||
|
|
[stage]
|
||
|
|
)
|
||
|
|
|
||
|
|
if reset_proofs? do
|
||
|
|
Repo.query!("DELETE FROM score_attachments WHERE stage = ?1", [stage])
|
||
|
|
end
|
||
|
|
end)
|
||
|
|
end) do
|
||
|
|
{:ok, _} -> :ok
|
||
|
|
{:error, reason} -> {:error, :internal, inspect(reason)}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
def load_score_proof_image(stage, player_id) do
|
||
|
|
case Repo.one(
|
||
|
|
from sa in ScoreAttachment,
|
||
|
|
where: sa.stage == ^stage and sa.player_id == ^player_id,
|
||
|
|
select: sa.image_data
|
||
|
|
) do
|
||
|
|
nil -> {:error, :not_found}
|
||
|
|
image -> {:ok, image}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
def load_current_score(stage, player_id) do
|
||
|
|
score =
|
||
|
|
Repo.one(
|
||
|
|
from s in Score, where: s.stage == ^stage and s.player_id == ^player_id, select: s.score
|
||
|
|
)
|
||
|
|
|
||
|
|
{:ok, score || 0}
|
||
|
|
end
|
||
|
|
|
||
|
|
defp player_updates(attrs) do
|
||
|
|
updates = %{}
|
||
|
|
|
||
|
|
updates =
|
||
|
|
case Map.get(attrs, "nameAr") do
|
||
|
|
nil ->
|
||
|
|
updates
|
||
|
|
|
||
|
|
name ->
|
||
|
|
trimmed = to_string(name) |> String.trim()
|
||
|
|
|
||
|
|
if trimmed == "",
|
||
|
|
do: Map.put(updates, :_error, "arabic name cannot be empty"),
|
||
|
|
else: Map.put(updates, :name_ar, trimmed)
|
||
|
|
end
|
||
|
|
|
||
|
|
updates =
|
||
|
|
case Map.get(attrs, "nameEn") do
|
||
|
|
nil ->
|
||
|
|
updates
|
||
|
|
|
||
|
|
name ->
|
||
|
|
trimmed = to_string(name) |> String.trim()
|
||
|
|
|
||
|
|
if trimmed == "",
|
||
|
|
do: Map.put(updates, :_error, "english name cannot be empty"),
|
||
|
|
else: Map.put(updates, :name_en, trimmed)
|
||
|
|
end
|
||
|
|
|
||
|
|
updates =
|
||
|
|
if Map.has_key?(attrs, "groupCode"),
|
||
|
|
do: Map.put(updates, :group_code, normalize_group(Map.get(attrs, "groupCode"))),
|
||
|
|
else: updates
|
||
|
|
|
||
|
|
updates =
|
||
|
|
if Map.has_key?(attrs, "imageData") do
|
||
|
|
image = attrs |> Map.get("imageData") |> to_string() |> String.trim()
|
||
|
|
Map.put(updates, :image_data, image)
|
||
|
|
else
|
||
|
|
updates
|
||
|
|
end
|
||
|
|
|
||
|
|
updates =
|
||
|
|
if truthy?(Map.get(attrs, "clearImage")) do
|
||
|
|
Map.put(updates, :image_data, "")
|
||
|
|
else
|
||
|
|
updates
|
||
|
|
end
|
||
|
|
|
||
|
|
case Map.pop(updates, :_error) do
|
||
|
|
{nil, sanitized} ->
|
||
|
|
if byte_size(Map.get(sanitized, :image_data, "")) > 2_500_000 do
|
||
|
|
%{_error: "image payload too large"}
|
||
|
|
else
|
||
|
|
sanitized
|
||
|
|
end
|
||
|
|
|
||
|
|
{message, _} ->
|
||
|
|
%{_error: message}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
defp normalize_group(group), do: group |> to_string() |> String.trim()
|
||
|
|
|
||
|
|
defp truthy?(value) do
|
||
|
|
case value do
|
||
|
|
true -> true
|
||
|
|
1 -> true
|
||
|
|
"1" -> true
|
||
|
|
"true" -> true
|
||
|
|
"yes" -> true
|
||
|
|
"on" -> true
|
||
|
|
_ -> false
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
defp format_changeset_errors(changeset) do
|
||
|
|
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|
||
|
|
|> Enum.map(fn {field, msgs} -> "#{field} #{Enum.join(msgs, ",")}" end)
|
||
|
|
|> Enum.join("; ")
|
||
|
|
end
|
||
|
|
end
|