Skip to content

Commit 09e3eb5

Browse files
authored
Merge pull request #1067 from code-corps/1063-create-github-issues
Create GithubIssue and modify Task
2 parents b9cd900 + 1a005ea commit 09e3eb5

36 files changed

+783
-353
lines changed

lib/code_corps/comment/service.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ defmodule CodeCorps.Comment.Service do
99

1010
require Logger
1111

12-
@preloads [:user, task: [github_repo: :github_app_installation]]
12+
@preloads [:user, task: [:github_issue, github_repo: :github_app_installation]]
1313

1414
@doc ~S"""
1515
Creates a `Comment` record using the provided parameters
@@ -49,9 +49,9 @@ defmodule CodeCorps.Comment.Service do
4949

5050
@spec connect_to_github(Comment.t) :: {:ok, Comment.t} :: {:error, GitHub.api_error_struct}
5151
defp connect_to_github(
52-
%Comment{task: %Task{github_repo: nil, github_issue_number: nil}} = comment), do: {:ok, comment}
52+
%Comment{task: %Task{github_repo: nil}} = comment), do: {:ok, comment}
5353
defp connect_to_github(
54-
%Comment{task: %Task{github_repo: %GithubRepo{} = _, github_issue_number: _}} = comment) do
54+
%Comment{task: %Task{github_repo: %GithubRepo{}}} = comment) do
5555

5656
with {:ok, github_comment} <- comment |> GitHub.Comment.create do
5757
comment |> link_with_github_changeset(github_comment) |> Repo.update
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
defmodule CodeCorps.GitHub.Adapters.Issue do
2+
@moduledoc """
3+
Used to adapt a GitHub Issue payload into attributes for creating or updating
4+
a `CodeCorps.GithubIssue` and vice-versa.
5+
"""
6+
7+
alias CodeCorps.{
8+
Adapter.MapTransformer,
9+
GithubIssue
10+
}
11+
12+
@mapping [
13+
{:body, ["body"]},
14+
{:closed_at, ["closed_at"]},
15+
{:comments_url, ["comments_url"]},
16+
{:events_url, ["events_url"]},
17+
{:github_created_at, ["created_at"]},
18+
{:github_id, ["id"]},
19+
{:github_updated_at, ["updated_at"]},
20+
{:html_url, ["html_url"]},
21+
{:labels_url, ["labels_url"]},
22+
{:locked, ["locked"]},
23+
{:number, ["number"]},
24+
{:state, ["state"]},
25+
{:title, ["title"]},
26+
{:url, ["url"]}
27+
]
28+
29+
@doc ~S"""
30+
Converts a GitHub Issue payload into a set of attributes used to update or
31+
create a `GithubIssue` record.
32+
"""
33+
@spec from_api(map) :: map
34+
def from_api(%{} = payload) do
35+
payload |> MapTransformer.transform(@mapping)
36+
end
37+
38+
@autogenerated_github_keys ~w(closed_at comments_url created_at events_url html_url id labels_url number updated_at url)
39+
40+
@doc ~S"""
41+
Converts a `GithubIssue` into a set of attributes used to update or create an
42+
associated GitHub Issue.
43+
"""
44+
@spec to_api(GithubIssue.t) :: map
45+
def to_api(%GithubIssue{} = github_issue) do
46+
github_issue
47+
|> Map.from_struct
48+
|> MapTransformer.transform_inverse(@mapping)
49+
|> Map.drop(@autogenerated_github_keys)
50+
end
51+
end

lib/code_corps/github/comment/comment.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ defmodule CodeCorps.GitHub.Comment do
33
Handles GitHub API requests for actions on Comments
44
"""
55

6-
alias CodeCorps.{Comment, GitHub, GithubAppInstallation, GithubRepo, Task, User}
6+
alias CodeCorps.{Comment, GitHub, GithubAppInstallation, GithubIssue, GithubRepo, Task, User}
77

