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

Tags Page with more details #435

Merged
merged 21 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f6422ea
feat: adding more coluns to the tags
panoramix360 Sep 29, 2023
fc66b49
feat: creating the tags live view for the index of the tags page
panoramix360 Oct 2, 2023
c1b4194
feat: changing the tags table to use the LiveComponent Table
panoramix360 Oct 2, 2023
22b5bff
feat: adding the latest time a tag was used
panoramix360 Oct 17, 2023
98bb028
feat: adding items count column
panoramix360 Oct 17, 2023
000b4bc
feat: add click on the items_count
panoramix360 Oct 23, 2023
dd4ac85
feat: add the total time logged column
panoramix360 Oct 23, 2023
daa50aa
feat: distincting by the items count
panoramix360 Oct 23, 2023
967b8b4
feat: sorting columns
panoramix360 Oct 24, 2023
f4a3f59
test: testing repo tag and tag_controller
panoramix360 Nov 14, 2023
b534ce9
test: fixing stats live tests
panoramix360 Nov 14, 2023
99269c2
test: adding tags_live tests
panoramix360 Nov 14, 2023
0e94dc9
fix: merge conflicts fixes
panoramix360 Nov 14, 2023
fbec564
fix: merge conflicts tests
panoramix360 Nov 14, 2023
4750e95
fix: moving tests to lessen the modified files
panoramix360 Nov 14, 2023
822c6d9
test: changed comment tags so it can be tested correctly when needed
panoramix360 Nov 14, 2023
3e09111
style "Create Tag" button on /tags page #396
nelsonic Nov 14, 2023
47240b8
test: creating tests for tags_live
panoramix360 Jan 3, 2024
47df693
feat: removing format_date from tags_view since it's not needed anymore
panoramix360 Jan 3, 2024
e29ebb2
feat: removing validate_order based on PR review
panoramix360 Jan 14, 2024
74c584e
chore: adding the docs to BUILDIT
panoramix360 Jan 16, 2024
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
618 changes: 598 additions & 20 deletions BUILDIT.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions lib/app/repo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ defmodule App.Repo do
use Ecto.Repo,
otp_app: :app,
adapter: Ecto.Adapters.Postgres

def toggle_sort_order(:asc), do: :desc
def toggle_sort_order(:desc), do: :asc
end
27 changes: 1 addition & 26 deletions lib/app/stats.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,10 @@ defmodule App.Stats do
sort_order \\ :asc
) do
sort_column = to_string(sort_column)
sort_order = to_string(sort_order)

sort_column =
if validate_sort_column(sort_column), do: sort_column, else: "person_id"

sort_order = if validate_order(sort_order), do: sort_order, else: "asc"

sql = """
SELECT i.person_id,
COUNT(distinct i.id) AS "num_items",
Expand All @@ -43,7 +40,7 @@ defmodule App.Stats do
FROM items i
LEFT JOIN timers t ON t.item_id = i.id
GROUP BY i.person_id
ORDER BY #{sort_column} #{sort_order}
ORDER BY #{sort_column} #{to_string(sort_order)}
"""

Ecto.Adapters.SQL.query!(Repo, sql)
Expand Down Expand Up @@ -88,26 +85,4 @@ defmodule App.Stats do
column
)
end

@doc """
`validate_order/1` validates the ordering is one of `asc` or `desc`

## Examples

iex> App.Stats.validate_order("asc")
true

iex> App.Stats.validate_order(:invalid)
false

# Avoid common SQL injection attacks:
iex> App.Stats.validate_order("OR 1=1")
false
"""
def validate_order(order) do
Enum.member?(
~w(asc desc),
order
)
end
end
62 changes: 61 additions & 1 deletion lib/app/tag.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule App.Tag do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias App.{Item, ItemTag, Repo}
alias App.{Item, ItemTag, Repo, Timer}
alias __MODULE__

@derive {Jason.Encoder, only: [:id, :text, :person_id, :color]}
Expand All @@ -11,6 +11,10 @@ defmodule App.Tag do
field :person_id, :integer
field :text, :string

field :last_used_at, :naive_datetime, virtual: true
field :items_count, :integer, virtual: true
field :total_time_logged, :integer, virtual: true

many_to_many(:items, Item, join_through: ItemTag)
timestamps()
end
Expand Down Expand Up @@ -88,6 +92,40 @@ defmodule App.Tag do
|> Repo.all()
end

def list_person_tags_complete(
person_id,
sort_column \\ :text,
sort_order \\ :asc
) do
sort_column =
if validate_sort_column(sort_column), do: sort_column, else: :text

Tag
|> where(person_id: ^person_id)
|> join(:left, [t], it in ItemTag, on: t.id == it.tag_id)
|> join(:left, [t, it], i in Item, on: i.id == it.item_id)
|> join(:left, [t, it, i], tm in Timer, on: tm.item_id == i.id)
|> group_by([t], t.id)
|> select([t, it, i, tm], %{
t
| last_used_at: max(it.inserted_at),
items_count: fragment("count(DISTINCT ?)", i.id),
total_time_logged:
sum(
coalesce(
fragment(
"EXTRACT(EPOCH FROM (? - ?))",
tm.stop,
tm.start
),
0
)
)
})
|> order_by(^get_order_by_keyword(sort_column, sort_order))
|> Repo.all()
end

def list_person_tags_text(person_id) do
Tag
|> where(person_id: ^person_id)
Expand All @@ -105,4 +143,26 @@ defmodule App.Tag do
def delete_tag(%Tag{} = tag) do
Repo.delete(tag)
end

