Implement Projects V2 endpoints and finish the full Issues/Projects API.

81181fe0ba6c · AtlantisPleb · · parent bdf2b9727580

Implement Projects V2 endpoints and finish the full Issues/Projects API.

- Add OpenAgents.Projects, ProjectFields, and ProjectItems contexts and
  schemas
- Add all Projects V2 routes: list, create, show, items, add item,
  update item, and fields
- Complete the entire work plan from docs/issues-projects-work-plan.md
- mix precommit passes

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified lib/openagents/issues.ex
  • added lib/openagents/project_fields.ex
  • added lib/openagents/project_fields/project_field.ex
  • added lib/openagents/project_items.ex
  • added lib/openagents/project_items/project_item.ex
  • added lib/openagents/projects.ex
  • added lib/openagents/projects/project.ex
  • modified lib/openagents_web/controllers/comment_controller.ex
  • modified lib/openagents_web/controllers/issue_assignee_controller.ex
  • modified lib/openagents_web/controllers/issue_controller.ex
  • modified lib/openagents_web/controllers/issue_label_controller.ex
  • modified lib/openagents_web/controllers/label_json.ex
  • modified lib/openagents_web/controllers/milestone_controller.ex
  • added lib/openagents_web/controllers/project_controller.ex
  • added lib/openagents_web/controllers/project_json.ex
  • modified lib/openagents_web/router.ex
  • added priv/repo/migrations/20260819203939_create_projects.exs
  • added priv/repo/migrations/20260819203943_create_project_fields.exs
  • added priv/repo/migrations/20260819203944_create_project_items.exs
  • modified test/openagents/labels_test.exs
  • modified test/openagents/milestones_test.exs
  • added test/openagents/projects_test.exs
  • added test/support/fixtures/project_fields_fixtures.ex
  • added test/support/fixtures/project_items_fixtures.ex
  • added test/support/fixtures/projects_fixtures.ex

Diff

25 files changed, +912 -36

lib/openagents/issues.ex modified +1 -1

@@ -80,7 +80,7 @@ defmodule OpenAgents.Issues do

80 80
81 81
  def remove_assignees(%Issue{} = issue, logins) when is_list(logins) do
82 82
    logins = MapSet.new(logins)
83
    assignees = Enum.reject(issue.assignees || [], & &1["login"] in logins)
83
    assignees = Enum.reject(issue.assignees || [], &(&1["login"] in logins))
84 84
    update_issue(issue, %{"assignees" => assignees})
85 85
  end
86 86
lib/openagents/project_fields.ex added +104

@@ -0,0 +1,104 @@

1
defmodule OpenAgents.ProjectFields do
2
  @moduledoc """
3
  The ProjectFields context.
4
  """
5
6
  import Ecto.Query, warn: false
7
  alias OpenAgents.Repo
8
9
  alias OpenAgents.ProjectFields.ProjectField
10
11
  @doc """
12
  Returns the list of project_fields.
13
14
  ## Examples
15
16
      iex> list_project_fields()
17
      [%ProjectField{}, ...]
18
19
  """
20
  def list_project_fields do
21
    Repo.all(ProjectField)
22
  end
23
24
  @doc """
25
  Gets a single project_field.
26
27
  Raises `Ecto.NoResultsError` if the Project field does not exist.
28
29
  ## Examples
30
31
      iex> get_project_field!(123)
32
      %ProjectField{}
33
34
      iex> get_project_field!(456)
35
      ** (Ecto.NoResultsError)
36
37
  """
38
  def get_project_field!(id), do: Repo.get!(ProjectField, id)
39
40
  @doc """
41
  Creates a project_field.
42
43
  ## Examples
44
45
      iex> create_project_field(%{field: value})
46
      {:ok, %ProjectField{}}
47
48
      iex> create_project_field(%{field: bad_value})
49
      {:error, %Ecto.Changeset{}}
50
51
  """
52
  def create_project_field(attrs) do
53
    %ProjectField{}
54
    |> ProjectField.changeset(attrs)
55
    |> Repo.insert()
56
  end
57
58
  @doc """
59
  Updates a project_field.
60
61
  ## Examples
62
63
      iex> update_project_field(project_field, %{field: new_value})
64
      {:ok, %ProjectField{}}
65
66
      iex> update_project_field(project_field, %{field: bad_value})
67
      {:error, %Ecto.Changeset{}}
68
69
  """
70
  def update_project_field(%ProjectField{} = project_field, attrs) do
71
    project_field
72
    |> ProjectField.changeset(attrs)
73
    |> Repo.update()
74
  end
