Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Phoenix 1.3 #78

Closed
danielberkompas opened this issue Oct 24, 2017 · 1 comment
Closed

Support Phoenix 1.3 #78

danielberkompas opened this issue Oct 24, 2017 · 1 comment

Comments

@danielberkompas
Copy link
Contributor

danielberkompas commented Oct 24, 2017

We need to update Torch to support Phoenix 1.3 in an idiomatic way.

Generators

Our generator is too complicated and hard to use. It would be better if we went to a CLI approach.

Here's how you would build a new Torch area from scratch:

# First, generate the Schema using the Phoenix generator
$ mix phx.gen.schema MyApp.Post ...

$ mix torch.gen

What schema module do you want to use?

> MyApp.Post

What do you want to use as the key?
Options: [:id, :uuid]

> id

Which fields do you want to be able to filter on?
Options: [:title, :author, :body, ...]

> title,author,body,published

What context module do you want to generate business logic into?

> MyApp.Writing

What is the name of the `repo` module?

> MyApp.Repo

What should the controller be named?

> MyAppWeb.PostController

Does a user need to have specific permissions to edit `MyApp.Post`s?
yes/no

> yes

What is the name of the user module?

> MyApp.User

Generating...
Done!

Context

This is the code that we should append to the bottom of the given context.

alias MyApp.Repo
alias MyApp.Post
alias MyApp.User, warn: false

@post_filters [
  %Filtrex.Type.Config{type: :text, keys: ~w(title author)}
]

@doc """
Paginates posts.

## Example

    Writing.paginate_posts(%{
      "post" => %{
        "title_equals" => "Hello, World!"
      },
      "page" => 2
    }, %User{...})
"""
@spec paginate_posts(map, User.t) ::
  {:ok, Scrivener.Page.t} |
  {:error, String.t}
def paginate_posts(params, _current_user) do
  with {:ok, filter} <- Filtrex.parse(params["post"], @post_filters) do
    page =
      Post
      |> Filtrex.query(filter)
      |> Repo.paginate(params)

    {:ok, page}
  end
end

@doc """
Gets a post by ID.

## Example

    Writing.get_post(123, %User{...})
"""
@spec get_post(integer, User.t) :: 
  {:ok, post} |
  {:error, :not_found}
def get_post(id, _current_user) do
  post = Repo.get_by(Post, id)
  if post, do: {:ok, post}, else: {:error, :not_found}
end

@doc """
Returns an `Ecto.Changeset` for a post.

## Examples

    Writing.change_post(%Post{}, %User{})
    # => {:ok, %Ecto.Changeset{data: %Post{}, ...}}

    Writing.change_post(%Post{}, %{"title" => "Hello, World!"}, %User{})
    # => {:ok, %Ecto.Changeset{data: %Post{}, ...}}
"""
@spec change_post(Post.t, map, User.t) ::
  {:ok, Ecto.Changeset.t}
def change_post(post, params \\ %{}, _current_user) do
  {:ok, Post.changeset(post, params)}
end

@doc """
Creates a post.

## Examples

    Writing.create_post(%{"title" => "Hello, World!"}, %User{})
    # => {:ok, %Post{title: "Hello, World!"}}

    Writing.create_post(%{"title" => ""}, %User{})
    # => {:error, %Ecto.Changeset{...}}
"""
@spec create_post(map, User.t) ::
  {:ok, Post.t} |
  {:error, Ecto.Changeset.t}
def create_post(params, _current_user) do
  with {:ok, changeset} <- change_post(%Post{}, params) do
    Repo.insert(changeset)
  end
end

@doc """
Updates a post.

## Examples

    Writing.update_post(post, %{"title" => "Best Post Ever"}, %User{})
    # => {:ok, %Post{title: "Best Post Ever"}}

    Writing.update_post(post, %{"title" => ""}, %User{})
    # => {:error, %Ecto.Changeset{...}}
"""
@spec update_post(Post.t, params, User.t) ::
  {:ok, Post.t} |
  {:error, Ecto.Changeset.t}
