Skip to content

Commit

Permalink
Merge pull request #435 from dwyl/feature/tags-page-more-details
Browse files Browse the repository at this point in the history
Tags Page with more details
  • Loading branch information
nelsonic authored Jan 18, 2024
2 parents 95b213a + 74c584e commit 55f46bc
Show file tree
Hide file tree
Showing 17 changed files with 1,060 additions and 164 deletions.
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

0 comments on commit 55f46bc

Please sign in to comment.