75
76
  @doc """
77
  Deletes a project_field.
78
79
  ## Examples
80
81
      iex> delete_project_field(project_field)
82
      {:ok, %ProjectField{}}
83
84
      iex> delete_project_field(project_field)
85
      {:error, %Ecto.Changeset{}}
86
87
  """
88
  def delete_project_field(%ProjectField{} = project_field) do
89
    Repo.delete(project_field)
90
  end
91
92
  @doc """
93
  Returns an `%Ecto.Changeset{}` for tracking project_field changes.
94
95
  ## Examples
96
97
      iex> change_project_field(project_field)
98
      %Ecto.Changeset{data: %ProjectField{}}
99
100
  """
101
  def change_project_field(%ProjectField{} = project_field, attrs \\ %{}) do
102
    ProjectField.changeset(project_field, attrs)
103
  end
104
end
lib/openagents/project_fields/project_field.ex added +20

@@ -0,0 +1,20 @@

1
defmodule OpenAgents.ProjectFields.ProjectField do
2
  use Ecto.Schema
3
  import Ecto.Changeset
4
5
  schema "project_fields" do
6
    field :name, :string
7
    field :data_type, :string
8
    field :options, :map
9
    field :project_id, :id
10
11
    timestamps(type: :utc_datetime)
12
  end
13
14
  @doc false
15
  def changeset(project_field, attrs) do
16
    project_field
17
    |> cast(attrs, [:name, :data_type, :options, :project_id])
18
    |> validate_required([:name, :data_type, :project_id])
19
  end
20
end
lib/openagents/project_items.ex added +104

@@ -0,0 +1,104 @@

1
defmodule OpenAgents.ProjectItems do
2
  @moduledoc """
3
  The ProjectItems context.
4
  """
5
6
  import Ecto.Query, warn: false
7
  alias OpenAgents.Repo
8
9
  alias OpenAgents.ProjectItems.ProjectItem
10
11
  @doc """
12
  Returns the list of project_items.
13
14
  ## Examples
15
16
      iex> list_project_items()
17
      [%ProjectItem{}, ...]
18
19
  """
20
  def list_project_items do
21
    Repo.all(ProjectItem)
22
  end
23
24
  @doc """
25
  Gets a single project_item.
26
27
  Raises `Ecto.NoResultsError` if the Project item does not exist.
28
29
  ## Examples
30
31
      iex> get_project_item!(123)
32
      %ProjectItem{}
33
34
      iex> get_project_item!(456)
35
      ** (Ecto.NoResultsError)
36
37
  """
38
  def get_project_item!(id), do: Repo.get!(ProjectItem, id)
39
40
  @doc """
41
  Creates a project_item.
42
43
  ## Examples
44
45
      iex> create_project_item(%{field: value})
46
      {:ok, %ProjectItem{}}
47
48
      iex> create_project_item(%{field: bad_value})
49
      {:error, %Ecto.Changeset{}}
50
51
  """
52
  def create_project_item(attrs) do
53
    %ProjectItem{}
54
    |> ProjectItem.changeset(attrs)
55
    |> Repo.insert()
56
  end
57
58
  @doc """
59
  Updates a project_item.
60
61
  ## Examples
62
63
      iex> update_project_item(project_item, %{field: new_value})
64
      {:ok, %ProjectItem{}}
65
66
      iex> update_project_item(project_item, %{field: bad_value})
67
      {:error, %Ecto.Changeset{}}
68
69
  """
70
  def update_project_item(%ProjectItem{} = project_item, attrs) do
71
    project_item
72
    |> ProjectItem.changeset(attrs)
73
    |> Repo.update()
74
  end
75
76
  @doc """
77
  Deletes a project_item.
78
79
  ## Examples
80
81
      iex> delete_project_item(project_item)
82
      {:ok, %ProjectItem{}}
83
84
      iex> delete_project_item(project_item)
85
      {:error, %Ecto.Changeset{}}
86
87
  """
88
  def delete_project_item(%ProjectItem{} = project_item) do
89
    Repo.delete(project_item)
90
  end
91
92
  @doc """
93
  Returns an `%Ecto.Changeset{}` for tracking project_item changes.
94
95
  ## Examples
96
97
      iex> change_project_item(project_item)
98
      %Ecto.Changeset{data: %ProjectItem{}}
99
100
  """
101
  def change_project_item(%ProjectItem{} = project_item, attrs \\ %{}) do
102
    ProjectItem.changeset(project_item, attrs)
103
  end
104
end
lib/openagents/project_items/project_item.ex added +19

@@ -0,0 +1,19 @@

