diff --git a/lib/code_corps/helpers/query.ex b/lib/code_corps/helpers/query.ex index 440e5a301..2c387e054 100644 --- a/lib/code_corps/helpers/query.ex +++ b/lib/code_corps/helpers/query.ex @@ -28,11 +28,22 @@ defmodule CodeCorps.Helpers.Query do end def project_id_with_number_filter(query, _), do: query + def task_list_id_with_number_filter(query, %{"id" => number, "task_list_id" => task_list_id}) do + query |> where([object], object.number == ^number and object.task_list_id == ^task_list_id) + end + def task_list_id_with_number_filter(query, _), do: query + def project_filter(query, %{"project_id" => project_id}) do query |> where([object], object.project_id == ^project_id) end def project_filter(query, _), do: query + def task_list_filter(query, %{"task_list_ids" => task_list_ids}) do + task_list_ids = task_list_ids |> coalesce_id_string + query |> where([object], object.task_list_id in ^task_list_ids) + end + def task_list_filter(query, _), do: query + def task_type_filter(query, %{"task_type" => task_type_list}) do task_types = task_type_list |> coalesce_string query |> where([object], object.task_type in ^task_types) @@ -58,6 +69,8 @@ defmodule CodeCorps.Helpers.Query do def sort_by_newest_first(query), do: query |> order_by([desc: :inserted_at]) + def sort_by_order(query), do: query |> order_by([asc: :order]) + # end sorting # finders diff --git a/mix.exs b/mix.exs index 8eadef495..e968acef1 100644 --- a/mix.exs +++ b/mix.exs @@ -92,7 +92,8 @@ defmodule CodeCorps.Mixfile do {:stripity_stripe, "~> 2.0.0-alpha.5"}, # Stripe {:timber, "~> 0.4"}, # Logging {:timex, "~> 3.0"}, - {:timex_ecto, "~> 3.0"} + {:timex_ecto, "~> 3.0"}, + {:ecto_ordered, "0.2.0-beta1"} ] end diff --git a/mix.lock b/mix.lock index db0df1805..11b928675 100644 --- a/mix.lock +++ b/mix.lock @@ -8,16 +8,17 @@ "canary": {:hex, :canary, "1.1.0", "3599012f5393c2fdb18c9129853a9fc6cd115ebfdcc0725af7b52398b9ed5c7b", [:mix], [{:canada, "~> 1.0.0", [hex: :canada, optional: false]}, {:ecto, ">= 1.1.0", [hex: :ecto, optional: false]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}]}, "certifi": {:hex, :certifi, "0.7.0", "861a57f3808f7eb0c2d1802afeaae0fa5de813b0df0979153cbafcd853ababaf", [:rebar3], []}, "combine": {:hex, :combine, "0.9.3", "192e609b48b3f2210494e26f85db1712657be1a8f15795656710317ea43fc449", [:mix], []}, - "comeonin": {:hex, :comeonin, "2.6.0", "74c288338b33205f9ce97e2117bb9a2aaab103a1811d243443d76fdb62f904ac", [:make, :make, :mix], []}, + "comeonin": {:hex, :comeonin, "2.6.0", "74c288338b33205f9ce97e2117bb9a2aaab103a1811d243443d76fdb62f904ac", [:mix, :make, :make], []}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], []}, "corsica": {:hex, :corsica, "0.5.0", "eb5b2fccc5bc4f31b8e2b77dd15f5f302aca5d63286c953e8e916f806056d50c", [:mix], [{:cowboy, ">= 1.0.0", [hex: :cowboy, optional: false]}, {:plug, ">= 0.9.0", [hex: :plug, optional: false]}]}, - "cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [:make, :rebar], [{:cowlib, "~> 1.0.0", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, optional: false]}]}, + "cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [:rebar, :make], [{:cowlib, "~> 1.0.0", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, optional: false]}]}, "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []}, "credo": {:hex, :credo, "0.5.2", "92e8c9f86e0ffbf9f688595e9f4e936bc96a52e5606d2c19713e9e4d191d5c74", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, optional: false]}]}, "db_connection": {:hex, :db_connection, "1.1.0", "b2b88db6d7d12f99997b584d09fad98e560b817a20dab6a526830e339f54cdb3", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, optional: true]}]}, "decimal": {:hex, :decimal, "1.3.1", "157b3cedb2bfcb5359372a7766dd7a41091ad34578296e951f58a946fcab49c6", [:mix], []}, "earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], []}, "ecto": {:hex, :ecto, "2.0.6", "9dcbf819c2a77f67a66b83739b7fcc00b71aaf6c100016db4f798930fa4cfd47", [:mix], [{:db_connection, "~> 1.0", [hex: :db_connection, optional: true]}, {:decimal, "~> 1.1.2 or ~> 1.2", [hex: :decimal, optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, optional: true]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}, {:postgrex, "~> 0.12.0", [hex: :postgrex, optional: true]}, {:sbroker, "~> 1.0-beta", [hex: :sbroker, optional: true]}]}, + "ecto_ordered": {:hex, :ecto_ordered, "0.2.0-beta1", "cb066bc608f1c8913cea85af8293261720e6a88e3c99061e6877d7025352f045", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: false]}]}, "ex_aws": {:hex, :ex_aws, "0.5.0", "6ca02f1e8fe8340aa2eee66d9f08efcd6ff1f9f4ef7264669d0756e0b3917218", [:mix], [{:httpoison, "~> 0.8", [hex: :httpoison, optional: true]}, {:jsx, "~> 2.5", [hex: :jsx, optional: true]}, {:poison, "~> 1.2 or ~> 2.0", [hex: :poison, optional: true]}, {:sweet_xml, "~> 0.5", [hex: :sweet_xml, optional: true]}]}, "ex_doc": {:hex, :ex_doc, "0.14.3", "e61cec6cf9731d7d23d254266ab06ac1decbb7651c3d1568402ec535d387b6f7", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}, "ex_machina": {:hex, :ex_machina, "1.0.2", "1cc49e1a09e3f7ab2ecb630c17f14c2872dc4ec145d6d05a9c3621936a63e34f", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: true]}]}, @@ -26,7 +27,7 @@ "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []}, "gettext": {:hex, :gettext, "0.12.1", "c0624f52763469ef7a3674919ae28b8286d88195b90fa1516180f31bbbd26d14", [:mix], []}, "guardian": {:hex, :guardian, "0.13.0", "37c5b5302617276093570ee938baca146f53e1d5de1f5c2b8effb1d2fea596d2", [:mix], [{:jose, "~> 1.8", [hex: :jose, optional: false]}, {:phoenix, "~> 1.2.0", [hex: :phoenix, optional: true]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}, {:poison, ">= 1.3.0", [hex: :poison, optional: false]}, {:uuid, ">=1.1.1", [hex: :uuid, optional: false]}]}, - "hackney": {:hex, :hackney, "1.6.3", "d489d7ca2d4323e307bedc4bfe684323a7bf773ecfd77938f3ee8074e488e140", [:mix, :rebar3], [{:certifi, "0.7.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, + "hackney": {:hex, :hackney, "1.6.3", "d489d7ca2d4323e307bedc4bfe684323a7bf773ecfd77938f3ee8074e488e140", [:rebar3, :mix], [{:certifi, "0.7.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, "httpoison": {:hex, :httpoison, "0.10.0", "4727b3a5e57e9a4ff168a3c2883e20f1208103a41bccc4754f15a9366f49b676", [:mix], [{:hackney, "~> 1.6.3", [hex: :hackney, optional: false]}]}, "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, "inch_ex": {:hex, :inch_ex, "0.5.5", "b63f57e281467bd3456461525fdbc9e158c8edbe603da6e3e4671befde796a3d", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, optional: false]}]}, @@ -53,7 +54,7 @@ "scrivener_ecto": {:hex, :scrivener_ecto, "1.0.2", "4b10a2e6c23ed8aae59731d7ae71bfd55afea6559aae61b124e6e521055b4a9c", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: false]}, {:postgrex, "~> 0.11.0 or ~> 0.12.0", [hex: :postgrex, optional: true]}, {:scrivener, "~> 2.0", [hex: :scrivener, optional: false]}]}, "segment": {:git, "https://github.com/stueccles/analytics-elixir.git", "8fe520c16a8a9290d55c849bf4d67420396e1cdd", []}, "sentry": {:hex, :sentry, "2.0.2", "f08638758f7bf891e238466009f6cd702fc26d87286663af26927a78ed149346", [:mix], [{:hackney, "~> 1.6.1", [hex: :hackney, optional: false]}, {:plug, "~> 1.0", [hex: :plug, optional: true]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, optional: false]}, {:uuid, "~> 1.0", [hex: :uuid, optional: false]}]}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:rebar, :make], []}, "stripe_eventex": {:hex, :stripe_eventex, "1.0.0", "782016598b751c0fdb5489038c92c30a5aab034636d0d9d3a486f75a01fbf0b6", [:mix], [{:cowboy, "~> 1.0.0", [hex: :cowboy, optional: false]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}, {:poison, "~> 2.0", [hex: :poison, optional: false]}]}, "stripity_stripe": {:hex, :stripity_stripe, "2.0.0-alpha.5", "ba6d4ffc6251029135c76e9c6e2dd77580713f5c6833fb82da708336023bbfa2", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, optional: false]}, {:poison, "~> 2.0 or ~> 3.0", [hex: :poison, optional: false]}]}, "timber": {:hex, :timber, "0.4.7", "df3fcd79bcb4eb4b53874d906ef5f3a212937b4bc7b7c5b244745202cc389443", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: true]}, {:phoenix, "~> 1.2", [hex: :phoenix, optional: true]}, {:plug, "~> 1.2", [hex: :plug, optional: true]}, {:poison, "~> 2.0 or ~> 3.0", [hex: :poison, optional: false]}]}, diff --git a/priv/repo/migrations/20161209192504_create_task_list.exs b/priv/repo/migrations/20161209192504_create_task_list.exs new file mode 100644 index 000000000..fa2d70fdf --- /dev/null +++ b/priv/repo/migrations/20161209192504_create_task_list.exs @@ -0,0 +1,66 @@ +defmodule CodeCorps.Repo.Migrations.CreateTaskList do + use Ecto.Migration + import Ecto.Changeset + import Ecto.Query + + alias CodeCorps.Project + alias CodeCorps.Repo + alias CodeCorps.Task + alias CodeCorps.TaskList + + def change do + create table(:task_lists) do + add :name, :string + add :order, :integer + add :project_id, references(:projects, on_delete: :nothing) + + timestamps() + end + + create index(:task_lists, [:project_id]) + + alter table(:tasks) do + add :task_list_id, references(:task_lists, on_delete: :nothing) + add :order, :integer + end + + flush + + Application.ensure_all_started :timex + migrate_existing() + end + + def migrate_existing() do + Project + |> preload(:task_lists) + |> Repo.all() + |> Enum.each(&handle_project_migration/1) + end + + defp handle_project_migration(project) do + cond do + project.task_lists != [] -> + IO.puts "Task lists already exist for #{project.title}, skipping migration." + true -> + IO.puts "Generating default task lists for #{project.title}." + + {:ok, project} = Project.changeset(project, %{}) + |> put_assoc(:task_lists, TaskList.default_task_lists()) + |> Repo.update + + add_existing_tasks_to_inbox(project, hd(project.task_lists)) + end + end + + defp add_existing_tasks_to_inbox(project, task_list) do + Task + |> CodeCorps.Helpers.Query.project_filter(%{ project_id: project.id }) + |> Repo.all() + |> Enum.each(&assign_task_to_inbox(&1, task_list)) + end + + defp assign_task_to_inbox(task, task_list) do + Task.changeset(task, %{ task_list_id: task_list.id }) + |> Repo.update() + end +end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index d2fac7313..bd2c39c5c 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -295,7 +295,8 @@ cond do status: "open", number: i, project_id: 1, - user_id: 1 + user_id: 1, + task_list_id: 1 }) |> Repo.insert! end diff --git a/test/controllers/project_controller_test.exs b/test/controllers/project_controller_test.exs index b87321332..3b29004df 100644 --- a/test/controllers/project_controller_test.exs +++ b/test/controllers/project_controller_test.exs @@ -74,7 +74,9 @@ defmodule CodeCorps.ProjectControllerTest do organization = insert(:organization) insert(:organization_membership, role: "admin", member: current_user, organization: organization) attrs = @valid_attrs |> Map.merge(%{organization: organization}) - assert conn |> request_create(attrs) |> json_response(201) + response = conn |> request_create(attrs) + assert %{assigns: %{data: %{task_lists: [_inbox, _backlog, _in_progress, _done]}}} = response + assert response |> json_response(201) end @tag authenticated: :admin diff --git a/test/controllers/task_controller_test.exs b/test/controllers/task_controller_test.exs index 47c65b894..b907228a4 100644 --- a/test/controllers/task_controller_test.exs +++ b/test/controllers/task_controller_test.exs @@ -24,12 +24,12 @@ defmodule CodeCorps.TaskControllerTest do |> assert_ids_from_response([task_1.id, task_2.id]) end - test "lists all entries newest first", %{conn: conn} do + test "lists all entries, ordered correctly", %{conn: conn} do # Has to be done manually. Inserting as a list is too quick. # Field lacks the resolution to differentiate. - task_1 = insert(:task, inserted_at: Timex.to_date({2000, 1, 1})) - task_2 = insert(:task, inserted_at: Timex.to_date({2000, 1, 2})) - task_3 = insert(:task, inserted_at: Timex.to_date({2000, 1, 3})) + task_1 = insert(:task, order: 3000) + task_2 = insert(:task, order: 2000) + task_3 = insert(:task, order: 1000) path = conn |> task_path(:index) json = conn |> get(path) |> json_response(200) @@ -138,7 +138,8 @@ defmodule CodeCorps.TaskControllerTest do @tag :authenticated test "creates and renders resource when data is valid", %{conn: conn, current_user: current_user} do project = insert(:project) - attrs = @valid_attrs |> Map.merge(%{project: project, user: current_user}) + task_list = insert(:task_list, project: project) + attrs = @valid_attrs |> Map.merge(%{project: project, user: current_user, task_list: task_list}) json = conn |> request_create(attrs) |> json_response(201) # ensure record is reloaded from database before serialized, since number is added diff --git a/test/controllers/task_list_controller_test.exs b/test/controllers/task_list_controller_test.exs new file mode 100644 index 000000000..b0f6aca6c --- /dev/null +++ b/test/controllers/task_list_controller_test.exs @@ -0,0 +1,79 @@ +defmodule CodeCorps.TaskListControllerTest do + use CodeCorps.ApiCase, resource_name: :task_list + + @valid_attrs %{ + name: "Test task" + } + + @invalid_attrs %{ + name: nil + } + + describe "index" do + test "lists all entries", %{conn: conn} do + [task_list_1, task_list_2] = insert_pair(:task_list) + + conn + |> request_index + |> json_response(200) + |> assert_ids_from_response([task_list_1.id, task_list_2.id]) + end + + test "lists all entries by order", %{conn: conn} do + # Has to be done manually. Inserting as a list is too quick. + # Field lacks the resolution to differentiate. + project = insert(:project) + task_list_1 = insert(:task_list, project: project, order: 2000) + task_list_2 = insert(:task_list, project: project, order: 1000) + task_list_3 = insert(:task_list, project: project, order: 3000) + + path = conn |> task_list_path(:index) + + conn + |> get(path) + |> json_response(200) + |> assert_ids_from_response([task_list_2.id, task_list_1.id, task_list_3.id]) + end + + test "lists all task lists for a project", %{conn: conn} do + project_1 = insert(:project) + project_2 = insert(:project) + insert(:task_list, project: project_1) + insert(:task_list, project: project_1) + insert(:task_list, project: project_2) + + json = + conn + |> get("projects/#{project_1.id}/task-lists") + |> json_response(200) + + assert json["data"] |> Enum.count == 2 + end + end + + describe "show" do + test "shows chosen resource", %{conn: conn} do + task_list = insert(:task_list) + + conn + |> request_show(task_list) + |> json_response(200) + |> Map.get("data") + |> assert_result_id(task_list.id) + end + + test "shows task list by id for project", %{conn: conn} do + task_list = insert(:task_list) + + path = conn |> project_task_list_path(:show, task_list.project_id, task_list.id) + data = conn |> get(path) |> json_response(200) |> Map.get("data") + + assert data["id"] == "#{task_list.id}" + assert data["type"] == "task-list" + end + + test "renders 404 when id is nonexistent", %{conn: conn} do + assert conn |> request_show(:not_found) |> json_response(404) + end + end +end diff --git a/test/models/task_list_test.exs b/test/models/task_list_test.exs new file mode 100644 index 000000000..24428cc78 --- /dev/null +++ b/test/models/task_list_test.exs @@ -0,0 +1,18 @@ +defmodule CodeCorps.TaskListTest do + use CodeCorps.ModelCase + + alias CodeCorps.TaskList + + @valid_attrs %{name: "some content", position: 42} + @invalid_attrs %{} + + test "changeset with valid attributes" do + changeset = TaskList.changeset(%TaskList{}, @valid_attrs) + assert changeset.valid? + end + + test "changeset with invalid attributes" do + changeset = TaskList.changeset(%TaskList{}, @invalid_attrs) + refute changeset.valid? + end +end diff --git a/test/models/task_test.exs b/test/models/task_test.exs index 844773949..16a23e74c 100644 --- a/test/models/task_test.exs +++ b/test/models/task_test.exs @@ -42,12 +42,14 @@ defmodule CodeCorps.TaskTest do test "is valid with valid attributes" do user = insert(:user) project = insert(:project) + task_list = insert(:task_list) changeset = Task.create_changeset(%Task{}, %{ markdown: "some content", task_type: "issue", title: "some content", project_id: project.id, user_id: user.id, + task_list_id: task_list.id }) assert changeset.valid? end @@ -56,15 +58,18 @@ defmodule CodeCorps.TaskTest do user = insert(:user) project_a = insert(:project, title: "Project A") project_b = insert(:project, title: "Project B") + task_list_a = insert(:task_list, name: "Task List A", project: project_a) + task_list_b = insert(:task_list, name: "Task List B", project: project_b) - insert(:task, project: project_a, user: user, title: "Project A Task 1") - insert(:task, project: project_a, user: user, title: "Project A Task 2") + insert(:task, project: project_a, user: user, task_list: task_list_a, order: 2000, title: "Project A Task 1") + insert(:task, project: project_a, user: user, task_list: task_list_a, order: 1000, title: "Project A Task 2") - insert(:task, project: project_b, user: user, title: "Project B Task 1") + insert(:task, project: project_b, user: user, task_list: task_list_b, title: "Project B Task 1") changes = Map.merge(@valid_attrs, %{ project_id: project_a.id, - user_id: user.id + user_id: user.id, + task_list_id: task_list_a.id }) changeset = Task.create_changeset(%Task{}, changes) {:ok, result} = Repo.insert(changeset) @@ -72,13 +77,49 @@ defmodule CodeCorps.TaskTest do changes = Map.merge(@valid_attrs, %{ project_id: project_b.id, - user_id: user.id + user_id: user.id, + task_list_id: task_list_b.id }) changeset = Task.create_changeset(%Task{}, changes) {:ok, result} = Repo.insert(changeset) assert result.number == 2 end + test "auto-assigns order, beginning of list, scoped to task list" do + user = insert(:user) + project_a = insert(:project, title: "Project A") + task_list_a = insert(:task_list, name: "Task List A", project: project_a) + task_list_b = insert(:task_list, name: "Task List B", project: project_a) + + task_a_1 = insert(:task, project: project_a, user: user, task_list: task_list_a, order: 2000, title: "Project A Task 1") + task_a_2 = insert(:task, project: project_a, user: user, task_list: task_list_a, order: 1000, title: "Project A Task 2") + + task_b_1 = insert(:task, project: project_a, user: user, task_list: task_list_b, order: 2000, title: "Project B Task 1") + task_b_2 = insert(:task, project: project_a, user: user, task_list: task_list_b, order: 1000, title: "Project B Task 2") + + changes = Map.merge(@valid_attrs, %{ + project_id: project_a.id, + user_id: user.id, + task_list_id: task_list_a.id + }) + changeset = Task.create_changeset(%Task{}, changes) + {:ok, result_a} = Repo.insert(changeset) + assert result_a.order < task_a_1.order && result_a.order < task_a_2.order + + changes = Map.merge(@valid_attrs, %{ + project_id: project_a.id, + user_id: user.id, + task_list_id: task_list_b.id + }) + changeset = Task.create_changeset(%Task{}, changes) + {:ok, result_b} = Repo.insert(changeset) + assert result_b.order < task_b_1.order && result_b.order < task_b_2.order + + # Make sure that, given the same order configuration between task lists, + # the auto-assigned order is the same, meaning the order is correctly scoped + assert result_a.order == result_b.order + end + test "sets state to 'published'" do changeset = Task.create_changeset(%Task{}, %{}) assert changeset |> get_change(:state) == "published" diff --git a/test/support/factories.ex b/test/support/factories.ex index 4a41d4168..63c75b2dc 100644 --- a/test/support/factories.ex +++ b/test/support/factories.ex @@ -52,7 +52,16 @@ defmodule CodeCorps.Factories do status: "open", state: "published", project: build(:project), - user: build(:user) + user: build(:user), + task_list: build(:task_list) + } + end + + def task_list_factory do + %CodeCorps.TaskList{ + name: "Test task list", + position: 1, + project: build(:project) } end diff --git a/test/test_helper.exs b/test/test_helper.exs index 966eb7ab1..4d6041097 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -8,4 +8,3 @@ ExUnit.configure exclude: [:requires_env] ExUnit.start Ecto.Adapters.SQL.Sandbox.mode(CodeCorps.Repo, :manual) - diff --git a/test/views/project_view_test.exs b/test/views/project_view_test.exs index 969175f66..bf8fdd28b 100644 --- a/test/views/project_view_test.exs +++ b/test/views/project_view_test.exs @@ -11,7 +11,8 @@ defmodule CodeCorps.ProjectViewTest do project_category = insert(:project_category, project: project) project_skill = insert(:project_skill, project: project) stripe_connect_plan = insert(:stripe_connect_plan, project: project) - task = insert(:task, project: project) + task_list = insert(:task_list, project: project) + task = insert(:task, project: project, task_list: task_list) rendered_json = render(CodeCorps.ProjectView, "show.json-api", data: project) @@ -66,6 +67,14 @@ defmodule CodeCorps.ProjectViewTest do "type" => "stripe-connect-plan" } }, + "task-lists" => %{ + "data" => [ + %{ + "id" => task_list.id |> Integer.to_string, + "type" => "task-list" + } + ] + }, "tasks" => %{ "data" => [ %{ diff --git a/test/views/task_list_view_test.exs b/test/views/task_list_view_test.exs new file mode 100644 index 000000000..fe828888c --- /dev/null +++ b/test/views/task_list_view_test.exs @@ -0,0 +1,45 @@ +defmodule CodeCorps.TaskListViewTest do + use CodeCorps.ConnCase, async: true + + import Phoenix.View, only: [render: 3] + + test "renders all attributes and relationships properly" do + project = insert(:project) + task_list = insert(:task_list, order: 1000, project: project) + task = insert(:task, order: 1000, task_list: task_list) + + rendered_json = render(CodeCorps.TaskListView, "show.json-api", data: task_list) + + expected_json = %{ + "data" => %{ + "attributes" => %{ + "name" => task_list.name, + "order" => 1000, + "inserted-at" => task_list.inserted_at, + "updated-at" => task_list.updated_at, + }, + "id" => task_list.id |> Integer.to_string, + "relationships" => %{ + "project" => %{ + "data" => %{ + "id" => task_list.project_id |> Integer.to_string, + "type" => "project" + } + }, + "tasks" => %{ + "data" => [%{ + "id" => task.id |> Integer.to_string, + "type" => "task" + }] + } + }, + "type" => "task-list", + }, + "jsonapi" => %{ + "version" => "1.0" + } + } + + assert rendered_json == expected_json + end +end diff --git a/test/views/task_view_test.exs b/test/views/task_view_test.exs index 0acc8ce88..e677ae9e9 100644 --- a/test/views/task_view_test.exs +++ b/test/views/task_view_test.exs @@ -4,7 +4,7 @@ defmodule CodeCorps.TaskViewTest do import Phoenix.View, only: [render: 3] test "renders all attributes and relationships properly" do - task = insert(:task) + task = insert(:task, order: 1000) comment = insert(:comment, task: task) rendered_json = render(CodeCorps.TaskView, "show.json-api", data: task) @@ -16,11 +16,12 @@ defmodule CodeCorps.TaskViewTest do "inserted-at" => task.inserted_at, "markdown" => task.markdown, "number" => task.number, + "order" => task.order, "status" => task.status, "state" => task.state, "task-type" => task.task_type, "title" => task.title, - "updated-at" => task.updated_at, + "updated-at" => task.updated_at }, "id" => task.id |> Integer.to_string, "relationships" => %{ @@ -43,6 +44,12 @@ defmodule CodeCorps.TaskViewTest do "id" => task.user_id |> Integer.to_string, "type" => "user" } + }, + "task-list" => %{ + "data" => %{ + "id" => task.task_list_id |> Integer.to_string, + "type" => "task-list" + } } }, "type" => "task", diff --git a/web/controllers/task_controller.ex b/web/controllers/task_controller.ex index ee51b24a1..b61f969cf 100644 --- a/web/controllers/task_controller.ex +++ b/web/controllers/task_controller.ex @@ -3,8 +3,8 @@ defmodule CodeCorps.TaskController do use JaResource import CodeCorps.Helpers.Query, only: [ - project_filter: 2, project_id_with_number_filter: 2, sort_by_newest_first: 1, - task_type_filter: 2, task_status_filter: 2 + project_filter: 2, project_id_with_number_filter: 2, task_list_id_with_number_filter: 2, + sort_by_order: 1, task_list_filter: 2, task_type_filter: 2, task_status_filter: 2 ] alias CodeCorps.Task @@ -16,9 +16,10 @@ defmodule CodeCorps.TaskController do def handle_index(conn, params) do page = Task |> project_filter(params) + |> task_list_filter(params) |> task_type_filter(params) |> task_status_filter(params) - |> sort_by_newest_first + |> sort_by_order |> Repo.paginate(params["page"] || %{}) # TODO: Once we are able to more easily add top-level meta @@ -44,6 +45,13 @@ defmodule CodeCorps.TaskController do |> project_id_with_number_filter(params) |> Repo.one end + + def record(%Plug.Conn{params: %{"task_list_id" => _task_list_id} = params}, _number_as_id) do + Task + |> task_list_id_with_number_filter(params) + |> Repo.one + end + def record(_conn, id), do: Task |> Repo.get(id) def handle_create(conn, attributes) do diff --git a/web/controllers/task_list_controller.ex b/web/controllers/task_list_controller.ex new file mode 100644 index 000000000..ee9b17ce8 --- /dev/null +++ b/web/controllers/task_list_controller.ex @@ -0,0 +1,30 @@ +defmodule CodeCorps.TaskListController do + use CodeCorps.Web, :controller + use JaResource + + import CodeCorps.Helpers.Query, only: [ + project_filter: 2, sort_by_order: 1, + ] + + alias CodeCorps.TaskList + + plug :load_resource, model: TaskList, only: [:show] + plug JaResource + + def handle_index(conn, params) do + tasks = TaskList + |> project_filter(params) + |> sort_by_order + |> Repo.all() + + conn + |> render("index.json-api", data: tasks) + end + + def record(%Plug.Conn{params: %{"project_id" => _project_id} = params}, id) do + TaskList + |> project_filter(params) + |> Repo.get(id) + end + def record(_conn, id), do: TaskList |> Repo.get(id) +end diff --git a/web/models/project.ex b/web/models/project.ex index 21502ee00..75aa57d07 100644 --- a/web/models/project.ex +++ b/web/models/project.ex @@ -9,6 +9,7 @@ defmodule CodeCorps.Project do import CodeCorps.Helpers.Slug import CodeCorps.Validators.SlugValidator alias CodeCorps.Services.MarkdownRendererService + alias CodeCorps.TaskList schema "projects" do field :approved, :boolean @@ -28,6 +29,7 @@ defmodule CodeCorps.Project do has_many :donation_goals, CodeCorps.DonationGoal has_many :project_categories, CodeCorps.ProjectCategory has_many :project_skills, CodeCorps.ProjectSkill + has_many :task_lists, CodeCorps.TaskList has_many :tasks, CodeCorps.Task has_many :categories, through: [:project_categories, :category] @@ -55,7 +57,7 @@ defmodule CodeCorps.Project do struct |> cast(params, [:organization_id]) |> changeset(params) - + |> put_assoc(:task_lists, TaskList.default_task_lists()) end @doc """ diff --git a/web/models/task.ex b/web/models/task.ex index 464f1f462..e183781f7 100644 --- a/web/models/task.ex +++ b/web/models/task.ex @@ -1,5 +1,6 @@ defmodule CodeCorps.Task do use CodeCorps.Web, :model + import EctoOrdered alias CodeCorps.Services.MarkdownRendererService schema "tasks" do @@ -10,9 +11,12 @@ defmodule CodeCorps.Task do field :state, :string field :status, :string, default: "open" field :title, :string + field :position, :integer, virtual: true + field :order, :integer belongs_to :project, CodeCorps.Project belongs_to :user, CodeCorps.User + belongs_to :task_list, CodeCorps.TaskList has_many :comments, CodeCorps.Comment timestamps() @@ -20,9 +24,12 @@ defmodule CodeCorps.Task do def changeset(struct, params \\ %{}) do struct - |> cast(params, [:title, :markdown, :task_type]) + |> cast(params, [:title, :markdown, :task_type, :task_list_id, :position]) |> validate_required([:title, :markdown, :task_type]) |> validate_inclusion(:task_type, task_types) + |> assoc_constraint(:task_list) + |> apply_position() + |> set_order(:position, :order, :task_list_id) |> MarkdownRendererService.render_markdown_to_html(:markdown, :body) end @@ -30,7 +37,7 @@ defmodule CodeCorps.Task do struct |> changeset(params) |> cast(params, [:project_id, :user_id]) - |> validate_required([:project_id, :user_id]) + |> validate_required([:project_id, :user_id, :task_list_id]) |> assoc_constraint(:project) |> assoc_constraint(:user) |> put_change(:state, "published") @@ -43,7 +50,14 @@ defmodule CodeCorps.Task do |> cast(params, [:status]) |> validate_inclusion(:status, statuses) |> put_change(:state, "edited") + end + def apply_position(changeset) do + case get_field(changeset, :position) do + nil -> + put_change(changeset, :position, 0) + _ -> changeset + end end defp task_types do diff --git a/web/models/task_list.ex b/web/models/task_list.ex new file mode 100644 index 000000000..4a1abd48e --- /dev/null +++ b/web/models/task_list.ex @@ -0,0 +1,43 @@ +defmodule CodeCorps.TaskList do + use CodeCorps.Web, :model + import EctoOrdered + + schema "task_lists" do + field :name, :string + field :position, :integer, virtual: true + field :order, :integer + + belongs_to :project, CodeCorps.Project + has_many :tasks, CodeCorps.Task + + timestamps() + end + + def default_task_lists() do + [ + %{ + name: "Inbox", + position: 1 + }, %{ + name: "Backlog", + position: 2 + }, %{ + name: "In Progress", + position: 3 + }, %{ + name: "Done", + position: 4 + } + ] + end + + @doc """ + Builds a changeset based on the `struct` and `params`. + """ + def changeset(struct, params \\ %{}) do + struct + |> cast(params, [:name, :position]) + |> validate_required([:name, :position]) + |> set_order(:position, :order, :project_id) + end +end diff --git a/web/router.ex b/web/router.ex index 086fe8884..c48c92a94 100644 --- a/web/router.ex +++ b/web/router.ex @@ -92,6 +92,7 @@ defmodule CodeCorps.Router do resources "/organizations", OrganizationController, only: [:index, :show] resources "/organization-memberships", OrganizationMembershipController, only: [:index, :show] resources "/projects", ProjectController, only: [:index, :show] do + resources "/task-lists", TaskListController, only: [:index, :show] resources "/tasks", TaskController, only: [:index, :show] end resources "/project-categories", ProjectCategoryController, only: [:index, :show] @@ -99,6 +100,9 @@ defmodule CodeCorps.Router do resources "/roles", RoleController, only: [:index, :show] resources "/role-skills", RoleSkillController, only: [:index, :show] resources "/skills", SkillController, only: [:index, :show] + resources "/task-lists", TaskListController, only: [:index, :show] do + resources "/tasks", TaskController, only: [:index, :show] + end resources "/tasks", TaskController, only: [:index, :show] get "/users/email_available", UserController, :email_available get "/users/username_available", UserController, :username_available diff --git a/web/views/project_view.ex b/web/views/project_view.ex index 1b8adbdc5..7b19fa3ee 100644 --- a/web/views/project_view.ex +++ b/web/views/project_view.ex @@ -2,7 +2,7 @@ defmodule CodeCorps.ProjectView do use CodeCorps.PreloadHelpers, default_preloads: [ :donation_goals, :organization, :project_categories, - :stripe_connect_plan, :project_skills, :tasks + :stripe_connect_plan, :project_skills, :task_lists, :tasks ] use CodeCorps.Web, :view use JaSerializer.PhoenixView @@ -18,6 +18,7 @@ defmodule CodeCorps.ProjectView do has_many :donation_goals, serializer: CodeCorps.DonationGoalView, identifiers: :always has_many :project_categories, serializer: CodeCorps.ProjectCategoryView, identifiers: :always has_many :project_skills, serializer: CodeCorps.ProjectSkillView, identifiers: :always + has_many :task_lists, serializer: CodeCorps.TaskListView, identifiers: :always has_many :tasks, serializer: CodeCorps.TaskView, identifiers: :always def donations_active(project, _conn) do diff --git a/web/views/task_list_view.ex b/web/views/task_list_view.ex new file mode 100644 index 000000000..4dddcf0c2 --- /dev/null +++ b/web/views/task_list_view.ex @@ -0,0 +1,11 @@ +defmodule CodeCorps.TaskListView do + use CodeCorps.PreloadHelpers, default_preloads: [:project, :tasks] + use CodeCorps.Web, :view + use JaSerializer.PhoenixView + + attributes [:name, :order, :inserted_at, :updated_at] + + has_one :project, serializer: CodeCorps.ProjectView + + has_many :tasks, serializer: CodeCorps.TaskView, identifiers: :always +end diff --git a/web/views/task_view.ex b/web/views/task_view.ex index b7a610677..6321919b9 100644 --- a/web/views/task_view.ex +++ b/web/views/task_view.ex @@ -1,12 +1,13 @@ defmodule CodeCorps.TaskView do - use CodeCorps.PreloadHelpers, default_preloads: [:project, :user, :comments] + use CodeCorps.PreloadHelpers, default_preloads: [:project, :user, :task_list, :comments] use CodeCorps.Web, :view use JaSerializer.PhoenixView - attributes [:body, :markdown, :number, :task_type, :status, :state, :title, :inserted_at, :updated_at] + attributes [:body, :markdown, :number, :task_type, :status, :state, :title, :order, :inserted_at, :updated_at] has_one :project, serializer: CodeCorps.ProjectView has_one :user, serializer: CodeCorps.UserView + has_one :task_list, serializer: CodeCorps.TaskListView has_many :comments, serializer: CodeCorps.CommentView, identifiers: :always end