Skip to content

Commit

Permalink
feat(Live.Admin.TripPlanFeedback): click to download the data (#1932)
Browse files Browse the repository at this point in the history
* fix(DotcomWeb.Router): properly define the root layout
* feat(DotcomWeb.Live.Admin): new admin page
* feat(TripPlan.FeedbackCSV): turn feedback into table
* feat(Live.Admin.TripPlanFeedback): click to download the data
* fix(TripPlanController): set modes default value
  • Loading branch information
thecristen authored Mar 25, 2024
1 parent c7bdf47 commit fff1e8a
Show file tree
Hide file tree
Showing 16 changed files with 954 additions and 136 deletions.
1 change: 1 addition & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
inputs: [
"mix.exs",
"{config,lib,test}/**/*.{ex,exs}",
"lib/dotcom_web/**/*.{heex,.eex}",
"rel/**/*.{ex,exs}"
]
]
5 changes: 3 additions & 2 deletions config/deps/endpoint.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Config
config :dotcom, DotcomWeb.Endpoint,
url: [host: "localhost"],
secret_key_base: "yK6hUINZWlq04EPu3SJjAHNDYgka8MZqgXZykF+AQ2PvWs4Ua4IELdFl198aMvw0",
render_errors: [accepts: ~w(html), layout: {DotcomWeb.LayoutView, "app.html"}],
render_errors: [accepts: ~w(html), layout: {DotcomWeb.LayoutView, "root.html"}],
pubsub_server: Dotcom.PubSub,
live_view: [
signing_salt: "gsQiz0LdGqVmqDOR4snAgelIAAphhdfm"
Expand Down Expand Up @@ -37,7 +37,8 @@ if config_env() == :dev do
~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
~r{priv/gettext/.*(po)$},
~r{lib/dotcom_web/views/.*(ex)$},
~r{lib/dotcom_web/templates/.*(heex|eex)$}
~r{lib/dotcom_web/templates/.*(heex|eex)$},
~r{lib/dotcom_web/live/.*(heex|ex)$}
]
]
end
Expand Down
53 changes: 48 additions & 5 deletions lib/dotcom_web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,9 @@ defmodule DotcomWeb do
root: "lib/dotcom_web/templates",
namespace: DotcomWeb

import Phoenix.LiveView.Helpers
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_flash: 2, view_module: 1]

# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
use Dotcom.Components.Precompiler

import DotcomWeb.Components
Expand All @@ -80,21 +77,48 @@ defmodule DotcomWeb do
]

import DotcomWeb.CmsRouterHelpers
import DotcomWeb.ErrorHelpers
import DotcomWeb.Gettext
import DotcomWeb.ViewHelpers
import DotcomWeb.Views.Helpers.StopHelpers
import DotcomWeb.Views.Helpers.AlertHelpers
import DotcomWeb.PartialView.SvgIconWithCircle, only: [svg_icon_with_circle: 1]
import UrlHelpers

# Include shared imports and aliases for views
unquote(view_helpers())

@dialyzer :no_match
end
end

def live_view do
quote do
use Phoenix.LiveView

unquote(view_helpers())
end
end

def live_component do
quote do
use Phoenix.LiveComponent

unquote(view_helpers())
end
end

def component do
quote do
use Phoenix.Component

unquote(view_helpers())
end
end

def router do
quote do
use Phoenix.Router
import Plug.Conn
import Phoenix.Controller
import Phoenix.LiveView.Router
end
end
Expand All @@ -106,6 +130,25 @@ defmodule DotcomWeb do
end
end

defp view_helpers do
quote do
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML

# Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
import Phoenix.LiveView.Helpers

# Import basic rendering functionality (render, render_layout, etc)
import Phoenix.View

import DotcomWeb.ErrorHelpers
import DotcomWeb.Gettext
alias DotcomWeb.Router.Helpers

import DotcomWeb.Components
end
end

@doc """
When used, dispatch to the appropriate controller/view/etc.
"""
Expand Down
1 change: 0 additions & 1 deletion lib/dotcom_web/controllers/cms_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ defmodule DotcomWeb.CMSController do
defp handle_page_response(%{__struct__: struct} = page, conn)
when struct in @generic or struct in @transitional do
conn
|> put_layout({DotcomWeb.LayoutView, :app})
|> render_page(page)
end

