Skip to content

Commit 8f0ceba

Browse files
authored
feat: bounty page (#78)
1 parent 4f0cc0b commit 8f0ceba

File tree

23 files changed

+1144
-60
lines changed

23 files changed

+1144
-60
lines changed

config/dev.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,5 @@ config :algora,
142142
plausible_url: System.get_env("PLAUSIBLE_URL"),
143143
assets_url: System.get_env("ASSETS_URL"),
144144
ingest_url: System.get_env("INGEST_URL")
145+
146+
config :algora, AlgoraWeb.OGImageController, max_age: 0

config/prod.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,7 @@ config :algora,
3232
assets_url: System.get_env("ASSETS_URL"),
3333
ingest_url: System.get_env("INGEST_URL")
3434

35+
config :algora, AlgoraWeb.OGImageController, max_age: 600
36+
3537
# Runtime production configuration, including reading
3638
# of environment variables, is done on config/runtime.exs.

lib/algora/accounts/accounts.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ defmodule Algora.Accounts do
147147
type: u.type,
148148
id: u.id,
149149
handle: u.handle,
150+
provider_login: u.provider_login,
150151
name: u.name,
151152
provider_login: u.provider_login,
152153
provider_meta: u.provider_meta,

lib/algora/admin/admin.ex

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ defmodule Algora.Admin do
22
@moduledoc false
33
import Ecto.Query
44

5+
alias Algora.Accounts
56
alias Algora.Accounts.User
67
alias Algora.Activities.SendDiscord
78
alias Algora.Bounties.Claim
@@ -17,6 +18,57 @@ defmodule Algora.Admin do
1718

1819
require Logger
1920

21+
def seed do
22+
import Algora.Factory
23+
24+
title = "Monthly PR Review & Triage Bounty"
25+
26+
description = """
27+
## What needs doing
28+
29+
Review open pull requests across our repos and:
30+
31+
1. Test them locally
32+
2. Leave helpful comments
33+
3. Merge or close stale PRs
34+
4. Tag relevant people when needed
35+
36+
## Success looks like
37+
38+
- [ ] All PRs older than 2 weeks have been reviewed
39+
- [ ] Clear comments left on PRs needing changes
40+
- [ ] Stale PRs (>30 days old) closed with explanation
41+
- [ ] Weekly summary posted in #dev channel
42+
"""
43+
44+
org = Repo.get_by!(User, handle: "piedpiper0")
45+
user = Repo.get_by!(User, handle: "zcesur")
46+
47+
repository = insert!(:repository, user: org)
48+
ticket = insert!(:ticket, title: title, repository: repository, description: description)
49+
bounty = insert!(:bounty, ticket: ticket, owner: org, creator: user, amount: Money.new(1000, :USD))
50+
51+
for contributor <- Accounts.list_featured_developers() do
52+
insert!(:transaction,
53+
type: :debit,
54+
net_amount: Money.new(1000, :USD),
55+
user_id: org.id,
56+
bounty_id: bounty.id,
57+
status: :succeeded
58+
)
59+
60+
insert!(:transaction,
61+
type: :credit,
62+
net_amount: Money.new(1000, :USD),
63+
user_id: contributor.id,
64+
bounty_id: bounty.id,
65+
status: :succeeded
66+
)
67+
end
68+
69+
IO.puts("#{AlgoraWeb.Endpoint.url()}/org/#{org.handle}/bounties/#{bounty.id}")
70+
end
71+
2072
def magic(:email, email, return_to),
2173
do: AlgoraWeb.Endpoint.url() <> AlgoraWeb.UserAuth.generate_login_path(email, return_to)
2274

lib/algora/bounties/bounties.ex

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ defmodule Algora.Bounties do
2727
def base_query, do: Bounty
2828

2929
@type criterion ::
30-
{:limit, non_neg_integer() | :infinity}
30+
{:id, String.t()}
31+
| {:limit, non_neg_integer() | :infinity}
3132
| {:ticket_id, String.t()}
3233
| {:owner_id, String.t()}
3334
| {:status, :open | :paid}
@@ -695,15 +696,16 @@ defmodule Algora.Bounties do
695696
bounty_id: String.t(),
696697
claims: [Claim.t()]
697698
},
698-
opts :: [ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()}]
699+
opts :: [ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()}, recipient: User.t()]
699700
) ::
700701
{:ok, String.t()} | {:error, atom()}
701702
def reward_bounty(%{owner: owner, amount: amount, bounty_id: bounty_id, claims: claims}, opts \\ []) do
702703
create_payment_session(
703704
%{owner: owner, amount: amount, description: "Bounty payment for OSS contributions"},
704705
ticket_ref: opts[:ticket_ref],
705706
bounty_id: bounty_id,
706-
claims: claims
707+
claims: claims,
708+
recipient: opts[:recipient]
707709
)
708710
end
709711

