Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PR] Stage 1 - No auth, anonymous API for Timers and Items #263

Merged
merged 27 commits into from
Jan 16, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b144656
feat: Adding API router and ItemsController. #256
LuchoTurtle Jan 11, 2023
d23b4c7
feat: Adding timer API and TimerController. #256
LuchoTurtle Jan 11, 2023
3d56223
feat: Adding tests for timer schema and item controller. #256
LuchoTurtle Jan 11, 2023
0355e5c
feat: Timers and TimerController testing. #256
LuchoTurtle Jan 12, 2023
b6d10c7
feat: Adding item and timer fixture when testing timer controller. #256
LuchoTurtle Jan 12, 2023
600d123
feat: Creating API document. Adding validation when creating `timer`.…
LuchoTurtle Jan 12, 2023
3ec039c
fix: Mix format. Fixing typo in API document. #256
LuchoTurtle Jan 12, 2023
519f16c
fix: Adding schema Jason encoder section in API document. #256
LuchoTurtle Jan 12, 2023
0abf8af
fix: Removing try/rescue. #256
LuchoTurtle Jan 12, 2023
2edb2b7
fix: Mix format. #256
LuchoTurtle Jan 12, 2023
115b3e6
merge main
nelsonic Jan 12, 2023
8729c6a
redirect to auth (not full url)
nelsonic Jan 12, 2023
d4133aa
Merge branch 'main' into api-#256
LuchoTurtle Jan 13, 2023
fe061d1
fix: Fixing tests and API with Papertrail integration. #256
LuchoTurtle Jan 13, 2023
43954a1
fix: Mix format. #256
LuchoTurtle Jan 13, 2023
0d2d369
feat: Mentioning API in BUILDIT.md. #256
LuchoTurtle Jan 13, 2023
94941b8
fix: Item controller update handles gracefully when ID not found. #256
LuchoTurtle Jan 13, 2023
7555cb4
fix: Fixing updating timer when no ID is found. #256
LuchoTurtle Jan 13, 2023
819db65
remove redundant status code assertions in API tests #256
nelsonic Jan 15, 2023
4c2f9b7
simplify API folder structure and file names #256
nelsonic Jan 15, 2023
91b4ae1
add test for timer_text(start, stop) UNDER 1000s to ensure branch cov…
nelsonic Jan 15, 2023
0a98af0
Merge branch 'api-#256' of https://github.com/dwyl/mvp into api-#256
LuchoTurtle Jan 16, 2023
3ffe838
fix: Updating `api.md`. #256
LuchoTurtle Jan 16, 2023
f7e8bb8
fix: Mix format. #256
LuchoTurtle Jan 16, 2023
dceaaa0
improve API error message clarity #256
nelsonic Jan 16, 2023
e6d4ef0
fix merge conflicts in BUILDIT.md #256 (AGAIN!)
nelsonic Jan 16, 2023
ca34ba0
fix merge conflicts in BUILDIT.md #256
nelsonic Jan 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
650 changes: 650 additions & 0 deletions api.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions lib/app/item.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ defmodule App.Item do
alias __MODULE__
require Logger

@derive {Jason.Encoder, only: [:id, :person_id, :status, :text]}
schema "items" do
field :person_id, :integer
field :status, :integer
Expand Down
45 changes: 45 additions & 0 deletions lib/app/timer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule App.Timer do
alias __MODULE__
require Logger

@derive {Jason.Encoder, only: [:id, :start, :stop]}
schema "timers" do
field :start, :naive_datetime
field :stop, :naive_datetime
Expand All @@ -15,6 +16,24 @@ defmodule App.Timer do
timestamps()
end

defp validate_start_before_stop(changeset) do
start = get_field(changeset, :start)
stop = get_field(changeset, :stop)

# If start or stop is nil, no comparison occurs.
case is_nil(stop) or is_nil(start) do
true ->
changeset

false ->
if NaiveDateTime.compare(start, stop) == :gt do
add_error(changeset, :start, "cannot be later than 'stop'")
else
changeset
end
end
end

@doc false
def changeset(timer, attrs) do
timer
Expand All @@ -34,6 +53,30 @@ defmodule App.Timer do
"""
def get_timer!(id), do: Repo.get!(Timer, id)

@doc """
`list_timers/1` lists all the timer objects of a given item `id`.

## Examples

iex> list_timers(1)
[
%App.Timer{
id: 7,
start: ~N[2023-01-11 17:40:44],
stop: nil,
item_id: 1,
inserted_at: ~N[2023-01-11 18:01:43],
updated_at: ~N[2023-01-11 18:01:43]
}
]
"""
def list_timers(item_id) do
Timer
|> where(item_id: ^item_id)
|> order_by(:id)
|> Repo.all()
end

@doc """
`start/1` starts a timer.

Expand All @@ -46,6 +89,7 @@ defmodule App.Timer do
def start(attrs \\ %{}) do
%Timer{}
|> changeset(attrs)
|> validate_start_before_stop()
|> Repo.insert()
end

Expand Down Expand Up @@ -76,6 +120,7 @@ defmodule App.Timer do
def update_timer(attrs \\ %{}) do
get_timer!(attrs.id)
|> changeset(attrs)
|> validate_start_before_stop()
|> Repo.update()
end

Expand Down
88 changes: 88 additions & 0 deletions lib/app_web/controllers/api/item_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
defmodule AppWeb.API.ItemController do
use AppWeb, :controller
alias App.Item
import Ecto.Changeset

def show(conn, params) do
id = Map.get(params, "id")

