diff --git a/lib/frenzy/create_item_task.ex b/lib/frenzy/create_item_task.ex index d990f70..2bd993a 100644 --- a/lib/frenzy/create_item_task.ex +++ b/lib/frenzy/create_item_task.ex @@ -23,24 +23,27 @@ defmodule Frenzy.CreateItemTask do content: entry.content } - feed = Repo.preload(feed, :pipeline_stages) + feed = Repo.preload(feed, :pipeline) result = - feed.pipeline_stages - |> Enum.sort_by(& &1.index) - |> Enum.reduce({:ok, item_params}, fn - stage, {:ok, item_params} -> - apply(String.to_existing_atom("Elixir." <> stage.module_name), :apply, [ - stage.options, - item_params - ]) + if feed.pipeline do + feed.pipeline.stages + |> Enum.reduce({:ok, item_params}, fn + stage, {:ok, item_params} -> + apply(String.to_existing_atom("Elixir." <> stage["module_name"]), :apply, [ + stage["options"], + item_params + ]) - _stage, :tombstone -> - :tombstone + _stage, :tombstone -> + :tombstone - _stage, {:error, _error} = error -> - error - end) + _stage, {:error, _error} = error -> + error + end) + else + {:ok, item_params} + end case result do {:err, error} -> diff --git a/lib/frenzy/feed.ex b/lib/frenzy/feed.ex index 78cccbe..98a667c 100644 --- a/lib/frenzy/feed.ex +++ b/lib/frenzy/feed.ex @@ -36,9 +36,9 @@ defmodule Frenzy.Feed do field :title, :string belongs_to :group, Frenzy.Group + belongs_to :pipeline, Frenzy.Pipeline has_many :items, Frenzy.Item, on_delete: :delete_all - has_many :pipeline_stages, Frenzy.PipelineStage, on_delete: :delete_all timestamps() end @@ -51,8 +51,8 @@ defmodule Frenzy.Feed do site_url: String.t() | nil, title: String.t() | nil, group: Frenzy.Group.t() | Ecto.Association.NotLoaded.t(), + pipeline: Frenzy.Pipeline.t() | nil | Ecto.Association.NotLoaded.t(), items: [Frenzy.Item.t()] | Ecto.Association.NotLoaded.t(), - pipeline_stages: [Frenzy.PipelineStage.t()] | Ecto.Association.NotLoaded.t(), inserted_at: NaiveDateTime.t(), updated_at: NaiveDateTime.t() } @@ -64,7 +64,8 @@ defmodule Frenzy.Feed do :title, :feed_url, :site_url, - :last_updated + :last_updated, + :pipeline_id ]) |> validate_required([:feed_url]) end diff --git a/lib/frenzy/pipeline.ex b/lib/frenzy/pipeline.ex new file mode 100644 index 0000000..c23a9ec --- /dev/null +++ b/lib/frenzy/pipeline.ex @@ -0,0 +1,29 @@ +defmodule Frenzy.Pipeline do + use Ecto.Schema + import Ecto.Changeset + + schema "pipelines" do + field :name, :string + field :stages, {:array, :map} + + belongs_to :user, Frenzy.User + has_many :feeds, Frenzy.Feed + + timestamps() + end + + @type t() :: %__MODULE__{ + __meta__: Ecto.Schema.Metadata.t(), + id: integer() | nil, + name: String.t(), + stages: [map()], + user: Frenzy.User.t() | Ecto.Association.NotLoaded.t(), + feeds: [Frenzy.Feed.t()] | Ecto.Association.NotLoaded.t() + } + + def changeset(pipeline, attrs) do + pipeline + |> cast(attrs, [:name, :stages, :user_id]) + |> validate_required([:name, :stages, :user_id]) + end +end diff --git a/lib/frenzy/pipeline_stage.ex b/lib/frenzy/pipeline_stage.ex deleted file mode 100644 index 31dcf2d..0000000 --- a/lib/frenzy/pipeline_stage.ex +++ /dev/null @@ -1,31 +0,0 @@ -defmodule Frenzy.PipelineStage do - use Ecto.Schema - import Ecto.Changeset - - schema "pipeline_stages" do - field :index, :integer - field :module_name, :string - field :options, :map - - belongs_to :feed, Frenzy.Feed - - timestamps() - end - - @type t() :: %__MODULE__{ - __meta__: Ecto.Schema.Metadata.t(), - id: integer() | nil, - index: integer(), - module_name: String.t(), - options: map(), - feed: Frenzy.Feed.t() | Ecto.Association.NotLoaded.t(), - inserted_at: NaiveDateTime.t(), - updated_at: NaiveDateTime.t() - } - - def changeset(stage, attrs) do - stage - |> cast(attrs, [:index, :module_name, :options]) - |> validate_required([:index, :module_name]) - end -end diff --git a/lib/frenzy_web/controllers/feed_controller.ex b/lib/frenzy_web/controllers/feed_controller.ex index 679db27..e1cbf1a 100644 --- a/lib/frenzy_web/controllers/feed_controller.ex +++ b/lib/frenzy_web/controllers/feed_controller.ex @@ -1,6 +1,6 @@ defmodule FrenzyWeb.FeedController do use FrenzyWeb, :controller - alias Frenzy.{Repo, Group, Feed, Item} + alias Frenzy.{Repo, Group, Feed, Item, Pipeline} alias FrenzyWeb.Router.Helpers, as: Routes alias FrenzyWeb.Endpoint import Ecto.Query @@ -67,12 +67,17 @@ defmodule FrenzyWeb.FeedController do end def edit(conn, _params) do - feed = conn.assigns[:feed] |> Repo.preload(:pipeline_stages) - stages = Enum.sort_by(feed.pipeline_stages, fn stage -> stage.index end) + feed = conn.assigns[:feed] + edit_changeset = Feed.changeset(feed, %{}) + + pipelines = + Repo.all(Pipeline) + |> Enum.map(fn pipeline -> {pipeline.name, pipeline.id} end) render(conn, "edit.html", %{ feed: feed, - stages: stages + changeset: edit_changeset, + pipelines: [{"No Pipeline", nil} | pipelines] }) end diff --git a/lib/frenzy_web/controllers/pipeline_controller.ex b/lib/frenzy_web/controllers/pipeline_controller.ex index 6f10dd5..b0ce5e4 100644 --- a/lib/frenzy_web/controllers/pipeline_controller.ex +++ b/lib/frenzy_web/controllers/pipeline_controller.ex @@ -1,21 +1,20 @@ defmodule FrenzyWeb.PipelineController do use FrenzyWeb, :controller - alias Frenzy.{Repo, Feed, PipelineStage} + alias Frenzy.{Repo, Pipeline} alias FrenzyWeb.Router.Helpers, as: Routes alias FrenzyWeb.Endpoint import Ecto.Query - plug :user_owns_feed - plug :user_owns_stage + plug :user_owns_pipeline - defp user_owns_feed(%Plug.Conn{path_params: %{"feed_id" => feed_id}} = conn, _opts) do + defp user_owns_pipeline(%Plug.Conn{path_params: %{"id" => pipeline_id}} = conn, _opts) do user = conn.assigns[:user] - feed = Repo.get(Feed, feed_id) |> Repo.preload(:pipeline_stages) + pipeline = Repo.get(Pipeline, pipeline_id) - if Enum.any?(user.groups, fn g -> g.id == feed.group_id end) do + if pipeline.user_id == user.id do conn - |> assign(:feed, feed) + |> assign(:pipeline, pipeline) else conn |> put_flash(:error, "You do not have permission to access that resource.") @@ -24,133 +23,137 @@ defmodule FrenzyWeb.PipelineController do end end - defp user_owns_feed(conn, _opts), do: conn + defp user_owns_pipeline(conn, _opts), do: conn - defp user_owns_stage(%Plug.Conn{path_params: %{"stage_id" => stage_id}} = conn, _opts) do - feed = conn.assigns[:feed] + def index(conn, _params) do + user = conn.assigns[:user] - stage = Repo.get(PipelineStage, stage_id) + pipelines = Repo.all(from p in Pipeline, where: p.user_id == ^user.id, preload: [:feeds]) - if stage.feed_id == feed.id do - conn - |> assign(:stage, stage) - else - conn - |> put_flash(:error, "You do not have permission to access that resource.") - |> redirect(to: Routes.group_path(Endpoint, :index)) - |> halt() - end - end - - defp user_owns_stage(conn, _opts), do: conn - - def edit(conn, %{"stage_id" => stage_id}) do - feed = conn.assigns[:feed] - stage = conn.assigns[:stage] - {:ok, options_json} = Jason.encode(stage.options, pretty: true) - - changeset = - PipelineStage.changeset(stage, %{ - options: options_json - }) - - render(conn, "edit.html", %{ - feed: feed, - stage: stage, - changeset: changeset - }) - end - - def update(conn, %{"pipeline_stage" => %{"options" => options_json}}) do - feed = conn.assigns[:feed] - stage = conn.assigns[:stage] - - with {:ok, options} <- Jason.decode(options_json), - {:ok, options} <- - apply(String.to_existing_atom("Elixir." <> stage.module_name), :validate_opts, [ - options - ]) do - changeset = PipelineStage.changeset(stage, %{options: options}) - {:ok, _stage} = Repo.update(changeset) - - conn - |> put_flash(:info, "Pipeline Stage updated") - |> redirect(to: Routes.feed_path(Endpoint, :edit, feed.id)) - else - result -> - error_changeset = PipelineStage.changeset(stage, %{options: options_json}) - - conn - |> put_flash(:error, "Unable to update pipeline stage: #{inspect(result)}") - |> render("edit.html", %{ - feed: feed, - stage: stage, - changeset: error_changeset - }) - end + render(conn, "index.html", %{pipelines: pipelines}) end def new(conn, _params) do - feed = conn.assigns[:feed] - changeset = - PipelineStage.changeset(%PipelineStage{}, %{ - index: feed.pipeline_stages |> Enum.count(), - options: "{}" + Pipeline.changeset(%Pipeline{}, %{ + name: "", + stages: "[\n]" }) - render(conn, "new.html", %{ - feed: feed, - changeset: changeset + render(conn, "new.html", %{changeset: changeset}) + end + + def create(conn, %{"pipeline" => %{"name" => name, "stages" => stages_json}}) do + user = conn.assigns[:user] + + with {:ok, stages} <- Jason.decode(stages_json), + {:ok, stages} <- validate_pipeline(stages) do + changeset = Pipeline.changeset(%Pipeline{}, %{user_id: user.id, name: name, stages: stages}) + + {:ok, _pipeline} = Repo.insert(changeset) + + conn + |> put_flash(:info, "Pipeline created") + |> redirect(to: Routes.pipeline_path(Endpoint, :index)) + else + {:error, reason} -> + error_changeset = Pipeline.changeset(%Pipeline{}, %{name: name, stages: stages_json}) + + conn + |> put_flash(:error, "Unable to create pipeline: #{reason}") + |> render("new.html", %{changeset: error_changeset}) + end + end + + def show(conn, _params) do + pipeline = conn.assigns[:pipeline] + + render(conn, "show.html", %{pipeline: pipeline}) + end + + def delete(conn, _params) do + conn.assigns[:pipeline] + |> Repo.delete() + + redirect(conn, to: Routes.pipeline_path(Endpoint, :index)) + end + + def edit(conn, _params) do + pipeline = conn.assigns[:pipeline] + + {:ok, stages_json} = Jason.encode(pipeline.stages, pretty: true) + + render(conn, "edit.html", %{ + pipeline: pipeline, + name: pipeline.name, + stages_json: stages_json }) end - def create(conn, %{ - "pipeline_stage" => - %{ - "index" => index, - "module_name" => module_name, - "options" => options_json - } = params - }) do - feed = conn.assigns[:feed] + def update(conn, %{"pipeline" => %{"name" => name, "stages" => stages_json}}) do + pipeline = conn.assigns[:pipeline] - with {index, _} <- Integer.parse(index), - module_atom <- String.to_existing_atom("Elixir." <> module_name), - {:ok, options} <- Jason.decode(options_json), - {:ok, options} <- apply(module_atom, :validate_opts, [options]) do - changeset = - Ecto.build_assoc(feed, :pipeline_stages, %{ - index: index, - module_name: module_name, - options: options - }) + with {:ok, stages} <- Jason.decode(stages_json), + {:ok, stages} <- validate_pipeline(stages) do + changeset = Pipeline.changeset(%Pipeline{}, %{name: name, stages: stages}) - {:ok, _stage} = Repo.insert(changeset) + {:ok, _pipeline} = Repo.update(changeset) conn - |> put_flash(:info, "Pipeline Stage created") - |> redirect(to: Routes.feed_path(Endpoint, :edit, feed.id)) + |> put_flash(:info, "Pipeline edited") + |> redirect(to: Routes.pipeline_path(Endpoint, :show, pipeline.id)) else - result -> - error_changeset = PipelineStage.changeset(%PipelineStage{}, params) - + {:error, reason} -> conn - |> put_flash(:error, "Unable to create pipeline stage: #{inspect(result)}") - |> render("new.html", %{ - feed: feed, - changeset: error_changeset + |> put_flash(:error, "Unable to edit pipeline: #{reason}") + |> render("edit.html", %{ + pipeline: pipeline, + name: name, + stages_json: stages_json }) end end - def delete(conn, _params) do - feed = conn.assigns[:feed] - stage = conn.assigns[:stage] - {:ok, _stage} = Repo.delete(stage) + defp validate_pipeline(stages) do + stages + |> Enum.with_index() + |> Enum.reduce_while({:ok, []}, fn {stage, index}, {:ok, new_stages} -> + case validate_stage(stage) do + {:ok, stage} -> + {:cont, {:ok, new_stages ++ [stage]}} - conn - |> put_flash(:info, "Pipeline Stage deleted") - |> redirect(to: Routes.feed_path(Endpoint, :edit, feed.id)) + {:error, reason} -> + {:error, "invalid stage at #{index}: #{reason}"} + end + end) + end + + defp validate_stage(%{"module_name" => module_name, "options" => options}) do + with true <- module_exists(module_name), + {:ok, options} <- + apply(String.to_existing_atom("Elixir." <> module_name), :validate_opts, [ + options + ]) do + {:ok, %{"module_name" => module_name, "options" => options}} + else + false -> + {:error, "module #{module_name} does not exist"} + + {:error, reason} -> + {:error, "invalid options for #{module_name}: #{reason}"} + end + end + + defp validate_stage(_), + do: {:error, "pipeline stage must be a map with module_name and options"} + + defp module_exists(module_name) do + try do + String.to_existing_atom("Elixir." <> module_name) + true + rescue + ArgumentError -> + false + end end end diff --git a/lib/frenzy_web/router.ex b/lib/frenzy_web/router.ex index f110ca5..e93bfc4 100644 --- a/lib/frenzy_web/router.ex +++ b/lib/frenzy_web/router.ex @@ -52,13 +52,7 @@ defmodule FrenzyWeb.Router do resources "/feeds", FeedController, except: [:index, :new] post "/feeds/:id/refresh", FeedController, :refresh - # resources "/pipelines", PipelineController, only: [:edit, :update, :new, :create] - - get "/feeds/:feed_id/pipelines/:stage_id/edit", PipelineController, :edit - put "/feeds/:feed_id/pipelines/:stage_id/update", PipelineController, :update - delete "/feeds/:feed_id/pipelines/:stage_id/delete", PipelineController, :delete - get "/feeds/:feed_id/pipelines/new", PipelineController, :new - post "/feeds/:feed_id/pipelines/create", PipelineController, :create + resources "/pipelines", PipelineController resources "/items", ItemController, only: [:show] post "/items/:id/read", ItemController, :read diff --git a/lib/frenzy_web/templates/feed/edit.html.eex b/lib/frenzy_web/templates/feed/edit.html.eex index a42aabc..9cec1ab 100644 --- a/lib/frenzy_web/templates/feed/edit.html.eex +++ b/lib/frenzy_web/templates/feed/edit.html.eex @@ -1,29 +1,22 @@
Module | -- |
---|---|
- <%= stage.module_name %>
- |
- - - Edit - - <%= form_for @conn, Routes.pipeline_path(@conn, :delete, @feed.id, stage.id), [method: :delete, style: "display: inline-block;"], fn f -> %> - <%= submit "Delete", class: "btn btn-danger btn-sm" %> - <% end %> - | -