@@ -997,6 +999,9 @@ defmodule Algora.Bounties do
997999
@spec apply_criteria(Ecto.Queryable.t(), [criterion()]) :: Ecto.Queryable.t()
9981000
defp apply_criteria(query, criteria) do
9991001
Enum.reduce(criteria, query, fn
1002+
{:id, id}, query ->
1003+
from([b] in query, where: b.id == ^id)
1004+
10001005
{:limit, :infinity}, query ->
10011006
query
10021007

lib/algora/bounties/schemas/bounty.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ defmodule Algora.Bounties.Bounty do
1414
field :autopay_disabled, :boolean, default: false
1515
field :visibility, Ecto.Enum, values: [:community, :exclusive, :public], null: false, default: :community
1616
field :shared_with, {:array, :string}, null: false, default: []
17+
field :deadline, :utc_datetime_usec
1718

1819
belongs_to :ticket, Algora.Workspace.Ticket
1920
belongs_to :owner, User
@@ -42,6 +43,13 @@ defmodule Algora.Bounties.Bounty do
4243
|> Algora.Validations.validate_money_positive(:amount)
4344
end
4445

46+
def settings_changeset(bounty, attrs) do
47+
bounty
48+
|> cast(attrs, [:visibility, :shared_with, :deadline])
49+
|> Algora.Validations.validate_date_in_future(:deadline)
50+
|> validate_required([:visibility, :shared_with])
51+
end
52+
4553
def url(%{repository: %{name: name, owner: %{login: login}}, ticket: %{provider: "github", number: number}}) do
4654
"https://github.com/#{login}/#{name}/issues/#{number}"
4755
end

lib/algora/chat/chat.ex

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ defmodule Algora.Chat do
88
alias Algora.Chat.Thread
99
alias Algora.Repo
1010

11-
def broadcast(message) do
12-
Phoenix.PubSub.broadcast(Algora.PubSub, "chat:thread:#{message.thread_id}", message)
11+
defmodule MessageCreated do
12+
@moduledoc false
13+
defstruct message: nil, participant: nil
14+
end
15+
16+
def broadcast(%MessageCreated{} = event) do
17+
Phoenix.PubSub.broadcast(Algora.PubSub, "chat:thread:#{event.message.thread_id}", event)
1318
end
1419

1520
def subscribe(thread_id) do
@@ -23,7 +28,6 @@ defmodule Algora.Chat do
2328
|> Thread.changeset(%{title: "#{User.handle(user_1)} <> #{User.handle(user_2)}"})
2429
|> Repo.insert()
2530

26-
# Add participants
2731
for user <- [user_1, user_2] do
2832
%Participant{}
2933
|> Participant.changeset(%{
@@ -46,7 +50,7 @@ defmodule Algora.Chat do
4650
|> Repo.insert()
4751

4852
participants = Enum.uniq_by([user | admins], & &1.id)
49-
# Add participants
53+
5054
for u <- participants do
5155
%Participant{}
5256
|> Participant.changeset(%{
@@ -61,20 +65,41 @@ defmodule Algora.Chat do
6165
end)
6266
end
6367

68+
defp ensure_participant(thread_id, user_id) do
69+
case Repo.fetch_by(Participant, thread_id: thread_id, user_id: user_id) do
70+
{:ok, participant} ->
71+
{:ok, participant}
72+
73+
{:error, _} ->
74+
%Participant{}
75+
|> Participant.changeset(%{
76+
thread_id: thread_id,
77+
user_id: user_id,
78+
last_read_at: DateTime.utc_now()
79+
})
80+
|> Repo.insert()
81+
end
82+
end
83+
84+
defp insert_message(thread_id, sender_id, content) do
85+
%Message{}
86+
|> Message.changeset(%{
87+
thread_id: thread_id,
88+
sender_id: sender_id,
89+
content: content
90+
})
91+
|> Repo.insert()
92+
end
93+
6494
def send_message(thread_id, sender_id, content) do
65-
case %Message{}
66-
|> Message.changeset(%{
67-
thread_id: thread_id,
68-
sender_id: sender_id,
69-
content: content
70-
})
71-
|> Repo.insert() do
72-
{:ok, message} ->
73-
message |> Repo.preload(:sender) |> broadcast()
74-
{:ok, message}
75-
76-
{:error, changeset} ->
77-
{:error, changeset}
95+
with {:ok, participant} <- ensure_participant(thread_id, sender_id),
96+
{:ok, message} <- insert_message(thread_id, sender_id, content) do
97+
broadcast(%MessageCreated{
98+
message: Repo.preload(message, :sender),
99+
participant: Repo.preload(participant, :user)
100+
})
101+
102+
{:ok, message}
78103
end
79104
end
80105

@@ -107,6 +132,12 @@ defmodule Algora.Chat do
107132
|> Repo.all()
108133
end
109134

135+
def list_participants(thread_id) do
136+
Participant
137+
|> where(thread_id: ^thread_id)
138+
|> Repo.all()
139+
end
140+
110141
def mark_as_read(thread_id, user_id) do
111142
Participant
112143
|> where(thread_id: ^thread_id, user_id: ^user_id)
@@ -145,4 +176,16 @@ defmodule Algora.Chat do
145176
thread -> {:ok, thread}
146177
end
147178
end
179+
180+
def get_or_create_bounty_thread(bounty) do
181+
case Repo.fetch_by(Thread, bounty_id: bounty.id) do
182+
{:ok, thread} ->
183+
{:ok, thread}
184+
185+
{:error, _} ->
186+
%Thread{}
187+
|> Thread.changeset(%{title: "Contributor chat", bounty_id: bounty.id})
188+
|> Repo.insert()
189+
end
190+
end
148191
end

lib/algora/chat/schemas/thread.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ defmodule Algora.Chat.Thread do
66

77
typed_schema "threads" do
88
field :title, :string
9-
9+
field :bounty_id, :string
1010
has_many :messages, Algora.Chat.Message
1111
has_many :participants, Algora.Chat.Participant
1212
has_many :activities, {"thread_activities", Activity}, foreign_key: :assoc_id
@@ -16,8 +16,9 @@ defmodule Algora.Chat.Thread do
1616

1717
def changeset(thread, attrs) do
1818
thread
19-
|> cast(attrs, [:title])
19+
|> cast(attrs, [:title, :bounty_id])
2020
|> validate_required([:title])
2121
|> generate_id()
22+
|> unique_constraint(:bounty_id)
2223
end
2324
end

lib/algora/shared/validations.ex

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,14 @@ defmodule Algora.Validations do
3939
_ -> changeset
4040
end
4141
end
42+
43+
def validate_date_in_future(changeset, field) do
44+
validate_change(changeset, field, fn _, date ->
45+
if date && Date.before?(date, DateTime.utc_now()) do
46+
[{field, "must be in the future"}]
47+
else
48+
[]
49+
end
50+
end)
51+
end
4252
end

lib/algora/workspace/workspace.ex

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,13 @@ defmodule Algora.Workspace do
200200
end
201201
end
202202

203+
def ensure_user_by_provider_id(token, provider_id) do
204+
case Repo.get_by(User, provider: "github", provider_id: provider_id) do
205+
%User{} = user -> {:ok, user}
206+
nil -> create_user_from_github(token, provider_id)
207+
end
208+
end
209+
203210
def sync_user(user, repository, owner, repo) do
204211
github_user = repository["owner"]
205212

0 commit comments

Comments
 (0)