try do
item = Item.get_item!(id)
json(conn, item)
rescue
LuchoTurtle marked this conversation as resolved.
Show resolved Hide resolved
Ecto.NoResultsError ->
errors = %{
code: 404,
message: "No item found with the given \'id\'."
}

json(conn |> put_status(404), errors)

Ecto.Query.CastError ->
errors = %{
code: 400,
message: "The \'id\' is not an integer."
}

json(conn |> put_status(400), errors)
end
end

def create(conn, params) do
# Attributes to create item
# Person_id will be changed when auth is added
attrs = %{
text: Map.get(params, "text"),
person_id: 0,
status: 2
}

case Item.create_item(attrs) do
# Successfully creates item
{:ok, item} ->
id_item = Map.take(item, [:id])
json(conn, id_item)

# Error creating item
{:error, %Ecto.Changeset{} = changeset} ->
errors = make_changeset_errors_readable(changeset)

json(
conn |> put_status(400),
errors
)
end
end

def update(conn, params) do
id = Map.get(params, "id")
new_text = Map.get(params, "text")

item = Item.get_item!(id)

case Item.update_item(item, %{text: new_text}) do
# Successfully updates item
{:ok, item} ->
json(conn, item)

# Error creating item
{:error, %Ecto.Changeset{} = changeset} ->
errors = make_changeset_errors_readable(changeset)

json(
conn |> put_status(400),
errors
)
end
end

defp make_changeset_errors_readable(changeset) do
errors = %{
code: 400,
message: "Malformed request"
}

changeset_errors = traverse_errors(changeset, fn {msg, _opts} -> msg end)
Map.put(errors, :errors, changeset_errors)
end
end
96 changes: 96 additions & 0 deletions lib/app_web/controllers/api/timer_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
defmodule AppWeb.API.TimerController do
use AppWeb, :controller
alias App.Timer
import Ecto.Changeset

def index(conn, params) do
item_id = Map.get(params, "item_id")

timers = Timer.list_timers(item_id)
json(conn, timers)
end

def show(conn, params) do
id = Map.get(params, "id")

try do
timer = Timer.get_timer!(id)
json(conn, timer)
rescue
Ecto.NoResultsError ->
errors = %{
code: 404,
message: "No timer found with the given \'id\'."
}

json(conn |> put_status(404), errors)

Ecto.Query.CastError ->
errors = %{
code: 400,
message: "The \'id\' is not an integer."
}

json(conn |> put_status(400), errors)
end
end

def create(conn, params) do
# Attributes to create timer
attrs = %{
item_id: Map.get(params, "item_id"),
start: Map.get(params, "start"),
stop: Map.get(params, "stop")
}

case Timer.start(attrs) do
# Successfully creates item
{:ok, timer} ->
id_timer = Map.take(timer, [:id])
json(conn, id_timer)

# Error creating item
{:error, %Ecto.Changeset{} = changeset} ->
errors = make_changeset_errors_readable(changeset)

json(
conn |> put_status(400),
errors
)
end
end

def update(conn, params) do
# Attributes to update timer
attrs_to_update = %{
id: Map.get(params, "id"),
start: Map.get(params, "start"),
stop: Map.get(params, "stop")
}

case Timer.update_timer(attrs_to_update) do
# Successfully updates timer
{:ok, timer} ->
json(conn, timer)

# Error creating timer
{:error, %Ecto.Changeset{} = changeset} ->
errors = make_changeset_errors_readable(changeset)

json(
conn |> put_status(400),
errors
)
end
end

defp make_changeset_errors_readable(changeset) do
errors = %{
code: 400,
message: "Malformed request"
}

changeset_errors = traverse_errors(changeset, fn {msg, _opts} -> msg end)
Map.put(errors, :errors, changeset_errors)
end
end
18 changes: 17 additions & 1 deletion lib/app_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,21 @@ defmodule AppWeb.Router do
plug :put_secure_browser_headers
end

pipeline :api do
plug :accepts, ["json"]
plug :fetch_session
end

# No Auth
scope "/", AppWeb do
pipe_through :browser
get "/init", InitController, :index
get "/login", AuthController, :login
end

pipeline :authOptional, do: plug(AuthPlugOptional)
pipeline :authOptional do
plug(AuthPlugOptional)
end

scope "/", AppWeb do
pipe_through [:browser, :authOptional]
Expand All @@ -26,4 +33,13 @@ defmodule AppWeb.Router do
live "/stats", StatsLive
resources "/tags", TagController, except: [:show]
end

scope "/api", AppWeb do
pipe_through [:api, :authOptional]

resources "/items", API.ItemController, only: [:create, :update, :show]

resources "/items/:item_id/timers", API.TimerController,
only: [:create, :update, :show, :index]
end
end
23 changes: 23 additions & 0 deletions test/app/timer_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,28 @@ defmodule App.TimerTest do
assert updated_timer.start == start
assert updated_timer.stop == stop
end

test "update_timer(%{id: id, start: start, stop: stop}) with start later than stop should throw error" do
start = ~N[2022-10-27 05:00:00]
stop = ~N[2022-10-27 00:00:00]

{:ok, item} = Item.create_item(@valid_item_attrs)

{:ok, seven_seconds_ago} =
NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -7))

# Start the timer 7 seconds ago:
{:ok, timer} =
Timer.start(%{item_id: item.id, person_id: 1, start: seven_seconds_ago})

# Stop the timer based on its item_id
Timer.stop_timer_for_item_id(item.id)

# Update timer with stop earlier than start
{:error, changeset} =
Timer.update_timer(%{id: timer.id, start: start, stop: stop})

assert length(changeset.errors) > 0
end
end
end
Loading