defp validate_sort_column(column) do
Enum.member?(
[
:text,
:color,
:created_at,
:last_used_at,
:items_count,
:total_time_logged
],
column
)
end

defp get_order_by_keyword(sort_column, :asc) do
[asc: sort_column]
end

defp get_order_by_keyword(sort_column, :desc) do
[desc: sort_column]
end
end
11 changes: 0 additions & 11 deletions lib/app_web/controllers/tag_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,6 @@ defmodule AppWeb.TagController do
alias App.{Person, Tag}
plug :permission_tag when action in [:edit, :update, :delete]

def index(conn, _params) do
person_id = conn.assigns[:person][:id] || 0
tags = Tag.list_person_tags(person_id)

render(conn, "index.html",
tags: tags,
lists: App.List.get_lists_for_person(person_id),
custom_list: false
)
end

def new(conn, _params) do
changeset = Tag.changeset(%Tag{})
render(conn, "new.html", changeset: changeset)
Expand Down
7 changes: 2 additions & 5 deletions lib/app_web/live/stats_live.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule AppWeb.StatsLive do
require Logger
use AppWeb, :live_view
alias App.{Stats, DateTimeHelper, Person}
alias App.{Stats, DateTimeHelper, Person, Repo}
alias Phoenix.Socket.Broadcast

# run authentication on mount
Expand Down Expand Up @@ -74,7 +74,7 @@ defmodule AppWeb.StatsLive do

sort_order =
if socket.assigns.sort_column == sort_column do
toggle_sort_order(socket.assigns.sort_order)
Repo.toggle_sort_order(socket.assigns.sort_order)
else
:asc
end
Expand Down Expand Up @@ -114,7 +114,4 @@ defmodule AppWeb.StatsLive do

def is_highlighted_person?(metric, person_id),
do: metric.person_id == person_id

defp toggle_sort_order(:asc), do: :desc
defp toggle_sort_order(:desc), do: :asc
end
27 changes: 21 additions & 6 deletions lib/app_web/live/stats_live.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,45 @@
highlight={&is_highlighted_person?(&1, @person_id)}
>
<:column :let={metric} label="Id" key="person_id">
<td class="px-6 py-4" data-test-id="person_id">
<td class="px-6 py-4" data-test-id={"person_id_#{metric.person_id}"}>
<a href={person_link(metric.person_id)}>
<%= metric.person_id %>
</a>
</td>
</:column>

<:column :let={metric} label="Items" key="num_items">
<td class="px-6 py-4 text-center" data-test-id="num_items">
<td
class="px-6 py-4 text-center"
data-test-id={"num_items_#{metric.person_id}"}
>
<%= metric.num_items %>
</td>
</:column>

<:column :let={metric} label="Timers" key="num_timers">
<td class="px-6 py-4 text-center" data-test-id="num_timers">
<td
class="px-6 py-4 text-center"
data-test-id={"num_timers_#{metric.person_id}"}
>
<%= metric.num_timers %>
</td>
</:column>

<:column :let={metric} label="First Joined" key="first_inserted_at">
<td class="px-6 py-4 text-center" data-test-id="first_inserted_at">
<td
class="px-6 py-4 text-center"
data-test-id={"first_inserted_at_#{metric.person_id}"}
>
<%= format_date(metric.first_inserted_at) %>
</td>
</:column>

<:column :let={metric} label="Last Item Inserted" key="last_inserted_at">
<td class="px-6 py-4 text-center" data-test-id="last_inserted_at">
<td
class="px-6 py-4 text-center"
data-test-id={"last_inserted_at_#{metric.person_id}"}
>
<%= format_date(metric.last_inserted_at) %>
</td>
</:column>
Expand All @@ -49,7 +61,10 @@
label="Total Elapsed Time"
key="total_timers_in_seconds"
>
<td class="px-6 py-4 text-center" data-test-id="total_timers_in_seconds">
<td
class="px-6 py-4 text-center"
data-test-id={"total_timers_in_seconds_#{metric.person_id}"}
>
<%= format_seconds(metric.total_timers_in_seconds) %>
</td>
</:column>
Expand Down
60 changes: 60 additions & 0 deletions lib/app_web/live/tags_live.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
defmodule AppWeb.TagsLive do
use AppWeb, :live_view
alias App.{DateTimeHelper, Person, Tag, Repo}

# run authentication on mount
on_mount(AppWeb.AuthController)

@tags_topic "tags"

@impl true
def mount(_params, _session, socket) do
if connected?(socket), do: AppWeb.Endpoint.subscribe(@tags_topic)

person_id = Person.get_person_id(socket.assigns)

tags = Tag.list_person_tags_complete(person_id)

{:ok,
assign(socket,
tags: tags,
lists: App.List.get_lists_for_person(person_id),
custom_list: false,
sort_column: :text,
sort_order: :asc
)}
end

@impl true
def handle_event("sort", %{"key" => key}, socket) do
sort_column =
key
|> String.to_atom()

sort_order =
if socket.assigns.sort_column == sort_column do
Repo.toggle_sort_order(socket.assigns.sort_order)
else
:asc
end

person_id = Person.get_person_id(socket.assigns)

tags = Tag.list_person_tags_complete(person_id, sort_column, sort_order)

{:noreply,
assign(socket,
tags: tags,
sort_column: sort_column,
sort_order: sort_order
)}
end

def format_date(date) do
DateTimeHelper.format_date(date)
end

def format_seconds(seconds) do
DateTimeHelper.format_duration(seconds)
end
end
Loading
Loading