1
defmodule OpenAgents.ProjectItems.ProjectItem do
2
  use Ecto.Schema
3
  import Ecto.Changeset
4
5
  schema "project_items" do
6
    field :values, :map
7
    field :project_id, :id
8
    field :issue_id, :id
9
10
    timestamps(type: :utc_datetime)
11
  end
12
13
  @doc false
14
  def changeset(project_item, attrs) do
15
    project_item
16
    |> cast(attrs, [:values, :project_id, :issue_id])
17
    |> validate_required([:project_id, :issue_id])
18
  end
19
end
lib/openagents/projects.ex added +184

@@ -0,0 +1,184 @@

1
defmodule OpenAgents.Projects do
2
  @moduledoc """
3
  The Projects context.
4
  """
5
6
  import Ecto.Query, warn: false
7
  alias OpenAgents.Repo
8
9
  alias OpenAgents.Issues.Issue
10
  alias OpenAgents.ProjectFields.ProjectField
11
  alias OpenAgents.ProjectItems.ProjectItem
12
  alias OpenAgents.Projects.Project
13
14
  @doc """
15
  Returns the list of projects.
16
17
  ## Examples
18
19
      iex> list_projects()
20
      [%Project{}, ...]
21
22
  """
23
  def list_projects do
24
    Repo.all(Project)
25
  end
26
27
  @doc """
28
  Gets a single project.
29
30
  Raises `Ecto.NoResultsError` if the Project does not exist.
31
32
  ## Examples
33
34
      iex> get_project!(123)
35
      %Project{}
36
37
      iex> get_project!(456)
38
      ** (Ecto.NoResultsError)
39
40
  """
41
  def get_project!(id), do: Repo.get!(Project, id)
42
43
  def get_project_by_number!(number) when is_integer(number),
44
    do: Repo.get_by!(Project, number: number)
45
46
  @doc """
47
  Creates a project.
48
49
  ## Examples
50
51
      iex> create_project(%{field: value})
52
      {:ok, %Project{}}
53
54
      iex> create_project(%{field: bad_value})
55
      {:error, %Ecto.Changeset{}}
56
57
  """
58
  def create_project(attrs \\ %{}) do
59
    number = next_project_number()
60
    normalized = for {k, v} <- attrs, into: %{}, do: {to_string(k), v}
61
62
    %Project{}
63
    |> Project.changeset(Map.put_new(normalized, "number", number))
64
    |> Repo.insert()
65
  end
66
67
  def list_project_items(project_id) do
68
    ProjectItem
69
    |> where(project_id: ^project_id)
70
    |> Repo.all()
71
  end
72
73
  def get_project_item!(id), do: Repo.get!(ProjectItem, id)
74
75
  def create_project_item(%{issue_number: issue_number} = attrs, project_id) do
76
    issue = Repo.get_by!(Issue, number: issue_number)
77
    values = attrs["values"] || %{}
78
79
    %ProjectItem{}
80
    |> ProjectItem.changeset(%{
81
      "project_id" => project_id,
82
      "issue_id" => issue.id,
83
      "values" => values
84
    })
85
    |> Repo.insert()
86
  end
87
88
  def create_project_item(attrs, project_id) do
89
    attrs =
90
      for {k, v} <- attrs, into: %{} do
91
        {to_string(k), v}
92
      end
93
94
    issue_number = Map.get(attrs, "issue_number")
95
    issue = Repo.get_by!(Issue, number: issue_number)
96
97
    %ProjectItem{}
98
    |> ProjectItem.changeset(%{
99
      "project_id" => project_id,
100
      "issue_id" => issue.id,
101
      "values" => Map.get(attrs, "values", %{})
102
    })
103
    |> Repo.insert()
104
  end
105
106
  def update_project_item(%ProjectItem{} = item, attrs) do
107
    attrs =
108
      for {k, v} <- attrs, into: %{} do
109
        {to_string(k), v}
110
      end
111
112
    values = Map.merge(item.values || %{}, Map.get(attrs, "values", %{}))
113
114
    item
115
    |> ProjectItem.changeset(%{"values" => values})
116
    |> Repo.update()
117
  end
118
119
  def list_project_fields(project_id) do
120
    ProjectField
121
    |> where(project_id: ^project_id)
122
    |> Repo.all()
123
  end
124
125
  def create_project_field(attrs) do
126
    %ProjectField{}
127
    |> ProjectField.changeset(attrs)
128
    |> Repo.insert()
129
  end
130
131
  defp next_project_number do
132
    case Repo.aggregate(Project, :max, :number) do
