Add settings model (#54)
* Adds a basic settings model * Added more settings methods; Hooked up initial settings runner to app boot * Update onboarding flow to use settings model instead of session data
This commit is contained in:
@@ -10,6 +10,8 @@ defmodule Pinchflat.Application do
|
||||
children = [
|
||||
PinchflatWeb.Telemetry,
|
||||
Pinchflat.Repo,
|
||||
# {Task, &run_startup_tasks/0},
|
||||
Pinchflat.StartupTasks,
|
||||
{Oban, Application.fetch_env!(:pinchflat, Oban)},
|
||||
{DNSCluster, query: Application.get_env(:pinchflat, :dns_cluster_query) || :ignore},
|
||||
{Phoenix.PubSub, name: Pinchflat.PubSub},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
defmodule Pinchflat.Profiles.MediaProfile do
|
||||
@moduledoc """
|
||||
A media profile is a set of settings that can be applied to many media sources
|
||||
A media profile is a set of configuration options that can be applied to many media sources
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
defmodule Pinchflat.Settings do
|
||||
@moduledoc """
|
||||
The Settings context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Pinchflat.Repo
|
||||
|
||||
alias Pinchflat.Settings.Setting
|
||||
|
||||
@doc """
|
||||
Returns the list of settings.
|
||||
|
||||
Returns [%Setting{}, ...]
|
||||
"""
|
||||
def list_settings do
|
||||
Repo.all(Setting)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates or updates a setting, returning the parsed value.
|
||||
Raises if an unsupported datatype is used. Optionally allows
|
||||
specifying the datatype.
|
||||
|
||||
Returns value in type of `Ecto.Enum.mappings(Setting, :datatype)`
|
||||
"""
|
||||
def set!(name, value) do
|
||||
set!(name, value, infer_datatype(value))
|
||||
end
|
||||
|
||||
def set!(name, value, datatype) do
|
||||
# Only create if doesn't exist
|
||||
case Repo.get_by(Setting, name: to_string(name)) do
|
||||
nil -> create_setting!(name, value, datatype)
|
||||
setting -> update_setting!(setting, value, datatype)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the parsed value of a setting. Raises if the setting does not exist.
|
||||
|
||||
Returns value in type of `Ecto.Enum.mappings(Setting, :datatype)`
|
||||
"""
|
||||
def get!(name) do
|
||||
Setting
|
||||
|> Repo.get_by!(name: to_string(name))
|
||||
|> read_setting()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Attempts to find a setting by name or creates a setting with value
|
||||
if one doesn't exist, returning the parsed value. Optionally allows
|
||||
specifying the datatype.
|
||||
|
||||
Returns value in type of `Ecto.Enum.mappings(Setting, :datatype)`
|
||||
"""
|
||||
def fetch!(name, value) do
|
||||
fetch!(name, value, infer_datatype(value))
|
||||
end
|
||||
|
||||
def fetch!(name, value, datatype) do
|
||||
case Repo.get_by(Setting, name: to_string(name)) do
|
||||
nil -> create_setting!(name, value, datatype)
|
||||
setting -> read_setting(setting)
|
||||
end
|
||||
end
|
||||
|
||||
defp change_setting(setting, attrs) do
|
||||
Setting.changeset(setting, attrs)
|
||||
end
|
||||
|
||||
defp create_setting!(name, value, datatype) do
|
||||
%Setting{}
|
||||
|> change_setting(%{name: to_string(name), value: to_string(value), datatype: datatype})
|
||||
|> Repo.insert!()
|
||||
|> read_setting()
|
||||
end
|
||||
|
||||
defp update_setting!(setting, value, datatype) do
|
||||
setting
|
||||
|> change_setting(%{value: to_string(value), datatype: datatype})
|
||||
|> Repo.update!()
|
||||
|> read_setting()
|
||||
end
|
||||
|
||||
defp read_setting(%{value: value, datatype: :string}), do: value
|
||||
defp read_setting(%{value: value, datatype: :boolean}), do: value in ["true", "t", "1"]
|
||||
defp read_setting(%{value: value, datatype: :integer}), do: String.to_integer(value)
|
||||
defp read_setting(%{value: value, datatype: :float}), do: String.to_float(value)
|
||||
|
||||
defp infer_datatype(value) when is_boolean(value), do: :boolean
|
||||
defp infer_datatype(value) when is_integer(value), do: :integer
|
||||
defp infer_datatype(value) when is_float(value), do: :float
|
||||
defp infer_datatype(value) when is_binary(value), do: :string
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
defmodule Pinchflat.Settings.Setting do
|
||||
@moduledoc """
|
||||
A Setting is a key-value pair with a datatype used to track user-level settings.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "settings" do
|
||||
field :name, :string
|
||||
field :value, :string
|
||||
field :datatype, Ecto.Enum, values: ~w(boolean string integer float)a
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(setting, attrs) do
|
||||
setting
|
||||
|> cast(attrs, [:name, :value, :datatype])
|
||||
|> validate_required([:name, :value, :datatype])
|
||||
|> unique_constraint([:name])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
defmodule Pinchflat.StartupTasks do
|
||||
@moduledoc """
|
||||
This module is responsible for running startup tasks on app boot.
|
||||
|
||||
It's a GenServer because that plays REALLY nicely with the existing
|
||||
Phoenix supervision tree.
|
||||
"""
|
||||
|
||||
# restart: :temporary means that this process will never be restarted (ie: will run once and then die)
|
||||
use GenServer, restart: :temporary
|
||||
|
||||
alias Pinchflat.Settings
|
||||
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, %{}, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Runs application startup tasks.
|
||||
|
||||
Any code defined here will run every time the application starts. You must
|
||||
make sure that the code is idempotent and safe to run multiple times.
|
||||
|
||||
This is a good place to set up default settings, create initial records, stuff like that
|
||||
"""
|
||||
@impl true
|
||||
def init(state) do
|
||||
apply_default_settings()
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
defp apply_default_settings do
|
||||
Settings.fetch!(:onboarding, true)
|
||||
end
|
||||
end
|
||||
@@ -45,6 +45,7 @@ defmodule PinchflatWeb do
|
||||
import Plug.Conn
|
||||
import PinchflatWeb.Gettext
|
||||
|
||||
alias Pinchflat.Settings
|
||||
alias PinchflatWeb.Layouts
|
||||
|
||||
unquote(verified_routes())
|
||||
@@ -56,6 +57,8 @@ defmodule PinchflatWeb do
|
||||
use Phoenix.LiveView,
|
||||
layout: {PinchflatWeb.Layouts, :app}
|
||||
|
||||
alias Pinchflat.Settings
|
||||
|
||||
unquote(html_helpers())
|
||||
end
|
||||
end
|
||||
@@ -64,6 +67,8 @@ defmodule PinchflatWeb do
|
||||
quote do
|
||||
use Phoenix.LiveComponent
|
||||
|
||||
alias Pinchflat.Settings
|
||||
|
||||
unquote(html_helpers())
|
||||
end
|
||||
end
|
||||
@@ -76,6 +81,8 @@ defmodule PinchflatWeb do
|
||||
import Phoenix.Controller,
|
||||
only: [get_csrf_token: 0, view_module: 1, view_template: 1]
|
||||
|
||||
alias Pinchflat.Settings
|
||||
|
||||
# Include general helpers for rendering HTML
|
||||
unquote(html_helpers())
|
||||
end
|
||||
@@ -93,6 +100,7 @@ defmodule PinchflatWeb do
|
||||
import PinchflatWeb.CustomComponents.TableComponents
|
||||
import PinchflatWeb.CustomComponents.ButtonComponents
|
||||
|
||||
alias Pinchflat.Settings
|
||||
alias Pinchflat.Utils.StringUtils
|
||||
|
||||
# Shortcut for generating JS commands
|
||||
|
||||
@@ -13,29 +13,21 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileController do
|
||||
def new(conn, _params) do
|
||||
changeset = Profiles.change_media_profile(%MediaProfile{})
|
||||
|
||||
if get_session(conn, :onboarding) do
|
||||
render(conn, :new, changeset: changeset, layout: {Layouts, :onboarding})
|
||||
else
|
||||
render(conn, :new, changeset: changeset)
|
||||
end
|
||||
render(conn, :new, changeset: changeset, layout: get_onboarding_layout())
|
||||
end
|
||||
|
||||
def create(conn, %{"media_profile" => media_profile_params}) do
|
||||
case Profiles.create_media_profile(media_profile_params) do
|
||||
{:ok, media_profile} ->
|
||||
redirect_location =
|
||||
if get_session(conn, :onboarding), do: ~p"/?onboarding=1", else: ~p"/media_profiles/#{media_profile}"
|
||||
if Settings.get!(:onboarding), do: ~p"/?onboarding=1", else: ~p"/media_profiles/#{media_profile}"
|
||||
|
||||
conn
|
||||
|> put_flash(:info, "Media profile created successfully.")
|
||||
|> redirect(to: redirect_location)
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
if get_session(conn, :onboarding) do
|
||||
render(conn, :new, changeset: changeset, layout: {Layouts, :onboarding})
|
||||
else
|
||||
render(conn, :new, changeset: changeset)
|
||||
end
|
||||
render(conn, :new, changeset: changeset, layout: get_onboarding_layout())
|
||||
end
|
||||
end
|
||||
|
||||
@@ -85,4 +77,12 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileController do
|
||||
|> put_flash(:info, flash_message)
|
||||
|> redirect(to: ~p"/media_profiles")
|
||||
end
|
||||
|
||||
defp get_onboarding_layout do
|
||||
if Settings.get!(:onboarding) do
|
||||
{Layouts, :onboarding}
|
||||
else
|
||||
{Layouts, :app}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="mb-6 flex gap-3 flex-row items-center">
|
||||
<.link :if={!Plug.Conn.get_session(@conn, :onboarding)} navigate={~p"/media_profiles"}>
|
||||
<.link :if={!Settings.get!(:onboarding)} navigate={~p"/media_profiles"}>
|
||||
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
||||
</.link>
|
||||
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">New Media Profile</h2>
|
||||
|
||||
@@ -20,12 +20,12 @@ defmodule PinchflatWeb.Pages.PageController do
|
||||
end
|
||||
|
||||
defp render_home_page(conn) do
|
||||
Settings.set!(:onboarding, false)
|
||||
media_profile_count = Repo.aggregate(MediaProfile, :count, :id)
|
||||
source_count = Repo.aggregate(Source, :count, :id)
|
||||
media_item_count = Repo.aggregate(MediaItem, :count, :id)
|
||||
|
||||
conn
|
||||
|> put_session(:onboarding, false)
|
||||
|> render(:home,
|
||||
media_profile_count: media_profile_count,
|
||||
source_count: source_count,
|
||||
@@ -34,8 +34,9 @@ defmodule PinchflatWeb.Pages.PageController do
|
||||
end
|
||||
|
||||
defp render_onboarding_page(conn, media_profiles_exist, sources_exist) do
|
||||
Settings.set!(:onboarding, true)
|
||||
|
||||
conn
|
||||
|> put_session(:onboarding, true)
|
||||
|> render(:onboarding_checklist,
|
||||
media_profiles_exist: media_profiles_exist,
|
||||
sources_exist: sources_exist,
|
||||
|
||||
@@ -16,37 +16,29 @@ defmodule PinchflatWeb.Sources.SourceController do
|
||||
def new(conn, _params) do
|
||||
changeset = Sources.change_source(%Source{})
|
||||
|
||||
if get_session(conn, :onboarding) do
|
||||
render(conn, :new,
|
||||
changeset: changeset,
|
||||
media_profiles: media_profiles(),
|
||||
layout: {Layouts, :onboarding}
|
||||
)
|
||||
else
|
||||
render(conn, :new, changeset: changeset, media_profiles: media_profiles())
|
||||
end
|
||||
render(conn, :new,
|
||||
changeset: changeset,
|
||||
media_profiles: media_profiles(),
|
||||
layout: get_onboarding_layout()
|
||||
)
|
||||
end
|
||||
|
||||
def create(conn, %{"source" => source_params}) do
|
||||
case Sources.create_source(source_params) do
|
||||
{:ok, source} ->
|
||||
redirect_location =
|
||||
if get_session(conn, :onboarding), do: ~p"/?onboarding=1", else: ~p"/sources/#{source}"
|
||||
if Settings.get!(:onboarding), do: ~p"/?onboarding=1", else: ~p"/sources/#{source}"
|
||||
|
||||
conn
|
||||
|> put_flash(:info, "Source created successfully.")
|
||||
|> redirect(to: redirect_location)
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
if get_session(conn, :onboarding) do
|
||||
render(conn, :new,
|
||||
changeset: changeset,
|
||||
media_profiles: media_profiles(),
|
||||
layout: {Layouts, :onboarding}
|
||||
)
|
||||
else
|
||||
render(conn, :new, changeset: changeset, media_profiles: media_profiles())
|
||||
end
|
||||
render(conn, :new,
|
||||
changeset: changeset,
|
||||
media_profiles: media_profiles(),
|
||||
layout: get_onboarding_layout()
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -107,4 +99,12 @@ defmodule PinchflatWeb.Sources.SourceController do
|
||||
defp media_profiles do
|
||||
Profiles.list_media_profiles()
|
||||
end
|
||||
|
||||
defp get_onboarding_layout do
|
||||
if Settings.get!(:onboarding) do
|
||||
{Layouts, :onboarding}
|
||||
else
|
||||
{Layouts, :app}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="mb-6 flex gap-3 flex-row items-center">
|
||||
<.link :if={!Plug.Conn.get_session(@conn, :onboarding)} navigate={~p"/sources"}>
|
||||
<.link :if={!Settings.get!(:onboarding)} navigate={~p"/sources"}>
|
||||
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
||||
</.link>
|
||||
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">New Source</h2>
|
||||
|
||||
Reference in New Issue
Block a user