Skip to content

Commit 7fc56b2

Browse files
committed
feat: implement stargazer import functionality and UI integration
1 parent c2e8615 commit 7fc56b2

File tree

6 files changed

+204
-7
lines changed

6 files changed

+204
-7
lines changed

lib/algora/admin/admin.ex

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ defmodule Algora.Admin do
2020
alias Algora.Util
2121
alias Algora.Workspace
2222
alias Algora.Workspace.Installation
23+
alias Algora.Workspace.Jobs.FetchTopContributions
24+
alias Algora.Workspace.Jobs.ImportStargazer
2325
alias Algora.Workspace.Repository
2426
alias Algora.Workspace.Ticket
2527

@@ -110,7 +112,7 @@ defmodule Algora.Admin do
110112
|> Repo.stream()
111113
|> Enum.each(fn user ->
112114
%{provider_login: user.provider_login}
113-
|> Workspace.Jobs.FetchTopContributions.new()
115+
|> FetchTopContributions.new()
114116
|> Oban.insert()
115117
|> case do
116118
{:ok, _job} -> IO.puts("Enqueued job for #{user.provider_login}")
@@ -1003,4 +1005,55 @@ defmodule Algora.Admin do
10031005
{:error, :backfill_incomplete}
10041006
end
10051007
end
1008+
1009+
def import_stargazers(repo_url, max_pages \\ nil) when is_binary(repo_url) do
1010+
with {:ok, [owner: repo_owner, repo: repo_name]} <- parse_repo_url(repo_url),
1011+
{:ok, repo} <- Workspace.ensure_repository(token(), repo_owner, repo_name) do
1012+
fetch_and_import_stargazers(repo_owner, repo_name, repo.id, 1, max_pages)
1013+
else
1014+
error ->
1015+
Logger.error("Failed to import stargazers: #{inspect(error)}")
1016+
{:error, :failed_to_import_stargazers}
1017+
end
1018+
end
1019+
1020+
defp fetch_and_import_stargazers(repo_owner, repo_name, repo_id, page, max_pages) do
1021+
if max_pages && page > max_pages do
1022+
Logger.info("Reached maximum page number #{max_pages}, stopping import")
1023+
:ok
1024+
else
1025+
url = "/repos/#{repo_owner}/#{repo_name}/stargazers?per_page=3&page=#{page}"
1026+
1027+
case Github.Client.fetch(token(), url) do
1028+
{:ok, []} ->
1029+
Logger.info("Finished importing stargazers - no more pages")
1030+
:ok
1031+
1032+
{:ok, stargazers} ->
1033+
Enum.each(stargazers, fn user ->
1034+
%{provider_login: user["login"], repo_id: repo_id}
1035+
|> ImportStargazer.new()
1036+
|> Oban.insert()
1037+
|> case do
1038+
{:ok, _job} -> Logger.info("Enqueued job for #{user["login"]}")
1039+
{:error, error} -> Logger.error("Failed to enqueue job for #{user["login"]}: #{inspect(error)}")
1040+
end
1041+
end)
1042+
1043+
# Process next page
1044+
fetch_and_import_stargazers(repo_owner, repo_name, repo_id, page + 1, max_pages)
1045+
1046+
error ->
1047+
Logger.error("Failed to fetch stargazers page #{page}: #{inspect(error)}")
1048+
{:error, :failed_to_fetch_stargazers}
1049+
end
1050+
end
1051+
end
1052+
1053+
defp parse_repo_url(url) do
1054+
case Regex.run(~r{github\.com/([^/]+)/([^/]+)}, url) do
1055+
[_, owner, repo] -> {:ok, [owner: owner, repo: String.replace(repo, ".git", "")]}
1056+
_ -> {:error, :invalid_repo_url}
1057+
end
1058+
end
10061059
end
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
defmodule Algora.Workspace.Jobs.ImportStargazer do
2+
@moduledoc false
3+
use Oban.Worker,
4+
queue: :fetch_top_contributions,
5+
max_attempts: 3,
6+
# 30 days
7+
unique: [period: 30 * 24 * 60 * 60, fields: [:args]]
8+
9+
alias Algora.Github
10+
alias Algora.Repo
11+
alias Algora.Workspace.Stargazer
12+
13+
@impl Oban.Worker
14+
def perform(%Oban.Job{args: %{"provider_login" => provider_login, "repo_id" => repo_id}}) do
15+
with {:ok, user} <- Algora.Workspace.fetch_top_contributions_async(Github.TokenPool.get_token(), provider_login) do
16+
%Stargazer{}
17+
|> Stargazer.changeset(%{user_id: user.id, repository_id: repo_id})
18+
|> Repo.insert()
19+
end
20+
end
21+
22+
def timeout(_), do: :timer.seconds(30)
23+
end
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
defmodule Algora.Workspace.Stargazer do
2+
@moduledoc false
3+
use Algora.Schema
4+
5+
alias Algora.Accounts.User
6+
alias Algora.Workspace.Repository
7+
8+
typed_schema "stargazers" do
9+
belongs_to :repository, Repository
10+
belongs_to :user, User
11+
12+
timestamps()
13+
end
14+
15+
def changeset(stargazer, params) do
16+
stargazer
17+
|> cast(params, [:repository_id, :user_id])
18+
|> validate_required([:repository_id, :user_id])
19+
|> foreign_key_constraint(:repository_id)
20+
|> foreign_key_constraint(:user_id)
21+
|> unique_constraint([:repository_id, :user_id])
22+
|> generate_id()
23+
end
24+
25+
def filter_by_repository_id(query, nil), do: query
26+
27+
def filter_by_repository_id(query, repository_id) do
28+
from c in query,
29+
join: r in assoc(c, :repository),
30+
where: r.id == ^repository_id
31+
end
32+
end

lib/algora/workspace/workspace.ex

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ defmodule Algora.Workspace do
1414
alias Algora.Workspace.Installation
1515
alias Algora.Workspace.Jobs
1616
alias Algora.Workspace.Repository
17+
alias Algora.Workspace.Stargazer
1718
alias Algora.Workspace.Ticket
1819
alias Algora.Workspace.UserContribution
1920

@@ -569,6 +570,27 @@ defmodule Algora.Workspace do
569570
)
570571
end
571572

