From b82c00d3bc6f1d7ed595165062ca5f7e5afe9472 Mon Sep 17 00:00:00 2001 From: Sergey Zubkov Date: Wed, 14 Aug 2024 09:33:59 +0300 Subject: [PATCH] add metrics --- .erlang-history/erlang-shell-log.idx | Bin 0 -> 18 bytes .erlang-history/erlang-shell-log.siz | Bin 0 -> 13 bytes .gitignore | 2 + config/dev.exs | 2 +- flake.nix | 4 +- lib/habits/chains/chain.ex | 4 +- lib/habits/metrics.ex | 57 ++++ lib/habits/metrics/metric.ex | 20 ++ lib/habits_web/authentication.ex | 6 +- .../controllers/chain_controller.ex | 6 + lib/habits_web/controllers/chain_json.ex | 13 +- .../controllers/metric_controller.ex | 29 ++ lib/habits_web/controllers/metric_json.ex | 22 ++ lib/habits_web/controllers/session_json.ex | 4 +- lib/habits_web/endpoint.ex | 2 + lib/habits_web/router.ex | 5 +- mix.exs | 3 +- mix.lock | 1 + .../20240730173131_create_chains.exs | 7 +- .../20240808091302_create_metrics.exs | 15 + priv/repo/structure.sql | 319 ++++++++++++++++++ .../controllers/chain_controller_test.exs | 10 +- .../controllers/metric_controller_test.exs | 87 +++++ 23 files changed, 593 insertions(+), 25 deletions(-) create mode 100644 .erlang-history/erlang-shell-log.idx create mode 100644 .erlang-history/erlang-shell-log.siz create mode 100644 lib/habits/metrics.ex create mode 100644 lib/habits/metrics/metric.ex create mode 100644 lib/habits_web/controllers/metric_controller.ex create mode 100644 lib/habits_web/controllers/metric_json.ex create mode 100644 priv/repo/migrations/20240808091302_create_metrics.exs create mode 100644 priv/repo/structure.sql create mode 100644 test/habits_web/controllers/metric_controller_test.exs diff --git a/.erlang-history/erlang-shell-log.idx b/.erlang-history/erlang-shell-log.idx new file mode 100644 index 0000000000000000000000000000000000000000..700e4eef773a23053eb836f281eff30994991ac2 GIT binary patch literal 18 RcmZQz00Jf;W&|-n0000r00aO4 literal 0 HcmV?d00001 diff --git a/.erlang-history/erlang-shell-log.siz b/.erlang-history/erlang-shell-log.siz new file mode 100644 index 0000000000000000000000000000000000000000..b45272351550d55cf645b1a7cb54cf1d9e806030 GIT binary patch literal 13 PcmZQ#0E07UfD{)12=xJ_ literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore index d22b0eb..733ee6c 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,5 @@ erl_crash.dump habits-*.tar .direnv +.nix-mix +.erlang-history diff --git a/config/dev.exs b/config/dev.exs index f82e4d4..3bdac28 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -19,7 +19,7 @@ config :habits, Habits.Repo, config :habits, HabitsWeb.Endpoint, # Binding to loopback ipv4 address prevents access from other machines. # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. - http: [ip: {127, 0, 0, 1}, port: 4000], + http: [ip: {127, 0, 0, 1}, port: 5000], check_origin: false, code_reloader: true, debug_errors: true, diff --git a/flake.nix b/flake.nix index 96947c4..ecd4ec5 100644 --- a/flake.nix +++ b/flake.nix @@ -20,15 +20,15 @@ pkgs.inotify-tools ]; - hooks = '' + shellHook = '' mkdir -p .nix-mix .nix-hex export MIX_HOME=$PWD/.nix-mix export HEX_HOME=$PWD/.nix-mix export PATH=$MIX_HOME/bin:$HEX_HOME/bin:$PATH - mix local.hex --force export ERL_AFLAGS="-kernel shell_history enabled -kernel shell_history_path '\"$PWD/.erlang-history\"'" + export FRONT=http://habits.lcl:3000 ''; }; }); diff --git a/lib/habits/chains/chain.ex b/lib/habits/chains/chain.ex index f7a7e25..c84287f 100644 --- a/lib/habits/chains/chain.ex +++ b/lib/habits/chains/chain.ex @@ -2,6 +2,7 @@ defmodule Habits.Chains.Chain do use Ecto.Schema import Ecto.Changeset + @derive {Jason.Encoder, only: [:name, :id]} schema "chains" do field :active, :boolean, default: true field :name, :string @@ -9,13 +10,14 @@ defmodule Habits.Chains.Chain do field :description, :string belongs_to :user, Habits.Users.User + has_many :metrics, Habits.Metrics.Metric timestamps(type: :utc_datetime) end def changeset(chain, attrs) do chain - |> cast(attrs, [:name, :type, :active]) + |> cast(attrs, [:name, :type, :active, :description]) |> validate_required([:name, :type, :active]) end end diff --git a/lib/habits/metrics.ex b/lib/habits/metrics.ex new file mode 100644 index 0000000..df0c8d8 --- /dev/null +++ b/lib/habits/metrics.ex @@ -0,0 +1,57 @@ +defmodule Habits.Metrics do + import Ecto.Query, warn: false + alias Habits.Repo + + alias Habits.Metrics.Metric + alias Habits.Chains.Chain + + def get_by_date(user_id, date) do + case Date.from_iso8601(date) do + {:ok, date} -> + query = + from c in Chain, + left_join: m in Metric, + on: m.chain_id == c.id and m.date == ^date, + where: c.active == ^true, + where: c.user_id == ^user_id, + select: %{id: m.id, value: m.value, chain: c} + + Repo.all(query) + + error -> + error + end + end + + def upsert(chain, attrs) do + metric_changeset = + chain + |> Ecto.build_assoc(:metrics) + |> Metric.changeset(attrs) + + Repo.insert( + metric_changeset, + on_conflict: {:replace, [:value, :updated_at]}, + conflict_target: [:chain_id, :date], + returning: true + ) + end + + def get_metric!(user_id, metric_id) do + query = + from m in Metric, + join: c in Chain, + on: m.chain_id == c.id, + where: m.id == ^metric_id, + where: c.user_id == ^user_id + + case Repo.one(query) do + nil -> raise Ecto.NoResultsError, queryable: query + result -> result + end + end + + def delete_metric(%Metric{} = metric) do + Repo.delete(metric) + end +end diff --git a/lib/habits/metrics/metric.ex b/lib/habits/metrics/metric.ex new file mode 100644 index 0000000..e031e16 --- /dev/null +++ b/lib/habits/metrics/metric.ex @@ -0,0 +1,20 @@ +defmodule Habits.Metrics.Metric do + use Ecto.Schema + import Ecto.Changeset + + @derive {Jason.Encoder, only: [:value, :date, :chain, :updated_at]} + schema "metrics" do + field :value, :integer + field :date, :date + + belongs_to :chain, Habits.Chains.Chain + + timestamps(type: :utc_datetime) + end + + def changeset(chain, attrs) do + chain + |> cast(attrs, [:value, :chain_id, :date]) + |> validate_required([:value, :chain_id, :date]) + end +end diff --git a/lib/habits_web/authentication.ex b/lib/habits_web/authentication.ex index 9552097..e975437 100644 --- a/lib/habits_web/authentication.ex +++ b/lib/habits_web/authentication.ex @@ -2,6 +2,7 @@ defmodule HabitsWeb.Authentication do use HabitsWeb, :verified_routes import Plug.Conn + import Phoenix.Controller alias Habits.Users @@ -76,7 +77,10 @@ defmodule HabitsWeb.Authentication do if conn.assigns[:current_user] do conn else - conn |> put_status(:unauthorized) |> halt() + conn + |> put_status(:unauthorized) + |> json(%{error: "unauthorized"}) + |> halt() end end end diff --git a/lib/habits_web/controllers/chain_controller.ex b/lib/habits_web/controllers/chain_controller.ex index 5368eb9..c89ce93 100644 --- a/lib/habits_web/controllers/chain_controller.ex +++ b/lib/habits_web/controllers/chain_controller.ex @@ -18,6 +18,12 @@ defmodule HabitsWeb.ChainController do end end + def show(conn, %{"id" => id}) do + chain = Chains.get_chain!(conn.assigns.current_user.id, id) + + render(conn, :show, chain: chain) + end + def update(conn, %{"id" => id, "chain" => chain_params}) do chain = Chains.get_chain!(conn.assigns.current_user.id, id) diff --git a/lib/habits_web/controllers/chain_json.ex b/lib/habits_web/controllers/chain_json.ex index c799454..f5613a5 100644 --- a/lib/habits_web/controllers/chain_json.ex +++ b/lib/habits_web/controllers/chain_json.ex @@ -1,18 +1,12 @@ defmodule HabitsWeb.ChainJSON do alias Habits.Chains.Chain - @doc """ - Renders a list of chains. - """ def index(%{chains: chains}) do - %{data: for(chain <- chains, do: data(chain))} + for(chain <- chains, do: data(chain)) end - @doc """ - Renders a single chain. - """ def show(%{chain: chain}) do - %{data: data(chain)} + data(chain) end defp data(%Chain{} = chain) do @@ -20,7 +14,8 @@ defmodule HabitsWeb.ChainJSON do id: chain.id, name: chain.name, type: chain.type, - active: chain.active + active: chain.active, + description: chain.description } end end diff --git a/lib/habits_web/controllers/metric_controller.ex b/lib/habits_web/controllers/metric_controller.ex new file mode 100644 index 0000000..2a9f15b --- /dev/null +++ b/lib/habits_web/controllers/metric_controller.ex @@ -0,0 +1,29 @@ +defmodule HabitsWeb.MetricController do + use HabitsWeb, :controller + + alias Habits.Metrics + alias Habits.Chains + + action_fallback HabitsWeb.FallbackController + + def index(conn, params) do + metrics = Metrics.get_by_date(conn.assigns.current_user.id, params["date"]) + render(conn, :index, metrics: metrics) + end + + def create(conn, %{"metric" => metric_params}) do + chain = Chains.get_chain!(conn.assigns.current_user.id, metric_params["chain_id"]) + + with {:ok, metric} <- Metrics.upsert(chain, metric_params) do + render(conn, :create, metric: metric) + end + end + + def delete(conn, %{"id" => id}) do + metric = Metrics.get_metric!(conn.assigns.current_user.id, id) + + with {:ok, _metric} <- Metrics.delete_metric(metric) do + send_resp(conn, :no_content, "") + end + end +end diff --git a/lib/habits_web/controllers/metric_json.ex b/lib/habits_web/controllers/metric_json.ex new file mode 100644 index 0000000..339a633 --- /dev/null +++ b/lib/habits_web/controllers/metric_json.ex @@ -0,0 +1,22 @@ +defmodule HabitsWeb.MetricJSON do + def index(%{metrics: metrics}) do + for(metric <- metrics, do: data(metric)) + end + + def show(%{metric: metric}) do + data(metric) + end + + def create(%{metric: metric}) do + %{value: metric.value, updated_at: metric.updated_at, id: metric.id} + end + + defp data(metric) do + %{ + id: metric.id, + value: metric.value, + chain: metric.chain.name, + chain_id: metric.chain.id + } + end +end diff --git a/lib/habits_web/controllers/session_json.ex b/lib/habits_web/controllers/session_json.ex index 72d0e8a..21a076c 100644 --- a/lib/habits_web/controllers/session_json.ex +++ b/lib/habits_web/controllers/session_json.ex @@ -3,7 +3,7 @@ defmodule HabitsWeb.SessionJSON do %{id: user.id, email: user.email, handle: user.handle} end - def error(_) do - %{errors: "🐗"} + def error(params) do + %{errors: params.conn.assigns[:error_message]} end end diff --git a/lib/habits_web/endpoint.ex b/lib/habits_web/endpoint.ex index b7424b8..dc683bc 100644 --- a/lib/habits_web/endpoint.ex +++ b/lib/habits_web/endpoint.ex @@ -15,6 +15,8 @@ defmodule HabitsWeb.Endpoint do websocket: [connect_info: [session: @session_options]], longpoll: [connect_info: [session: @session_options]] + plug CORSPlug, origin: [System.get_env("FRONT")] + # Serve at "/" the static files from "priv/static" directory. # # You should set gzip to true if you are running phx.digest diff --git a/lib/habits_web/router.ex b/lib/habits_web/router.ex index d9837e8..c9b89b1 100644 --- a/lib/habits_web/router.ex +++ b/lib/habits_web/router.ex @@ -4,11 +4,11 @@ defmodule HabitsWeb.Router do import HabitsWeb.Authentication, only: [fetch_current_user: 2, require_authenticated_user: 2] pipeline :api do + plug :fetch_session plug :accepts, ["json"] end pipeline :user_api do - plug :fetch_session plug :fetch_current_user plug :require_authenticated_user end @@ -26,7 +26,8 @@ defmodule HabitsWeb.Router do resources "/users", UserController, only: [:update, :show] resources "/sessions", SessionController, only: [:delete, :show], singleton: true - resources "/chains", ChainController, except: [:new, :edit] + resources "/chains", ChainController, only: [:show, :index, :create, :update, :delete] + resources "/metrics", MetricController, only: [:index, :create, :delete] end # Enable LiveDashboard and Swoosh mailbox preview in development diff --git a/mix.exs b/mix.exs index 50a8420..030214e 100644 --- a/mix.exs +++ b/mix.exs @@ -45,7 +45,8 @@ defmodule Habits.MixProject do {:gettext, "~> 0.20"}, {:jason, "~> 1.2"}, {:dns_cluster, "~> 0.1.1"}, - {:bandit, "~> 1.5"} + {:bandit, "~> 1.5"}, + {:cors_plug, "~> 3.0"} ] end diff --git a/mix.lock b/mix.lock index 116825a..936bb45 100644 --- a/mix.lock +++ b/mix.lock @@ -3,6 +3,7 @@ "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"}, "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"}, "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, + "cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, diff --git a/priv/repo/migrations/20240730173131_create_chains.exs b/priv/repo/migrations/20240730173131_create_chains.exs index b43be5e..ab4c2f7 100644 --- a/priv/repo/migrations/20240730173131_create_chains.exs +++ b/priv/repo/migrations/20240730173131_create_chains.exs @@ -6,9 +6,14 @@ defmodule Habits.Repo.Migrations.CreateChains do add :name, :string, null: false add :description, :string add :type, :string, null: false - add :active, :boolean, default: true, null: false add :user_id, references(:users, on_delete: :delete_all), null: false + add :active, :boolean, default: true, null: false + add :time_range, :string + add :days_of_month, :string + add :days_of_week, :string + add :months, :string + timestamps(type: :utc_datetime) end diff --git a/priv/repo/migrations/20240808091302_create_metrics.exs b/priv/repo/migrations/20240808091302_create_metrics.exs new file mode 100644 index 0000000..126e48d --- /dev/null +++ b/priv/repo/migrations/20240808091302_create_metrics.exs @@ -0,0 +1,15 @@ +defmodule Habits.Repo.Migrations.CreateMetrics do + use Ecto.Migration + + def change do + create table(:metrics) do + add :value, :integer, null: false + add :chain_id, references(:chains, on_delete: :delete_all), null: false + add :date, :date, null: false + + timestamps(type: :utc_datetime) + end + + create unique_index(:metrics, [:chain_id, :date]) + end +end diff --git a/priv/repo/structure.sql b/priv/repo/structure.sql new file mode 100644 index 0000000..8756486 --- /dev/null +++ b/priv/repo/structure.sql @@ -0,0 +1,319 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 13.15 +-- Dumped by pg_dump version 13.15 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: citext; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS citext WITH SCHEMA public; + + +-- +-- Name: EXTENSION citext; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION citext IS 'data type for case-insensitive character strings'; + + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: chains; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.chains ( + id bigint NOT NULL, + name character varying(255) NOT NULL, + description character varying(255), + type character varying(255) NOT NULL, + active boolean DEFAULT true NOT NULL, + user_id bigint NOT NULL, + inserted_at timestamp(0) without time zone NOT NULL, + updated_at timestamp(0) without time zone NOT NULL +); + + +-- +-- Name: chains_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.chains_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: chains_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.chains_id_seq OWNED BY public.chains.id; + + +-- +-- Name: metrics; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.metrics ( + id bigint NOT NULL, + value integer NOT NULL, + chain_id bigint NOT NULL, + time_range character varying(255), + days_of_month character varying(255), + days_of_week character varying(255), + months character varying(255), + inserted_at timestamp(0) without time zone NOT NULL, + updated_at timestamp(0) without time zone NOT NULL +); + + +-- +-- Name: metrics_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.metrics_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: metrics_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.metrics_id_seq OWNED BY public.metrics.id; + + +-- +-- Name: schema_migrations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.schema_migrations ( + version bigint NOT NULL, + inserted_at timestamp(0) without time zone +); + + +-- +-- Name: users; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.users ( + id bigint NOT NULL, + handle character varying(255) NOT NULL, + email public.citext NOT NULL, + hashed_password character varying(255) NOT NULL, + confirmed_at timestamp(0) without time zone, + inserted_at timestamp(0) without time zone NOT NULL, + updated_at timestamp(0) without time zone NOT NULL +); + + +-- +-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.users_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id; + + +-- +-- Name: users_tokens; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.users_tokens ( + id bigint NOT NULL, + user_id bigint NOT NULL, + token bytea NOT NULL, + context character varying(255) NOT NULL, + sent_to character varying(255), + inserted_at timestamp(0) without time zone NOT NULL +); + + +-- +-- Name: users_tokens_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.users_tokens_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: users_tokens_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.users_tokens_id_seq OWNED BY public.users_tokens.id; + + +-- +-- Name: chains id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.chains ALTER COLUMN id SET DEFAULT nextval('public.chains_id_seq'::regclass); + + +-- +-- Name: metrics id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.metrics ALTER COLUMN id SET DEFAULT nextval('public.metrics_id_seq'::regclass); + + +-- +-- Name: users id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass); + + +-- +-- Name: users_tokens id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users_tokens ALTER COLUMN id SET DEFAULT nextval('public.users_tokens_id_seq'::regclass); + + +-- +-- Name: chains chains_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.chains + ADD CONSTRAINT chains_pkey PRIMARY KEY (id); + + +-- +-- Name: metrics metrics_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.metrics + ADD CONSTRAINT metrics_pkey PRIMARY KEY (id); + + +-- +-- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.schema_migrations + ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); + + +-- +-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_pkey PRIMARY KEY (id); + + +-- +-- Name: users_tokens users_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users_tokens + ADD CONSTRAINT users_tokens_pkey PRIMARY KEY (id); + + +-- +-- Name: chains_user_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX chains_user_id_index ON public.chains USING btree (user_id); + + +-- +-- Name: metrics_chain_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX metrics_chain_id_index ON public.metrics USING btree (chain_id); + + +-- +-- Name: users_handle_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX users_handle_index ON public.users USING btree (handle); + + +-- +-- Name: users_tokens_context_token_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX users_tokens_context_token_index ON public.users_tokens USING btree (context, token); + + +-- +-- Name: users_tokens_user_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX users_tokens_user_id_index ON public.users_tokens USING btree (user_id); + + +-- +-- Name: chains chains_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.chains + ADD CONSTRAINT chains_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: metrics metrics_chain_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.metrics + ADD CONSTRAINT metrics_chain_id_fkey FOREIGN KEY (chain_id) REFERENCES public.chains(id) ON DELETE CASCADE; + + +-- +-- Name: users_tokens users_tokens_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users_tokens + ADD CONSTRAINT users_tokens_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- PostgreSQL database dump complete +-- + +INSERT INTO public."schema_migrations" (version) VALUES (20240706194718); +INSERT INTO public."schema_migrations" (version) VALUES (20240727065353); +INSERT INTO public."schema_migrations" (version) VALUES (20240730173131); +INSERT INTO public."schema_migrations" (version) VALUES (20240808091302); diff --git a/test/habits_web/controllers/chain_controller_test.exs b/test/habits_web/controllers/chain_controller_test.exs index 95dc37b..4b80c1b 100644 --- a/test/habits_web/controllers/chain_controller_test.exs +++ b/test/habits_web/controllers/chain_controller_test.exs @@ -4,7 +4,7 @@ defmodule HabitsWeb.ChainControllerTest do import Habits.Factory setup %{conn: conn} do - user = insert!(:user, password: "secure🐗password") + user = insert!(:user) conn = conn |> init_test_session([]) |> log_in_user(user) %{user: user, conn: conn} @@ -16,7 +16,9 @@ defmodule HabitsWeb.ChainControllerTest do conn = get(conn, ~p"/chains") - assert [%{"active" => true, "id" => _, "name" => "elixir", "type" => "integer"}] = json_response(conn, 200)["data"] + assert [ + %{"active" => true, "id" => _, "name" => "elixir", "type" => "integer"} + ] = json_response(conn, 200)["data"] end end @@ -65,9 +67,7 @@ defmodule HabitsWeb.ChainControllerTest do test "renders errors when data is invalid", %{conn: conn, user: user} do chain = insert!(:chain, user: user) - chain_attrs = %{ - "name" => "", - } + chain_attrs = %{"name" => ""} conn = put(conn, ~p"/chains/#{chain.id}", chain: chain_attrs) assert json_response(conn, 422)["errors"] != %{} diff --git a/test/habits_web/controllers/metric_controller_test.exs b/test/habits_web/controllers/metric_controller_test.exs new file mode 100644 index 0000000..9072466 --- /dev/null +++ b/test/habits_web/controllers/metric_controller_test.exs @@ -0,0 +1,87 @@ +defmodule HabitsWeb.MetricControllerTest do + use HabitsWeb.ConnCase + + import Habits.Factory + + setup %{conn: conn} do + user = insert!(:user) + conn = conn |> init_test_session([]) |> log_in_user(user) + chain = insert!(:chain, user: user) + + %{conn: conn, user: user, chain: chain} + end + + describe "index" do + test "lists all metrics", %{conn: conn, user: user} do + insert!(:metric, user: user) + + conn = get(conn, ~p"/metrics") + + assert [ + %{"active" => true, "id" => _, "name" => "elixir", "type" => "integer"} + ] = json_response(conn, 200)["data"] + end + end + + describe "create metric" do + test "renders metric when data is valid", %{conn: conn} do + metric_attrs = %{ + "name" => "rust", + "active" => "true", + "type" => "integer", + "description" => "pomodoro" + } + + conn = post(conn, ~p"/metrics", metric: metric_attrs) + assert %{"id" => _id} = json_response(conn, 200)["data"] + end + + test "renders errors when data is invalid", %{conn: conn} do + metric_attrs = %{ + "active" => "true", + "type" => "integer" + } + + conn = post(conn, ~p"/metrics", metric: metric_attrs) + assert json_response(conn, 422)["errors"] != %{} + end + end + + describe "update metric" do + test "renders metric when data is valid", %{conn: conn, user: user} do + metric = insert!(:metric, user: user) + id = metric.id + + update_attrs = %{ + "active" => "false", + "name" => "pullups" + } + + conn = put(conn, ~p"/metrics/", metric: update_attrs) + + assert %{ + "id" => ^id, + "active" => false, + "name" => "pullups", + "type" => "integer" + } = json_response(conn, 200)["data"] + end + + test "renders errors when data is invalid", %{conn: conn, user: user} do + metric = insert!(:metric, user: user) + metric_attrs = %{"name" => ""} + + conn = put(conn, ~p"/metrics/#{metric.id}", metric: metric_attrs) + assert json_response(conn, 422)["errors"] != %{} + end + end + + describe "delete metric" do + test "deletes chosen metric", %{conn: conn, user: user} do + metric = insert!(:metric, user: user) + + conn = delete(conn, ~p"/metrics/#{metric.id}") + assert response(conn, 204) + end + end +end