Expand Down
2 changes: 1 addition & 1 deletion lib/dotcom_web/controllers/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ defmodule DotcomWeb.ControllerHelpers do
def render_not_found(conn) do
conn
|> put_status(:not_found)
|> put_layout({DotcomWeb.LayoutView, "app.html"})
|> put_layout({DotcomWeb.LayoutView, "root.html"})
|> put_view(DotcomWeb.ErrorView)
|> render("404.html", [])
|> halt()
Expand Down
22 changes: 22 additions & 0 deletions lib/dotcom_web/controllers/trip_plan/feedback.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,34 @@ defmodule DotcomWeb.TripPlan.Feedback do
use Nebulex.Caching

alias Dotcom.Cache.TripPlanFeedback.KeyGenerator
alias DotcomWeb.TripPlan.FeedbackCSV

@cache Application.compile_env!(:dotcom, :trip_plan_feedback_cache)

def delete(conn, params), do: cache_and_response(conn, params, :delete)
def put(conn, params), do: cache_and_response(conn, params, :put)

def download(conn, _params) do
data = get_cached_data()

send_download(conn, {:binary, data},
content_type: "application/csv",
filename: "trip_plan_feedback.csv",
disposition: :attachment
)
end

def get_cached_data do
@cache.all("dotcom_web.trip_plan.feedback|#{env_name()}*")
|> @cache.get_all()
|> Stream.map(&elem(&1, 1))
|> FeedbackCSV.rows()
end

defp env_name do
System.get_env("SENTRY_ENVIRONMENT", "local")
end

defp cache_and_response(conn, params, action) do
Logger.info("dotcom_web.trip_plan.feedback action=#{action}")

Expand Down
154 changes: 154 additions & 0 deletions lib/dotcom_web/controllers/trip_plan/feedback_csv.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
defmodule DotcomWeb.TripPlan.FeedbackCSV do
@moduledoc """
Handle formatting feedback into a spreadsheet-friendly format.
"""

alias TripPlan.PersonalDetail.Step

@headers [
"generated_time",
"itinerary_index",
"feedback_vote",
"feedback_long",
"mode_subway",
"mode_commuter_rail",
"mode_bus",
"mode_ferry",
"query_wheelchair",
"query_time_type",
"query_date_time",
"query_from",
"query_to",
"itinerary_0_accessible",
"itinerary_0_tag",
"itinerary_0_start_stop",
"itinerary_0_legs",
"itinerary_1_accessible",
"itinerary_1_tag",
"itinerary_1_start_stop",
"itinerary_1_legs",
"itinerary_2_accessible",
"itinerary_2_tag",
"itinerary_2_start_stop",
"itinerary_2_legs",
"itinerary_3_accessible",
"itinerary_3_tag",
"itinerary_3_start_stop",
"itinerary_3_legs",
"itinerary_4_accessible",
"itinerary_4_tag",
"itinerary_4_start_stop",
"itinerary_4_legs"
]

def rows(data) do
data
|> Enum.map(&format_all/1)
|> CSV.encode(headers: @headers)
|> Enum.to_list()
end

def format_all(data) do
data_map =
data
|> Enum.reduce(%{}, fn
{key, value}, acc when key in ["feedback_vote", "feedback_long", "itinerary_index"] ->
Map.put(acc, key, value)

{"generated_time", time_string}, acc ->
Map.put(acc, "generated_time", format_time(time_string))

{"modes", modes}, acc ->
modes = Enum.map(modes, fn {k, v} -> {"mode_#{k}", v} end) |> Enum.into(%{})
Map.merge(acc, modes)

{"query",
%{
"from" => from,
"to" => to,
"date_time" => date_time,
"time_type" => time_type,
"wheelchair" => wheelchair,
"itineraries" => itineraries
}},
acc ->
acc
|> Map.merge(%{
"query_from" => place_description(from),
"query_to" => place_description(to),
"query_date_time" => format_time(date_time),
"query_time_type" => time_type,
"query_wheelchair" => wheelchair
})
|> Map.merge(mapped_itineraries(itineraries))

_, acc ->
acc
end)