573+
def list_stargazers(id) do
574+
Repo.all(
575+
from(s in Stargazer,
576+
join: r in assoc(s, :repository),
577+
where: r.provider == "github",
578+
join: ro in assoc(r, :user),
579+
where: ro.id == ^id,
580+
join: u in assoc(s, :user),
581+
where: u.type != :bot,
582+
where: not ilike(u.provider_login, "%bot"),
583+
left_join: m in Member,
584+
on: m.user_id == u.id and m.org_id == r.user_id,
585+
where: is_nil(m.id),
586+
distinct: [s.user_id],
587+
select_merge: %{user: u},
588+
order_by: [desc: s.inserted_at, asc: s.id],
589+
limit: 100
590+
)
591+
)
592+
end
593+
572594
def fetch_contributor(repository_id, user_id) do
573595
Repo.fetch_by(Contributor, repository_id: repository_id, user_id: user_id)
574596
end
@@ -691,7 +713,7 @@ defmodule Algora.Workspace do
691713
with {:ok, contributions} <- Algora.Cloud.top_contributions(provider_login),
692714
{:ok, user} <- ensure_user(token, provider_login),
693715
{:ok, _} <- add_contributions_async(token, user.id, contributions) do
694-
{:ok, nil}
716+
{:ok, user}
695717
else
696718
{:error, reason} ->
697719
Logger.error("Failed to fetch contributions for #{provider_login}: #{inspect(reason)}")