133
      nil -> 1
134
      n -> n + 1
135
    end
136
  end
137
138
  @doc """
139
  Updates a project.
140
141
  ## Examples
142
143
      iex> update_project(project, %{field: new_value})
144
      {:ok, %Project{}}
145
146
      iex> update_project(project, %{field: bad_value})
147
      {:error, %Ecto.Changeset{}}
148
149
  """
150
  def update_project(%Project{} = project, attrs) do
151
    project
152
    |> Project.changeset(attrs)
153
    |> Repo.update()
154
  end
155
156
  @doc """
157
  Deletes a project.
158
159
  ## Examples
160
161
      iex> delete_project(project)
162
      {:ok, %Project{}}
163
164
      iex> delete_project(project)
165
      {:error, %Ecto.Changeset{}}
166
167
  """
168
  def delete_project(%Project{} = project) do
169
    Repo.delete(project)
170
  end
171
172
  @doc """
173
  Returns an `%Ecto.Changeset{}` for tracking project changes.
174
175
  ## Examples
176
177
      iex> change_project(project)
178
      %Ecto.Changeset{data: %Project{}}
179
180
  """
181
  def change_project(%Project{} = project, attrs \\ %{}) do
182
    Project.changeset(project, attrs)
183
  end
184
end
lib/openagents/projects/project.ex added +20

@@ -0,0 +1,20 @@

1
defmodule OpenAgents.Projects.Project do
2
  use Ecto.Schema
3
  import Ecto.Changeset
4
5
  schema "projects" do
6
    field :number, :integer
7
    field :title, :string
8
    field :owner, :string
9
    field :state, :string, default: "open"
10
11
    timestamps(type: :utc_datetime)
12
  end
13
14
  @doc false
15
  def changeset(project, attrs) do
16
    project
17
    |> cast(attrs, [:number, :title, :owner, :state])
18
    |> validate_required([:number, :title, :owner, :state])
19
  end
20
end
lib/openagents_web/controllers/comment_controller.ex modified +8 -5

@@ -19,11 +19,14 @@ defmodule OpenAgentsWeb.CommentController do

19 19
      |> json(%{message: "Not Found"})
20 20
  end
21 21
22
  def create(conn, %{
23
        "owner" => _owner,
24
        "repo" => _repo,
25
        "issue_number" => issue_number
26
      } = params) do
22
  def create(
23
        conn,
24
        %{
25
          "owner" => _owner,
26
          "repo" => _repo,
27
          "issue_number" => issue_number
28
        } = params
29
      ) do
27 30
    issue = Issues.get_issue_by_number!(String.to_integer(issue_number))
28 31
29 32
    case Issues.create_comment(Map.put(params, :issue_id, issue.id)) do
lib/openagents_web/controllers/issue_assignee_controller.ex modified +16 -10

@@ -13,11 +13,14 @@ defmodule OpenAgentsWeb.IssueAssigneeController do

13 13
      |> json(%{message: "Not Found"})
14 14
  end
15 15
16
  def create(conn, %{
17
        "owner" => _owner,
18
        "repo" => _repo,
19
        "issue_number" => issue_number
20
      } = params) do
16
  def create(
17
        conn,
18
        %{
19
          "owner" => _owner,
20
          "repo" => _repo,
21
          "issue_number" => issue_number
22
        } = params
23
      ) do
21 24
    issue = Issues.get_issue_by_number!(String.to_integer(issue_number))
22 25
    logins = params["assignees"] || []
23 26

@@ -37,11 +40,14 @@ defmodule OpenAgentsWeb.IssueAssigneeController do

37 40
      |> json(%{message: "Not Found"})
38 41
  end
39 42
40
  def delete(conn, %{
41
        "owner" => _owner,
42
        "repo" => _repo,
43
        "issue_number" => issue_number
44
      } = params) do
43
  def delete(
44
        conn,
45
        %{
46
          "owner" => _owner,
47
          "repo" => _repo,
48
          "issue_number" => issue_number
49
        } = params
50
      ) do
45 51
    issue = Issues.get_issue_by_number!(String.to_integer(issue_number))
46 52
    logins = params["assignees"] || []
47 53
lib/openagents_web/controllers/issue_controller.ex modified +8 -5

@@ -38,11 +38,14 @@ defmodule OpenAgentsWeb.IssueController do

38 38
      |> json(%{message: "Not Found"})
39 39
  end
40 40
41
  def update(conn, %{
42
        "owner" => owner,
43
        "repo" => repo,
44
        "issue_number" => issue_number
45
      } = params) do
