Current section
37 Versions
Jump to
Current section
37 Versions
Compare versions
28
files changed
+918
additions
-289
deletions
| @@ -1,13 +1,26 @@ | |
| 1 | - # Thesis |
| 1 | + # Thesis Content Editing System |
| 2 2 | |
| 3 | - Thesis is an Elixir/Phoenix hex package for quickly and easily adding content |
| 4 | - editing to any page. |
| 3 | + [](https://semaphoreci.com/ir/thesis-phoenix) |
| 5 4 | |
| 6 | - It's inspired by the [Rails gem](https://github.com/infinitered/thesis-rails) by |
| 7 | - the same name and author. |
| 5 | + Thesis is a lightweight and flexible Elixir/Phoenix hex package for quickly and easily |
| 6 | + adding content editing to any page on a Phoenix website. |
| 7 | + |
| 8 | + It's not quite a Phoenix CMS, so we call it a Phoenix CES -- content editing system. |
| 9 | + |
| 10 | + See also the Thesis [Rails gem](https://github.com/infinitered/thesis-rails). |
| 8 11 | |
| 9 12 |  |
| 10 13 | |
| 14 | + ## Thesis Features |
| 15 | + |
| 16 | + * Elixir/Phoenix hex package, uses React.js for its user interface |
| 17 | + * Lightweight, bolt-on, doesn't hijack your development workflow |
| 18 | + * On-page rich text editing |
| 19 | + * On-page plain text editing |
| 20 | + * Raw HTML editing for Youtube embeds or other flexible uses |
| 21 | + * Image URL editing, both `img` tag and `div` with background image |
| 22 | + * Page meta title and description editing |
| 23 | + * Easily bring your own authentication system in one tiny function |
| 11 24 | |
| 12 25 | ## Installation and Configuration |
| 13 26 | |
| @@ -17,7 +30,7 @@ _If you are having problems, view `README_INSTALL.md` for manual instructions._ | |
| 17 30 | |
| 18 31 | ```elixir |
| 19 32 | def deps do |
| 20 | - [{:thesis, "~> 0.0.16"}] |
| 33 | + [{:thesis, "~> 0.0.17"}] |
| 21 34 | end |
| 22 35 | |
| 23 36 | def application do |
| @@ -28,7 +41,7 @@ end | |
| 28 41 | #### 2. Run `mix thesis.install` |
| 29 42 | |
| 30 43 | This install script will add Thesis to your `config.exs` and `web.ex`, as well |
| 31 | - as generate a migration and an authorization module in `lib/thesis_auth.ex`. |
| 44 | + as generate migrations and an authorization module in your `lib/thesis_auth.ex`. |
| 32 45 | |
| 33 46 | #### 3. Add the Thesis editor to your layout |
| 34 47 | |
| @@ -46,15 +59,15 @@ $ mix ecto.migrate | |
| 46 59 | ## Making Pages Editable |
| 47 60 | |
| 48 61 | Use the `Thesis.View.content/4` view helper function to make a content area |
| 49 | - editable. If you added `use Thesis.View` in your `web.ex` file, this function |
| 62 | + editable. If you have `use Thesis.View` in your `web.ex` file, this function |
| 50 63 | is already available on all of your views. |
| 51 64 | |
| 52 65 | Thesis will add a wrapper `<div>` around editable HTML and plain-text content |
| 53 | - areas. |
| 66 | + areas, both in read mode and edit mode, so plan your CSS accordingly. |
| 54 67 | |
| 55 68 | ### Rich Text Areas |
| 56 69 | |
| 57 | - Simply wrap your HTML in a `content` function call. |
| 70 | + Simply wrap your HTML in a `content` function call, specifying `html` as the content type. |
| 58 71 | |
| 59 72 | ```eex |
| 60 73 | <h1>Title</h1> |
| @@ -88,12 +101,59 @@ becomes... | |
| 88 101 | <h1><%= content(@conn, "Title identifier", :text, do: "My Title") %></h1> |
| 89 102 | ``` |
| 90 103 | |
| 104 | + ### Custom HTML Areas |
| 105 | + |
| 106 | + For video embeds, iframes, and any other custom HTML, use the `:raw_html` content type: |
| 107 | + |
| 108 | + ```eex |
| 109 | + <iframe width="560" height="315" src="https://www.youtube.com/embed/5SVLs_NN_uY" frameborder="0" allowfullscreen></iframe> |
| 110 | + ``` |
| 111 | + |
| 112 | + becomes... |
| 113 | + |
| 114 | + ```eex |
| 115 | + <%= content(@conn, "Section identifier", :raw_html) do %> |
| 116 | + <iframe width="560" height="315" src="https://www.youtube.com/embed/5SVLs_NN_uY" frameborder="0" allowfullscreen></iframe> |
| 117 | + <% end %> |
| 118 | + ``` |
| 119 | + |
| 120 | + ### Images (by URL) |
| 121 | + |
| 122 | + You can have the user specify an image URL and display the image with the `image` content type. |
| 123 | + |
| 124 | + ```eex |
| 125 | + <img src="http://placekitten.com/200/300"> |
| 126 | + ``` |
| 127 | + |
| 128 | + becomes... |
| 129 | + |
| 130 | + ```eex |
| 131 | + <%= content(@conn, "Image identifier", :image, alt: "My alt tag", do: "http://placekitten.com/200/300") |
| 132 | + %> |
| 133 | + ``` |
| 134 | + |
| 135 | + If you prefer to use a `div` with a background image, you can use the `background_image` |
| 136 | + content type. |
| 137 | + |
| 138 | + ```eex |
| 139 | + <div style="background-image: url(http://placekitten.com/200/300)"></div> |
| 140 | + ``` |
| 141 | + |
| 142 | + becomes... |
| 143 | + |
| 144 | + ```eex |
| 145 | + <%= content(@conn, "Image identifier", :background_image, do: "http://placekitten.com/200/300") |
| 146 | + %> |
| 147 | + ``` |
| 148 | + |
| 149 | + _Note: Image uploads are coming soon._ |
| 150 | + |
| 91 151 | ### Global Content Areas |
| 92 152 | |
| 93 153 | Content areas in Thesis are page-specific. However, if you want an editable |
| 94 | - region that can be displayed on multiple pages, use the |
| 95 | - `Thesis.View.global_content/4` function. Internally, the page_id will be set |
| 96 | - to `nil` to indicate it applies globally, rather than to a specific page. |
| 154 | + area that can be displayed on multiple pages, use the |
| 155 | + `Thesis.View.global_content/4` function. Any page using that content area identifier |
| 156 | + will display the edited content across the whole website. |
| 97 157 | |
| 98 158 | ```eex |
| 99 159 | <%= global_content(@conn, "Footer Text", :html) do %> |
| @@ -105,23 +165,36 @@ to `nil` to indicate it applies globally, rather than to a specific page. | |
| 105 165 | <% end %> |
| 106 166 | ``` |
| 107 167 | |
| 108 | - ### Meta Title and Description |
| 168 | + ### Customizing the Thesis markup |
| 109 169 | |
| 110 | - In your layout, you can output the current title and description like so: |
| 170 | + Thesis adds an additional `<div>` around your editable content areas. We suggest that |
| 171 | + you not style these divs heavily, since Thesis uses them as editors and adds its own styles |
| 172 | + in edit-mode. However, sometimes, you need to modify that markup slightly for better presentation. |
| 173 | + You can provide an ID and additional classes by specifying `id` and `classes`, respectively. |
| 174 | + |
| 175 | + ```eex |
| 176 | + <%= content(@conn, "Ident", :html, id: "my-id", classes: "more classes") do %> |
| 177 | + <h1>Title</h1> |
| 178 | + <% end %> |
| 179 | + ``` |
| 180 | + |
| 181 | + ### Page Meta Title and Description |
| 182 | + |
| 183 | + Thesis provides a settings tray to edit each page's title and description. In your |
| 184 | + layout, you can output the current title and description like so: |
| 111 185 | |
| 112 186 | ```eex |
| 113 187 | <title><%= page_title(@conn, "Default Title") %></title> |
| 114 188 | <meta name="description" content="<%= page_description(@conn, "Default Description") %>" /> |
| 115 189 | ``` |
| 116 190 | |
| 117 | - Some prefer to set the page title and description in their controller actions: |
| 191 | + Some prefer to set the page title and description as assigns in their controller actions: |
| 118 192 | |
| 119 193 | ```eex |
| 120 194 | def about(conn, params) do |
| 121 195 | @title = Thesis.View.page_title(conn, "About My Company") |
| 122 196 | @description = Thesis.View.page_description(conn, "A relevant description here.") |
| 123 197 | end |
| 124 | - |
| 125 198 | ``` |
| 126 199 | |
| 127 200 | ## Authorization |
| @@ -138,6 +211,8 @@ Here's an example which we use on our own website, [https://infinite.red](https: | |
| 138 211 | |
| 139 212 | ```elixir |
| 140 213 | defmodule IrWebsite.ThesisAuth do |
| 214 | + @behaviour Thesis.Auth |
| 215 | + |
| 141 216 | def page_is_editable?(conn) do |
| 142 217 | IrWebsite.AuthController.logged_in?(conn) |
| 143 218 | end |
| @@ -161,15 +236,32 @@ So, in this case, we're simply checking to see if the user has been logged in | |
| 161 236 | or not. Since only Infinite Red employees have logins, it's safe for us to |
| 162 237 | assume that if they're logged in, they have permission to edit the page. |
| 163 238 | |
| 239 | + If you use [Guardian](https://github.com/ueberauth/guardian) or something similar, |
| 240 | + you may need additional manipulations to your `conn` to properly authenticate the |
| 241 | + user. Add those to your auth module like this: |
| 242 | + |
| 243 | + ```eex |
| 244 | + defmodule MyApp.ThesisAuth do |
| 245 | + @moduledoc """ |
| 246 | + Contains functions for handling Thesis authorization. |
| 247 | + """ |
| 248 | + |
| 249 | + def page_is_editable?(conn) do |
| 250 | + conn |
| 251 | + |> Guardian.Plug.VerifySession.call(%{}) |
| 252 | + |> Guardian.Plug.LoadResource.call(%{}) |
| 253 | + |> MyApp.SessionController.logged_in_and_admin? |
| 254 | + end |
| 255 | + end |
| 256 | + ``` |
| 257 | + |
| 164 258 | ## What Thesis Isn't |
| 165 259 | |
| 166 260 | You can't have it all. Thesis isn't the same as other -bloated- full-function |
| 167 261 | content management systems out there. This is a list of what it's not now and |
| 168 262 | not likely to be in the future. |
| 169 263 | |
| 170 | - _We reserve the right to change our mind, however. :-)_ |
| 171 | - |
| 172 | - * Not a WordPress Replacement |
| 264 | + * Not a complete WordPress Replacement |
| 173 265 | * Not a full featured CMS |
| 174 266 | * Not a full featured WYSIWYG editor |
| 175 267 | * Not an authentication or permission system |
| @@ -185,7 +277,7 @@ websites. Please help us improve! | |
| 185 277 | 3. Commit your changes (`git commit -am 'Add some feature'`) |
| 186 278 | 4. Run `brunch watch` during development |
| 187 279 | 5. Write tests for your new feature |
| 188 | - 6. Run `rake spec` in the root directory to ensure that all tests pass. |
| 280 | + 6. Run `mix test` in the root directory to ensure that all tests pass. |
| 189 281 | 7. Push to the branch (`git push origin my-new-feature`) |
| 190 282 | 8. Create new Pull Request |
| 191 283 | |
| @@ -194,6 +286,7 @@ websites. Please help us improve! | |
| 194 286 | * Jamon Holmgren [@jamonholmgren](https://twitter.com/jamonholmgren) |
| 195 287 | * Ken Miller [@seriousken](https://github.com/kemiller) |
| 196 288 | * Daniel Berkompas [@dberkom](https://twitter.com/dberkom) |
| 289 | + * Yulian Glukhenko [@yulianglukhenko](https://github.com/yulianglukhenko) |
| 197 290 | |
| 198 291 | Also supported by others on the [Infinite Red](https://infinite.red) team. |
| @@ -6,7 +6,7 @@ For automatic setup, see `README.md`. | |
| 6 6 | |
| 7 7 | ```elixir |
| 8 8 | def deps do |
| 9 | - [{:thesis, "~> 0.0.16"}] |
| 9 | + [{:thesis, "~> 0.0.17"}] |
| 10 10 | end |
| 11 11 | |
| 12 12 | def application do |
| @@ -21,7 +21,7 @@ Run `mix deps.get`. | |
| 21 21 | ```elixir |
| 22 22 | config :thesis, |
| 23 23 | store: Thesis.EctoStore, |
| 24 | - authorization: IrWebsite.ThesisAuth |
| 24 | + authorization: <MyApp>.ThesisAuth |
| 25 25 | config :thesis, Thesis.EctoStore, repo: <MyApp>.Repo |
| 26 26 | ``` |
| 27 27 | |
| @@ -29,6 +29,8 @@ config :thesis, Thesis.EctoStore, repo: <MyApp>.Repo | |
| 29 29 | |
| 30 30 | ```elixir |
| 31 31 | defmodule <MyApp>.ThesisAuth do |
| 32 | + @behaviour Thesis.Auth |
| 33 | + |
| 32 34 | def page_is_editable?(conn) do |
| 33 35 | true # editable by the world |
| 34 36 | end |
| @@ -55,6 +57,7 @@ defmodule <MyApp>.Repo.Migrations.CreateThesisTables do | |
| 55 57 | add :name, :string, nil: false |
| 56 58 | add :content, :text, default: "Edit this content area" |
| 57 59 | add :content_type, :string, default: "html" |
| 60 | + add :meta, :text |
| 58 61 | |
| 59 62 | timestamps |
| 60 63 | end |
| @@ -75,6 +78,35 @@ end | |
| 75 78 | $ mix ecto.migrate |
| 76 79 | ``` |
| 77 80 | |
| 78 | - #### 7. Done! |
| 81 | + #### 7. Add Thesis.Controller, Thesis.View, and Thesis.Router to your `web/web.ex` file |
| 82 | + |
| 83 | + ```elixir |
| 84 | + def controller do |
| 85 | + quote do |
| 86 | + use Phoenix.Controller |
| 87 | + use Thesis.Controller |
| 88 | + |
| 89 | + # ... |
| 90 | + end |
| 91 | + end |
| 92 | + |
| 93 | + def view do |
| 94 | + quote do |
| 95 | + use Phoenix.View, root: "web/templates" |
| 96 | + use Thesis.View |
| 97 | + |
| 98 | + # ... |
| 99 | + end |
| 100 | + end |
| 101 | + |
| 102 | + def router do |
| 103 | + quote do |
| 104 | + use Phoenix.Router |
| 105 | + use Thesis.Router |
| 106 | + end |
| 107 | + end |
| 108 | + ``` |
| 109 | + |
| 110 | + #### 8. Done! |
| 79 111 | |
| 80 112 | Now, start adding content areas. See the `README.md` for further instructions. |
| @@ -6,12 +6,14 @@ | |
| 6 6 | {<<"files">>, |
| 7 7 | [<<"lib/mix/tasks/thesis.install.ex">>,<<"lib/mix/utils.ex">>, |
| 8 8 | <<"lib/thesis.ex">>,<<"lib/thesis/api_controller.ex">>, |
| 9 | - <<"lib/thesis/config.ex">>,<<"lib/thesis/controller.ex">>, |
| 10 | - <<"lib/thesis/models/page.ex">>,<<"lib/thesis/models/page_content.ex">>, |
| 9 | + <<"lib/thesis/auth.ex">>,<<"lib/thesis/config.ex">>, |
| 10 | + <<"lib/thesis/controller.ex">>,<<"lib/thesis/models/page.ex">>, |
| 11 | + <<"lib/thesis/models/page_content.ex">>,<<"lib/thesis/render.ex">>, |
| 11 12 | <<"lib/thesis/router.ex">>,<<"lib/thesis/store.ex">>, |
| 12 13 | <<"lib/thesis/stores/ecto_store.ex">>,<<"lib/thesis/view.ex">>, |
| 13 14 | <<"priv/static/thesis-editor.js">>,<<"priv/static/thesis.css">>, |
| 14 | - <<"priv/templates/thesis.install/migration.exs">>, |
| 15 | + <<"priv/templates/thesis.install/add_meta_to_thesis_page_contents.exs">>, |
| 16 | + <<"priv/templates/thesis.install/create_thesis_tables.exs">>, |
| 15 17 | <<"priv/templates/thesis.install/thesis_auth.exs">>, |
| 16 18 | <<"web/static/css/_extends/_extends.sass">>, |
| 17 19 | <<"web/static/css/_extends/_forms.sass">>, |
| @@ -112,15 +114,19 @@ | |
| 112 114 | <<"web/static/js/components/cancel_button.js">>, |
| 113 115 | <<"web/static/js/components/delete_button.js">>, |
| 114 116 | <<"web/static/js/components/edit_button.js">>, |
| 117 | + <<"web/static/js/components/image_tray.js">>, |
| 118 | + <<"web/static/js/components/raw_html_tray.js">>, |
| 115 119 | <<"web/static/js/components/save_button.js">>, |
| 116 120 | <<"web/static/js/components/settings_button.js">>, |
| 117 121 | <<"web/static/js/components/settings_tray.js">>, |
| 122 | + <<"web/static/js/content_types/html.js">>, |
| 123 | + <<"web/static/js/content_types/raw_html.js">>, |
| 118 124 | <<"web/static/js/thesis-editor.js">>,<<"web/static/js/utilities/net.js">>, |
| 119 125 | <<"mix.exs">>,<<"README.md">>,<<"README_INSTALL.md">>,<<"LICENSE.md">>, |
| 120 126 | <<"package.json">>]}. |
| 121 127 | {<<"licenses">>,[<<"MIT">>]}. |
| 122 128 | {<<"links">>, |
| 123 | - [{<<"Docs">>,<<"https://hexdocs.pm/thesis/0.0.16/api-reference.html">>}, |
| 129 | + [{<<"Docs">>,<<"https://hexdocs.pm/thesis/0.0.17/api-reference.html">>}, |
| 124 130 | {<<"GitHub">>,<<"https://github.com/infinite_red/thesis">>}]}. |
| 125 131 | {<<"maintainers">>, |
| 126 132 | [<<"Jamon Holmgren">>,<<"Ken Miller">>,<<"Daniel Berkompas">>]}. |
| @@ -129,21 +135,25 @@ | |
| 129 135 | [[{<<"app">>,<<"phoenix">>}, |
| 130 136 | {<<"name">>,<<"phoenix">>}, |
| 131 137 | {<<"optional">>,false}, |
| 132 | - {<<"requirement">>,<<">= 0.0.0">>}], |
| 138 | + {<<"requirement">>,<<">= 1.0.0">>}], |
| 133 139 | [{<<"app">>,<<"phoenix_html">>}, |
| 134 140 | {<<"name">>,<<"phoenix_html">>}, |
| 135 141 | {<<"optional">>,false}, |
| 136 | - {<<"requirement">>,<<">= 0.0.0">>}], |
| 142 | + {<<"requirement">>,<<">= 2.0.0">>}], |
| 137 143 | [{<<"app">>,<<"ecto">>}, |
| 138 144 | {<<"name">>,<<"ecto">>}, |
| 139 145 | {<<"optional">>,false}, |
| 140 | - {<<"requirement">>,<<">= 0.0.0">>}], |
| 146 | + {<<"requirement">>,<<">= 2.0.0">>}], |
| 141 147 | [{<<"app">>,<<"plug">>}, |
| 142 148 | {<<"name">>,<<"plug">>}, |
| 143 149 | {<<"optional">>,false}, |
| 144 | - {<<"requirement">>,<<"~> 1.0">>}], |
| 150 | + {<<"requirement">>,<<">= 1.0.0">>}], |
| 151 | + [{<<"app">>,<<"poison">>}, |
| 152 | + {<<"name">>,<<"poison">>}, |
| 153 | + {<<"optional">>,false}, |
| 154 | + {<<"requirement">>,<<">= 1.0.0">>}], |
| 145 155 | [{<<"app">>,<<"html_sanitize_ex">>}, |
| 146 156 | {<<"name">>,<<"html_sanitize_ex">>}, |
| 147 157 | {<<"optional">>,false}, |
| 148 | - {<<"requirement">>,<<"~> 1.0.0">>}]]}. |
| 149 | - {<<"version">>,<<"0.0.16">>}. |
| 158 | + {<<"requirement">>,<<">= 1.0.1">>}]]}. |
| 159 | + {<<"version">>,<<"0.0.17">>}. |
| @@ -21,17 +21,21 @@ defmodule Mix.Tasks.Thesis.Install do | |
| 21 21 | thesis_templates |
| 22 22 | thesis_config |
| 23 23 | thesis_web |
| 24 | + |
| 25 | + status_msg("done", |
| 26 | + "Now run #{IO.ANSI.blue}mix ecto.migrate#{IO.ANSI.reset} to ensure your database is up to date.") |
| 24 27 | end |
| 25 28 | |
| 26 29 | @doc false |
| 27 30 | def thesis_templates do |
| 28 | - template_files = [ {"priv/templates/thesis.install/thesis_auth.exs", "lib/thesis_auth.ex" } ] |
| 29 | - migration_exists = File.ls!("priv/repo/migrations") |> Enum.map(fn (f) -> String.contains?(f, "create_thesis_tables") end) |> Enum.any? |
| 30 | - unless migration_exists do |
| 31 | - template_files = [{"priv/templates/thesis.install/migration.exs", "priv/repo/migrations/#{timestamp}_create_thesis_tables.exs"} | template_files] |
| 32 | - end |
| 31 | + migrations = ["create_thesis_tables", "add_meta_to_thesis_page_contents"] |
| 32 | + migration_files = Enum.filter_map(migrations, &migration_missing?/1, fn (filename) -> |
| 33 | + {"priv/templates/thesis.install/#{filename}.exs", "priv/repo/migrations/#{timestamp}_#{filename}.exs"} |
| 34 | + end) |
| 33 35 | |
| 34 | - template_files |
| 36 | + template_files = [ {"priv/templates/thesis.install/thesis_auth.exs", "lib/thesis_auth.ex" } ] |
| 37 | + |
| 38 | + template_files ++ migration_files |
| 35 39 | |> Stream.map(&render_eex/1) |
| 36 40 | |> Stream.map(©_to_target/1) |
| 37 41 | |> Stream.run |
| @@ -93,4 +97,9 @@ defmodule Mix.Tasks.Thesis.Install do | |
| 93 97 | end |
| 94 98 | end |
| 95 99 | |
| 100 | + defp migration_missing?(filename) do |
| 101 | + "priv/repo/migrations" |
| 102 | + |> File.ls! |
| 103 | + |> Enum.all?(fn (f) -> !String.contains?(f, filename) end) |
| 104 | + end |
| 96 105 | end |
| @@ -1,14 +1,6 @@ | |
| 1 1 | defmodule Mix.Thesis.Utils do |
| 2 2 | @moduledoc false |
| 3 3 | |
| 4 | - @doc false |
| 5 | - def get_package_path do |
| 6 | - __ENV__.file |
| 7 | - |> Path.dirname |
| 8 | - |> String.split("/lib/mix") |
| 9 | - |> hd |
| 10 | - end |
| 11 | - |
| 12 4 | @doc false |
| 13 5 | def get_module do |
| 14 6 | Mix.Project.get |
| @@ -19,7 +11,7 @@ defmodule Mix.Thesis.Utils do | |
| 19 11 | |
| 20 12 | @doc false |
| 21 13 | def status_msg(status, message), |
| 22 | - do: IO.puts "#{IO.ANSI.green}* #{status}#{IO.ANSI.reset} #{message}" |
| 14 | + do: IO.puts "#{IO.ANSI.green}* #{String.rjust(status, 10)}#{IO.ANSI.reset} #{message}" |
| 23 15 | |
| 24 16 | @doc false |
| 25 17 | def debug(message), do: IO.puts "==> #{message}" |
| @@ -49,9 +41,10 @@ defmodule Mix.Thesis.Utils do | |
| 49 41 | @doc false |
| 50 42 | def copy_to_target({contents, target}) do |
| 51 43 | if File.exists?(target) do |
| 52 | - status_msg("exists", "#{target}") |
| 44 | + status_msg("exists", target) |
| 53 45 | else |
| 54 46 | File.write!(target, contents) |
| 47 | + status_msg("creating", target) |
| 55 48 | end |
| 56 49 | end |
Loading more files…