From 4524a2a21b1d51649d76de288c54df33de9903b2 Mon Sep 17 00:00:00 2001 From: WoodenMaiden Date: Sun, 14 Apr 2024 16:20:11 +0200 Subject: [PATCH] feat: generated timer + check DateTime Signed-off-by: WoodenMaiden --- lib/timecopsync_projects_api/projects.ex | 116 ++++++++++++++++++ .../projects/timer.ex | 43 +++++++ .../controllers/timer_controller.ex | 43 +++++++ .../controllers/timer_json.ex | 32 +++++ lib/timecopsync_projects_api_web/router.ex | 1 + .../20240225232829_create_timers.exs | 18 +++ test/support/fixtures/projects_fixtures.ex | 17 +++ .../projects_test.exs | 60 +++++++++ .../controllers/timer_controller_test.exs | 96 +++++++++++++++ 9 files changed, 426 insertions(+) create mode 100644 lib/timecopsync_projects_api/projects/timer.ex create mode 100644 lib/timecopsync_projects_api_web/controllers/timer_controller.ex create mode 100644 lib/timecopsync_projects_api_web/controllers/timer_json.ex create mode 100644 priv/repo/migrations/20240225232829_create_timers.exs create mode 100644 test/timecopsync_projects_api_web/controllers/timer_controller_test.exs diff --git a/lib/timecopsync_projects_api/projects.ex b/lib/timecopsync_projects_api/projects.ex index c43e4a2..7e01fd2 100644 --- a/lib/timecopsync_projects_api/projects.ex +++ b/lib/timecopsync_projects_api/projects.ex @@ -135,4 +135,120 @@ defmodule TimecopsyncProjectsApi.Projects do def change_project(%Project{} = project, attrs \\ %{}) do Project.changeset(project, attrs) end + + alias TimecopsyncProjectsApi.Projects.Timer + + @doc """ + Returns the list of timers. + + ## Examples + + iex> list_timers() + [%Timer{}, ...] + + """ + def list_timers do + Repo.all(Timer) + end + + @doc """ + Gets a single timer. + + Raises `Ecto.NoResultsError` if the Timer does not exist. + + ## Examples + + iex> get_timer!(123) + %Timer{} + + iex> get_timer!(456) + ** (Ecto.NoResultsError) + + """ + def get_timer!(id), do: Repo.get!(Timer, id) + + @doc """ + Gets a single timer and returns it in a ok tuple. returns an error tuple if the timer does not exist. + + ## Examples + + iex> get_timer(123) + {:ok, %Timer{}} + + iex> get_timer(456) + {:error, "Timer not found"} + + """ + @spec get_timer(Ecto.UUID.t() | String.t()) :: {:error, String.t()} | {:ok, any()} + def get_timer(id) do + case Repo.get(Timer, id) do + t when t != nil -> {:ok, t} + _ -> {:error, "Timer not found"} + end + end + + @doc """ + Creates a timer. + + ## Examples + + iex> create_timer(%{field: value}) + {:ok, %Timer{}} + + iex> create_timer(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_timer(attrs \\ %{}) do + %Timer{} + |> Timer.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a timer. + + ## Examples + + iex> update_timer(timer, %{field: new_value}) + {:ok, %Timer{}} + + iex> update_timer(timer, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_timer(%Timer{} = timer, attrs) do + timer + |> Timer.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a timer. + + ## Examples + + iex> delete_timer(timer) + {:ok, %Timer{}} + + iex> delete_timer(timer) + {:error, %Ecto.Changeset{}} + + """ + def delete_timer(%Timer{} = timer) do + Repo.delete(timer) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking timer changes. + + ## Examples + + iex> change_timer(timer) + %Ecto.Changeset{data: %Timer{}} + + """ + def change_timer(%Timer{} = timer, attrs \\ %{}) do + Timer.changeset(timer, attrs) + end end diff --git a/lib/timecopsync_projects_api/projects/timer.ex b/lib/timecopsync_projects_api/projects/timer.ex new file mode 100644 index 0000000..fda741d --- /dev/null +++ b/lib/timecopsync_projects_api/projects/timer.ex @@ -0,0 +1,43 @@ +defmodule TimecopsyncProjectsApi.Projects.Timer do + use Ecto.Schema + import Ecto.Changeset + + require Logger + + @primary_key {:id, :binary_id, autogenerate: true} + @foreign_key_type :binary_id + schema "timers" do + field :description, :string + field :notes, :string + field :start_time, :utc_datetime + field :end_time, :utc_datetime + field :project_id, :binary_id + + timestamps(type: :utc_datetime) + end + + + defp check_chronology(_, ended) when is_nil(ended), do: :lt + + defp check_chronology(started, ended) do + DateTime.compare(started, ended) + end + + def validate_datetime_chronology(changeset) do + start_time = get_field(changeset, :start_time) + end_time = get_field(changeset, :end_time) + + case check_chronology(start_time, end_time) do + :gt -> add_error(changeset, :end_time, "A timer cannot end before it started!") + _ -> changeset + end + end + + @doc false + def changeset(timer, attrs) do + timer + |> cast(attrs, [:description, :notes, :start_time, :end_time]) + |> validate_required([:start_time]) + |> validate_datetime_chronology() + end +end diff --git a/lib/timecopsync_projects_api_web/controllers/timer_controller.ex b/lib/timecopsync_projects_api_web/controllers/timer_controller.ex new file mode 100644 index 0000000..b851182 --- /dev/null +++ b/lib/timecopsync_projects_api_web/controllers/timer_controller.ex @@ -0,0 +1,43 @@ +defmodule TimecopsyncProjectsApiWeb.TimerController do + use TimecopsyncProjectsApiWeb, :controller + + alias TimecopsyncProjectsApi.Projects + alias TimecopsyncProjectsApi.Projects.Timer + + action_fallback TimecopsyncProjectsApiWeb.FallbackController + + def index(conn, _params) do + timers = Projects.list_timers() + render(conn, :index, timers: timers) + end + + def create(conn, %{"timer" => timer_params}) do + with {:ok, %Timer{} = timer} <- Projects.create_timer(timer_params) do + conn + |> put_status(:created) + |> put_resp_header("location", ~p"/api/v1/timers/#{timer}") + |> render(:show, timer: timer) + end + end + + def show(conn, %{"id" => id}) do + timer = Projects.get_timer!(id) + render(conn, :show, timer: timer) + end + + def update(conn, %{"id" => id, "timer" => timer_params}) do + timer = Projects.get_timer!(id) + + with {:ok, %Timer{} = timer} <- Projects.update_timer(timer, timer_params) do + render(conn, :show, timer: timer) + end + end + + def delete(conn, %{"id" => id}) do + timer = Projects.get_timer!(id) + + with {:ok, %Timer{}} <- Projects.delete_timer(timer) do + send_resp(conn, :no_content, "") + end + end +end diff --git a/lib/timecopsync_projects_api_web/controllers/timer_json.ex b/lib/timecopsync_projects_api_web/controllers/timer_json.ex new file mode 100644 index 0000000..f414081 --- /dev/null +++ b/lib/timecopsync_projects_api_web/controllers/timer_json.ex @@ -0,0 +1,32 @@ +defmodule TimecopsyncProjectsApiWeb.TimerJSON do + alias TimecopsyncProjectsApi.Projects.Timer + + @doc """ + Renders a list of timers. + """ + def index(%{timers: timers}) do + %{ + metadata: %{ + total: length(timers) + # page: page, + }, + data: for(timer <- timers, do: data(timer))} + end + + @doc """ + Renders a single timer. + """ + def show(%{timer: timer}) do + %{data: data(timer)} + end + + defp data(%Timer{} = timer) do + %{ + id: timer.id, + description: timer.description, + notes: timer.notes, + start_time: timer.start_time, + end_time: timer.end_time + } + end +end diff --git a/lib/timecopsync_projects_api_web/router.ex b/lib/timecopsync_projects_api_web/router.ex index e1d1678..4f40b75 100644 --- a/lib/timecopsync_projects_api_web/router.ex +++ b/lib/timecopsync_projects_api_web/router.ex @@ -9,6 +9,7 @@ defmodule TimecopsyncProjectsApiWeb.Router do pipe_through :api resources "/projects", ProjectController, except: [:new, :edit] + resources "/timers", TimerController, except: [:new, :edit] end # Enable LiveDashboard in development diff --git a/priv/repo/migrations/20240225232829_create_timers.exs b/priv/repo/migrations/20240225232829_create_timers.exs new file mode 100644 index 0000000..99f39bb --- /dev/null +++ b/priv/repo/migrations/20240225232829_create_timers.exs @@ -0,0 +1,18 @@ +defmodule TimecopsyncProjectsApi.Repo.Migrations.CreateTimers do + use Ecto.Migration + + def change do + create table(:timers, primary_key: false) do + add :id, :binary_id, primary_key: true + add :description, :string + add :notes, :string + add :start_time, :utc_datetime + add :end_time, :utc_datetime + add :project_id, references(:projects, on_delete: :nothing, type: :binary_id) + + timestamps(type: :utc_datetime) + end + + create index(:timers, [:project_id]) + end +end diff --git a/test/support/fixtures/projects_fixtures.ex b/test/support/fixtures/projects_fixtures.ex index 131e0e2..def9c5c 100644 --- a/test/support/fixtures/projects_fixtures.ex +++ b/test/support/fixtures/projects_fixtures.ex @@ -19,4 +19,21 @@ defmodule TimecopsyncProjectsApi.ProjectsFixtures do project end + + @doc """ + Generate a timer. + """ + def timer_fixture(attrs \\ %{}) do + {:ok, timer} = + attrs + |> Enum.into(%{ + description: "some description", + end_time: ~U[2024-02-24 23:28:00Z], + notes: "some notes", + start_time: ~U[2024-02-24 23:28:00Z] + }) + |> TimecopsyncProjectsApi.Projects.create_timer() + + timer + end end diff --git a/test/timecopsync_projects_api/projects_test.exs b/test/timecopsync_projects_api/projects_test.exs index 6463a01..23df058 100644 --- a/test/timecopsync_projects_api/projects_test.exs +++ b/test/timecopsync_projects_api/projects_test.exs @@ -76,4 +76,64 @@ defmodule TimecopsyncProjectsApi.ProjectsTest do assert %Ecto.Changeset{} = Projects.change_project(project) end end + + describe "timers" do + alias TimecopsyncProjectsApi.Projects.Timer + + import TimecopsyncProjectsApi.ProjectsFixtures + + @invalid_attrs %{description: nil, notes: nil, start_time: nil, end_time: nil} + + test "list_timers/0 returns all timers" do + timer = timer_fixture() + assert Projects.list_timers() == [timer] + end + + test "get_timer!/1 returns the timer with given id" do + timer = timer_fixture() + assert Projects.get_timer!(timer.id) == timer + end + + test "create_timer/1 with valid data creates a timer" do + valid_attrs = %{description: "some description", notes: "some notes", start_time: ~U[2024-02-24 23:28:00Z], end_time: ~U[2024-02-24 23:28:00Z]} + + assert {:ok, %Timer{} = timer} = Projects.create_timer(valid_attrs) + assert timer.description == "some description" + assert timer.notes == "some notes" + assert timer.start_time == ~U[2024-02-24 23:28:00Z] + assert timer.end_time == ~U[2024-02-24 23:28:00Z] + end + + test "create_timer/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Projects.create_timer(@invalid_attrs) + end + + test "update_timer/2 with valid data updates the timer" do + timer = timer_fixture() + update_attrs = %{description: "some updated description", notes: "some updated notes", start_time: ~U[2024-02-25 23:28:00Z], end_time: ~U[2024-02-25 23:28:00Z]} + + assert {:ok, %Timer{} = timer} = Projects.update_timer(timer, update_attrs) + assert timer.description == "some updated description" + assert timer.notes == "some updated notes" + assert timer.start_time == ~U[2024-02-25 23:28:00Z] + assert timer.end_time == ~U[2024-02-25 23:28:00Z] + end + + test "update_timer/2 with invalid data returns error changeset" do + timer = timer_fixture() + assert {:error, %Ecto.Changeset{}} = Projects.update_timer(timer, @invalid_attrs) + assert timer == Projects.get_timer!(timer.id) + end + + test "delete_timer/1 deletes the timer" do + timer = timer_fixture() + assert {:ok, %Timer{}} = Projects.delete_timer(timer) + assert_raise Ecto.NoResultsError, fn -> Projects.get_timer!(timer.id) end + end + + test "change_timer/1 returns a timer changeset" do + timer = timer_fixture() + assert %Ecto.Changeset{} = Projects.change_timer(timer) + end + end end diff --git a/test/timecopsync_projects_api_web/controllers/timer_controller_test.exs b/test/timecopsync_projects_api_web/controllers/timer_controller_test.exs new file mode 100644 index 0000000..9a262c6 --- /dev/null +++ b/test/timecopsync_projects_api_web/controllers/timer_controller_test.exs @@ -0,0 +1,96 @@ +defmodule TimecopsyncProjectsApiWeb.TimerControllerTest do + use TimecopsyncProjectsApiWeb.ConnCase + + import TimecopsyncProjectsApi.ProjectsFixtures + + alias TimecopsyncProjectsApi.Projects.Timer + + @create_attrs %{ + description: "some description", + notes: "some notes", + start_time: ~U[2024-02-24 23:28:00Z], + end_time: ~U[2024-02-24 23:28:00Z] + } + @update_attrs %{ + description: "some updated description", + notes: "some updated notes", + start_time: ~U[2024-02-25 23:28:00Z], + end_time: ~U[2024-02-25 23:28:00Z] + } + @invalid_attrs %{description: nil, notes: nil, start_time: nil, end_time: nil} + + setup %{conn: conn} do + {:ok, conn: put_req_header(conn, "accept", "application/json")} + end + + describe "index" do + test "lists all timers", %{conn: conn} do + conn = get(conn, ~p"/api/v1/timers") + assert json_response(conn, 200)["data"] == [] + end + end + + describe "create timer" do + test "renders timer when data is valid", %{conn: conn} do + conn = post(conn, ~p"/api/v1/timers", timer: @create_attrs) + assert %{"id" => id} = json_response(conn, 201)["data"] + + conn = get(conn, ~p"/api/v1/timers/#{id}") + + assert %{ + "id" => ^id, + "description" => "some description", + "end_time" => "2024-02-24T23:28:00Z", + "notes" => "some notes", + "start_time" => "2024-02-24T23:28:00Z" + } = json_response(conn, 200)["data"] + end + + test "renders errors when data is invalid", %{conn: conn} do + conn = post(conn, ~p"/api/v1/timers", timer: @invalid_attrs) + assert json_response(conn, 422)["errors"] != %{} + end + end + + describe "update timer" do + setup [:create_timer] + + test "renders timer when data is valid", %{conn: conn, timer: %Timer{id: id} = timer} do + conn = put(conn, ~p"/api/v1/timers/#{timer}", timer: @update_attrs) + assert %{"id" => ^id} = json_response(conn, 200)["data"] + + conn = get(conn, ~p"/api/v1/timers/#{id}") + + assert %{ + "id" => ^id, + "description" => "some updated description", + "end_time" => "2024-02-25T23:28:00Z", + "notes" => "some updated notes", + "start_time" => "2024-02-25T23:28:00Z" + } = json_response(conn, 200)["data"] + end + + test "renders errors when data is invalid", %{conn: conn, timer: timer} do + conn = put(conn, ~p"/api/v1/timers/#{timer}", timer: @invalid_attrs) + assert json_response(conn, 422)["errors"] != %{} + end + end + + describe "delete timer" do + setup [:create_timer] + + test "deletes chosen timer", %{conn: conn, timer: timer} do + conn = delete(conn, ~p"/api/v1/timers/#{timer}") + assert response(conn, 204) + + assert_error_sent 404, fn -> + get(conn, ~p"/api/v1/timers/#{timer}") + end + end + end + + defp create_timer(_) do + timer = timer_fixture() + %{timer: timer} + end +end