41
  def update(
42
        conn,
43
        %{
44
          "owner" => owner,
45
          "repo" => repo,
46
          "issue_number" => issue_number
47
        } = params
48
      ) do
46 49
    issue = Issues.get_issue_by_number!(String.to_integer(issue_number))
47 50
48 51
    case Issues.update_issue(issue, params) do
lib/openagents_web/controllers/issue_label_controller.ex modified +8 -5

@@ -13,11 +13,14 @@ defmodule OpenAgentsWeb.IssueLabelController do

13 13
      |> json(%{message: "Not Found"})
14 14
  end
15 15
16
  def create(conn, %{
17
        "owner" => _owner,
18
        "repo" => _repo,
19
        "issue_number" => issue_number
20
      } = params) do
16
  def create(
17
        conn,
18
        %{
19
          "owner" => _owner,
20
          "repo" => _repo,
21
          "issue_number" => issue_number
22
        } = params
23
      ) do
21 24
    issue = Issues.get_issue_by_number!(String.to_integer(issue_number))
22 25
    names = params["labels"] || []
23 26
lib/openagents_web/controllers/label_json.ex modified +2 -1

@@ -25,7 +25,8 @@ defmodule OpenAgentsWeb.LabelJSON do

25 25
      color: label.color,
26 26
      description: label.description,
27 27
      default: false,
28
      url: "https://openagents.com/api/v3/repos/#{owner}/#{repo}/labels/#{URI.encode_www_form(label.name)}"
28
      url:
29
        "https://openagents.com/api/v3/repos/#{owner}/#{repo}/labels/#{URI.encode_www_form(label.name)}"
29 30
    }
30 31
  end
31 32
lib/openagents_web/controllers/milestone_controller.ex modified +8 -5

@@ -37,11 +37,14 @@ defmodule OpenAgentsWeb.MilestoneController do

37 37
      |> json(%{message: "Not Found"})
38 38
  end
39 39
40
  def update(conn, %{
41
        "owner" => owner,
42
        "repo" => repo,
43
        "milestone_number" => milestone_number
44
      } = params) do
40
  def update(
41
        conn,
42
        %{
43
          "owner" => owner,
44
          "repo" => repo,
45
          "milestone_number" => milestone_number
46
        } = params
47
      ) do
45 48
    milestone = Milestones.get_milestone_by_number!(String.to_integer(milestone_number))
46 49
47 50
    case Milestones.update_milestone(milestone, params) do
lib/openagents_web/controllers/project_controller.ex added +123

@@ -0,0 +1,123 @@

1
defmodule OpenAgentsWeb.ProjectController do
2
  use OpenAgentsWeb, :controller
3
4
  alias OpenAgents.Projects
5
  alias OpenAgents.Projects.Project
6
7
  def index(conn, %{"username" => username}) do
8
    projects =
9
      Projects.list_projects()
10
      |> Enum.filter(&(&1.owner == username))
11
12
    render(conn, :index, projects: projects)
13
  end
14
15
  def create(conn, %{"owner" => owner} = params) do
16
    attrs =
17
      params
18
      |> Map.put("owner", owner)
19
20
    case Projects.create_project(attrs) do
21
      {:ok, %Project{} = project} ->
22
        conn
23
        |> put_status(:created)
24
        |> render(:show, project: project)
25
26
      {:error, %Ecto.Changeset{} = changeset} ->
27
        conn
28
        |> put_status(:unprocessable_entity)
29
        |> render(:error, changeset: changeset)
30
    end
31
  end
32
33
  def show(conn, %{"username" => _username, "project_number" => project_number}) do
34
    project = Projects.get_project_by_number!(String.to_integer(project_number))
35
    render(conn, :show, project: project)
36
  rescue
37
    Ecto.NoResultsError ->
38
      conn
39
      |> put_status(:not_found)
40
      |> json(%{message: "Not Found"})
41
  end
42
43
  def items(conn, %{
44
        "username" => _username,
45
        "project_number" => project_number
46
      }) do
47
    project = Projects.get_project_by_number!(String.to_integer(project_number))
48
    items = Projects.list_project_items(project.id)
49
    render(conn, :items, items: items)
50
  rescue
51
    Ecto.NoResultsError ->
52
      conn
53
      |> put_status(:not_found)
54
      |> json(%{message: "Not Found"})
55
  end
56
57
  def create_item(
58
        conn,
59
        %{
60
          "username" => _username,
61
          "project_number" => project_number
62
        } = params
63
      ) do
64
    project = Projects.get_project_by_number!(String.to_integer(project_number))
65
66
    case Projects.create_project_item(params, project.id) do
