From 89c3830e9d443d565a7693cc65358f8cd922c8fb Mon Sep 17 00:00:00 2001 From: Alex Garibay Date: Fri, 14 Jul 2017 20:38:28 -0500 Subject: [PATCH] Add support for layouts for phoenix views --- CHANGELOG.md | 7 + README.md | 28 +++- lib/sendgrid/email.ex | 141 ++++++++++++++++--- mix.exs | 8 +- test/email_test.exs | 68 ++++++++- test/support/templates/email/layout.html.eex | 2 + test/support/templates/email/layout.txt.eex | 2 + 7 files changed, 224 insertions(+), 32 deletions(-) create mode 100644 test/support/templates/email/layout.html.eex create mode 100644 test/support/templates/email/layout.txt.eex diff --git a/CHANGELOG.md b/CHANGELOG.md index cf9559d..a203a8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 1.6.0 (2017-7-14) +* Enhancements + * Relax dependency versions + * add `put_phoenix_layout/2` in `SendGrid.Email` to render views in +* Breaking Changes + * `put_phoenix_template/3` now expects an atom for implicit template rendering + ## 1.5.0 (2017-7-3) * Enhancements * update docs diff --git a/README.md b/README.md index 1a224a2..6e11b2b 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ SendGrid.Email.build() Add the following code to your dependencies in your **`mix.exs`** file: ```elixir -{:sendgrid, "~> 1.5.0"} +{:sendgrid, "~> 1.6.0"} ``` ## Configuration @@ -47,11 +47,12 @@ defp application do end ``` - ## Phoenix Views You can use Phoenix Views to set your HTML and text content of your emails. You just have -to provide a view module and template name and you're good to go! See `SendGrid.Email.put_phoenix_template/3` for complete usage. +to provide a view module and template name and you're good to go! Additionally, you can set +a layout to render the view in with `SendGrid.Email.put_phoenix_layout/2`. See `SendGrid.Email.put_phoenix_template/3` +for complete usage. ### Examples @@ -69,17 +70,32 @@ import SendGrid.Email |> put_phoenix_template("welcome_email.txt", user: user) # Using both an HTML and text template -# Notice that there is no extension. %SendGrid.Email{} |> put_phoenix_view(MyApp.Web.EmailView) -|> put_phoenix_template("welcome_email", user: user) +|> put_phoenix_template(:welcome_email, user: user) + + +# Setting the layout +%SendGrid.Email{} +|> put_phoenix_layout({MyApp.Web.EmailView, :layout}) +|> put_phoenix_view(MyApp.Web.EmailView) +|> put_phoenix_template(:welcome_email, user: user) ``` ### Using a Default Phoenix View You can set a default Phoenix View to use for rendering templates. Just set the `:phoenix_view` config value -```elxir +```elixir config :sendgrid, phoenix_view: MyApp.Web.EmailView ``` + +### Using a Default Layout + +You can set a default layout to render your view in. Set the `:phoenix_layout` config value. + +```elixir +config :sendgrid, + phoenix_layout: {MyApp.Web.EmailView, :layout} +``` \ No newline at end of file diff --git a/lib/sendgrid/email.ex b/lib/sendgrid/email.ex index 050f697..4eebd9d 100644 --- a/lib/sendgrid/email.ex +++ b/lib/sendgrid/email.ex @@ -38,7 +38,8 @@ defmodule SendGrid.Email do ## Phoenix Views You can use Phoenix Views to set your HTML and text content of your emails. You just have - to provide a view module and template name and you're good to go! See `put_phoenix_template/3` + to provide a view module and template name and you're good to go! Additionally, you can set + a layout to render the view in with `put_phoenix_layout/2`. See `put_phoenix_template/3` for complete usage. ### Examples @@ -54,18 +55,32 @@ defmodule SendGrid.Email do |> put_phoenix_template("welcome_email.txt", user: user) # Using both an HTML and text template - # Notice that there is no extension. %Email{} |> put_phoenix_view(MyApp.Web.EmailView) - |> put_phoenix_template("welcome_email", user: user) + |> put_phoenix_template(:welcome_email, user: user) + + # Setting the layout + %Email{} + |> put_phoenix_layout({MyApp.Web.EmailView, :layout}) + |> put_phoenix_view(MyApp.Web.EmailView) + |> put_phoenix_template(:welcome_email, user: user) + ### Using a Default Phoenix View You can set a default Phoenix View to use for rendering templates. Just set the `:phoenix_view` - config value + config value. + + config :sendgrid, + phoenix_view: MyApp.Web.EmailView + + + ### Using a Default View Layout + + You can set a default layout to render the view in. Just set the `:phoenix_layout` config value. config :sendgrid, - :phoenix_view: MyApp.Web.EmailView + phoenix_layout: {MyApp.Web.EmailView, :layout} """ @@ -83,7 +98,8 @@ defmodule SendGrid.Email do send_at: nil, headers: nil, attachments: nil, - __phoenix_view__: nil + __phoenix_view__: nil, + __phoenix_layout__: nil @type t :: %Email{to: nil | [recipient], @@ -99,7 +115,8 @@ defmodule SendGrid.Email do send_at: nil | integer, headers: nil | [header], attachments: nil | [attachment], - __phoenix_view__: nil | atom} + __phoenix_view__: nil | atom, + __phoenix_layout__: nil | %{optional(:text) => String.t, optional(:html) => String.t}} @type recipient :: %{email: String.t, name: String.t | nil} @type content :: %{type: String.t, value: String.t} @@ -393,6 +410,53 @@ defmodule SendGrid.Email do list ++ [address(email, name)] end + @doc """ + Sets the layout to use for the Phoenix Template. + + Expects a tuple of the view module and layout to use. If you provide an atom as the second element, + the text and HMTL versions of that template will be used for the respective content types. + + Alernatively, you can set a default layout to use by setting the `:phoenix_view` key in your config as + an atom which will be used for both text and HTML emails. + + config :sendgrid, + phoenix_layout: {MyApp.Web.EmailView, :layout} + + ## Examples + + put_phoenix_layout(email, {MyApp.Web.EmailView, "layout.html"}) + put_phoenix_layout(email, {MyApp.Web.EmailView, "layout.txt"}) + put_phoenix_layout(email, {MyApp.Web.EmailView, :layout}) + + """ + @spec put_phoenix_layout(t, {atom, atom}) :: t + def put_phoenix_layout(%Email{} = email, {module, layout}) when is_atom(module) and is_atom(layout) do + layouts = build_layouts({module, layout}) + %Email{email | __phoenix_layout__: layouts} + end + @spec put_phoenix_layout(t, {atom, String.t}) :: t + def put_phoenix_layout(%Email{__phoenix_layout__: layouts} = email, {module, layout}) when is_atom(module) do + layouts = layouts || %{} + updated_layout = build_layouts({module, layout}) + %Email{email | __phoenix_layout__: Map.merge(layouts, updated_layout)} + end + + # Build layout map + defp build_layouts({module, layout}) when is_atom(module) and is_atom(layout) do + base_name = Atom.to_string(layout) + %{ + text: {module, base_name <> ".txt"}, + html: {module, base_name <> ".html"} + } + end + defp build_layouts({module, layout} = args) when is_atom(module) do + case Path.extname(layout) do + ".html" -> %{html: args} + ".txt" -> %{text: args} + _ -> raise ArgumentError, "unsupported file type" + end + end + @doc """ Sets the Phoenix View to use. @@ -413,7 +477,8 @@ defmodule SendGrid.Email do Renders the Phoenix template with the given assigns. You can set the default Phoenix View to use for your templates by setting the `:phoenix_view` config value. - Additionally, you can set the view on a per email basis by calling `put_phoenix_view/2`. + Additionally, you can set the view on a per email basis by calling `put_phoenix_view/2`. Furthermore, you can have + the template rendered inside a layout. See `put_phoenix_layout/2` for more details. ## Explicit Template Extensions @@ -427,7 +492,7 @@ defmodule SendGrid.Email do You can omit a template's extension and attempt to have both a text template and HTML template rendered. To have both types rendered, both templates must share the same base file name. For example, if you have a template named `"some_template.txt"` and a template named `"some_template.html"` - and you call `put_phoenix_template(email, "some_template")`, both templates will be used and will + and you call `put_phoenix_template(email, :some_template)`, both templates will be used and will set the email content for both content types. The only caveat is *both files must exist*, otherwise you'll have an exception raised. @@ -439,33 +504,56 @@ defmodule SendGrid.Email do iex> put_phoenix_template(email, "some_template.txt", name: "John Doe") %Email{content: [%{type: "text/plain", value: ...}], ...} - iex> put_phoenix_template(email, "some_template", user: user) + iex> put_phoenix_template(email, :some_template, user: user) %Email{content: [%{type: "text/plain", value: ...}, %{type: "text/html", value: ...}], ...} """ + def put_phoenix_template(email, template_name, assigns \\ []) + @spec put_phoenix_template(t, atom, []) :: t + def put_phoenix_template(%Email{} = email, template_name, assigns) when is_atom(template_name) do + with true <- ensure_phoenix_loaded(), + view_mod <- phoenix_view_module(email), + layouts <- phoenix_layouts(email), + template_name <- Atom.to_string(template_name) do + email + |> render_html(view_mod, template_name <> ".html", layouts, assigns) + |> render_text(view_mod, template_name <> ".txt", layouts, assigns) + end + end @spec put_phoenix_template(t, String.t, []) :: t - def put_phoenix_template(%Email{} = email, template_name, assigns \\ []) do + def put_phoenix_template(%Email{} = email, template_name, assigns) do with true <- ensure_phoenix_loaded(), - view_mod <- phoenix_view_module(email) do + view_mod <- phoenix_view_module(email), + layouts <- phoenix_layouts(email) do case Path.extname(template_name) do ".html" -> - render_html(email, view_mod, template_name, assigns) + render_html(email, view_mod, template_name, layouts, assigns) ".txt" -> - render_text(email, view_mod, template_name, assigns) - _ -> - email - |> render_html(view_mod, template_name <> ".html", assigns) - |> render_text(view_mod, template_name <> ".txt", assigns) + render_text(email, view_mod, template_name, layouts, assigns) end end end - defp render_html(email, view_mod, template_name, assigns) do + defp render_html(email, view_mod, template_name, layouts, assigns) do + assigns = + if Map.has_key?(layouts, :html) do + Keyword.put(assigns, :layout, Map.get(layouts, :html)) + else + assigns + end + html = Phoenix.View.render_to_string(view_mod, template_name, assigns) put_html(email, html) end - defp render_text(email, view_mod, template_name, assigns) do + defp render_text(email, view_mod, template_name, layouts, assigns) do + assigns = + if Map.has_key?(layouts, :text) do + Keyword.put(assigns, :layout, Map.get(layouts, :text)) + else + assigns + end + text = Phoenix.View.render_to_string(view_mod, template_name, assigns) put_text(email, text) end @@ -478,6 +566,19 @@ defmodule SendGrid.Email do true end + defp phoenix_layouts(%Email{__phoenix_layout__: layouts}) do + layouts = layouts || %{} + case config(:phoenix_layout) do + nil -> layouts + {module, layout} when is_atom(module) and is_atom(layout) -> + configured_layouts = build_layouts({module, layout}) + Map.merge(configured_layouts, layouts) + _ -> + raise ArgumentError, "Invalid configuration set for :phoenix_layout. " <> + "Ensure the configuration is a tuple of a module and atom ({MyApp.View, :layout})." + end + end + defp phoenix_view_module(%Email{__phoenix_view__: nil}) do mod = config(:phoenix_view) unless mod do diff --git a/mix.exs b/mix.exs index 741531d..d400703 100644 --- a/mix.exs +++ b/mix.exs @@ -3,7 +3,7 @@ defmodule SendGrid.Mixfile do def project do [app: :sendgrid, - version: "1.5.0", + version: "1.6.0", elixir: "~> 1.4", package: package(), compilers: compilers(Mix.env), @@ -36,11 +36,11 @@ defmodule SendGrid.Mixfile do [ {:earmark, "~> 1.2", only: :dev}, {:ex_doc, "~> 0.16.2", only: :dev}, - {:httpoison, "~> 0.11.0"}, - {:poison, "~> 2.0"}, + {:httpoison, ">= 0.11.0"}, + {:poison, ">= 2.0.0 or >= 3.0.0"}, {:phoenix, "~> 1.2", only: :test}, {:phoenix_html, "~> 2.9", only: :test} - ] + ] end defp description do diff --git a/test/email_test.exs b/test/email_test.exs index 4000df8..cbc3ba1 100644 --- a/test/email_test.exs +++ b/test/email_test.exs @@ -213,6 +213,50 @@ defmodule SendGrid.Email.Test do use Phoenix.View, root: "test/support/templates", namespace: SendGrid.Email.Test end + + describe "put_phoenix_layout/2" do + test "works with html layouts" do + result = + Email.build() + |> Email.put_phoenix_layout({SendGrid.Email.Test.EmailView, "layout.html"}) + + assert result.__phoenix_layout__ == %{html: {SendGrid.Email.Test.EmailView, "layout.html"}} + end + + test "works with text layouts" do + result = + Email.build() + |> Email.put_phoenix_layout({SendGrid.Email.Test.EmailView, "layout.txt"}) + + assert result.__phoenix_layout__ == %{text: {SendGrid.Email.Test.EmailView, "layout.txt"}} + end + + test "works with setting both a text and an html layout" do + result = + Email.build() + |> Email.put_phoenix_layout({SendGrid.Email.Test.EmailView, "layout.txt"}) + |> Email.put_phoenix_layout({SendGrid.Email.Test.EmailView, "layout.html"}) + + expected = %{ + html: {SendGrid.Email.Test.EmailView, "layout.html"}, + text: {SendGrid.Email.Test.EmailView, "layout.txt"} + } + assert result.__phoenix_layout__ == expected + end + + test "works with an atom as a layout" do + result = + Email.build() + |> Email.put_phoenix_layout({SendGrid.Email.Test.EmailView, :layout}) + + expected = %{ + html: {SendGrid.Email.Test.EmailView, "layout.html"}, + text: {SendGrid.Email.Test.EmailView, "layout.txt"} + } + assert result.__phoenix_layout__ == expected + end + end + test "put_phoenix_view/2" do result = Email.build() @@ -241,14 +285,14 @@ defmodule SendGrid.Email.Test do test "renders templates with implicit extensions" do result = Email.build() - |> Email.put_phoenix_template("test", test: "awesome") + |> Email.put_phoenix_template(:test, test: "awesome") assert %Email{content: [%{type: "text/plain", value: "awesome"}, %{type: "text/html", value: "