# sometimes fields are empty or not populated, so seed them here
@headers
|> Enum.map(&{&1, ""})
|> Map.new()
|> Map.merge(data_map)
end

defp place_description(%{"name" => name, "stop_id" => stop_id}) when not is_nil(stop_id),
do: name <> " (id: #{stop_id})"

defp place_description(%{"name" => name, "latitude" => lat, "longitude" => lon}),
do: "#{name} (#{lat}, #{lon})"

defp format_time(t) do
with {:ok, dt} <- Timex.parse(t, "{ISO:Extended}"),
{:ok, formatted} <- Timex.format(dt, "{ISOdate} {kitchen}") do
formatted
end
end

defp mapped_itineraries(itineraries) do
itineraries
|> Enum.with_index()
|> Enum.map(fn {%{
"tag" => tag,
"accessible?" => accessible,
"stop" => stop,
"start" => start,
"legs" => legs
}, index} ->
prefix = "itinerary_#{index}_"

%{
(prefix <> "tag") => tag,
(prefix <> "accessible") => accessible,
(prefix <> "start_stop") => "#{format_time(start)}, #{format_time(stop)}",
(prefix <> "legs") => Enum.map(legs, &mapped_leg/1) |> Enum.join(";\n")
}
end)
|> Enum.reduce(&Map.merge/2)
end

defp mapped_leg(%{"from" => from, "to" => to, "mode" => mode}) do
"#{place_description(from)} to #{place_description(to)} via #{mode_description(mode)}"
end

defp mode_description(%{"route_id" => route_id, "trip_id" => trip_id}),
do: route_id <> " route on trip " <> trip_id

defp mode_description(%{"steps" => steps, "distance" => distance}) do
step_description =
Enum.map(steps, fn %{"relative_direction" => direction, "street_name" => street_name} ->
relative_direction = String.to_atom(direction)

[
Step.human_relative_direction(relative_direction),
Step.human_relative_preposition(relative_direction),
street_name
]
|> Enum.join(" ")
end)
|> Enum.join(";\n\t")

"walking #{distance} meters:\n\t#{step_description}"
end
end
12 changes: 12 additions & 0 deletions lib/dotcom_web/controllers/trip_plan_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,18 @@ defmodule DotcomWeb.TripPlanController do
)
end

@doc """
if other plan params are filled, such as from or to, but no modes, set all
modes to true. this can happen when getting trip plans from the homepage.
"""
def modes(%Plug.Conn{params: %{"plan" => _}} = conn, _) do
assign(
conn,
:modes,
%{subway: true, bus: true, commuter_rail: true, ferry: true}
)
end

def modes(%Plug.Conn{} = conn, _) do
assign(conn, :modes, %{})
end
Expand Down
35 changes: 35 additions & 0 deletions lib/dotcom_web/live/admin.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
defmodule DotcomWeb.Live.Admin do
use DotcomWeb, :live_view

def mount(_params, _session, socket) do
{:ok,
assign(
socket,
:admin_features,
[
%{
url: Helpers.live_path(socket, DotcomWeb.Live.Admin.TripPlanFeedback),
title: "Trip Planner Feedback",
description: "Find and download the latest comments and votes."
}
]
)}
end

def render(assigns) do
~H"""
<section style="display: grid; gap: .5rem; grid-template-columns: 1fr 1fr 1fr">
<%= for feature <- @admin_features do %>
<%= link to: feature.url, class: "btn btn-secondary", style: "white-space: inherit;" do %>
<header class="h3 mt-0"><%= feature.title %></header>
<p class="mb-0"><%= feature.description %></p>
<% end %>
<% end %>
<div class="btn btn-secondary disabled" style="white-space: inherit;">
<header class="h3 mt-0">???</header>
<p class="mb-0">Your idea here (just send a message to <code>@thecristen</code>)</p>
</div>
</section>
"""
end
end
Loading

0 comments on commit fff1e8a

Please sign in to comment.