88
@spec create(Comment.t) :: GitHub.response
99
def create(
@@ -64,7 +64,9 @@ defmodule CodeCorps.GitHub.Comment do
6464
defp create_endpoint_for(
6565
%Comment{
6666
task: %Task{
67-
github_issue_number: number,
67+
github_issue: %GithubIssue{
68+
number: number
69+
},
6870
github_repo: %GithubRepo{
6971
github_account_login: owner, name: repo
7072
},

lib/code_corps/github/event/common/repo_finder.ex

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,10 @@ defmodule CodeCorps.GitHub.Event.Common.RepoFinder do
1414
- `{:error, :unmatched_project}` if record was found, but has no associated
1515
`ProjectGithubRepo` children
1616
"""
17-
@spec find_repo(map) :: {:ok, GithubRepo.t} | {:error, :unmatched_repository} | {:error, :unmatched_project}
17+
@spec find_repo(map) :: {:ok, GithubRepo.t} | {:error, :unmatched_repository}
1818
def find_repo(%{"repository" => %{"id" => github_id}}) do
19-
case GithubRepo |> Repo.get_by(github_id: github_id) |> Repo.preload(:project_github_repos) do
20-
# a GithubRepo with at least some ProjectGithubRepo children
21-
%GithubRepo{project_github_repos: [_ | _]} = github_repo -> {:ok, github_repo}
22-
# a GithubRepo with no ProjectGithubRepo children
23-
%GithubRepo{project_github_repos: []} -> {:error, :unmatched_project}
19+
case GithubRepo |> Repo.get_by(github_id: github_id) do
20+
%GithubRepo{} = github_repo -> {:ok, github_repo}
2421
nil -> {:error, :unmatched_repository}
2522
end
2623
end

lib/code_corps/github/event/issue_comment.ex

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ defmodule CodeCorps.GitHub.Event.IssueComment do
88

99
alias CodeCorps.{
1010
Comment,
11+
GithubRepo,
1112
GitHub.Event.Common.RepoFinder,
1213
GitHub.Event.Issues,
1314
GitHub.Event.Issues.TaskSyncer,
@@ -22,6 +23,7 @@ defmodule CodeCorps.GitHub.Event.IssueComment do
2223
{:error, :unexpected_action} |
2324
{:error, :unexpected_payload} |
2425
{:error, :repository_not_found} |
26+
{:error, :validation_error_on_inserting_issue_for_task} |
2527
{:error, :validation_error_on_inserting_user_for_task} |
2628
{:error, :multiple_github_users_matched_same_cc_user_for_task} |
2729
{:error, :validation_error_on_inserting_user_for_comment} |
@@ -60,12 +62,13 @@ defmodule CodeCorps.GitHub.Event.IssueComment do
6062
end
6163

6264
@spec operational_multi(map) :: Multi.t
63-
defp operational_multi(%{"action" => action} = payload) when action in ~w(created edited) do
65+
defp operational_multi(%{"action" => action, "issue" => issue_payload} = payload) when action in ~w(created edited) do
6466
Multi.new
6567
|> Multi.run(:repo, fn _ -> RepoFinder.find_repo(payload) end)
66-
|> Multi.run(:issue_user, fn _ -> Issues.UserLinker.find_or_create_user(payload) end)
68+
|> Multi.run(:issue, fn %{repo: %GithubRepo{} = github_repo} -> github_repo |> Issues.IssueLinker.create_or_update_issue(issue_payload) end)
69+
|> Multi.run(:issue_user, fn %{issue: github_issue} -> github_issue |> Issues.UserLinker.find_or_create_user(payload) end)
6770
|> Multi.run(:comment_user, fn _ -> IssueComment.UserLinker.find_or_create_user(payload) end)
68-
|> Multi.run(:tasks, fn %{repo: github_repo, issue_user: user} -> TaskSyncer.sync_all(github_repo, user, payload) end)
71+
|> Multi.run(:tasks, fn %{issue: github_issue, issue_user: user} -> github_issue |> TaskSyncer.sync_all(user, payload) end)
6972
|> Multi.run(:comments, fn %{tasks: tasks, comment_user: user} -> CommentSyncer.sync_all(tasks, user, payload) end)
7073
end
7174
defp operational_multi(%{"action" => "deleted"} = payload) do
@@ -80,6 +83,7 @@ defmodule CodeCorps.GitHub.Event.IssueComment do
8083
defp marshall_result({:error, :action, :unexpected_action, _steps}), do: {:error, :unexpected_action}
8184
defp marshall_result({:error, :repo, :unmatched_project, _steps}), do: {:ok, []}
8285
defp marshall_result({:error, :repo, :unmatched_repository, _steps}), do: {:error, :repository_not_found}
86+
defp marshall_result({:error, :issue, %Ecto.Changeset{}, _steps}), do: {:error, :validation_error_on_inserting_issue_for_task}
8387
defp marshall_result({:error, :issue_user, %Ecto.Changeset{}, _steps}), do: {:error, :validation_error_on_inserting_user_for_task}
8488
defp marshall_result({:error, :issue_user, :multiple_users, _steps}), do: {:error, :multiple_github_users_matched_same_cc_user_for_task}
8589
defp marshall_result({:error, :comment_user, %Ecto.Changeset{}, _steps}), do: {:error, :validation_error_on_inserting_user_for_comment}

lib/code_corps/github/event/issues.ex

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ defmodule CodeCorps.GitHub.Event.Issues do
99

1010
alias CodeCorps.{
1111
GitHub.Event.Common.RepoFinder,
12+
GitHub.Event.Issues.IssueLinker,
1213
GitHub.Event.Issues.TaskSyncer,
1314
GitHub.Event.Issues.UserLinker,
1415
GitHub.Event.Issues.Validator,
@@ -50,12 +51,18 @@ defmodule CodeCorps.GitHub.Event.Issues do
5051
|> Multi.run(:payload, fn _ -> payload |> validate_payload() end)
5152
|> Multi.run(:action, fn _ -> payload |> validate_action() end)
5253
|> Multi.run(:repo, fn _ -> RepoFinder.find_repo(payload) end)
53-
|> Multi.run(:user, fn _ -> UserLinker.find_or_create_user(payload) end)
54-
|> Multi.run(:tasks, fn %{repo: github_repo, user: user} -> TaskSyncer.sync_all(github_repo, user, payload) end)
54+
|> Multi.run(:issue, fn %{repo: github_repo} -> link_issue(github_repo, payload) end)
55+
|> Multi.run(:user, fn %{issue: github_issue} -> UserLinker.find_or_create_user(github_issue, payload) end)
56+
|> Multi.run(:tasks, fn %{issue: github_issue, user: user} -> github_issue |> TaskSyncer.sync_all(user, payload) end)
5557
|> Repo.transaction
5658
|> marshall_result()
5759
end
5860

61+
@spec link_issue(GithubRepo.t, map) :: {:ok, GithubIssue.t} | {:error, Ecto.Changeset.t}
62+
defp link_issue(github_repo, %{"issue" => attrs}) do
63+
IssueLinker.create_or_update_issue(github_repo, attrs)
64+
end
65+
5966
@spec marshall_result(tuple) :: tuple
6067
defp marshall_result({:ok, %{tasks: tasks}}), do: {:ok, tasks}
6168
defp marshall_result({:error, :payload, :invalid, _steps}), do: {:error, :unexpected_payload}

lib/code_corps/github/event/issues/changeset_builder.ex

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ defmodule CodeCorps.GitHub.Event.Issues.ChangesetBuilder do
55
"""
66

77
alias CodeCorps.{
8+
GithubIssue,
89
ProjectGithubRepo,
910
Repo,
1011
Services.MarkdownRendererService,
@@ -22,24 +23,26 @@ defmodule CodeCorps.GitHub.Event.Issues.ChangesetBuilder do
2223
2324
The changeset can be used to create or update a `Task`
2425
"""
25-
@spec build_changeset(Task.t, map, ProjectGithubRepo.t, User.t) :: Changeset.t
26+
@spec build_changeset(Task.t, map, GithubIssue.t, ProjectGithubRepo.t, User.t) :: Changeset.t
2627
def build_changeset(
2728
%Task{id: task_id} = task,
2829
%{"issue" => issue_attrs},
30+
%GithubIssue{} = github_issue,
2931
%ProjectGithubRepo{} = project_github_repo,
3032
%User{} = user) do
3133

3234
case is_nil(task_id) do
33-
true -> create_changeset(task, issue_attrs, project_github_repo, user)
35+
true -> create_changeset(task, issue_attrs, github_issue, project_github_repo, user)
3436
false -> update_changeset(task, issue_attrs)
3537
end
3638
end
3739

38-
@create_attrs ~w(created_at github_issue_number markdown modified_at status title)a
39-
@spec create_changeset(Task.t, map, ProjectGithubRepo.t, User.t) :: Changeset.t
40+
@create_attrs ~w(created_at markdown modified_at status title)a
41+
@spec create_changeset(Task.t, map, GithubIssue.t, ProjectGithubRepo.t, User.t) :: Changeset.t
4042
defp create_changeset(
4143
%Task{} = task,
4244
%{} = issue_attrs,
45+
%GithubIssue{id: github_issue_id},
4346
%ProjectGithubRepo{project_id: project_id, github_repo_id: github_repo_id},
4447
%User{id: user_id}) do
4548

@@ -51,18 +54,20 @@ defmodule CodeCorps.GitHub.Event.Issues.ChangesetBuilder do
5154
|> MarkdownRendererService.render_markdown_to_html(:markdown, :body)
5255
|> Changeset.put_change(:created_from, "github")
5356
|> Changeset.put_change(:modified_from, "github")
57+
|> Changeset.put_change(:github_issue_id, github_issue_id)
5458
|> Changeset.put_change(:github_repo_id, github_repo_id)
5559
|> Changeset.put_change(:project_id, project_id)
5660
|> Changeset.put_change(:task_list_id, task_list_id)
5761
|> Changeset.put_change(:user_id, user_id)
5862
|> Changeset.validate_required([:project_id, :task_list_id, :title, :user_id])
63+
|> Changeset.assoc_constraint(:github_issue)
5964
|> Changeset.assoc_constraint(:github_repo)
6065
|> Changeset.assoc_constraint(:project)
6166
|> Changeset.assoc_constraint(:task_list)
6267
|> Changeset.assoc_constraint(:user)
6368
end
6469

65-
@update_attrs ~w(github_issue_number markdown modified_at status title)a
70+
@update_attrs ~w(markdown modified_at status title)a
6671
@spec update_changeset(Task.t, map) :: Changeset.t
6772
defp update_changeset(%Task{} = task, %{} = issue_attrs) do
6873
task
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
defmodule CodeCorps.GitHub.Event.Issues.IssueLinker do
2+
@moduledoc ~S"""
3+
In charge of finding a issue to link with a Task when processing an Issues
4+
webhook.
5+
6+
The only entry point is `create_or_update_issue/1`.
7+
"""
8+
9+
alias CodeCorps.{
10+
GithubIssue,
11+
GithubRepo,
12+
Repo
13+
}
14+
15+
alias CodeCorps.GitHub.Adapters.Issue, as: IssueAdapter
16+
17+
@typep linking_result :: {:ok, GithubIssue.t} |
18+
{:error, Ecto.Changeset.t}
19+
20+
@doc ~S"""
21+
Finds or creates a `GithubIssue` using the data in a GitHub Issue payload.
22+
23+
The process is as follows:
24+
25+
- Search for the issue in our database with the payload data.
26+
- If we return a single `GithubIssue`, then the `GithubIssue` should be
27+
updated.
28+
- If there are no matching `GithubIssue` records, then a `GithubIssue`
29+
should be created.
30+
"""
31+
@spec create_or_update_issue(GithubRepo.t, map) :: linking_result
32+
def create_or_update_issue(%GithubRepo{} = github_repo, %{"id" => github_issue_id} = attrs) do
33+
params = IssueAdapter.from_api(attrs)
34+
35+
case Repo.get_by(GithubIssue, github_id: github_issue_id) do
36+
nil -> create_issue(github_repo, params)
37+
%GithubIssue{} = issue -> update_issue(issue, params)
38+
end
39+
end
40+
41+
defp create_issue(%GithubRepo{id: github_repo_id}, params) do
42+
params = Map.put(params, :github_repo_id, github_repo_id)
43+
44+
%GithubIssue{}
45+
|> GithubIssue.create_changeset(params)
46+
|> Repo.insert
47+
end
48+
49+
defp update_issue(%GithubIssue{} = github_issue, params) do
50+
github_issue
51+
|> GithubIssue.update_changeset(params)
52+
|> Repo.update
53+
end
54+
end

lib/code_corps/github/event/issues/task_syncer.ex

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
defmodule CodeCorps.GitHub.Event.Issues.TaskSyncer do
22
alias CodeCorps.{
3+
GithubIssue,
34
GithubRepo,
45
GitHub.Event.Common.ResultAggregator,
56
GitHub.Event.Issues.ChangesetBuilder,
@@ -15,34 +16,41 @@ defmodule CodeCorps.GitHub.Event.Issues.TaskSyncer do
1516
{:error, {list(Task.t), list(Changeset.t)}}
1617

1718
@doc """
18-
When provided a `GithubRepo`, a `User` and a GitHub API payload, for each
19-
`Project` associated to that `GithubRepo` via a `ProjectGithubRepo`, it
20-
creates or updates a `Task` associated to the specified `User`.
19+
When provided a `CodeCorps.GithubIssue`, a `CodeCorps.User` and a GitHub API
20+
payload, for each `CodeCorps.Project` associated to that
21+
`CodeCorps.GithubRepo` via a `CodeCorps.ProjectGithubRepo`, it
22+
creates or updates a `CodeCorps.Task`.
2123
"""
22-
@spec sync_all(GithubRepo.t, User.t, map) :: {:ok, list(Task.t)}
23-
def sync_all(%GithubRepo{project_github_repos: project_github_repos}, %User{} = user, %{} = payload) do
24+
@spec sync_all(GithubIssue.t, User.t, map) :: {:ok, list(Task.t)}
25+
def sync_all(%GithubIssue{} = github_issue, %User{} = user, %{} = payload) do
26+
27+
%GithubIssue{
28+
github_repo: %GithubRepo{project_github_repos: project_github_repos}
29+
} = github_issue |> Repo.preload(github_repo: :project_github_repos)
30+
2431
project_github_repos
25-
|> Enum.map(&sync(&1, user, payload))
32+
|> Enum.map(&sync(github_issue, &1, user, payload))
2633
|> ResultAggregator.aggregate
2734
end
2835

29-
@spec sync(ProjectGithubRepo.t, User.t, map) :: {:ok, ProjectGithubRepo.t} | {:error, Changeset.t}
30-
defp sync(%ProjectGithubRepo{} = project_github_repo, %User{} = user, %{} = payload) do
36+
@spec sync(GithubIssue.t, ProjectGithubRepo.t, User.t, map) :: {:ok, ProjectGithubRepo.t} | {:error, Changeset.t}
37+
defp sync(%GithubIssue{} = github_issue, %ProjectGithubRepo{} = project_github_repo, %User{} = user, %{} = payload) do
3138
project_github_repo
32-
|> find_or_init_task(payload)
33-
|> ChangesetBuilder.build_changeset(payload, project_github_repo, user)
39+
|> find_or_init_task(github_issue)
40+
|> ChangesetBuilder.build_changeset(payload, github_issue, project_github_repo, user)
3441
|> commit()
3542
end
3643

37-
@spec find_or_init_task(ProjectGithubRepo.t, map) :: Task.t
44+
@spec find_or_init_task(ProjectGithubRepo.t, GithubIssue.t) :: Task.t
3845
defp find_or_init_task(
3946
%ProjectGithubRepo{project_id: project_id, github_repo_id: github_repo_id},
40-
%{"issue" => %{"number" => github_issue_number}}) do
47+
%GithubIssue{id: github_issue_id}
48+
) do
4149

4250
query_params = [
43-
github_issue_number: github_issue_number,
44-
project_id: project_id,
45-
github_repo_id: github_repo_id
51+
github_issue_id: github_issue_id,
52+
github_repo_id: github_repo_id,
53+
project_id: project_id
4654
]
4755

4856
case Task |> Repo.get_by(query_params) do

0 commit comments

Comments
 (0)