awesome

"}]} = result end test "raises when a template doesn't exist for implicit extensions" do assert_raise Phoenix.Template.UndefinedError, fn -> - Email.put_phoenix_template(Email.build(), "test2") + Email.put_phoenix_template(Email.build(), :test2) end end @@ -258,5 +302,25 @@ defmodule SendGrid.Email.Test do |> Email.put_phoenix_template("test.txt", test: "awesome") assert %Email{content: [%{type: "text/plain", value: "awesome"}]} = result end + + test "renders in a text layout" do + result = + Email.build() + |> Email.put_phoenix_layout({SendGrid.Email.Test.EmailView, :layout}) + |> Email.put_phoenix_template("test.txt", test: "awesome") + + assert Enum.at(result.content, 0).value =~ "TEXT LAYOUT" + assert Enum.at(result.content, 0).value =~ "awesome" + end + + test "renders in an html layout" do + result = + Email.build() + |> Email.put_phoenix_layout({SendGrid.Email.Test.EmailView, :layout}) + |> Email.put_phoenix_template("test.html", test: "awesome") + + assert Enum.at(result.content, 0).value =~ "HTML LAYOUT" + assert Enum.at(result.content, 0).value =~ "awesome" + end end end diff --git a/test/support/templates/email/layout.html.eex b/test/support/templates/email/layout.html.eex new file mode 100644 index 0000000..fab2a07 --- /dev/null +++ b/test/support/templates/email/layout.html.eex @@ -0,0 +1,2 @@ +HTML LAYOUT +<%= render @view_module, @view_template, assigns %> \ No newline at end of file diff --git a/test/support/templates/email/layout.txt.eex b/test/support/templates/email/layout.txt.eex new file mode 100644 index 0000000..459f4a3 --- /dev/null +++ b/test/support/templates/email/layout.txt.eex @@ -0,0 +1,2 @@ +TEXT LAYOUT +<%= render @view_module, @view_template, assigns %> \ No newline at end of file