From ae48435945067d2d42dbe2afe5d45b23e0c44ab3 Mon Sep 17 00:00:00 2001 From: zafer Date: Fri, 28 Mar 2025 02:08:27 +0200 Subject: [PATCH 1/5] init --- lib/algora/bounties/bounties.ex | 3 + .../controllers/api/bounty_controller.ex | 122 ++++++++++++++++++ lib/algora_web/controllers/api/bounty_json.ex | 81 ++++++++++++ lib/algora_web/controllers/api/error_json.ex | 30 +++++ .../controllers/api/fallback_controller.ex | 34 +++++ lib/algora_web/router.ex | 11 ++ 6 files changed, 281 insertions(+) create mode 100644 lib/algora_web/controllers/api/bounty_controller.ex create mode 100644 lib/algora_web/controllers/api/bounty_json.ex create mode 100644 lib/algora_web/controllers/api/error_json.ex create mode 100644 lib/algora_web/controllers/api/fallback_controller.ex diff --git a/lib/algora/bounties/bounties.ex b/lib/algora/bounties/bounties.ex index 48ddfc130..ca55b9e86 100644 --- a/lib/algora/bounties/bounties.ex +++ b/lib/algora/bounties/bounties.ex @@ -1007,6 +1007,9 @@ defmodule Algora.Bounties do {:owner_id, owner_id}, query -> from([b, r: r] in query, where: b.owner_id == ^owner_id or r.user_id == ^owner_id) + {:owner_handle, owner_handle}, query -> + from([b, o: o] in query, where: o.handle == ^owner_handle) + {:status, status}, query -> query = where(query, [b], b.status == ^status) diff --git a/lib/algora_web/controllers/api/bounty_controller.ex b/lib/algora_web/controllers/api/bounty_controller.ex new file mode 100644 index 000000000..55b8d50f3 --- /dev/null +++ b/lib/algora_web/controllers/api/bounty_controller.ex @@ -0,0 +1,122 @@ +defmodule AlgoraWeb.API.BountyController do + use AlgoraWeb, :controller + + alias Algora.Bounties + alias Algora.Bounties.Bounty + alias AlgoraWeb.API.FallbackController + + action_fallback FallbackController + + @doc """ + Get a list of bounties with optional filtering parameters. + + Query Parameters: + - type: string (optional) - Filter by bounty type (e.g., "standard") + - kind: string (optional) - Filter by bounty kind (e.g., "dev") + - reward_type: string (optional) - Filter by reward type (e.g., "cash") + - visibility: string (optional) - Filter by visibility (e.g., "public") + - status: string (optional) - Filter by status (open, cancelled, paid) + """ + def index(conn, %{"batch" => _batch, "input" => input} = _params) do + with {:ok, decoded} <- Jason.decode(input), + %{"0" => %{"json" => json}} <- decoded do + criteria = to_criteria(json) + bounties = Bounties.list_bounties(criteria) + render(conn, :index, bounties: bounties) + end + end + + def index(conn, params) do + criteria = to_criteria(params) + bounties = Bounties.list_bounties(criteria) + render(conn, :index, bounties: bounties) + end + + # Convert JSON/map parameters to keyword list criteria + defp to_criteria(params) when is_map(params) do + Keyword.new(params, fn + {"status", status} -> {:status, parse_status(status)} + {"visibility", visibility} -> {:visibility, parse_visibility(visibility)} + {"org", org_handle} -> {:owner_handle, org_handle} + {k, v} -> {String.to_existing_atom(k), v} + end) + rescue + _ -> [] + end + + defp to_criteria(_), do: [] + + # Parse status string to corresponding enum atom + defp parse_status(status) when is_binary(status) do + case String.downcase(status) do + "active" -> :open + "open" -> :open + "cancelled" -> :cancelled + "canceled" -> :cancelled + "paid" -> :paid + _ -> :open + end + end + + defp parse_status(_), do: :open + + # Parse visibility string to corresponding enum atom + defp parse_visibility(visibility) when is_binary(visibility) do + case String.downcase(visibility) do + "public" -> :public + "community" -> :community + "exclusive" -> :exclusive + _ -> :public + end + end + + defp parse_visibility(_), do: :public + + @doc """ + Get a specific bounty by ID. + """ + def show(conn, %{"id" => id}) do + with {:ok, bounty} <- Bounties.get_bounty(id) do + render(conn, :show, bounty: bounty) + end + end + + @doc """ + Create a new bounty. + + Required Parameters: + - amount: integer - Bounty amount in cents + - ticket_id: string - Associated ticket ID + - type: string - Bounty type (e.g., "standard") + - kind: string - Bounty kind (e.g., "dev") + - reward_type: string - Type of reward (e.g., "cash") + - visibility: string - Visibility setting (e.g., "public") + """ + def create(conn, params) do + with {:ok, %Bounty{} = bounty} <- Bounties.create_bounty(params) do + conn + |> put_status(:created) + |> render(:show, bounty: bounty) + end + end + + @doc """ + Update an existing bounty. + """ + def update(conn, %{"id" => id} = params) do + with {:ok, bounty} <- Bounties.get_bounty(id), + {:ok, updated_bounty} <- Bounties.update_bounty(bounty, params) do + render(conn, :show, bounty: updated_bounty) + end + end + + @doc """ + Delete a bounty. + """ + def delete(conn, %{"id" => id}) do + with {:ok, bounty} <- Bounties.get_bounty(id), + {:ok, _deleted} <- Bounties.delete_bounty(bounty) do + send_resp(conn, :no_content, "") + end + end +end diff --git a/lib/algora_web/controllers/api/bounty_json.ex b/lib/algora_web/controllers/api/bounty_json.ex new file mode 100644 index 000000000..eec83c4f2 --- /dev/null +++ b/lib/algora_web/controllers/api/bounty_json.ex @@ -0,0 +1,81 @@ +defmodule AlgoraWeb.API.BountyJSON do + alias Algora.Bounties.Bounty + + @doc """ + Renders a list of bounties. + """ + def index(%{bounties: bounties}) do + %{data: for(bounty <- bounties, do: data(bounty))} + end + + @doc """ + Renders a single bounty. + """ + def show(%{bounty: bounty}) do + %{data: data(bounty)} + end + + defp data(%Bounty{} = bounty) do + %{ + id: bounty.id, + amount: bounty.amount, + status: bounty.status, + type: "standard", + kind: "dev", + reward_type: "cash", + visibility: bounty.visibility, + autopay_disabled: bounty.autopay_disabled, + timeouts_disabled: true, + manual_assignments: false, + created_at: bounty.inserted_at, + updated_at: bounty.updated_at, + ticket: ticket_data(bounty.ticket), + owner: user_data(bounty.owner), + creator: user_data(bounty.creator) + } + end + + defp data(%{} = bounty) do + %{ + id: bounty.id, + amount: bounty.amount, + status: bounty.status, + type: "standard", + kind: "dev", + reward_type: "cash", + visibility: Map.get(bounty, :visibility, :public), + autopay_disabled: Map.get(bounty, :autopay_disabled, false), + timeouts_disabled: true, + manual_assignments: false, + created_at: bounty.inserted_at, + updated_at: Map.get(bounty, :updated_at, bounty.inserted_at), + ticket: ticket_data(bounty.ticket), + owner: user_data(bounty.owner), + creator: user_data(Map.get(bounty, :creator)) + } + end + + defp ticket_data(nil), do: nil + + defp ticket_data(ticket) do + %{ + id: ticket.id, + url: ticket.url, + number: ticket.number, + # provider: ticket.provider, + # hash: ticket.hash, + tech: [] + } + end + + defp user_data(nil), do: nil + + defp user_data(user) do + %{ + id: user.id, + handle: user.handle, + name: user.name, + avatar_url: user.avatar_url + } + end +end diff --git a/lib/algora_web/controllers/api/error_json.ex b/lib/algora_web/controllers/api/error_json.ex new file mode 100644 index 000000000..748de622c --- /dev/null +++ b/lib/algora_web/controllers/api/error_json.ex @@ -0,0 +1,30 @@ +defmodule AlgoraWeb.API.ErrorJSON do + @doc """ + Renders changeset errors. + """ + def error(%{changeset: changeset}) do + %{ + errors: Ecto.Changeset.traverse_errors(changeset, &translate_error/1) + } + end + + @doc """ + Renders error message. + """ + def error(%{message: message}) do + %{error: message} + end + + defp translate_error({msg, opts}) do + Enum.reduce(opts, msg, fn + {key, value}, acc when is_binary(value) -> + String.replace(acc, "%{#{key}}", value) + + {key, value}, acc when is_integer(value) -> + String.replace(acc, "%{#{key}}", Integer.to_string(value)) + + {key, value}, acc -> + String.replace(acc, "%{#{key}}", inspect(value)) + end) + end +end diff --git a/lib/algora_web/controllers/api/fallback_controller.ex b/lib/algora_web/controllers/api/fallback_controller.ex new file mode 100644 index 000000000..249b4109a --- /dev/null +++ b/lib/algora_web/controllers/api/fallback_controller.ex @@ -0,0 +1,34 @@ +defmodule AlgoraWeb.API.FallbackController do + use AlgoraWeb, :controller + + alias AlgoraWeb.API.ErrorJSON + + def call(conn, {:error, %Ecto.Changeset{} = changeset}) do + conn + |> put_status(:unprocessable_entity) + |> put_view(json: ErrorJSON) + |> render(:error, changeset: changeset) + end + + def call(conn, {:error, :not_found}) do + conn + |> put_status(:not_found) + |> put_view(json: ErrorJSON) + |> render(:error, message: "Not found") + end + + def call(conn, {:error, :unauthorized}) do + conn + |> put_status(:unauthorized) + |> put_view(json: ErrorJSON) + |> render(:error, message: "Unauthorized") + end + + # Catch-all error handler + def call(conn, {:error, error}) do + conn + |> put_status(:bad_request) + |> put_view(json: ErrorJSON) + |> render(:error, message: to_string(error)) + end +end diff --git a/lib/algora_web/router.ex b/lib/algora_web/router.ex index 9393828a7..e3d46a1c3 100644 --- a/lib/algora_web/router.ex +++ b/lib/algora_web/router.ex @@ -158,6 +158,17 @@ defmodule AlgoraWeb.Router do end end + scope "/api", AlgoraWeb.API do + pipe_through :api + + # Legacy tRPC endpoints + get "/trpc/bounty.list", BountyController, :index + post "/trpc/bounty.list", BountyController, :index + + # REST endpoints + resources "/bounties", BountyController, except: [:new, :edit] + end + # Other scopes may use custom stacks. # scope "/api", AlgoraWeb do # pipe_through :api From cf8a8044067cb47baf0c4ff8f6abcaaf682893d8 Mon Sep 17 00:00:00 2001 From: zafer Date: Fri, 28 Mar 2025 15:18:19 +0200 Subject: [PATCH 2/5] refactor: restructure bounty and task data rendering in API - Updated the index and show functions to return data in a nested structure, including placeholders for pagination and additional fields. - Enhanced the data function to include new attributes such as point rewards, reward tiers, and external status. - Refactored task_data and org_data functions to accommodate new fields and improve data organization. - Added TODOs for future implementation of features like pagination, reward tiers, and member fetching. --- lib/algora_web/controllers/api/bounty_json.ex | 161 +++++++++++++++--- 1 file changed, 142 insertions(+), 19 deletions(-) diff --git a/lib/algora_web/controllers/api/bounty_json.ex b/lib/algora_web/controllers/api/bounty_json.ex index eec83c4f2..35cf5c6ea 100644 --- a/lib/algora_web/controllers/api/bounty_json.ex +++ b/lib/algora_web/controllers/api/bounty_json.ex @@ -5,77 +5,200 @@ defmodule AlgoraWeb.API.BountyJSON do Renders a list of bounties. """ def index(%{bounties: bounties}) do - %{data: for(bounty <- bounties, do: data(bounty))} + %{ + data: [ + %{ + result: %{ + data: %{ + json: %{ + # TODO: Implement pagination + next_cursor: nil, + items: for(bounty <- bounties, do: data(bounty)) + } + } + } + } + ] + } end @doc """ Renders a single bounty. """ def show(%{bounty: bounty}) do - %{data: data(bounty)} + %{ + data: [ + %{ + result: %{ + data: %{ + json: %{ + next_cursor: nil, + items: [data(bounty)] + } + } + } + } + ] + } end defp data(%Bounty{} = bounty) do %{ id: bounty.id, - amount: bounty.amount, + # TODO: Implement point rewards + point_reward: nil, + reward: %{ + amount: bounty.amount.amount, + currency: bounty.amount.currency + }, + reward_formatted: "#{bounty.amount.amount} #{bounty.amount.currency}", + # TODO: Implement reward tiers + reward_tiers: [], + tech: bounty.ticket.tech || [], status: bounty.status, + # TODO: Determine if bounty is external + is_external: false, + org: org_data(bounty.owner), + task: task_data(bounty.ticket), type: "standard", kind: "dev", reward_type: "cash", visibility: bounty.visibility, + # TODO: Implement bids + bids: [], autopay_disabled: bounty.autopay_disabled, timeouts_disabled: true, manual_assignments: false, created_at: bounty.inserted_at, - updated_at: bounty.updated_at, - ticket: ticket_data(bounty.ticket), - owner: user_data(bounty.owner), - creator: user_data(bounty.creator) + updated_at: bounty.updated_at } end defp data(%{} = bounty) do %{ id: bounty.id, - amount: bounty.amount, + point_reward: nil, + reward: %{ + amount: bounty.amount.amount, + currency: bounty.amount.currency + }, + reward_formatted: "#{bounty.amount.amount} #{bounty.amount.currency}", + reward_tiers: [], + tech: bounty.ticket.tech || [], status: bounty.status, + is_external: false, + org: org_data(bounty.owner), + task: task_data(bounty.ticket), type: "standard", kind: "dev", reward_type: "cash", visibility: Map.get(bounty, :visibility, :public), + bids: [], autopay_disabled: Map.get(bounty, :autopay_disabled, false), timeouts_disabled: true, manual_assignments: false, created_at: bounty.inserted_at, - updated_at: Map.get(bounty, :updated_at, bounty.inserted_at), - ticket: ticket_data(bounty.ticket), - owner: user_data(bounty.owner), - creator: user_data(Map.get(bounty, :creator)) + updated_at: Map.get(bounty, :updated_at, bounty.inserted_at) } end - defp ticket_data(nil), do: nil + defp task_data(nil), do: nil - defp ticket_data(ticket) do + defp task_data(ticket) do %{ id: ticket.id, - url: ticket.url, + # TODO: Make this dynamic based on ticket provider + forge: "github", + # TODO: Extract from ticket URL + repo_owner: "TODO", + # TODO: Extract from ticket URL + repo_name: "TODO", number: ticket.number, - # provider: ticket.provider, - # hash: ticket.hash, - tech: [] + source: %{ + # TODO: Make this dynamic based on ticket provider + type: "github", + data: %{ + id: ticket.id, + html_url: ticket.url, + # TODO: Add title to ticket schema + title: "TODO", + # TODO: Add body to ticket schema + body: "TODO", + user: %{ + # TODO: Add creator info to ticket schema + id: 0, + login: "TODO", + avatar_url: "TODO", + html_url: "TODO", + name: "TODO", + company: "TODO", + location: "TODO", + twitter_username: "TODO" + } + } + }, + # TODO: Map ticket status + status: "open", + # TODO: Add title to ticket schema + title: "TODO", + url: ticket.url, + # TODO: Add body to ticket schema + body: "TODO", + # TODO: Make this dynamic + type: "issue", + hash: ticket.hash || "", + tech: ticket.tech || [] } end + defp org_data(nil), do: nil + + defp org_data(org) do + %{ + id: org.id, + created_at: org.inserted_at, + handle: org.handle, + name: org.name, + display_name: org.display_name || org.name, + description: org.bio || "", + avatar_url: org.avatar_url, + website_url: org.website_url || "", + twitter_url: org.twitter_url || "", + youtube_url: org.youtube_url || "", + discord_url: org.discord_url || "", + slack_url: org.slack_url, + stargazers_count: org.stargazers_count || 0, + tech: org.tech_stack || [], + # TODO: Add to org schema + accepts_sponsorships: false, + members: members_data(org), + # TODO: Add to org schema + enabled_expert_recs: false, + # TODO: Add to org schema + enabled_private_bounties: false, + days_until_timeout: nil, + github_handle: org.provider_login + } + end + + defp members_data(org) do + # TODO: Implement actual member fetching + [] + end + defp user_data(nil), do: nil defp user_data(user) do %{ id: user.id, handle: user.handle, + image: user.avatar_url, name: user.name, - avatar_url: user.avatar_url + github_handle: user.provider_login, + youtube_handle: nil, + twitch_handle: nil, + # TODO: Implement org fetching for user + orgs: [] } end end From f590dbf3fbc63851fc69b09ccf9aa39d8f245db2 Mon Sep 17 00:00:00 2001 From: zafer Date: Fri, 28 Mar 2025 15:42:25 +0200 Subject: [PATCH 3/5] refactor: remove unused bounty actions and clean up API controllers - Eliminated the show, create, update, and delete actions from the BountyController to streamline the API. - Removed commented-out user_data function and adjusted members_data function for clarity. - Cleaned up error_json module by removing unnecessary documentation comments. --- .../controllers/api/bounty_controller.ex | 49 ------------------- lib/algora_web/controllers/api/bounty_json.ex | 30 ++++++------ lib/algora_web/controllers/api/error_json.ex | 6 --- lib/algora_web/router.ex | 3 -- 4 files changed, 15 insertions(+), 73 deletions(-) diff --git a/lib/algora_web/controllers/api/bounty_controller.ex b/lib/algora_web/controllers/api/bounty_controller.ex index 55b8d50f3..87e05b501 100644 --- a/lib/algora_web/controllers/api/bounty_controller.ex +++ b/lib/algora_web/controllers/api/bounty_controller.ex @@ -2,7 +2,6 @@ defmodule AlgoraWeb.API.BountyController do use AlgoraWeb, :controller alias Algora.Bounties - alias Algora.Bounties.Bounty alias AlgoraWeb.API.FallbackController action_fallback FallbackController @@ -71,52 +70,4 @@ defmodule AlgoraWeb.API.BountyController do end defp parse_visibility(_), do: :public - - @doc """ - Get a specific bounty by ID. - """ - def show(conn, %{"id" => id}) do - with {:ok, bounty} <- Bounties.get_bounty(id) do - render(conn, :show, bounty: bounty) - end - end - - @doc """ - Create a new bounty. - - Required Parameters: - - amount: integer - Bounty amount in cents - - ticket_id: string - Associated ticket ID - - type: string - Bounty type (e.g., "standard") - - kind: string - Bounty kind (e.g., "dev") - - reward_type: string - Type of reward (e.g., "cash") - - visibility: string - Visibility setting (e.g., "public") - """ - def create(conn, params) do - with {:ok, %Bounty{} = bounty} <- Bounties.create_bounty(params) do - conn - |> put_status(:created) - |> render(:show, bounty: bounty) - end - end - - @doc """ - Update an existing bounty. - """ - def update(conn, %{"id" => id} = params) do - with {:ok, bounty} <- Bounties.get_bounty(id), - {:ok, updated_bounty} <- Bounties.update_bounty(bounty, params) do - render(conn, :show, bounty: updated_bounty) - end - end - - @doc """ - Delete a bounty. - """ - def delete(conn, %{"id" => id}) do - with {:ok, bounty} <- Bounties.get_bounty(id), - {:ok, _deleted} <- Bounties.delete_bounty(bounty) do - send_resp(conn, :no_content, "") - end - end end diff --git a/lib/algora_web/controllers/api/bounty_json.ex b/lib/algora_web/controllers/api/bounty_json.ex index 35cf5c6ea..fadfb8636 100644 --- a/lib/algora_web/controllers/api/bounty_json.ex +++ b/lib/algora_web/controllers/api/bounty_json.ex @@ -181,24 +181,24 @@ defmodule AlgoraWeb.API.BountyJSON do } end - defp members_data(org) do + defp members_data(_org) do # TODO: Implement actual member fetching [] end - defp user_data(nil), do: nil + # defp user_data(nil), do: nil - defp user_data(user) do - %{ - id: user.id, - handle: user.handle, - image: user.avatar_url, - name: user.name, - github_handle: user.provider_login, - youtube_handle: nil, - twitch_handle: nil, - # TODO: Implement org fetching for user - orgs: [] - } - end + # defp user_data(user) do + # %{ + # id: user.id, + # handle: user.handle, + # image: user.avatar_url, + # name: user.name, + # github_handle: user.provider_login, + # youtube_handle: nil, + # twitch_handle: nil, + # # TODO: Implement org fetching for user + # orgs: [] + # } + # end end diff --git a/lib/algora_web/controllers/api/error_json.ex b/lib/algora_web/controllers/api/error_json.ex index 748de622c..3dfe962cd 100644 --- a/lib/algora_web/controllers/api/error_json.ex +++ b/lib/algora_web/controllers/api/error_json.ex @@ -1,16 +1,10 @@ defmodule AlgoraWeb.API.ErrorJSON do - @doc """ - Renders changeset errors. - """ def error(%{changeset: changeset}) do %{ errors: Ecto.Changeset.traverse_errors(changeset, &translate_error/1) } end - @doc """ - Renders error message. - """ def error(%{message: message}) do %{error: message} end diff --git a/lib/algora_web/router.ex b/lib/algora_web/router.ex index e3d46a1c3..6b224d4bc 100644 --- a/lib/algora_web/router.ex +++ b/lib/algora_web/router.ex @@ -164,9 +164,6 @@ defmodule AlgoraWeb.Router do # Legacy tRPC endpoints get "/trpc/bounty.list", BountyController, :index post "/trpc/bounty.list", BountyController, :index - - # REST endpoints - resources "/bounties", BountyController, except: [:new, :edit] end # Other scopes may use custom stacks. From bf16d55097873b5902ffc61677ff67295763caf3 Mon Sep 17 00:00:00 2001 From: zafer Date: Fri, 28 Mar 2025 15:54:12 +0200 Subject: [PATCH 4/5] refactor: simplify bounty filtering parameters in API - Removed unused filtering options for visibility and adjusted status filtering to include only 'open' and 'paid'. - Cleaned up the to_criteria function to eliminate unnecessary visibility parsing and streamline parameter handling. --- .../controllers/api/bounty_controller.ex | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/lib/algora_web/controllers/api/bounty_controller.ex b/lib/algora_web/controllers/api/bounty_controller.ex index 87e05b501..059586502 100644 --- a/lib/algora_web/controllers/api/bounty_controller.ex +++ b/lib/algora_web/controllers/api/bounty_controller.ex @@ -10,11 +10,7 @@ defmodule AlgoraWeb.API.BountyController do Get a list of bounties with optional filtering parameters. Query Parameters: - - type: string (optional) - Filter by bounty type (e.g., "standard") - - kind: string (optional) - Filter by bounty kind (e.g., "dev") - - reward_type: string (optional) - Filter by reward type (e.g., "cash") - - visibility: string (optional) - Filter by visibility (e.g., "public") - - status: string (optional) - Filter by status (open, cancelled, paid) + - status: string (optional) - Filter by status (open, paid) """ def index(conn, %{"batch" => _batch, "input" => input} = _params) do with {:ok, decoded} <- Jason.decode(input), @@ -33,12 +29,13 @@ defmodule AlgoraWeb.API.BountyController do # Convert JSON/map parameters to keyword list criteria defp to_criteria(params) when is_map(params) do - Keyword.new(params, fn + params + |> Keyword.new(fn {"status", status} -> {:status, parse_status(status)} - {"visibility", visibility} -> {:visibility, parse_visibility(visibility)} {"org", org_handle} -> {:owner_handle, org_handle} - {k, v} -> {String.to_existing_atom(k), v} + {_k, _v} -> nil end) + |> Enum.reject(&is_nil/1) rescue _ -> [] end @@ -48,26 +45,10 @@ defmodule AlgoraWeb.API.BountyController do # Parse status string to corresponding enum atom defp parse_status(status) when is_binary(status) do case String.downcase(status) do - "active" -> :open - "open" -> :open - "cancelled" -> :cancelled - "canceled" -> :cancelled "paid" -> :paid _ -> :open end end defp parse_status(_), do: :open - - # Parse visibility string to corresponding enum atom - defp parse_visibility(visibility) when is_binary(visibility) do - case String.downcase(visibility) do - "public" -> :public - "community" -> :community - "exclusive" -> :exclusive - _ -> :public - end - end - - defp parse_visibility(_), do: :public end From f835de2900114505c5e7555852a050004636bf31 Mon Sep 17 00:00:00 2001 From: zafer Date: Fri, 28 Mar 2025 22:00:12 +0200 Subject: [PATCH 5/5] fix: bounty data structure and API filtering - Added new fields to the bounty owner data structure, including inserted_at and provider_login. - Updated the bounty controller to support additional query parameters for organization handle and limit. - Refactored the to_criteria function to improve parameter parsing and handling. - Simplified the bounty JSON rendering by removing unnecessary comments and restructuring the data output. --- lib/algora/bounties/bounties.ex | 2 + .../controllers/api/bounty_controller.ex | 25 ++- lib/algora_web/controllers/api/bounty_json.ex | 185 +++++------------- 3 files changed, 67 insertions(+), 145 deletions(-) diff --git a/lib/algora/bounties/bounties.ex b/lib/algora/bounties/bounties.ex index ca55b9e86..d95de9623 100644 --- a/lib/algora/bounties/bounties.ex +++ b/lib/algora/bounties/bounties.ex @@ -1117,8 +1117,10 @@ defmodule Algora.Bounties do status: b.status, owner: %{ id: o.id, + inserted_at: o.inserted_at, name: o.name, handle: o.handle, + provider_login: o.provider_login, avatar_url: o.avatar_url, tech_stack: o.tech_stack }, diff --git a/lib/algora_web/controllers/api/bounty_controller.ex b/lib/algora_web/controllers/api/bounty_controller.ex index 059586502..2479f0111 100644 --- a/lib/algora_web/controllers/api/bounty_controller.ex +++ b/lib/algora_web/controllers/api/bounty_controller.ex @@ -11,6 +11,8 @@ defmodule AlgoraWeb.API.BountyController do Query Parameters: - status: string (optional) - Filter by status (open, paid) + - org: string (optional) - Filter by organization handle + - limit: integer (optional) - Limit the number of bounties returned """ def index(conn, %{"batch" => _batch, "input" => input} = _params) do with {:ok, decoded} <- Jason.decode(input), @@ -30,18 +32,27 @@ defmodule AlgoraWeb.API.BountyController do # Convert JSON/map parameters to keyword list criteria defp to_criteria(params) when is_map(params) do params - |> Keyword.new(fn - {"status", status} -> {:status, parse_status(status)} - {"org", org_handle} -> {:owner_handle, org_handle} - {_k, _v} -> nil - end) + |> Enum.map(&parse_params/1) |> Enum.reject(&is_nil/1) - rescue - _ -> [] + |> Keyword.new() end defp to_criteria(_), do: [] + defp parse_params({"status", status}) do + {:status, parse_status(status)} + end + + defp parse_params({"org", org_handle}) do + {:owner_handle, org_handle} + end + + defp parse_params({"limit", limit}) do + {:limit, limit} + end + + defp parse_params(_), do: nil + # Parse status string to corresponding enum atom defp parse_status(status) when is_binary(status) do case String.downcase(status) do diff --git a/lib/algora_web/controllers/api/bounty_json.ex b/lib/algora_web/controllers/api/bounty_json.ex index fadfb8636..79d696006 100644 --- a/lib/algora_web/controllers/api/bounty_json.ex +++ b/lib/algora_web/controllers/api/bounty_json.ex @@ -1,77 +1,19 @@ defmodule AlgoraWeb.API.BountyJSON do alias Algora.Bounties.Bounty - @doc """ - Renders a list of bounties. - """ def index(%{bounties: bounties}) do - %{ - data: [ - %{ - result: %{ - data: %{ - json: %{ - # TODO: Implement pagination - next_cursor: nil, - items: for(bounty <- bounties, do: data(bounty)) - } - } - } - } - ] - } - end - - @doc """ - Renders a single bounty. - """ - def show(%{bounty: bounty}) do - %{ - data: [ - %{ - result: %{ - data: %{ - json: %{ - next_cursor: nil, - items: [data(bounty)] - } + [ + %{ + result: %{ + data: %{ + json: %{ + next_cursor: nil, + items: for(bounty <- bounties, do: data(bounty)) } } } - ] - } - end - - defp data(%Bounty{} = bounty) do - %{ - id: bounty.id, - # TODO: Implement point rewards - point_reward: nil, - reward: %{ - amount: bounty.amount.amount, - currency: bounty.amount.currency - }, - reward_formatted: "#{bounty.amount.amount} #{bounty.amount.currency}", - # TODO: Implement reward tiers - reward_tiers: [], - tech: bounty.ticket.tech || [], - status: bounty.status, - # TODO: Determine if bounty is external - is_external: false, - org: org_data(bounty.owner), - task: task_data(bounty.ticket), - type: "standard", - kind: "dev", - reward_type: "cash", - visibility: bounty.visibility, - # TODO: Implement bids - bids: [], - autopay_disabled: bounty.autopay_disabled, - timeouts_disabled: true, - manual_assignments: false, - created_at: bounty.inserted_at, - updated_at: bounty.updated_at - } + } + ] end defp data(%{} = bounty) do @@ -79,75 +21,62 @@ defmodule AlgoraWeb.API.BountyJSON do id: bounty.id, point_reward: nil, reward: %{ - amount: bounty.amount.amount, + amount: Algora.MoneyUtils.to_minor_units(bounty.amount), currency: bounty.amount.currency }, - reward_formatted: "#{bounty.amount.amount} #{bounty.amount.currency}", + reward_formatted: Money.to_string!(bounty.amount, no_fraction_if_integer: true), reward_tiers: [], - tech: bounty.ticket.tech || [], + tech: bounty.owner.tech_stack, status: bounty.status, is_external: false, org: org_data(bounty.owner), - task: task_data(bounty.ticket), + task: task_data(bounty), type: "standard", kind: "dev", reward_type: "cash", - visibility: Map.get(bounty, :visibility, :public), + visibility: "public", bids: [], - autopay_disabled: Map.get(bounty, :autopay_disabled, false), - timeouts_disabled: true, + autopay_disabled: false, + timeouts_disabled: false, manual_assignments: false, created_at: bounty.inserted_at, - updated_at: Map.get(bounty, :updated_at, bounty.inserted_at) + updated_at: bounty.inserted_at } end - defp task_data(nil), do: nil - - defp task_data(ticket) do + defp task_data(bounty) do %{ - id: ticket.id, - # TODO: Make this dynamic based on ticket provider + id: bounty.ticket.id, forge: "github", - # TODO: Extract from ticket URL - repo_owner: "TODO", - # TODO: Extract from ticket URL - repo_name: "TODO", - number: ticket.number, + repo_owner: bounty.repository.owner.provider_login, + repo_name: bounty.repository.name, + number: bounty.ticket.number, source: %{ - # TODO: Make this dynamic based on ticket provider type: "github", data: %{ - id: ticket.id, - html_url: ticket.url, - # TODO: Add title to ticket schema - title: "TODO", - # TODO: Add body to ticket schema - body: "TODO", + id: bounty.ticket.id, + html_url: bounty.ticket.url, + title: bounty.ticket.title, + body: "", user: %{ - # TODO: Add creator info to ticket schema id: 0, - login: "TODO", - avatar_url: "TODO", - html_url: "TODO", - name: "TODO", - company: "TODO", - location: "TODO", - twitter_username: "TODO" + login: "", + avatar_url: "", + html_url: "", + name: "", + company: "", + location: "", + twitter_username: "" } } }, - # TODO: Map ticket status status: "open", - # TODO: Add title to ticket schema - title: "TODO", - url: ticket.url, - # TODO: Add body to ticket schema - body: "TODO", - # TODO: Make this dynamic + title: bounty.ticket.title, + url: bounty.ticket.url, + body: "", type: "issue", - hash: ticket.hash || "", - tech: ticket.tech || [] + hash: Bounty.path(bounty), + tech: [] } end @@ -159,22 +88,19 @@ defmodule AlgoraWeb.API.BountyJSON do created_at: org.inserted_at, handle: org.handle, name: org.name, - display_name: org.display_name || org.name, - description: org.bio || "", + display_name: org.name, + description: "", avatar_url: org.avatar_url, - website_url: org.website_url || "", - twitter_url: org.twitter_url || "", - youtube_url: org.youtube_url || "", - discord_url: org.discord_url || "", - slack_url: org.slack_url, - stargazers_count: org.stargazers_count || 0, - tech: org.tech_stack || [], - # TODO: Add to org schema + website_url: "", + twitter_url: "", + youtube_url: "", + discord_url: "", + slack_url: "", + stargazers_count: 0, + tech: org.tech_stack, accepts_sponsorships: false, members: members_data(org), - # TODO: Add to org schema enabled_expert_recs: false, - # TODO: Add to org schema enabled_private_bounties: false, days_until_timeout: nil, github_handle: org.provider_login @@ -182,23 +108,6 @@ defmodule AlgoraWeb.API.BountyJSON do end defp members_data(_org) do - # TODO: Implement actual member fetching [] end - - # defp user_data(nil), do: nil - - # defp user_data(user) do - # %{ - # id: user.id, - # handle: user.handle, - # image: user.avatar_url, - # name: user.name, - # github_handle: user.provider_login, - # youtube_handle: nil, - # twitch_handle: nil, - # # TODO: Implement org fetching for user - # orgs: [] - # } - # end end