67
      {:ok, item} ->
68
        conn
69
        |> put_status(:created)
70
        |> render(:items, items: [item])
71
72
      {:error, %Ecto.Changeset{} = changeset} ->
73
        conn
74
        |> put_status(:unprocessable_entity)
75
        |> render(:error, changeset: changeset)
76
    end
77
  rescue
78
    Ecto.NoResultsError ->
79
      conn
80
      |> put_status(:not_found)
81
      |> json(%{message: "Not Found"})
82
  end
83
84
  def update_item(
85
        conn,
86
        %{
87
          "username" => _username,
88
          "project_number" => _project_number,
89
          "item_id" => item_id
90
        } = params
91
      ) do
92
    item = Projects.get_project_item!(String.to_integer(item_id))
93
94
    case Projects.update_project_item(item, params) do
95
      {:ok, item} ->
96
        render(conn, :items, items: [item])
97
98
      {:error, %Ecto.Changeset{} = changeset} ->
99
        conn
100
        |> put_status(:unprocessable_entity)
101
        |> render(:error, changeset: changeset)
102
    end
103
  rescue
104
    Ecto.NoResultsError ->
105
      conn
106
      |> put_status(:not_found)
107
      |> json(%{message: "Not Found"})
108
  end
109
110
  def fields(conn, %{
111
        "username" => _username,
112
        "project_number" => project_number
113
      }) do
114
    project = Projects.get_project_by_number!(String.to_integer(project_number))
115
    fields = Projects.list_project_fields(project.id)
116
    render(conn, :fields, fields: fields)
117
  rescue
118
    Ecto.NoResultsError ->
119
      conn
120
      |> put_status(:not_found)
121
      |> json(%{message: "Not Found"})
122
  end
123
end
lib/openagents_web/controllers/project_json.ex added +58

@@ -0,0 +1,58 @@

1
defmodule OpenAgentsWeb.ProjectJSON do
2
  @moduledoc """
3
  Renders GitHub-compatible project and project item JSON.
4
  """
5
6
  def render("index.json", %{projects: projects}) do
7
    %{projects: Enum.map(projects, &project_json/1)}
8
  end
9
10
  def render("show.json", %{project: project}) do
11
    project_json(project)
12
  end
13
14
  def render("items.json", %{items: items}) do
15
    %{items: Enum.map(items, &item_json/1)}
16
  end
17
18
  def render("fields.json", %{fields: fields}) do
19
    %{fields: Enum.map(fields, &field_json/1)}
20
  end
21
22
  def render("error.json", %{changeset: changeset}) do
23
    %{errors: Ecto.Changeset.traverse_errors(changeset, &translate_error/1)}
24
  end
25
26
  defp project_json(project) do
27
    %{
28
      id: project.id,
29
      number: project.number,
30
      title: project.title,
31
      owner: project.owner,
32
      state: project.state
33
    }
34
  end
35
36
  defp item_json(item) do
37
    %{
38
      id: item.id,
39
      issue_id: item.issue_id,
40
      values: item.values
41
    }
42
  end
43
44
  defp field_json(field) do
45
    %{
46
      id: field.id,
47
      name: field.name,
48
      data_type: field.data_type,
49
      options: field.options
50
    }
51
  end
52
53
  defp translate_error({msg, opts}) do
54
    Regex.replace(~r/%{(\w+)}/, msg, fn _, key ->
55
      to_string(Keyword.get(opts, String.to_existing_atom(key), key))
56
    end)
57
  end
58
end
lib/openagents_web/router.ex modified +12

@@ -57,6 +57,18 @@ defmodule OpenAgentsWeb.Router do

57 57
58 58
    get "/repos/:owner/:repo/assignees", AssigneeController, :index
59 59
    get "/repos/:owner/:repo/assignees/:assignee", AssigneeController, :show
60
61
    get "/users/:username/projectsV2", ProjectController, :index
62
    post "/:owner/projectsV2", ProjectController, :create
63
    get "/users/:username/projectsV2/:project_number", ProjectController, :show
64
    get "/users/:username/projectsV2/:project_number/items", ProjectController, :items
65
    post "/users/:username/projectsV2/:project_number/items", ProjectController, :create_item
66
67
    patch "/users/:username/projectsV2/:project_number/items/:item_id",
68
          ProjectController,
69
          :update_item
70
71
    get "/users/:username/projectsV2/:project_number/fields", ProjectController, :fields
60 72
  end
61 73
62 74
  # Enable LiveDashboard and Swoosh mailbox preview in development
priv/repo/migrations/20260819203939_create_projects.exs added +14

