4.8 KiB · text 5af55d9
defmodule GitGud.Profiles do
@moduledoc """
User + organization profile concerns that don't belong on the account
or org context modules — currently just pinned repositories.
"""
import Ecto.Query, warn: false
alias GitGud.Accounts.User
alias GitGud.Organizations.Organization
alias GitGud.Profiles.Pin
alias GitGud.Repo
alias GitGud.Repositories.Repository
@max_pins 7
@doc "Maximum number of pinned repositories per user/org."
def max_pins, do: @max_pins
@doc "List pins for a user or organization, ordered by `position` asc."
def list_pins(%User{id: uid}) do
Pin
|> where([p], p.owner_id == ^uid)
|> order_by([p], asc: p.position, asc: p.id)
|> preload(repository: [:owner, :organization])
|> Repo.all()
end
def list_pins(%Organization{id: oid}) do
Pin
|> where([p], p.organization_id == ^oid)
|> order_by([p], asc: p.position, asc: p.id)
|> preload(repository: [:owner, :organization])
|> Repo.all()
end
@doc """
Pin a repository for `owner`. Returns `{:ok, pin}` or
`{:error, :max_reached | %Ecto.Changeset{}}`.
Append-only — new pins go to the end (highest existing position + 1).
Caller can reorder afterwards via `reorder/2`.
"""
def pin(%User{} = user, %Repository{} = repo) do
do_pin(:owner_id, user.id, repo.id)
end
def pin(%Organization{} = org, %Repository{} = repo) do
do_pin(:organization_id, org.id, repo.id)
end
defp do_pin(owner_key, owner_id, repo_id) do
existing = count(owner_key, owner_id)
cond do
existing >= @max_pins ->
{:error, :max_reached}
true ->
attrs =
Map.new([
{owner_key, owner_id},
{:repository_id, repo_id},
{:position, existing}
])
%Pin{} |> Pin.changeset(attrs) |> Repo.insert()
end
end
@doc """
Drop every pin `user` has on repos belonging to `org`. Called when a
user loses membership — they shouldn't be left advertising org work
they no longer have access to. Compacts positions afterwards.
"""
def unpin_org_repos(%User{id: uid}, %Organization{id: oid}) do
deleted =
Repo.delete_all(
from p in Pin,
where:
p.owner_id == ^uid and
p.repository_id in subquery(
from r in Repository, where: r.organization_id == ^oid, select: r.id
)
)
_ = compact_positions(:owner_id, uid)
deleted
end
@doc "Remove the pin for `owner` + `repo`. No-op if not pinned."
def unpin(%User{id: uid}, %Repository{id: rid}) do
do_unpin(:owner_id, uid, rid)
end
def unpin(%Organization{id: oid}, %Repository{id: rid}) do
do_unpin(:organization_id, oid, rid)
end
defp do_unpin(owner_key, owner_id, repo_id) do
deleted =
Repo.delete_all(
from p in Pin,
where: field(p, ^owner_key) == ^owner_id and p.repository_id == ^repo_id
)
_ = compact_positions(owner_key, owner_id)
deleted
end
@doc """
Reorder `owner`'s pins. `pin_ids` is the desired order — any pins not
in the list keep their relative order at the end. Pin ids that don't
belong to `owner` are silently dropped.
"""
def reorder(%User{id: uid}, pin_ids), do: do_reorder(:owner_id, uid, pin_ids)
def reorder(%Organization{id: oid}, pin_ids),
do: do_reorder(:organization_id, oid, pin_ids)
defp do_reorder(owner_key, owner_id, pin_ids) when is_list(pin_ids) do
pins =
from(p in Pin, where: field(p, ^owner_key) == ^owner_id)
|> Repo.all()
valid_id_set = MapSet.new(pins, & &1.id)
requested =
pin_ids
|> Enum.map(&to_int/1)
|> Enum.filter(&(&1 != nil and MapSet.member?(valid_id_set, &1)))
|> Enum.uniq()
untouched =
pins
|> Enum.reject(&(&1.id in requested))
|> Enum.sort_by(&{&1.position, &1.id})
|> Enum.map(& &1.id)
final_order = requested ++ untouched
Repo.transaction(fn ->
Enum.with_index(final_order)
|> Enum.each(fn {pin_id, pos} ->
Repo.update_all(
from(p in Pin, where: p.id == ^pin_id),
set: [position: pos]
)
end)
end)
:ok
end
defp count(owner_key, owner_id) do
Repo.one(
from p in Pin,
where: field(p, ^owner_key) == ^owner_id,
select: count(p.id)
)
end
defp compact_positions(owner_key, owner_id) do
pins =
from(p in Pin, where: field(p, ^owner_key) == ^owner_id, order_by: [asc: p.position, asc: p.id])
|> Repo.all()
Repo.transaction(fn ->
Enum.with_index(pins)
|> Enum.each(fn {%Pin{id: id}, pos} ->
Repo.update_all(from(p in Pin, where: p.id == ^id), set: [position: pos])
end)
end)
:ok
end
defp to_int(v) when is_integer(v), do: v
defp to_int(v) when is_binary(v) do
case Integer.parse(v) do
{n, ""} -> n
_ -> nil
end
end
defp to_int(_), do: nil
end