lib/algora_web/live/org/job_live.ex

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,9 @@ defmodule AlgoraWeb.Org.JobLive do
242242
<%= for {tab, label, count} <- [
243243
{"applicants", "Applicants", length(@applicants)},
244244
{"imports", "Imports", length(@imports)},
245+
if(length(@stargazers) > 0, do: {"stargazers", "Stargazers", length(@stargazers)}, else: nil),
245246
{"matches", "Matches", length(@matches)}
246-
] do %>
247+
] |> Enum.reject(& is_nil(&1)) do %>
247248
<label class={[
248249
"group relative flex cursor-pointer rounded-lg px-4 py-2 shadow-sm focus:outline-none",
249250
"border-2 bg-background transition-all duration-200 hover:border-primary hover:bg-primary/10",
@@ -359,7 +360,7 @@ defmodule AlgoraWeb.Org.JobLive do
359360
</.card>
360361
<% else %>
361362
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
362-
<%= for match <- @matches |> Enum.take(if @current_org.hiring_subscription == :active, do: length(@matches), else: 15) do %>
363+
<%= for match <- @truncated_matches do %>
363364
<div>
364365
<.match_card
365366
user={match.user}
@@ -369,20 +370,54 @@ defmodule AlgoraWeb.Org.JobLive do
369370
/>
370371
</div>
371372
<% end %>
372-
<%= if @current_org.hiring_subscription != :active do %>
373+
<%= if @current_org.hiring_subscription != :active && length(@truncated_matches) > 0 do %>
373374
<div class="relative lg:col-span-3">
374375
<img
375376
src={~p"/images/screenshots/job-matches-more.png"}
376377
class="w-full aspect-[1368/398]"
377378
/>
378379
<div class="absolute inset-0 flex items-center font-bold text-foreground justify-center text-3xl md:text-4xl">
379-
+ {length(@matches) - 15} more matches
380+
+ {length(@matches) - length(@truncated_matches)} more matches
380381
</div>
381382
</div>
382383
<% end %>
383384
</div>
384385
<% end %>
385386
</.section>
387+
<% "stargazers" -> %>
388+
<.section title="Stargazers" subtitle="Top stargazers of your repositories">
389+
<:actions>
390+
<.button variant="default" phx-click="toggle_import_drawer">
391+
Import
392+
</.button>
393+
</:actions>
394+
<%= if Enum.empty?(@stargazers) do %>
395+
<.card class="rounded-lg bg-card py-12 text-center lg:rounded-[2rem]">
396+
<.card_header>
397+
<div class="mx-auto mb-2 rounded-full bg-muted p-4">
398+
<.icon name="tabler-users" class="h-8 w-8 text-muted-foreground" />
399+
</div>
400+
<.card_title>No stargazers yet</.card_title>
401+
<.card_description>
402+
Stargazers will appear here once you import your repositories
403+
</.card_description>
404+
</.card_header>
405+
</.card>
406+
<% else %>
407+
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
408+
<%= for stargazer <- @stargazers do %>
409+
<div>
410+
<.match_card
411+
user={stargazer.user}
412+
tech_stack={@job.tech_stack |> Enum.take(1)}
413+
contributions={Map.get(@contributions_map, stargazer.user.id, [])}
414+
contract_type="bring_your_own"
415+
/>
416+
</div>
417+
<% end %>
418+
</div>
419+
<% end %>
420+
</.section>
386421
<% end %>
387422
</div>
388423
@@ -859,19 +894,32 @@ defmodule AlgoraWeb.Org.JobLive do
859894

860895
defp assign_applicants(socket) do
861896
all_applicants = Jobs.list_job_applications(socket.assigns.job)
897+
stargazers = Algora.Workspace.list_stargazers(socket.assigns.current_org.id)
862898
applicants = Enum.reject(all_applicants, & &1.imported_at)
863899
imports = Enum.filter(all_applicants, & &1.imported_at)
864900
matches = Settings.get_job_matches(socket.assigns.job)
865901

866-
developers = matches |> Enum.concat(all_applicants) |> Enum.map(& &1.user)
902+
truncated_matches =
903+
case Code.ensure_compiled(AlgoraCloud) do
904+
{:module, _} -> AlgoraCloud.truncate_matches(socket.assigns.current_org, matches)
905+
_ -> Enum.take(matches, 3)
906+
end
907+
908+
developers =
909+
matches
910+
|> Enum.concat(all_applicants)
911+
|> Enum.concat(stargazers)
912+
|> Enum.map(& &1.user)
867913

868914
contributions_map = fetch_applicants_contributions(developers, socket.assigns.job.tech_stack)
869915

870916
socket
871917
|> assign(:developers, developers)
872918
|> assign(:applicants, sort_by_contributions(socket.assigns.job, applicants, contributions_map))
873919
|> assign(:imports, sort_by_contributions(socket.assigns.job, imports, contributions_map))
920+
|> assign(:stargazers, sort_by_contributions(socket.assigns.job, stargazers, contributions_map))
874921
|> assign(:matches, matches)
922+
|> assign(:truncated_matches, truncated_matches)
875923
|> assign(:contributions_map, contributions_map)
876924
end
877925

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
defmodule Algora.Repo.Migrations.CreateStargazers do
2+
use Ecto.Migration
3+
4+
def change do
5+
create table(:stargazers) do
6+
add :repository_id, references(:repositories, on_delete: :delete_all), null: false
7+
add :user_id, references(:users, on_delete: :delete_all), null: false
8+
9+
timestamps()
10+
end
11+
12+
# Create an index for the foreign keys
13+
create index(:stargazers, [:repository_id])
14+
create index(:stargazers, [:user_id])
15+
16+
# Create a unique index to prevent duplicate stars
17+
create unique_index(:stargazers, [:repository_id, :user_id])
18+
end
19+
end

0 commit comments

Comments
 (0)