@@ -0,0 +1,14 @@

1
defmodule OpenAgents.Repo.Migrations.CreateProjects do
2
  use Ecto.Migration
3
4
  def change do
5
    create table(:projects) do
6
      add :number, :integer
7
      add :title, :string
8
      add :owner, :string
9
      add :state, :string
10
11
      timestamps(type: :utc_datetime)
12
    end
13
  end
14
end
priv/repo/migrations/20260819203943_create_project_fields.exs added +16

@@ -0,0 +1,16 @@

1
defmodule OpenAgents.Repo.Migrations.CreateProjectFields do
2
  use Ecto.Migration
3
4
  def change do
5
    create table(:project_fields) do
6
      add :name, :string
7
      add :data_type, :string
8
      add :options, :map
9
      add :project_id, references(:projects, on_delete: :nothing)
10
11
      timestamps(type: :utc_datetime)
12
    end
13
14
    create index(:project_fields, [:project_id])
15
  end
16
end
priv/repo/migrations/20260819203944_create_project_items.exs added +16

@@ -0,0 +1,16 @@

1
defmodule OpenAgents.Repo.Migrations.CreateProjectItems do
2
  use Ecto.Migration
3
4
  def change do
5
    create table(:project_items) do
6
      add :values, :map
7
      add :project_id, references(:projects, on_delete: :nothing)
8
      add :issue_id, references(:issues, on_delete: :nothing)
9
10
      timestamps(type: :utc_datetime)
11
    end
12
13
    create index(:project_items, [:project_id])
14
    create index(:project_items, [:issue_id])
15
  end
16
end
test/openagents/labels_test.exs modified +6 -1

@@ -35,7 +35,12 @@ defmodule OpenAgents.LabelsTest do

35 35
36 36
    test "update_label/2 with valid data updates the label" do
37 37
      label = label_fixture()
38
      update_attrs = %{name: "some updated name", description: "some updated description", color: "some updated color"}
38
39
      update_attrs = %{
40
        name: "some updated name",
41
        description: "some updated description",
42
        color: "some updated color"
43
      }
39 44
40 45
      assert {:ok, %Label{} = label} = Labels.update_label(label, update_attrs)
41 46
      assert label.name == "some updated name"
test/openagents/milestones_test.exs modified +18 -3

@@ -21,7 +21,13 @@ defmodule OpenAgents.MilestonesTest do

21 21
    end
22 22
23 23
    test "create_milestone/1 with valid data creates a milestone" do
24
      valid_attrs = %{state: "some state", description: "some description", title: "some title", number: 42, due_on: "some due_on"}
24
      valid_attrs = %{
25
        state: "some state",
26
        description: "some description",
27
        title: "some title",
28
        number: 42,
29
        due_on: "some due_on"
30
      }
25 31
26 32
      assert {:ok, %Milestone{} = milestone} = Milestones.create_milestone(valid_attrs)
27 33
      assert milestone.state == "some state"

@@ -37,9 +43,18 @@ defmodule OpenAgents.MilestonesTest do

37 43
38 44
    test "update_milestone/2 with valid data updates the milestone" do
39 45
      milestone = milestone_fixture()
40
      update_attrs = %{state: "some updated state", description: "some updated description", title: "some updated title", number: 43, due_on: "some updated due_on"}
41 46
42
      assert {:ok, %Milestone{} = milestone} = Milestones.update_milestone(milestone, update_attrs)
47
      update_attrs = %{
48
        state: "some updated state",
49
        description: "some updated description",
50
        title: "some updated title",
51
        number: 43,
52
        due_on: "some updated due_on"
53
      }
54
55
      assert {:ok, %Milestone{} = milestone} =
56
               Milestones.update_milestone(milestone, update_attrs)
57
43 58
      assert milestone.state == "some updated state"
44 59
      assert milestone.description == "some updated description"
45 60
      assert milestone.title == "some updated title"
test/openagents/projects_test.exs added +71

@@ -0,0 +1,71 @@

1
defmodule OpenAgents.ProjectsTest do
2
  use OpenAgents.DataCase
3
4
  alias OpenAgents.Projects
5
6
  describe "projects" do
7
    alias OpenAgents.Projects.Project
8
9
    import OpenAgents.ProjectsFixtures
10
11
    @invalid_attrs %{owner: nil, state: nil, title: nil, number: nil}
12
13
    test "list_projects/0 returns all projects" do
14
      project = project_fixture()
15
      assert Projects.list_projects() == [project]
16
    end
17
18
    test "get_project!/1 returns the project with given id" do
19
      project = project_fixture()
20
      assert Projects.get_project!(project.id) == project
21
    end
22
23
    test "create_project/1 with valid data creates a project" do
24
      valid_attrs = %{owner: "some owner", state: "some state", title: "some title", number: 42}
25
26
      assert {:ok, %Project{} = project} = Projects.create_project(valid_attrs)
27
      assert project.owner == "some owner"
28
      assert project.state == "some state"
29
      assert project.title == "some title"
30
      assert project.number == 42
31
    end
32
33
    test "create_project/1 with invalid data returns error changeset" do
34
      assert {:error, %Ecto.Changeset{}} = Projects.create_project(@invalid_attrs)
35
    end
36
37
    test "update_project/2 with valid data updates the project" do
38
      project = project_fixture()
39
40
      update_attrs = %{
41
        owner: "some updated owner",
42
        state: "some updated state",
43
        title: "some updated title",
44
        number: 43
45
      }
46
47
      assert {:ok, %Project{} = project} = Projects.update_project(project, update_attrs)
48
      assert project.owner == "some updated owner"
49
      assert project.state == "some updated state"
50
      assert project.title == "some updated title"
51
      assert project.number == 43
52
    end
53
54
    test "update_project/2 with invalid data returns error changeset" do
55
      project = project_fixture()
56
      assert {:error, %Ecto.Changeset{}} = Projects.update_project(project, @invalid_attrs)
57
      assert project == Projects.get_project!(project.id)
58
    end
59
60
    test "delete_project/1 deletes the project" do
61
      project = project_fixture()
62
      assert {:ok, %Project{}} = Projects.delete_project(project)
63
      assert_raise Ecto.NoResultsError, fn -> Projects.get_project!(project.id) end
64
    end
65
66
    test "change_project/1 returns a project changeset" do
67
      project = project_fixture()
68
      assert %Ecto.Changeset{} = Projects.change_project(project)
69
    end
70
  end
71
end
test/support/fixtures/project_fields_fixtures.ex added +26

@@ -0,0 +1,26 @@

1
defmodule OpenAgents.ProjectFieldsFixtures do
2
  @moduledoc """
3
  This module defines test helpers for creating
4
  entities via the `OpenAgents.ProjectFields` context.
5
  """
6
7
  @doc """
8
  Generate a project_field.
9
  """
10
  def project_field_fixture(attrs \\ %{}) do
11
    {:ok, project} =
12
      OpenAgents.Projects.create_project(%{title: "Fixture project", owner: "OpenAgents"})
13
14
    {:ok, project_field} =
15
      attrs
16
      |> Enum.into(%{
17
        data_type: "some data_type",
18
        name: "some name",
19
        options: %{},
20
        project_id: project.id
21
      })
22
      |> OpenAgents.ProjectFields.create_project_field()
23
24
    project_field
25
  end
26
end
test/support/fixtures/project_items_fixtures.ex added +27

@@ -0,0 +1,27 @@

1
defmodule OpenAgents.ProjectItemsFixtures do
2
  @moduledoc """
3
  This module defines test helpers for creating
4
  entities via the `OpenAgents.ProjectItems` context.
5
  """
6
7
  @doc """
8
  Generate a project_item.
9
  """
10
  def project_item_fixture(attrs \\ %{}) do
11
    {:ok, project} =
12
      OpenAgents.Projects.create_project(%{title: "Fixture project", owner: "OpenAgents"})
13
14
    {:ok, issue} = OpenAgents.Issues.create_issue(%{title: "Fixture issue"})
15
16
    {:ok, project_item} =
17
      attrs
18
      |> Enum.into(%{
19
        values: %{},
20
        project_id: project.id,
21
        issue_id: issue.id
22
      })
23
      |> OpenAgents.ProjectItems.create_project_item()
24
25
    project_item
26
  end
27
end
test/support/fixtures/projects_fixtures.ex added +23

@@ -0,0 +1,23 @@

1
defmodule OpenAgents.ProjectsFixtures do
2
  @moduledoc """
3
  This module defines test helpers for creating
4
  entities via the `OpenAgents.Projects` context.
5
  """
6
7
  @doc """
8
  Generate a project.
9
  """
10
  def project_fixture(attrs \\ %{}) do
11
    {:ok, project} =
12
      attrs
13
      |> Enum.into(%{
14
        number: 42,
15
        owner: "some owner",
16
        state: "some state",
17
        title: "some title"
18
      })
19
      |> OpenAgents.Projects.create_project()
20
21
    project
22
  end
23
end

This page updates live while a promote is in flight · changelog