def update_post(post, params, _current_user) do
  with {:ok, changeset} <- change_post(post, params) do
    Repo.update(changeset)
  end
end

@doc """
Deletes a post.

## Examples

    Writing.delete_post(post, %User{})
    # => {:ok, %Post{...}}

    Writing.delete_post(post, %User{})
    # => {:error, %Ecto.Changeset{...}}
"""
@spec delete_post(Post.t) ::
  {:ok, Post.t} |
  {:error, Ecto.Changeset.t}
def delete_post(post, _current_user) do
  post
  |> change_post
  |> Repo.delete
end

Controller

defmodule MyAppWeb.PostController do
  @moduledoc """
  Torch-generated controller to manage Posts.
  """

  use MyAppWeb, :controller
  use Torch.Controller, assigns: [:current_user]

  alias MyApp.Writing
  alias MyAppWeb.TorchFallbackController

  plug :assign_post when action in [:edit, :update, :delete]

  action_fallback TorchFallbackController

  def index(conn, params, current_user) do
    with {:ok, page} <- Writing.paginate_posts(params, current_user) do
      conn
      |> assign_pagination(page)
      |> render("index.html")
    end
  end

  def new(conn, _params, current_user) do
    with {:ok, changeset} <- Writing.change_post(%Post{}, current_user) do
      render(conn, "new.html", changeset: changeset)
    end
  end

  def create(conn, params, current_user) do
    with {:ok, post} <- Writing.create_post(params["post"], current_user) do
      conn
      |> put_flash(:success, "Post created!")
      |> redirect(to: post_path(conn, :index))
    end
  end

  def edit(conn, _params, current_user) do
    with {:ok, changeset} <- Writing.change_post(conn.assigns.post, current_user) do
      render(conn, "edit.html", changeset: changeset)
    end
  end

  def update(conn, params, current_user) do
    with {:ok, post} <- Writing.update_post(conn.assigns.post, params["post"], current_user) do
      conn
      |> put_flash(:success, "Post updated!")
      |> redirect(to: post_path(conn, :index))
    end
  end

  def delete(conn, _params, current_user) do
    with {:ok, _post} <- Writing.delete_post(conn.assigns.post, current_user) do
      conn
      |> put_flash(:success, "Post deleted!")
      |> redirect(to: post_path(conn, :index))
    end
  end

  defp assign_post(conn, _opts) do
    case Writing.get_post(conn.params["id"]) do
      {:ok, post} ->
        assign(conn, :post, post)
      error ->
        conn
        |> TorchFallbackController.call(error)
        |> halt
    end
  end

  defp assign_fallback_opts(conn, _opts) do
    assign(conn, :fallback, %{
      index_path: post_path(conn, :index)
    })
  end
end

Fallback Controller

This helps us DRY up our controller code.

defmodule MyAppWeb.TorchFallbackController do
  import Plug.Conn

  def call(conn, {:error, :not_found}) do
    conn
    |> put_flash(:error, "Record not found!")
    |> redirect(to: conn.assigns.fallback.index_path)
  end

  def call(conn, {:error, <<"Unknown filter key", _rest::binary>> = error}) do
    conn
    |> put_flash(:error, "Invalid search: #{inspect(error)}")
    |> redirect(to: conn.assigns.index_path)
  end

  def call(conn, {:error, %Ecto.Changeset{data: %{id: id}} = changeset})
  when id == nil do
    conn
    |> validation_error(changeset)
    |> render("new.html")
  end

  def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
    conn
    |> validation_error(changeset)
    |> render("edit.html")
  end

  defp validation_error(conn, changeset) do
    conn
    |> put_flash(:error, "Oops! There were errors on the form!")
    |> assign(:changeset, changeset)
  end
end

View

defmodule MyAppWeb.PostView do
  use MyAppWeb, :view

  import Torch.View
end

Templates

Pretty much unchanged from what we have now.

@zberkom
Copy link
Contributor

zberkom commented Apr 13, 2018

Closing since this has been addressed in v2.0

@zberkom zberkom closed this as completed Apr 13, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants