├── README.md ├── book-cover.jpg ├── hello ├── .gitignore ├── README.md ├── brunch-config.js ├── config │ ├── config.exs │ ├── dev.exs │ ├── prod.exs │ └── test.exs ├── lib │ ├── hello.ex │ └── hello │ │ ├── endpoint.ex │ │ └── repo.ex ├── mix.exs ├── mix.lock ├── package.json ├── priv │ ├── gettext │ │ ├── en │ │ │ └── LC_MESSAGES │ │ │ │ └── errors.po │ │ └── errors.pot │ └── repo │ │ └── seeds.exs ├── test │ ├── controllers │ │ └── page_controller_test.exs │ ├── support │ │ ├── channel_case.ex │ │ ├── conn_case.ex │ │ └── model_case.ex │ ├── test_helper.exs │ └── views │ │ ├── error_view_test.exs │ │ ├── layout_view_test.exs │ │ └── page_view_test.exs └── web │ ├── channels │ └── user_socket.ex │ ├── controllers │ ├── hello_controller.ex │ └── page_controller.ex │ ├── gettext.ex │ ├── router.ex │ ├── static │ ├── assets │ │ ├── favicon.ico │ │ ├── images │ │ │ └── phoenix.png │ │ └── robots.txt │ ├── css │ │ └── app.css │ └── js │ │ ├── app.js │ │ └── socket.js │ ├── templates │ ├── hello │ │ └── world.html.eex │ ├── layout │ │ └── app.html.eex │ └── page │ │ └── index.html.eex │ ├── views │ ├── error_helpers.ex │ ├── error_view.ex │ ├── hello_view.ex │ ├── layout_view.ex │ └── page_view.ex │ └── web.ex └── rumbl ├── .gitignore ├── Elixir.Rumbl.User.beam ├── README.md ├── brunch-config.js ├── config ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── lib ├── rumbl.ex └── rumbl │ ├── endpoint.ex │ └── repo.ex ├── mix.exs ├── mix.lock ├── package.json ├── priv ├── gettext │ ├── en │ │ └── LC_MESSAGES │ │ │ └── errors.po │ └── errors.pot └── repo │ ├── migrations │ ├── 20160301002445_create_extension_uuid.exs │ ├── 20160301003531_create_user.exs │ ├── 20160303194447_create_video.exs │ ├── 20160304020448_create_category.exs │ ├── 20160304020700_add_category_id_to_video.exs │ └── 20160304235733_category_on_delete_nilify_all.exs │ └── seeds.exs ├── test ├── controllers │ ├── auth_test.exs │ ├── page_controller_test.exs │ └── video_controller_test.exs ├── models │ ├── category_repo_test.exs │ ├── category_test.exs │ ├── user_repo_test.exs │ ├── user_test.exs │ └── video_test.exs ├── support │ ├── channel_case.ex │ ├── conn_case.ex │ ├── model_case.ex │ └── test_helpers.ex ├── test_helper.exs └── views │ ├── error_view_test.exs │ ├── layout_view_test.exs │ ├── page_view_test.exs │ └── video_view_test.exs └── web ├── channels └── user_socket.ex ├── controllers ├── auth.ex ├── page_controller.ex ├── session_controller.ex ├── user_controller.ex ├── video_controller.ex └── watch_controller.ex ├── gettext.ex ├── graphql └── schema.ex ├── models ├── category.ex ├── user.ex └── video.ex ├── router.ex ├── static ├── assets │ ├── favicon.ico │ ├── images │ │ └── phoenix.png │ └── robots.txt ├── css │ └── app.css └── js │ ├── app.js │ └── socket.js ├── templates ├── layout │ └── app.html.eex ├── page │ └── index.html.eex ├── session │ └── new.html.eex ├── user │ ├── index.html.eex │ ├── new.html.eex │ ├── show.html.eex │ └── user.html.eex ├── video │ ├── edit.html.eex │ ├── form.html.eex │ ├── index.html.eex │ ├── new.html.eex │ └── show.html.eex └── watch │ └── show.html.eex ├── views ├── error_helpers.ex ├── error_view.ex ├── layout_view.ex ├── page_view.ex ├── session_view.ex ├── user_view.ex ├── video_view.ex └── watch_view.ex └── web.ex /README.md: -------------------------------------------------------------------------------- 1 | ## Programming Phoenix 2 | 3 | [![Book cover](book-cover.jpg)](https://pragprog.com/book/phoenix/programming-phoenix) 4 | 5 | Personal code while reading [Programming Elixir](https://pragprog.com/book/phoenix/programming-phoenix). ᕕ(¬ ͜ ¬)ᕗ 6 | 7 | --- 8 | [Sean Omlor](http://seanomlor.com) 9 | -------------------------------------------------------------------------------- /book-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauntedhost/programming-phoenix/37bf009a2649a245a6027ea7bcbb92d8e36169de/book-cover.jpg -------------------------------------------------------------------------------- /hello/.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | 7 | # Generate on crash by the VM 8 | erl_crash.dump 9 | 10 | # Static artifacts 11 | /node_modules 12 | 13 | # Since we are building assets from web/static, 14 | # we ignore priv/static. You may want to comment 15 | # this depending on your deployment strategy. 16 | /priv/static/ 17 | 18 | # The config/prod.secret.exs file by default contains sensitive 19 | # data and you should not commit it into version control. 20 | # 21 | # Alternatively, you may comment the line below and commit the 22 | # secrets file as long as you replace its contents by environment 23 | # variables. 24 | /config/prod.secret.exs 25 | -------------------------------------------------------------------------------- /hello/README.md: -------------------------------------------------------------------------------- 1 | # Hello 2 | 3 | To start your Phoenix app: 4 | 5 | * Install dependencies with `mix deps.get` 6 | * Create and migrate your database with `mix ecto.create && mix ecto.migrate` 7 | * Install Node.js dependencies with `npm install` 8 | * Start Phoenix endpoint with `mix phoenix.server` 9 | 10 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 11 | 12 | Ready to run in production? Please [check our deployment guides](http://www.phoenixframework.org/docs/deployment). 13 | 14 | ## Learn more 15 | 16 | * Official website: http://www.phoenixframework.org/ 17 | * Guides: http://phoenixframework.org/docs/overview 18 | * Docs: http://hexdocs.pm/phoenix 19 | * Mailing list: http://groups.google.com/group/phoenix-talk 20 | * Source: https://github.com/phoenixframework/phoenix 21 | -------------------------------------------------------------------------------- /hello/brunch-config.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | // See http://brunch.io/#documentation for docs. 3 | files: { 4 | javascripts: { 5 | joinTo: "js/app.js" 6 | 7 | // To use a separate vendor.js bundle, specify two files path 8 | // https://github.com/brunch/brunch/blob/stable/docs/config.md#files 9 | // joinTo: { 10 | // "js/app.js": /^(web\/static\/js)/, 11 | // "js/vendor.js": /^(web\/static\/vendor)|(deps)/ 12 | // } 13 | // 14 | // To change the order of concatenation of files, explicitly mention here 15 | // https://github.com/brunch/brunch/tree/master/docs#concatenation 16 | // order: { 17 | // before: [ 18 | // "web/static/vendor/js/jquery-2.1.1.js", 19 | // "web/static/vendor/js/bootstrap.min.js" 20 | // ] 21 | // } 22 | }, 23 | stylesheets: { 24 | joinTo: "css/app.css" 25 | }, 26 | templates: { 27 | joinTo: "js/app.js" 28 | } 29 | }, 30 | 31 | conventions: { 32 | // This option sets where we should place non-css and non-js assets in. 33 | // By default, we set this to "/web/static/assets". Files in this directory 34 | // will be copied to `paths.public`, which is "priv/static" by default. 35 | assets: /^(web\/static\/assets)/ 36 | }, 37 | 38 | // Phoenix paths configuration 39 | paths: { 40 | // Dependencies and current project directories to watch 41 | watched: [ 42 | "web/static", 43 | "test/static" 44 | ], 45 | 46 | // Where to compile files to 47 | public: "priv/static" 48 | }, 49 | 50 | // Configure your plugins 51 | plugins: { 52 | babel: { 53 | // Do not use ES6 compiler in vendor code 54 | ignore: [/web\/static\/vendor/] 55 | } 56 | }, 57 | 58 | modules: { 59 | autoRequire: { 60 | "js/app.js": ["web/static/js/app"] 61 | } 62 | }, 63 | 64 | npm: { 65 | enabled: true, 66 | // Whitelist the npm deps to be pulled in as front-end assets. 67 | // All other deps in package.json will be excluded from the bundle. 68 | whitelist: ["phoenix", "phoenix_html"] 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /hello/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | # Configures the endpoint 9 | config :hello, Hello.Endpoint, 10 | url: [host: "localhost"], 11 | root: Path.dirname(__DIR__), 12 | secret_key_base: "QXShG76XooOJixl70BQH0IlYG4aVhxYuLoYqBEqEg9U9N/htL/exOSWQchrhNc+U", 13 | render_errors: [accepts: ~w(html json)], 14 | pubsub: [name: Hello.PubSub, 15 | adapter: Phoenix.PubSub.PG2] 16 | 17 | # Configures Elixir's Logger 18 | config :logger, :console, 19 | format: "$time $metadata[$level] $message\n", 20 | metadata: [:request_id] 21 | 22 | # Import environment specific config. This must remain at the bottom 23 | # of this file so it overrides the configuration defined above. 24 | import_config "#{Mix.env}.exs" 25 | 26 | # Configure phoenix generators 27 | config :phoenix, :generators, 28 | migration: true, 29 | binary_id: false 30 | -------------------------------------------------------------------------------- /hello/config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with brunch.io to recompile .js and .css sources. 9 | config :hello, Hello.Endpoint, 10 | http: [port: 4000], 11 | debug_errors: true, 12 | code_reloader: true, 13 | check_origin: false, 14 | watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin"]] 15 | 16 | # Watch static and templates for browser reloading. 17 | config :hello, Hello.Endpoint, 18 | live_reload: [ 19 | patterns: [ 20 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, 21 | ~r{priv/gettext/.*(po)$}, 22 | ~r{web/views/.*(ex)$}, 23 | ~r{web/templates/.*(eex)$} 24 | ] 25 | ] 26 | 27 | # Do not include metadata nor timestamps in development logs 28 | config :logger, :console, format: "[$level] $message\n" 29 | 30 | # Set a higher stacktrace during development. 31 | # Do not configure such in production as keeping 32 | # and calculating stacktraces is usually expensive. 33 | config :phoenix, :stacktrace_depth, 20 34 | 35 | # Configure your database 36 | config :hello, Hello.Repo, 37 | adapter: Ecto.Adapters.Postgres, 38 | username: "postgres", 39 | password: "postgres", 40 | database: "hello_dev", 41 | hostname: "localhost", 42 | pool_size: 10 43 | -------------------------------------------------------------------------------- /hello/config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, we configure the host to read the PORT 4 | # from the system environment. Therefore, you will need 5 | # to set PORT=80 before running your server. 6 | # 7 | # You should also configure the url host to something 8 | # meaningful, we use this information when generating URLs. 9 | # 10 | # Finally, we also include the path to a manifest 11 | # containing the digested version of static files. This 12 | # manifest is generated by the mix phoenix.digest task 13 | # which you typically run after static files are built. 14 | config :hello, Hello.Endpoint, 15 | http: [port: {:system, "PORT"}], 16 | url: [host: "example.com", port: 80], 17 | cache_static_manifest: "priv/static/manifest.json" 18 | 19 | # Do not print debug messages in production 20 | config :logger, level: :info 21 | 22 | # ## SSL Support 23 | # 24 | # To get SSL working, you will need to add the `https` key 25 | # to the previous section and set your `:url` port to 443: 26 | # 27 | # config :hello, Hello.Endpoint, 28 | # ... 29 | # url: [host: "example.com", port: 443], 30 | # https: [port: 443, 31 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 32 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")] 33 | # 34 | # Where those two env variables return an absolute path to 35 | # the key and cert in disk or a relative path inside priv, 36 | # for example "priv/ssl/server.key". 37 | # 38 | # We also recommend setting `force_ssl`, ensuring no data is 39 | # ever sent via http, always redirecting to https: 40 | # 41 | # config :hello, Hello.Endpoint, 42 | # force_ssl: [hsts: true] 43 | # 44 | # Check `Plug.SSL` for all available options in `force_ssl`. 45 | 46 | # ## Using releases 47 | # 48 | # If you are doing OTP releases, you need to instruct Phoenix 49 | # to start the server for all endpoints: 50 | # 51 | # config :phoenix, :serve_endpoints, true 52 | # 53 | # Alternatively, you can configure exactly which server to 54 | # start per endpoint: 55 | # 56 | # config :hello, Hello.Endpoint, server: true 57 | # 58 | # You will also need to set the application root to `.` in order 59 | # for the new static assets to be served after a hot upgrade: 60 | # 61 | # config :hello, Hello.Endpoint, root: "." 62 | 63 | # Finally import the config/prod.secret.exs 64 | # which should be versioned separately. 65 | import_config "prod.secret.exs" 66 | -------------------------------------------------------------------------------- /hello/config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :hello, Hello.Endpoint, 6 | http: [port: 4001], 7 | server: false 8 | 9 | # Print only warnings and errors during test 10 | config :logger, level: :warn 11 | 12 | # Configure your database 13 | config :hello, Hello.Repo, 14 | adapter: Ecto.Adapters.Postgres, 15 | username: "postgres", 16 | password: "postgres", 17 | database: "hello_test", 18 | hostname: "localhost", 19 | pool: Ecto.Adapters.SQL.Sandbox 20 | -------------------------------------------------------------------------------- /hello/lib/hello.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec, warn: false 8 | 9 | children = [ 10 | # Start the endpoint when the application starts 11 | supervisor(Hello.Endpoint, []), 12 | # Start the Ecto repository 13 | supervisor(Hello.Repo, []), 14 | # Here you could define other workers and supervisors as children 15 | # worker(Hello.Worker, [arg1, arg2, arg3]), 16 | ] 17 | 18 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 19 | # for other strategies and supported options 20 | opts = [strategy: :one_for_one, name: Hello.Supervisor] 21 | Supervisor.start_link(children, opts) 22 | end 23 | 24 | # Tell Phoenix to update the endpoint configuration 25 | # whenever the application is updated. 26 | def config_change(changed, _new, removed) do 27 | Hello.Endpoint.config_change(changed, removed) 28 | :ok 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /hello/lib/hello/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :hello 3 | 4 | socket "/socket", Hello.UserSocket 5 | 6 | # Serve at "/" the static files from "priv/static" directory. 7 | # 8 | # You should set gzip to true if you are running phoenix.digest 9 | # when deploying your static files in production. 10 | plug Plug.Static, 11 | at: "/", from: :hello, gzip: false, 12 | only: ~w(css fonts images js favicon.ico robots.txt) 13 | 14 | # Code reloading can be explicitly enabled under the 15 | # :code_reloader configuration of your endpoint. 16 | if code_reloading? do 17 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 18 | plug Phoenix.LiveReloader 19 | plug Phoenix.CodeReloader 20 | end 21 | 22 | plug Plug.RequestId 23 | plug Plug.Logger 24 | 25 | plug Plug.Parsers, 26 | parsers: [:urlencoded, :multipart, :json], 27 | pass: ["*/*"], 28 | json_decoder: Poison 29 | 30 | plug Plug.MethodOverride 31 | plug Plug.Head 32 | 33 | plug Plug.Session, 34 | store: :cookie, 35 | key: "_hello_key", 36 | signing_salt: "Ej4A1i5X" 37 | 38 | plug Hello.Router 39 | end 40 | -------------------------------------------------------------------------------- /hello/lib/hello/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello.Repo do 2 | use Ecto.Repo, otp_app: :hello 3 | end 4 | -------------------------------------------------------------------------------- /hello/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Hello.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :hello, 6 | version: "0.0.1", 7 | elixir: "~> 1.0", 8 | elixirc_paths: elixirc_paths(Mix.env), 9 | compilers: [:phoenix, :gettext] ++ Mix.compilers, 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | aliases: aliases, 13 | deps: deps] 14 | end 15 | 16 | # Configuration for the OTP application. 17 | # 18 | # Type `mix help compile.app` for more information. 19 | def application do 20 | [mod: {Hello, []}, 21 | applications: [:phoenix, :phoenix_html, :cowboy, :logger, :gettext, 22 | :phoenix_ecto, :postgrex]] 23 | end 24 | 25 | # Specifies which paths to compile per environment. 26 | defp elixirc_paths(:test), do: ["lib", "web", "test/support"] 27 | defp elixirc_paths(_), do: ["lib", "web"] 28 | 29 | # Specifies your project dependencies. 30 | # 31 | # Type `mix help deps` for examples and options. 32 | defp deps do 33 | [{:phoenix, "~> 1.1.4"}, 34 | {:postgrex, ">= 0.0.0"}, 35 | {:phoenix_ecto, "~> 2.0"}, 36 | {:phoenix_html, "~> 2.4"}, 37 | {:phoenix_live_reload, "~> 1.0", only: :dev}, 38 | {:gettext, "~> 0.9"}, 39 | {:cowboy, "~> 1.0"}] 40 | end 41 | 42 | # Aliases are shortcut or tasks specific to the current project. 43 | # For example, to create, migrate and run the seeds file at once: 44 | # 45 | # $ mix ecto.setup 46 | # 47 | # See the documentation for `Mix` for more info on aliases. 48 | defp aliases do 49 | ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 50 | "ecto.reset": ["ecto.drop", "ecto.setup"]] 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /hello/mix.lock: -------------------------------------------------------------------------------- 1 | %{"connection": {:hex, :connection, "1.0.2"}, 2 | "cowboy": {:hex, :cowboy, "1.0.4"}, 3 | "cowlib": {:hex, :cowlib, "1.0.2"}, 4 | "db_connection": {:hex, :db_connection, "0.2.4"}, 5 | "decimal": {:hex, :decimal, "1.1.1"}, 6 | "ecto": {:hex, :ecto, "1.1.4"}, 7 | "fs": {:hex, :fs, "0.9.2"}, 8 | "gettext": {:hex, :gettext, "0.10.0"}, 9 | "phoenix": {:hex, :phoenix, "1.1.4"}, 10 | "phoenix_ecto": {:hex, :phoenix_ecto, "2.0.1"}, 11 | "phoenix_html": {:hex, :phoenix_html, "2.5.0"}, 12 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.3"}, 13 | "plug": {:hex, :plug, "1.1.2"}, 14 | "poison": {:hex, :poison, "1.5.2"}, 15 | "poolboy": {:hex, :poolboy, "1.5.1"}, 16 | "postgrex": {:hex, :postgrex, "0.11.1"}, 17 | "ranch": {:hex, :ranch, "1.2.1"}} 18 | -------------------------------------------------------------------------------- /hello/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": { 3 | }, 4 | "dependencies": { 5 | "babel-brunch": "~6.0.0", 6 | "brunch": "~2.1.3", 7 | "clean-css-brunch": "~1.8.0", 8 | "css-brunch": "~1.7.0", 9 | "javascript-brunch": "~1.8.0", 10 | "uglify-js-brunch": "~1.7.0", 11 | "phoenix": "file:deps/phoenix", 12 | "phoenix_html": "file:deps/phoenix_html" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /hello/priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. Do not add, change, or 2 | ## remove `msgid`s manually here as they're tied to the ones in the 3 | ## corresponding POT file (with the same domain). Use `mix gettext.extract 4 | ## --merge` or `mix gettext.merge` to merge POT files into PO files. 5 | msgid "" 6 | msgstr "" 7 | "Language: en\n" 8 | 9 | ## From Ecto.Changeset.cast/4 10 | msgid "can't be blank" 11 | msgstr "" 12 | 13 | ## From Ecto.Changeset.unique_constraint/3 14 | msgid "has already been taken" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.put_change/3 18 | msgid "is invalid" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.validate_format/3 22 | msgid "has invalid format" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_subset/3 26 | msgid "has an invalid entry" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_exclusion/3 30 | msgid "is reserved" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_confirmation/3 34 | msgid "does not match confirmation" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.no_assoc_constraint/3 38 | msgid "is still associated to this entry" 39 | msgstr "" 40 | 41 | msgid "are still associated to this entry" 42 | msgstr "" 43 | 44 | ## From Ecto.Changeset.validate_length/3 45 | msgid "should be %{count} character(s)" 46 | msgid_plural "should be %{count} character(s)" 47 | msgstr[0] "" 48 | msgstr[1] "" 49 | 50 | msgid "should have %{count} item(s)" 51 | msgid_plural "should have %{count} item(s)" 52 | msgstr[0] "" 53 | msgstr[1] "" 54 | 55 | msgid "should be at least %{count} character(s)" 56 | msgid_plural "should be at least %{count} character(s)" 57 | msgstr[0] "" 58 | msgstr[1] "" 59 | 60 | msgid "should have at least %{count} item(s)" 61 | msgid_plural "should have at least %{count} item(s)" 62 | msgstr[0] "" 63 | msgstr[1] "" 64 | 65 | msgid "should be at most %{count} character(s)" 66 | msgid_plural "should be at most %{count} character(s)" 67 | msgstr[0] "" 68 | msgstr[1] "" 69 | 70 | msgid "should have at most %{count} item(s)" 71 | msgid_plural "should have at most %{count} item(s)" 72 | msgstr[0] "" 73 | msgstr[1] "" 74 | 75 | ## From Ecto.Changeset.validate_number/3 76 | msgid "must be less than %{count}" 77 | msgid_plural "must be less than %{count}" 78 | msgstr[0] "" 79 | msgstr[1] "" 80 | 81 | msgid "must be greater than %{count}" 82 | msgid_plural "must be greater than %{count}" 83 | msgstr[0] "" 84 | msgstr[1] "" 85 | 86 | msgid "must be less than or equal to %{count}" 87 | msgid_plural "must be less than or equal to %{count}" 88 | msgstr[0] "" 89 | msgstr[1] "" 90 | 91 | msgid "must be greater than or equal to %{count}" 92 | msgid_plural "must be greater than or equal to %{count}" 93 | msgstr[0] "" 94 | msgstr[1] "" 95 | 96 | msgid "must be equal to %{count}" 97 | msgid_plural "must be equal to %{count}" 98 | msgstr[0] "" 99 | msgstr[1] "" 100 | -------------------------------------------------------------------------------- /hello/priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This file is a PO Template file. `msgid`s here are often extracted from 2 | ## source code; add new translations manually only if they're dynamic 3 | ## translations that can't be statically extracted. Run `mix 4 | ## gettext.extract` to bring this file up to date. Leave `msgstr`s empty as 5 | ## changing them here as no effect; edit them in PO (`.po`) files instead. 6 | 7 | ## From Ecto.Changeset.cast/4 8 | msgid "can't be blank" 9 | msgstr "" 10 | 11 | ## From Ecto.Changeset.unique_constraint/3 12 | msgid "has already been taken" 13 | msgstr "" 14 | 15 | ## From Ecto.Changeset.put_change/3 16 | msgid "is invalid" 17 | msgstr "" 18 | 19 | ## From Ecto.Changeset.validate_format/3 20 | msgid "has invalid format" 21 | msgstr "" 22 | 23 | ## From Ecto.Changeset.validate_subset/3 24 | msgid "has an invalid entry" 25 | msgstr "" 26 | 27 | ## From Ecto.Changeset.validate_exclusion/3 28 | msgid "is reserved" 29 | msgstr "" 30 | 31 | ## From Ecto.Changeset.validate_confirmation/3 32 | msgid "does not match confirmation" 33 | msgstr "" 34 | 35 | ## From Ecto.Changeset.no_assoc_constraint/3 36 | msgid "is still associated to this entry" 37 | msgstr "" 38 | 39 | msgid "are still associated to this entry" 40 | msgstr "" 41 | 42 | ## From Ecto.Changeset.validate_length/3 43 | msgid "should be %{count} character(s)" 44 | msgid_plural "should be %{count} character(s)" 45 | msgstr[0] "" 46 | msgstr[1] "" 47 | 48 | msgid "should have %{count} item(s)" 49 | msgid_plural "should have %{count} item(s)" 50 | msgstr[0] "" 51 | msgstr[1] "" 52 | 53 | msgid "should be at least %{count} character(s)" 54 | msgid_plural "should be at least %{count} character(s)" 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | msgid "should have at least %{count} item(s)" 59 | msgid_plural "should have at least %{count} item(s)" 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | 63 | msgid "should be at most %{count} character(s)" 64 | msgid_plural "should be at most %{count} character(s)" 65 | msgstr[0] "" 66 | msgstr[1] "" 67 | 68 | msgid "should have at most %{count} item(s)" 69 | msgid_plural "should have at most %{count} item(s)" 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | ## From Ecto.Changeset.validate_number/3 74 | msgid "must be less than %{count}" 75 | msgid_plural "must be less than %{count}" 76 | msgstr[0] "" 77 | msgstr[1] "" 78 | 79 | msgid "must be greater than %{count}" 80 | msgid_plural "must be greater than %{count}" 81 | msgstr[0] "" 82 | msgstr[1] "" 83 | 84 | msgid "must be less than or equal to %{count}" 85 | msgid_plural "must be less than or equal to %{count}" 86 | msgstr[0] "" 87 | msgstr[1] "" 88 | 89 | msgid "must be greater than or equal to %{count}" 90 | msgid_plural "must be greater than or equal to %{count}" 91 | msgstr[0] "" 92 | msgstr[1] "" 93 | 94 | msgid "must be equal to %{count}" 95 | msgid_plural "must be equal to %{count}" 96 | msgstr[0] "" 97 | msgstr[1] "" 98 | -------------------------------------------------------------------------------- /hello/priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # Hello.Repo.insert!(%Hello.SomeModel{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /hello/test/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Hello.PageControllerTest do 2 | use Hello.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get conn, "/" 6 | assert html_response(conn, 200) =~ "Welcome to Phoenix!" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /hello/test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | imports other functionality to make it easier 8 | to build and query models. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | alias Hello.Repo 24 | import Ecto 25 | import Ecto.Changeset 26 | import Ecto.Query, only: [from: 1, from: 2] 27 | 28 | 29 | # The default endpoint for testing 30 | @endpoint Hello.Endpoint 31 | end 32 | end 33 | 34 | setup tags do 35 | unless tags[:async] do 36 | Ecto.Adapters.SQL.restart_test_transaction(Hello.Repo, []) 37 | end 38 | 39 | :ok 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /hello/test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | imports other functionality to make it easier 8 | to build and query models. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with connections 21 | use Phoenix.ConnTest 22 | 23 | alias Hello.Repo 24 | import Ecto 25 | import Ecto.Changeset 26 | import Ecto.Query, only: [from: 1, from: 2] 27 | 28 | import Hello.Router.Helpers 29 | 30 | # The default endpoint for testing 31 | @endpoint Hello.Endpoint 32 | end 33 | end 34 | 35 | setup tags do 36 | unless tags[:async] do 37 | Ecto.Adapters.SQL.restart_test_transaction(Hello.Repo, []) 38 | end 39 | 40 | {:ok, conn: Phoenix.ConnTest.conn()} 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /hello/test/support/model_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello.ModelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | model tests. 5 | 6 | You may define functions here to be used as helpers in 7 | your model tests. See `errors_on/2`'s definition as reference. 8 | 9 | Finally, if the test case interacts with the database, 10 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | using do 18 | quote do 19 | alias Hello.Repo 20 | 21 | import Ecto 22 | import Ecto.Changeset 23 | import Ecto.Query, only: [from: 1, from: 2] 24 | import Hello.ModelCase 25 | end 26 | end 27 | 28 | setup tags do 29 | unless tags[:async] do 30 | Ecto.Adapters.SQL.restart_test_transaction(Hello.Repo, []) 31 | end 32 | 33 | :ok 34 | end 35 | 36 | @doc """ 37 | Helper for returning list of errors in model when passed certain data. 38 | 39 | ## Examples 40 | 41 | Given a User model that lists `:name` as a required field and validates 42 | `:password` to be safe, it would return: 43 | 44 | iex> errors_on(%User{}, %{password: "password"}) 45 | [password: "is unsafe", name: "is blank"] 46 | 47 | You could then write your assertion like: 48 | 49 | assert {:password, "is unsafe"} in errors_on(%User{}, %{password: "password"}) 50 | 51 | You can also create the changeset manually and retrieve the errors 52 | field directly: 53 | 54 | iex> changeset = User.changeset(%User{}, password: "password") 55 | iex> {:password, "is unsafe"} in changeset.errors 56 | true 57 | """ 58 | def errors_on(model, data) do 59 | model.__struct__.changeset(model, data).errors 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /hello/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | 3 | Mix.Task.run "ecto.create", ~w(-r Hello.Repo --quiet) 4 | Mix.Task.run "ecto.migrate", ~w(-r Hello.Repo --quiet) 5 | Ecto.Adapters.SQL.begin_test_transaction(Hello.Repo) 6 | 7 | -------------------------------------------------------------------------------- /hello/test/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Hello.ErrorViewTest do 2 | use Hello.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(Hello.ErrorView, "404.html", []) == 9 | "Page not found" 10 | end 11 | 12 | test "render 500.html" do 13 | assert render_to_string(Hello.ErrorView, "500.html", []) == 14 | "Server internal error" 15 | end 16 | 17 | test "render any other" do 18 | assert render_to_string(Hello.ErrorView, "505.html", []) == 19 | "Server internal error" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /hello/test/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Hello.LayoutViewTest do 2 | use Hello.ConnCase, async: true 3 | end -------------------------------------------------------------------------------- /hello/test/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Hello.PageViewTest do 2 | use Hello.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /hello/web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "rooms:*", Hello.RoomChannel 6 | 7 | ## Transports 8 | transport :websocket, Phoenix.Transports.WebSocket 9 | # transport :longpoll, Phoenix.Transports.LongPoll 10 | 11 | # Socket params are passed from the client and can 12 | # be used to verify and authenticate a user. After 13 | # verification, you can put default assigns into 14 | # the socket that will be set for all channels, ie 15 | # 16 | # {:ok, assign(socket, :user_id, verified_user_id)} 17 | # 18 | # To deny connection, return `:error`. 19 | # 20 | # See `Phoenix.Token` documentation for examples in 21 | # performing token verification on connect. 22 | def connect(_params, socket) do 23 | {:ok, socket} 24 | end 25 | 26 | # Socket id's are topics that allow you to identify all sockets for a given user: 27 | # 28 | # def id(socket), do: "users_socket:#{socket.assigns.user_id}" 29 | # 30 | # Would allow you to broadcast a "disconnect" event and terminate 31 | # all active sockets and channels for a given user: 32 | # 33 | # Hello.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{}) 34 | # 35 | # Returning `nil` makes this socket anonymous. 36 | def id(_socket), do: nil 37 | end 38 | -------------------------------------------------------------------------------- /hello/web/controllers/hello_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello.HelloController do 2 | use Hello.Web, :controller 3 | 4 | def world(conn, params) do 5 | render(conn, "world.html", name: params["name"] || "World") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /hello/web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello.PageController do 2 | use Hello.Web, :controller 3 | 4 | def index(conn, _params) do 5 | render conn, "index.html" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /hello/web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](http://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import Hello.Gettext 9 | 10 | # Simple translation 11 | gettext "Here is the string to translate" 12 | 13 | # Plural translation 14 | ngettext "Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3 17 | 18 | # Domain-based translation 19 | dgettext "errors", "Here is the error message to translate" 20 | 21 | See the [Gettext Docs](http://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :hello 24 | end 25 | -------------------------------------------------------------------------------- /hello/web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello.Router do 2 | use Hello.Web, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_flash 8 | plug :protect_from_forgery 9 | plug :put_secure_browser_headers 10 | end 11 | 12 | pipeline :api do 13 | plug :accepts, ["json"] 14 | end 15 | 16 | scope "/", Hello do 17 | pipe_through :browser # Use the default browser stack 18 | 19 | get "/hello", HelloController, :world 20 | get "/hello/:name", HelloController, :world 21 | get "/", PageController, :index 22 | end 23 | 24 | # Other scopes may use custom stacks. 25 | # scope "/api", Hello do 26 | # pipe_through :api 27 | # end 28 | end 29 | -------------------------------------------------------------------------------- /hello/web/static/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauntedhost/programming-phoenix/37bf009a2649a245a6027ea7bcbb92d8e36169de/hello/web/static/assets/favicon.ico -------------------------------------------------------------------------------- /hello/web/static/assets/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauntedhost/programming-phoenix/37bf009a2649a245a6027ea7bcbb92d8e36169de/hello/web/static/assets/images/phoenix.png -------------------------------------------------------------------------------- /hello/web/static/assets/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /hello/web/static/js/app.js: -------------------------------------------------------------------------------- 1 | // Brunch automatically concatenates all files in your 2 | // watched paths. Those paths can be configured at 3 | // config.paths.watched in "brunch-config.js". 4 | // 5 | // However, those files will only be executed if 6 | // explicitly imported. The only exception are files 7 | // in vendor, which are never wrapped in imports and 8 | // therefore are always executed. 9 | 10 | // Import dependencies 11 | // 12 | // If you no longer want to use a dependency, remember 13 | // to also remove its path from "config.paths.watched". 14 | import "phoenix_html" 15 | 16 | // Import local files 17 | // 18 | // Local files can be imported directly using relative 19 | // paths "./socket" or full ones "web/static/js/socket". 20 | 21 | // import socket from "./socket" 22 | -------------------------------------------------------------------------------- /hello/web/static/js/socket.js: -------------------------------------------------------------------------------- 1 | // NOTE: The contents of this file will only be executed if 2 | // you uncomment its entry in "web/static/js/app.js". 3 | 4 | // To use Phoenix channels, the first step is to import Socket 5 | // and connect at the socket path in "lib/my_app/endpoint.ex": 6 | import {Socket} from "phoenix" 7 | 8 | let socket = new Socket("/socket", {params: {token: window.userToken}}) 9 | 10 | // When you connect, you'll often need to authenticate the client. 11 | // For example, imagine you have an authentication plug, `MyAuth`, 12 | // which authenticates the session and assigns a `:current_user`. 13 | // If the current user exists you can assign the user's token in 14 | // the connection for use in the layout. 15 | // 16 | // In your "web/router.ex": 17 | // 18 | // pipeline :browser do 19 | // ... 20 | // plug MyAuth 21 | // plug :put_user_token 22 | // end 23 | // 24 | // defp put_user_token(conn, _) do 25 | // if current_user = conn.assigns[:current_user] do 26 | // token = Phoenix.Token.sign(conn, "user socket", current_user.id) 27 | // assign(conn, :user_token, token) 28 | // else 29 | // conn 30 | // end 31 | // end 32 | // 33 | // Now you need to pass this token to JavaScript. You can do so 34 | // inside a script tag in "web/templates/layout/app.html.eex": 35 | // 36 | // 37 | // 38 | // You will need to verify the user token in the "connect/2" function 39 | // in "web/channels/user_socket.ex": 40 | // 41 | // def connect(%{"token" => token}, socket) do 42 | // # max_age: 1209600 is equivalent to two weeks in seconds 43 | // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do 44 | // {:ok, user_id} -> 45 | // {:ok, assign(socket, :user, user_id)} 46 | // {:error, reason} -> 47 | // :error 48 | // end 49 | // end 50 | // 51 | // Finally, pass the token on connect as below. Or remove it 52 | // from connect if you don't care about authentication. 53 | 54 | socket.connect() 55 | 56 | // Now that you are connected, you can join channels with a topic: 57 | let channel = socket.channel("topic:subtopic", {}) 58 | channel.join() 59 | .receive("ok", resp => { console.log("Joined successfully", resp) }) 60 | .receive("error", resp => { console.log("Unable to join", resp) }) 61 | 62 | export default socket 63 | -------------------------------------------------------------------------------- /hello/web/templates/hello/world.html.eex: -------------------------------------------------------------------------------- 1 |

2 | Hello <%= String.capitalize(@name) %>! 3 |

4 | -------------------------------------------------------------------------------- /hello/web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Hello Hello! 11 | "> 12 | 13 | 14 | 15 |
16 |
17 | 22 | 23 |
24 | 25 | 26 | 27 | 28 |
29 | <%= render @view_module, @view_template, assigns %> 30 |
31 | 32 |
33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /hello/web/templates/page/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |

<%= gettext "Welcome to %{name}", name: "Phoenix!" %>

3 |

A productive web framework that
does not compromise speed and maintainability.

4 |
5 | 6 |
7 |
8 |

Resources

9 | 20 |
21 | 22 |
23 |

Help

24 | 35 |
36 |
37 | -------------------------------------------------------------------------------- /hello/web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | if error = form.errors[field] do 13 | content_tag :span, translate_error(error), class: "help-block" 14 | end 15 | end 16 | 17 | @doc """ 18 | Translates an error message using gettext. 19 | """ 20 | def translate_error({msg, opts}) do 21 | # Because error messages were defined within Ecto, we must 22 | # call the Gettext module passing our Gettext backend. We 23 | # also use the "errors" domain as translations are placed 24 | # in the errors.po file. On your own code and templates, 25 | # this could be written simply as: 26 | # 27 | # dngettext "errors", "1 file", "%{count} files", count 28 | # 29 | Gettext.dngettext(Hello.Gettext, "errors", msg, msg, opts[:count], opts) 30 | end 31 | 32 | def translate_error(msg) do 33 | Gettext.dgettext(Hello.Gettext, "errors", msg) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /hello/web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello.ErrorView do 2 | use Hello.Web, :view 3 | 4 | def render("404.html", _assigns) do 5 | "Page not found" 6 | end 7 | 8 | def render("500.html", _assigns) do 9 | "Server internal error" 10 | end 11 | 12 | # In case no render clause matches or no 13 | # template is found, let's render it as 500 14 | def template_not_found(_template, assigns) do 15 | render "500.html", assigns 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /hello/web/views/hello_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello.HelloView do 2 | use Hello.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /hello/web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello.LayoutView do 2 | use Hello.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /hello/web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello.PageView do 2 | use Hello.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /hello/web/web.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello.Web do 2 | @moduledoc """ 3 | A module that keeps using definitions for controllers, 4 | views and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use Hello.Web, :controller 9 | use Hello.Web, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. 17 | """ 18 | 19 | def model do 20 | quote do 21 | use Ecto.Schema 22 | 23 | import Ecto 24 | import Ecto.Changeset 25 | import Ecto.Query, only: [from: 1, from: 2] 26 | end 27 | end 28 | 29 | def controller do 30 | quote do 31 | use Phoenix.Controller 32 | 33 | alias Hello.Repo 34 | import Ecto 35 | import Ecto.Query, only: [from: 1, from: 2] 36 | 37 | import Hello.Router.Helpers 38 | import Hello.Gettext 39 | end 40 | end 41 | 42 | def view do 43 | quote do 44 | use Phoenix.View, root: "web/templates" 45 | 46 | # Import convenience functions from controllers 47 | import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] 48 | 49 | # Use all HTML functionality (forms, tags, etc) 50 | use Phoenix.HTML 51 | 52 | import Hello.Router.Helpers 53 | import Hello.ErrorHelpers 54 | import Hello.Gettext 55 | end 56 | end 57 | 58 | def router do 59 | quote do 60 | use Phoenix.Router 61 | end 62 | end 63 | 64 | def channel do 65 | quote do 66 | use Phoenix.Channel 67 | 68 | alias Hello.Repo 69 | import Ecto 70 | import Ecto.Query, only: [from: 1, from: 2] 71 | import Hello.Gettext 72 | end 73 | end 74 | 75 | @doc """ 76 | When used, dispatch to the appropriate controller/view/etc. 77 | """ 78 | defmacro __using__(which) when is_atom(which) do 79 | apply(__MODULE__, which, []) 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /rumbl/.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | 7 | # Generate on crash by the VM 8 | erl_crash.dump 9 | 10 | # Static artifacts 11 | /node_modules 12 | 13 | # Since we are building assets from web/static, 14 | # we ignore priv/static. You may want to comment 15 | # this depending on your deployment strategy. 16 | /priv/static/ 17 | 18 | # The config/prod.secret.exs file by default contains sensitive 19 | # data and you should not commit it into version control. 20 | # 21 | # Alternatively, you may comment the line below and commit the 22 | # secrets file as long as you replace its contents by environment 23 | # variables. 24 | /config/prod.secret.exs 25 | -------------------------------------------------------------------------------- /rumbl/Elixir.Rumbl.User.beam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauntedhost/programming-phoenix/37bf009a2649a245a6027ea7bcbb92d8e36169de/rumbl/Elixir.Rumbl.User.beam -------------------------------------------------------------------------------- /rumbl/README.md: -------------------------------------------------------------------------------- 1 | # Rumbl 2 | 3 | To start your Phoenix app: 4 | 5 | * Install dependencies with `mix deps.get` 6 | * Create and migrate your database with `mix ecto.create && mix ecto.migrate` 7 | * Install Node.js dependencies with `npm install` 8 | * Start Phoenix endpoint with `mix phoenix.server` 9 | 10 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 11 | 12 | Ready to run in production? Please [check our deployment guides](http://www.phoenixframework.org/docs/deployment). 13 | 14 | ## Learn more 15 | 16 | * Official website: http://www.phoenixframework.org/ 17 | * Guides: http://phoenixframework.org/docs/overview 18 | * Docs: http://hexdocs.pm/phoenix 19 | * Mailing list: http://groups.google.com/group/phoenix-talk 20 | * Source: https://github.com/phoenixframework/phoenix 21 | -------------------------------------------------------------------------------- /rumbl/brunch-config.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | // See http://brunch.io/#documentation for docs. 3 | files: { 4 | javascripts: { 5 | joinTo: "js/app.js" 6 | 7 | // To use a separate vendor.js bundle, specify two files path 8 | // https://github.com/brunch/brunch/blob/stable/docs/config.md#files 9 | // joinTo: { 10 | // "js/app.js": /^(web\/static\/js)/, 11 | // "js/vendor.js": /^(web\/static\/vendor)|(deps)/ 12 | // } 13 | // 14 | // To change the order of concatenation of files, explicitly mention here 15 | // https://github.com/brunch/brunch/tree/master/docs#concatenation 16 | // order: { 17 | // before: [ 18 | // "web/static/vendor/js/jquery-2.1.1.js", 19 | // "web/static/vendor/js/bootstrap.min.js" 20 | // ] 21 | // } 22 | }, 23 | stylesheets: { 24 | joinTo: "css/app.css" 25 | }, 26 | templates: { 27 | joinTo: "js/app.js" 28 | } 29 | }, 30 | 31 | conventions: { 32 | // This option sets where we should place non-css and non-js assets in. 33 | // By default, we set this to "/web/static/assets". Files in this directory 34 | // will be copied to `paths.public`, which is "priv/static" by default. 35 | assets: /^(web\/static\/assets)/ 36 | }, 37 | 38 | // Phoenix paths configuration 39 | paths: { 40 | // Dependencies and current project directories to watch 41 | watched: [ 42 | "web/static", 43 | "test/static" 44 | ], 45 | 46 | // Where to compile files to 47 | public: "priv/static" 48 | }, 49 | 50 | // Configure your plugins 51 | plugins: { 52 | babel: { 53 | // Do not use ES6 compiler in vendor code 54 | ignore: [/web\/static\/vendor/] 55 | } 56 | }, 57 | 58 | modules: { 59 | autoRequire: { 60 | "js/app.js": ["web/static/js/app"] 61 | } 62 | }, 63 | 64 | npm: { 65 | enabled: true, 66 | // Whitelist the npm deps to be pulled in as front-end assets. 67 | // All other deps in package.json will be excluded from the bundle. 68 | whitelist: ["phoenix", "phoenix_html"] 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /rumbl/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | # Configures the endpoint 9 | config :rumbl, Rumbl.Endpoint, 10 | url: [host: "localhost"], 11 | root: Path.dirname(__DIR__), 12 | secret_key_base: "3ILQ1vBCO8U4kTKvUW/WprEEULB1wViJYzq67nXzYnvmolXJedit1wQNsE4y92to", 13 | render_errors: [accepts: ~w(html json)], 14 | pubsub: [name: Rumbl.PubSub, 15 | adapter: Phoenix.PubSub.PG2] 16 | 17 | # Configures Elixir's Logger 18 | config :logger, :console, 19 | format: "$time $metadata[$level] $message\n", 20 | metadata: [:request_id] 21 | 22 | # Import environment specific config. This must remain at the bottom 23 | # of this file so it overrides the configuration defined above. 24 | import_config "#{Mix.env}.exs" 25 | 26 | # Configure phoenix generators 27 | config :phoenix, :generators, 28 | migration: true, 29 | binary_id: false 30 | -------------------------------------------------------------------------------- /rumbl/config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with brunch.io to recompile .js and .css sources. 9 | config :rumbl, Rumbl.Endpoint, 10 | http: [port: 4000], 11 | debug_errors: true, 12 | code_reloader: true, 13 | check_origin: false, 14 | watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin"]] 15 | 16 | # Watch static and templates for browser reloading. 17 | config :rumbl, Rumbl.Endpoint, 18 | live_reload: [ 19 | patterns: [ 20 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, 21 | ~r{priv/gettext/.*(po)$}, 22 | ~r{web/views/.*(ex)$}, 23 | ~r{web/templates/.*(eex)$} 24 | ] 25 | ] 26 | 27 | # Do not include metadata nor timestamps in development logs 28 | config :logger, :console, format: "[$level] $message\n" 29 | 30 | # Set a higher stacktrace during development. 31 | # Do not configure such in production as keeping 32 | # and calculating stacktraces is usually expensive. 33 | config :phoenix, :stacktrace_depth, 20 34 | 35 | # Configure your database 36 | config :rumbl, Rumbl.Repo, 37 | adapter: Ecto.Adapters.Postgres, 38 | username: "postgres", 39 | password: "postgres", 40 | database: "rumbl_dev", 41 | hostname: "localhost", 42 | pool_size: 10 43 | -------------------------------------------------------------------------------- /rumbl/config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, we configure the host to read the PORT 4 | # from the system environment. Therefore, you will need 5 | # to set PORT=80 before running your server. 6 | # 7 | # You should also configure the url host to something 8 | # meaningful, we use this information when generating URLs. 9 | # 10 | # Finally, we also include the path to a manifest 11 | # containing the digested version of static files. This 12 | # manifest is generated by the mix phoenix.digest task 13 | # which you typically run after static files are built. 14 | config :rumbl, Rumbl.Endpoint, 15 | http: [port: {:system, "PORT"}], 16 | url: [host: "example.com", port: 80], 17 | cache_static_manifest: "priv/static/manifest.json" 18 | 19 | # Do not print debug messages in production 20 | config :logger, level: :info 21 | 22 | # ## SSL Support 23 | # 24 | # To get SSL working, you will need to add the `https` key 25 | # to the previous section and set your `:url` port to 443: 26 | # 27 | # config :rumbl, Rumbl.Endpoint, 28 | # ... 29 | # url: [host: "example.com", port: 443], 30 | # https: [port: 443, 31 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 32 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")] 33 | # 34 | # Where those two env variables return an absolute path to 35 | # the key and cert in disk or a relative path inside priv, 36 | # for example "priv/ssl/server.key". 37 | # 38 | # We also recommend setting `force_ssl`, ensuring no data is 39 | # ever sent via http, always redirecting to https: 40 | # 41 | # config :rumbl, Rumbl.Endpoint, 42 | # force_ssl: [hsts: true] 43 | # 44 | # Check `Plug.SSL` for all available options in `force_ssl`. 45 | 46 | # ## Using releases 47 | # 48 | # If you are doing OTP releases, you need to instruct Phoenix 49 | # to start the server for all endpoints: 50 | # 51 | # config :phoenix, :serve_endpoints, true 52 | # 53 | # Alternatively, you can configure exactly which server to 54 | # start per endpoint: 55 | # 56 | # config :rumbl, Rumbl.Endpoint, server: true 57 | # 58 | # You will also need to set the application root to `.` in order 59 | # for the new static assets to be served after a hot upgrade: 60 | # 61 | # config :rumbl, Rumbl.Endpoint, root: "." 62 | 63 | # Finally import the config/prod.secret.exs 64 | # which should be versioned separately. 65 | import_config "prod.secret.exs" 66 | -------------------------------------------------------------------------------- /rumbl/config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :rumbl, Rumbl.Endpoint, 6 | http: [port: 4001], 7 | server: false 8 | 9 | # Print only warnings and errors during test 10 | config :logger, level: :warn 11 | 12 | # Configure your database 13 | config :rumbl, Rumbl.Repo, 14 | adapter: Ecto.Adapters.Postgres, 15 | username: "postgres", 16 | password: "postgres", 17 | database: "rumbl_test", 18 | hostname: "localhost", 19 | pool: Ecto.Adapters.SQL.Sandbox 20 | 21 | config :comeonin, 22 | bcrypt_log_rounds: 4, 23 | pbkdf2_rounds: 1 24 | -------------------------------------------------------------------------------- /rumbl/lib/rumbl.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec, warn: false 8 | 9 | children = [ 10 | # Start the endpoint when the application starts 11 | supervisor(Rumbl.Endpoint, []), 12 | # Start the Ecto repository 13 | supervisor(Rumbl.Repo, []), 14 | # Here you could define other workers and supervisors as children 15 | # worker(Rumbl.Worker, [arg1, arg2, arg3]), 16 | ] 17 | 18 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 19 | # for other strategies and supported options 20 | opts = [strategy: :one_for_one, name: Rumbl.Supervisor] 21 | Supervisor.start_link(children, opts) 22 | end 23 | 24 | # Tell Phoenix to update the endpoint configuration 25 | # whenever the application is updated. 26 | def config_change(changed, _new, removed) do 27 | Rumbl.Endpoint.config_change(changed, removed) 28 | :ok 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /rumbl/lib/rumbl/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :rumbl 3 | 4 | socket "/socket", Rumbl.UserSocket 5 | 6 | # Serve at "/" the static files from "priv/static" directory. 7 | # 8 | # You should set gzip to true if you are running phoenix.digest 9 | # when deploying your static files in production. 10 | plug Plug.Static, 11 | at: "/", from: :rumbl, gzip: false, 12 | only: ~w(css fonts images js favicon.ico robots.txt) 13 | 14 | # Code reloading can be explicitly enabled under the 15 | # :code_reloader configuration of your endpoint. 16 | if code_reloading? do 17 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 18 | plug Phoenix.LiveReloader 19 | plug Phoenix.CodeReloader 20 | end 21 | 22 | plug Plug.RequestId 23 | plug Plug.Logger 24 | 25 | plug Plug.Parsers, 26 | parsers: [:urlencoded, :multipart, :json], 27 | pass: ["*/*"], 28 | json_decoder: Poison 29 | 30 | plug Plug.MethodOverride 31 | plug Plug.Head 32 | 33 | plug Plug.Session, 34 | store: :cookie, 35 | key: "_rumbl_key", 36 | signing_salt: "V8YPJgBm" 37 | 38 | plug Rumbl.Router 39 | end 40 | -------------------------------------------------------------------------------- /rumbl/lib/rumbl/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.Repo do 2 | use Ecto.Repo, otp_app: :rumbl 3 | import Ecto.Query, only: [from: 2, order_by: 3] 4 | 5 | def get_first(query) do 6 | one from r in query, 7 | order_by: [asc: r.inserted_at], 8 | limit: 1 9 | end 10 | 11 | def get_last(query) do 12 | one from r in query, 13 | order_by: [desc: r.inserted_at], 14 | limit: 1 15 | end 16 | 17 | def get_by_uuid!(model, id) do 18 | case Ecto.Type.dump(Ecto.UUID, id) do 19 | {:ok, _} -> 20 | get!(model, id) 21 | :error -> 22 | raise Ecto.NoResultsError, queryable: model 23 | end 24 | end 25 | 26 | def get_by_uuid(model, id) do 27 | case Ecto.Type.dump(Ecto.UUID, id) do 28 | {:ok, _} -> 29 | get(model, id) 30 | :error -> 31 | nil 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /rumbl/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :rumbl, 6 | version: "0.0.1", 7 | elixir: "~> 1.0", 8 | elixirc_paths: elixirc_paths(Mix.env), 9 | compilers: [:phoenix, :gettext] ++ Mix.compilers, 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | aliases: aliases, 13 | deps: deps] 14 | end 15 | 16 | # Configuration for the OTP application. 17 | # 18 | # Type `mix help compile.app` for more information. 19 | def application do 20 | [ 21 | mod: {Rumbl, []}, 22 | applications: [ 23 | :phoenix, 24 | :phoenix_html, 25 | :cowboy, 26 | :logger, 27 | :gettext, 28 | :phoenix_ecto, 29 | :postgrex, 30 | :comeonin, 31 | :plug_graphql 32 | ] 33 | ] 34 | end 35 | 36 | # Specifies which paths to compile per environment. 37 | defp elixirc_paths(:test), do: ["lib", "web", "test/support"] 38 | defp elixirc_paths(_), do: ["lib", "web"] 39 | 40 | # Specifies your project dependencies. 41 | # 42 | # Type `mix help deps` for examples and options. 43 | defp deps do 44 | [ 45 | {:comeonin, "~> 2.0"}, 46 | {:cowboy, "~> 1.0"}, 47 | {:gettext, "~> 0.9"}, 48 | {:plug_graphql, "~> 0.1.5"}, 49 | {:phoenix, "~> 1.1.4"}, 50 | {:phoenix_ecto, "~> 2.0"}, 51 | {:phoenix_html, "~> 2.4"}, 52 | {:phoenix_live_reload, "~> 1.0", only: :dev}, 53 | {:postgrex, ">= 0.0.0"}, 54 | ] 55 | end 56 | 57 | # Aliases are shortcut or tasks specific to the current project. 58 | # For example, to create, migrate and run the seeds file at once: 59 | # 60 | # $ mix ecto.setup 61 | # 62 | # See the documentation for `Mix` for more info on aliases. 63 | defp aliases do 64 | ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 65 | "ecto.reset": ["ecto.drop", "ecto.setup"]] 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /rumbl/mix.lock: -------------------------------------------------------------------------------- 1 | %{"comeonin": {:hex, :comeonin, "2.1.1"}, 2 | "connection": {:hex, :connection, "1.0.2"}, 3 | "cowboy": {:hex, :cowboy, "1.0.4"}, 4 | "cowlib": {:hex, :cowlib, "1.0.2"}, 5 | "db_connection": {:hex, :db_connection, "0.2.4"}, 6 | "decimal": {:hex, :decimal, "1.1.1"}, 7 | "ecto": {:hex, :ecto, "1.1.4"}, 8 | "fs": {:hex, :fs, "0.9.2"}, 9 | "gettext": {:hex, :gettext, "0.10.0"}, 10 | "graphql": {:hex, :graphql, "0.1.2"}, 11 | "phoenix": {:hex, :phoenix, "1.1.4"}, 12 | "phoenix_ecto": {:hex, :phoenix_ecto, "2.0.1"}, 13 | "phoenix_html": {:hex, :phoenix_html, "2.5.0"}, 14 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.3"}, 15 | "plug": {:hex, :plug, "1.1.2"}, 16 | "plug_graphql": {:hex, :plug_graphql, "0.1.5"}, 17 | "poison": {:hex, :poison, "1.5.2"}, 18 | "poolboy": {:hex, :poolboy, "1.5.1"}, 19 | "postgrex": {:hex, :postgrex, "0.11.1"}, 20 | "ranch": {:hex, :ranch, "1.2.1"}} 21 | -------------------------------------------------------------------------------- /rumbl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": { 3 | }, 4 | "dependencies": { 5 | "babel-brunch": "~6.0.0", 6 | "brunch": "~2.1.3", 7 | "clean-css-brunch": "~1.8.0", 8 | "css-brunch": "~1.7.0", 9 | "javascript-brunch": "~1.8.0", 10 | "uglify-js-brunch": "~1.7.0", 11 | "phoenix": "file:deps/phoenix", 12 | "phoenix_html": "file:deps/phoenix_html" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /rumbl/priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. Do not add, change, or 2 | ## remove `msgid`s manually here as they're tied to the ones in the 3 | ## corresponding POT file (with the same domain). Use `mix gettext.extract 4 | ## --merge` or `mix gettext.merge` to merge POT files into PO files. 5 | msgid "" 6 | msgstr "" 7 | "Language: en\n" 8 | 9 | ## From Ecto.Changeset.cast/4 10 | msgid "can't be blank" 11 | msgstr "" 12 | 13 | ## From Ecto.Changeset.unique_constraint/3 14 | msgid "has already been taken" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.put_change/3 18 | msgid "is invalid" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.validate_format/3 22 | msgid "has invalid format" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_subset/3 26 | msgid "has an invalid entry" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_exclusion/3 30 | msgid "is reserved" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_confirmation/3 34 | msgid "does not match confirmation" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.no_assoc_constraint/3 38 | msgid "is still associated to this entry" 39 | msgstr "" 40 | 41 | msgid "are still associated to this entry" 42 | msgstr "" 43 | 44 | ## From Ecto.Changeset.validate_length/3 45 | msgid "should be %{count} character(s)" 46 | msgid_plural "should be %{count} character(s)" 47 | msgstr[0] "" 48 | msgstr[1] "" 49 | 50 | msgid "should have %{count} item(s)" 51 | msgid_plural "should have %{count} item(s)" 52 | msgstr[0] "" 53 | msgstr[1] "" 54 | 55 | msgid "should be at least %{count} character(s)" 56 | msgid_plural "should be at least %{count} character(s)" 57 | msgstr[0] "" 58 | msgstr[1] "" 59 | 60 | msgid "should have at least %{count} item(s)" 61 | msgid_plural "should have at least %{count} item(s)" 62 | msgstr[0] "" 63 | msgstr[1] "" 64 | 65 | msgid "should be at most %{count} character(s)" 66 | msgid_plural "should be at most %{count} character(s)" 67 | msgstr[0] "" 68 | msgstr[1] "" 69 | 70 | msgid "should have at most %{count} item(s)" 71 | msgid_plural "should have at most %{count} item(s)" 72 | msgstr[0] "" 73 | msgstr[1] "" 74 | 75 | ## From Ecto.Changeset.validate_number/3 76 | msgid "must be less than %{count}" 77 | msgid_plural "must be less than %{count}" 78 | msgstr[0] "" 79 | msgstr[1] "" 80 | 81 | msgid "must be greater than %{count}" 82 | msgid_plural "must be greater than %{count}" 83 | msgstr[0] "" 84 | msgstr[1] "" 85 | 86 | msgid "must be less than or equal to %{count}" 87 | msgid_plural "must be less than or equal to %{count}" 88 | msgstr[0] "" 89 | msgstr[1] "" 90 | 91 | msgid "must be greater than or equal to %{count}" 92 | msgid_plural "must be greater than or equal to %{count}" 93 | msgstr[0] "" 94 | msgstr[1] "" 95 | 96 | msgid "must be equal to %{count}" 97 | msgid_plural "must be equal to %{count}" 98 | msgstr[0] "" 99 | msgstr[1] "" 100 | -------------------------------------------------------------------------------- /rumbl/priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This file is a PO Template file. `msgid`s here are often extracted from 2 | ## source code; add new translations manually only if they're dynamic 3 | ## translations that can't be statically extracted. Run `mix 4 | ## gettext.extract` to bring this file up to date. Leave `msgstr`s empty as 5 | ## changing them here as no effect; edit them in PO (`.po`) files instead. 6 | 7 | ## From Ecto.Changeset.cast/4 8 | msgid "can't be blank" 9 | msgstr "" 10 | 11 | ## From Ecto.Changeset.unique_constraint/3 12 | msgid "has already been taken" 13 | msgstr "" 14 | 15 | ## From Ecto.Changeset.put_change/3 16 | msgid "is invalid" 17 | msgstr "" 18 | 19 | ## From Ecto.Changeset.validate_format/3 20 | msgid "has invalid format" 21 | msgstr "" 22 | 23 | ## From Ecto.Changeset.validate_subset/3 24 | msgid "has an invalid entry" 25 | msgstr "" 26 | 27 | ## From Ecto.Changeset.validate_exclusion/3 28 | msgid "is reserved" 29 | msgstr "" 30 | 31 | ## From Ecto.Changeset.validate_confirmation/3 32 | msgid "does not match confirmation" 33 | msgstr "" 34 | 35 | ## From Ecto.Changeset.no_assoc_constraint/3 36 | msgid "is still associated to this entry" 37 | msgstr "" 38 | 39 | msgid "are still associated to this entry" 40 | msgstr "" 41 | 42 | ## From Ecto.Changeset.validate_length/3 43 | msgid "should be %{count} character(s)" 44 | msgid_plural "should be %{count} character(s)" 45 | msgstr[0] "" 46 | msgstr[1] "" 47 | 48 | msgid "should have %{count} item(s)" 49 | msgid_plural "should have %{count} item(s)" 50 | msgstr[0] "" 51 | msgstr[1] "" 52 | 53 | msgid "should be at least %{count} character(s)" 54 | msgid_plural "should be at least %{count} character(s)" 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | msgid "should have at least %{count} item(s)" 59 | msgid_plural "should have at least %{count} item(s)" 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | 63 | msgid "should be at most %{count} character(s)" 64 | msgid_plural "should be at most %{count} character(s)" 65 | msgstr[0] "" 66 | msgstr[1] "" 67 | 68 | msgid "should have at most %{count} item(s)" 69 | msgid_plural "should have at most %{count} item(s)" 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | ## From Ecto.Changeset.validate_number/3 74 | msgid "must be less than %{count}" 75 | msgid_plural "must be less than %{count}" 76 | msgstr[0] "" 77 | msgstr[1] "" 78 | 79 | msgid "must be greater than %{count}" 80 | msgid_plural "must be greater than %{count}" 81 | msgstr[0] "" 82 | msgstr[1] "" 83 | 84 | msgid "must be less than or equal to %{count}" 85 | msgid_plural "must be less than or equal to %{count}" 86 | msgstr[0] "" 87 | msgstr[1] "" 88 | 89 | msgid "must be greater than or equal to %{count}" 90 | msgid_plural "must be greater than or equal to %{count}" 91 | msgstr[0] "" 92 | msgstr[1] "" 93 | 94 | msgid "must be equal to %{count}" 95 | msgid_plural "must be equal to %{count}" 96 | msgstr[0] "" 97 | msgstr[1] "" 98 | -------------------------------------------------------------------------------- /rumbl/priv/repo/migrations/20160301002445_create_extension_uuid.exs: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.Repo.Migrations.CreateExtensionUuid do 2 | use Ecto.Migration 3 | 4 | def up do 5 | execute "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"" 6 | end 7 | 8 | def down do 9 | execute "DROP EXTENSION IF EXISTS \"uuid-ossp\"" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /rumbl/priv/repo/migrations/20160301003531_create_user.exs: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.Repo.Migrations.CreateUser do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:users, primary_key: false) do 6 | add :id, :uuid, primary_key: true 7 | add :name, :string 8 | add :username, :string, null: false 9 | add :password_hash, :string 10 | 11 | timestamps 12 | end 13 | create unique_index(:users, [:username]) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /rumbl/priv/repo/migrations/20160303194447_create_video.exs: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.Repo.Migrations.CreateVideo do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:videos, primary_key: false) do 6 | add :id, :uuid, primary_key: true 7 | add :url, :string 8 | add :title, :string 9 | add :description, :text 10 | add :user_id, references(:users, type: :uuid, on_delete: :nothing) 11 | 12 | timestamps 13 | end 14 | create index(:videos, [:user_id]) 15 | 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /rumbl/priv/repo/migrations/20160304020448_create_category.exs: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.Repo.Migrations.CreateCategory do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:categories, primary_key: false) do 6 | add :id, :uuid, primary_key: true 7 | add :name, :string, null: false 8 | 9 | timestamps 10 | end 11 | create unique_index(:categories, [:name]) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /rumbl/priv/repo/migrations/20160304020700_add_category_id_to_video.exs: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.Repo.Migrations.AddCategoryIdToVideo do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:videos) do 6 | add :category_id, references(:categories, type: :binary_id, on_delete: :nothing) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /rumbl/priv/repo/migrations/20160304235733_category_on_delete_nilify_all.exs: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.Repo.Migrations.CategoryOnDeleteNilifyAll do 2 | use Ecto.Migration 3 | 4 | def up do 5 | execute "ALTER TABLE videos DROP CONSTRAINT videos_category_id_fkey" 6 | alter table(:videos) do 7 | modify :category_id, references(:categories, type: :binary_id, on_delete: :nilify_all) 8 | end 9 | end 10 | 11 | def down do 12 | execute "ALTER TABLE videos DROP CONSTRAINT videos_category_id_fkey" 13 | alter table(:videos) do 14 | modify :category_id, references(:categories, type: :binary_id, on_delete: :nothing) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /rumbl/priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # Rumbl.Repo.insert!(%Rumbl.SomeModel{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | 13 | alias Rumbl.Repo 14 | alias Rumbl.Category 15 | 16 | for category <- ~w(λ Elixir JavaScript Music) do 17 | Repo.get_by(Category, name: category) || 18 | Repo.insert!(%Category{name: category}) 19 | end 20 | -------------------------------------------------------------------------------- /rumbl/test/controllers/auth_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.AuthTest do 2 | use Rumbl.ConnCase 3 | alias Rumbl.Auth 4 | alias Rumbl.User 5 | 6 | setup(%{conn: conn}) do 7 | conn = conn 8 | |> bypass_through(Rumbl.Router, :browser) 9 | |> get("/") 10 | {:ok, %{conn: conn}} 11 | end 12 | 13 | test "authenticate_user halts when no current_user exists", %{conn: conn} do 14 | conn = Auth.authenticate_user(conn, []) 15 | 16 | assert conn.halted 17 | end 18 | 19 | test "authenticate_user continues when the current_user exists", %{conn: conn} do 20 | conn = conn 21 | |> assign(:current_user, %User{id: 123}) 22 | |> Auth.authenticate_user([]) 23 | 24 | refute conn.halted 25 | end 26 | 27 | test "login puts the user in the session", %{conn: conn} do 28 | user_id = Ecto.UUID.generate 29 | login_conn = conn 30 | |> Auth.login(%User{id: user_id}) 31 | |> send_resp(:ok, "") 32 | 33 | next_conn = get(login_conn, "/") 34 | 35 | assert get_session(next_conn, :user_id) == user_id 36 | end 37 | 38 | test "logout drops the session", %{conn: conn} do 39 | user_id = Ecto.UUID.generate 40 | logout_conn = conn 41 | |> put_session(:user_id, user_id) 42 | |> Auth.logout 43 | |> send_resp(:ok, "") 44 | 45 | next_conn = get(logout_conn, "/") 46 | 47 | refute get_session(next_conn, :user_id) 48 | end 49 | 50 | test "call places user from session into assigns", %{conn: conn} do 51 | user = insert_user() 52 | conn = conn 53 | |> put_session(:user_id, user.id) 54 | |> Auth.call(Repo) 55 | 56 | assert conn.assigns.current_user.id == user.id 57 | end 58 | 59 | test "call with no session sets current_user assign to nil", %{conn: conn} do 60 | conn = Auth.call(conn, Repo) 61 | 62 | assert conn.assigns.current_user == nil 63 | end 64 | 65 | test "login with a valid email and pass", %{conn: conn} do 66 | user = insert_user(%{username: "batman", password: "supersecret"}) 67 | {:ok, conn} = Auth.login_by_username_and_pass(conn, "batman", "supersecret", repo: Repo) 68 | 69 | assert conn.assigns.current_user.id == user.id 70 | end 71 | 72 | test "login with a not found user", %{conn: conn} do 73 | assert {:error, :not_found, _conn} = 74 | Auth.login_by_username_and_pass(conn, "ghost", "secret", repo: Repo) 75 | end 76 | 77 | test "login with password mismatch", %{conn: conn} do 78 | insert_user(%{username: "batman", password: "supersecret"}) 79 | assert {:error, :unauthorized, _conn} = 80 | Auth.login_by_username_and_pass(conn, "batman", "wrongpass", repo: Repo) 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /rumbl/test/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.PageControllerTest do 2 | use Rumbl.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get conn, "/" 6 | 7 | assert html_response(conn, 200) =~ "Rumble out loud" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /rumbl/test/controllers/video_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.VideoControllerTest do 2 | use Rumbl.ConnCase 3 | alias Rumbl.Video 4 | 5 | @valid_attrs %{url: "http://youtu.be", title: "cats", description: "meow"} 6 | @invalid_attrs %{title: "nope"} 7 | 8 | setup(%{conn: conn} = config) do 9 | if username = config[:login_as] do 10 | user = insert_user(%{username: username}) 11 | conn = assign(conn, :current_user, user) 12 | {:ok, conn: conn} 13 | else 14 | :ok 15 | end 16 | end 17 | 18 | defp video_count(query) do 19 | Repo.one from v in query, select: count(v.id) 20 | end 21 | 22 | test "requires user authentication on all actions", %{conn: conn} do 23 | Enum.each([ 24 | get(conn, video_path(conn, :new)), 25 | get(conn, video_path(conn, :index)), 26 | get(conn, video_path(conn, :show, "123")), 27 | get(conn, video_path(conn, :edit, "123")), 28 | put(conn, video_path(conn, :update, "123", %{})), 29 | post(conn, video_path(conn, :create, %{})), 30 | delete(conn, video_path(conn, :delete, "123")), 31 | ], fn(conn) -> 32 | assert html_response(conn, 302) 33 | assert conn.halted 34 | end) 35 | end 36 | 37 | @tag login_as: "batman" 38 | test "lists all current user videos on index", %{conn: conn} do 39 | user = conn.assigns.current_user 40 | user_video = insert_video(user, %{title: "hilarious cats"}) 41 | other_video = insert_video(insert_user(), title: "other video") 42 | 43 | conn = get(conn, video_path(conn, :index)) 44 | 45 | assert html_response(conn, 200) =~ ~r/listing videos/i 46 | assert String.contains?(conn.resp_body, user_video.title) 47 | refute String.contains?(conn.resp_body, other_video.title) 48 | end 49 | 50 | @tag login_as: "batman" 51 | test "creates current user video and redirects", %{conn: conn} do 52 | before_count = video_count(Video) 53 | user = conn.assigns.current_user 54 | 55 | conn = post(conn, video_path(conn, :create), video: @valid_attrs) 56 | 57 | assert redirected_to(conn) == video_path(conn, :index) 58 | assert Repo.get_by!(Video, @valid_attrs).user_id == user.id 59 | assert video_count(Video) == before_count + 1 60 | end 61 | 62 | @tag login_as: "batman" 63 | test "given invalid params, does not create video, renders errors", %{conn: conn} do 64 | before_count = video_count(Video) 65 | 66 | conn = post(conn, video_path(conn, :create), video: @invalid_attrs) 67 | 68 | assert html_response(conn, 200) =~ ~r/please check the errors below/i 69 | assert video_count(Video) == before_count 70 | end 71 | 72 | @tag login_as: "batman" 73 | test "[PENDING] authorizes actions for current user video", %{conn: conn} do 74 | # show 75 | # edit 76 | # update with valid 77 | # update with invalid 78 | # delete 79 | end 80 | 81 | @tag login_as: "batman" 82 | test "rejects actions against other user video", %{conn: conn} do 83 | other_user = insert_user(%{username: "robin"}) 84 | other_video = insert_video(other_user, %{title: "private to robin"}) 85 | 86 | assert_error_sent :not_found, fn -> 87 | get(conn, video_path(conn, :show, other_video)) 88 | end 89 | 90 | assert_error_sent :not_found, fn -> 91 | get(conn, video_path(conn, :edit, other_video)) 92 | end 93 | 94 | assert_error_sent :not_found, fn -> 95 | put(conn, video_path(conn, :update, other_video, video: @valid_attrs)) 96 | end 97 | 98 | assert_error_sent :not_found, fn -> 99 | delete(conn, video_path(conn, :update, other_video)) 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /rumbl/test/models/category_repo_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.CategoryRepoTest do 2 | use Rumbl.ModelCase 3 | alias Rumbl.Category 4 | 5 | test "alphabetical/1 orders by name" do 6 | Repo.insert!(%Category{name: "c"}) 7 | Repo.insert!(%Category{name: "a"}) 8 | Repo.insert!(%Category{name: "b"}) 9 | 10 | query = Category |> Category.alphabetical() 11 | query = from c in query, select: c.name 12 | assert Repo.all(query) == ~w(a b c) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /rumbl/test/models/category_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.CategoryTest do 2 | use Rumbl.ModelCase 3 | 4 | alias Rumbl.Category 5 | 6 | @valid_attrs %{name: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Category.changeset(%Category{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Category.changeset(%Category{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /rumbl/test/models/user_repo_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.UserRepoTest do 2 | use Rumbl.ModelCase 3 | alias Rumbl.User 4 | 5 | @valid_attrs %{name: "Batman", username: "batman"} 6 | 7 | test "converts unique_constraint on username to error" do 8 | insert_user(username: "batman") 9 | attrs = Map.put(@valid_attrs, :username, "batman") 10 | changeset = User.changeset(%User{}, attrs) 11 | 12 | assert {:error, changeset} = Repo.insert(changeset) 13 | assert {:username, "has already been taken"} in changeset.errors 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /rumbl/test/models/user_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.UserTest do 2 | use Rumbl.ModelCase, async: true 3 | alias Rumbl.User 4 | 5 | @valid_attrs %{name: "Batman", username: "batman", password: "supersecret"} 6 | @invalid_attrs %{} 7 | 8 | test "changeset with valid attributes" do 9 | changeset = User.changeset(%User{}, @valid_attrs) 10 | assert changeset.valid? 11 | end 12 | 13 | test "changeset with invalid attributes" do 14 | changeset = User.changeset(%User{}, @invalid_attrs) 15 | refute changeset.valid? 16 | end 17 | 18 | test "changeset does not accept short usernames" do 19 | attrs = Map.put(@valid_attrs, :username, "no") 20 | assert { 21 | :username, 22 | {"should be at least %{count} character(s)", [count: 3]} 23 | } in errors_on(%User{}, attrs) 24 | end 25 | 26 | test "registration changeset password must be at least 6 characters" do 27 | attrs = Map.put(@valid_attrs, :password, "12345") 28 | changeset = User.registration_changeset(%User{}, attrs) 29 | assert { 30 | :password, 31 | {"should be at least %{count} character(s)", [count: 6]} 32 | } in changeset.errors 33 | end 34 | 35 | test "registration changeset with valid attributes hashes password" do 36 | attrs = Map.put(@valid_attrs, :password, "123456") 37 | changeset = User.registration_changeset(%User{}, attrs) 38 | %{password: pass, password_hash: pass_hash} = changeset.changes 39 | 40 | assert changeset.valid? 41 | assert pass_hash 42 | assert Comeonin.Bcrypt.checkpw(pass, pass_hash) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /rumbl/test/models/video_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.VideoTest do 2 | use Rumbl.ModelCase 3 | 4 | alias Rumbl.Video 5 | 6 | @valid_attrs %{description: "some content", title: "some content", url: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Video.changeset(%Video{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Video.changeset(%Video{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /rumbl/test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | imports other functionality to make it easier 8 | to build and query models. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | alias Rumbl.Repo 24 | import Ecto 25 | import Ecto.Changeset 26 | import Ecto.Query, only: [from: 1, from: 2] 27 | 28 | 29 | # The default endpoint for testing 30 | @endpoint Rumbl.Endpoint 31 | end 32 | end 33 | 34 | setup tags do 35 | unless tags[:async] do 36 | Ecto.Adapters.SQL.restart_test_transaction(Rumbl.Repo, []) 37 | end 38 | 39 | :ok 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /rumbl/test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | imports other functionality to make it easier 8 | to build and query models. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with connections 21 | use Phoenix.ConnTest 22 | 23 | alias Rumbl.Repo 24 | import Ecto 25 | import Ecto.Changeset 26 | import Ecto.Query, only: [from: 1, from: 2] 27 | 28 | import Rumbl.Router.Helpers 29 | import Rumbl.TestHelpers 30 | 31 | # The default endpoint for testing 32 | @endpoint Rumbl.Endpoint 33 | end 34 | end 35 | 36 | setup tags do 37 | unless tags[:async] do 38 | Ecto.Adapters.SQL.restart_test_transaction(Rumbl.Repo, []) 39 | end 40 | 41 | {:ok, conn: Phoenix.ConnTest.conn()} 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /rumbl/test/support/model_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.ModelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | model tests. 5 | 6 | You may define functions here to be used as helpers in 7 | your model tests. See `errors_on/2`'s definition as reference. 8 | 9 | Finally, if the test case interacts with the database, 10 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | using do 18 | quote do 19 | alias Rumbl.Repo 20 | 21 | import Ecto 22 | import Ecto.Changeset 23 | import Ecto.Query, only: [from: 1, from: 2] 24 | import Rumbl.ModelCase 25 | import Rumbl.TestHelpers 26 | end 27 | end 28 | 29 | setup tags do 30 | unless tags[:async] do 31 | Ecto.Adapters.SQL.restart_test_transaction(Rumbl.Repo, []) 32 | end 33 | 34 | :ok 35 | end 36 | 37 | @doc """ 38 | Helper for returning list of errors in model when passed certain data. 39 | 40 | ## Examples 41 | 42 | Given a User model that lists `:name` as a required field and validates 43 | `:password` to be safe, it would return: 44 | 45 | iex> errors_on(%User{}, %{password: "password"}) 46 | [password: "is unsafe", name: "is blank"] 47 | 48 | You could then write your assertion like: 49 | 50 | assert {:password, "is unsafe"} in errors_on(%User{}, %{password: "password"}) 51 | 52 | You can also create the changeset manually and retrieve the errors 53 | field directly: 54 | 55 | iex> changeset = User.changeset(%User{}, password: "password") 56 | iex> {:password, "is unsafe"} in changeset.errors 57 | true 58 | """ 59 | def errors_on(model, data) do 60 | model.__struct__.changeset(model, data).errors 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /rumbl/test/support/test_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.TestHelpers do 2 | alias Rumbl.Repo 3 | 4 | def insert_user(attrs \\ %{}) do 5 | # NOTE: even though Dict is deprecated, getting intermittent errors with 6 | # this function when using Map.merge 7 | changes = Dict.merge(%{ 8 | name: "Example User", 9 | username: "user-#{random_hash}", 10 | password: "supersecret" 11 | }, attrs) 12 | 13 | %Rumbl.User{} 14 | |> Rumbl.User.registration_changeset(changes) 15 | |> Repo.insert! 16 | end 17 | 18 | def insert_video(user, attrs \\ %{}) do 19 | changeset = Ecto.build_assoc(user, :videos, attrs) 20 | Rumbl.Repo.insert!(changeset) 21 | end 22 | 23 | defp random_hash do 24 | Base.encode16(:crypto.rand_bytes(8)) 25 | |> String.downcase 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /rumbl/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | ExUnit.configure trace: true 3 | 4 | Mix.Task.run "ecto.create", ~w(-r Rumbl.Repo --quiet) 5 | Mix.Task.run "ecto.migrate", ~w(-r Rumbl.Repo --quiet) 6 | Ecto.Adapters.SQL.begin_test_transaction(Rumbl.Repo) 7 | -------------------------------------------------------------------------------- /rumbl/test/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.ErrorViewTest do 2 | use Rumbl.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(Rumbl.ErrorView, "404.html", []) == 9 | "Page not found" 10 | end 11 | 12 | test "render 500.html" do 13 | assert render_to_string(Rumbl.ErrorView, "500.html", []) == 14 | "Server internal error" 15 | end 16 | 17 | test "render any other" do 18 | assert render_to_string(Rumbl.ErrorView, "505.html", []) == 19 | "Server internal error" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /rumbl/test/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.LayoutViewTest do 2 | use Rumbl.ConnCase, async: true 3 | end -------------------------------------------------------------------------------- /rumbl/test/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.PageViewTest do 2 | use Rumbl.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /rumbl/test/views/video_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.VideoViewTest do 2 | use Rumbl.ConnCase, async: true 3 | import Phoenix.View 4 | alias Rumbl.Video 5 | alias Rumbl.VideoView 6 | 7 | test "renders index.html", %{conn: conn} do 8 | videos = [ 9 | %Video{id: "1", title: "dogs"}, 10 | %Video{id: "2", title: "cats"}, 11 | ] 12 | 13 | content = render_to_string(VideoView, "index.html", conn: conn, videos: videos) 14 | 15 | assert String.contains?(content, "Listing videos") 16 | for video <- videos do 17 | assert String.contains?(content, video.title) 18 | end 19 | end 20 | 21 | test "renders new.html", %{conn: conn} do 22 | changeset = Video.changeset(%Video{}) 23 | categories = [{"cats", 123}] 24 | 25 | content = render_to_string(VideoView, "new.html", 26 | conn: conn, 27 | changeset: changeset, 28 | categories: categories 29 | ) 30 | 31 | assert String.contains?(content, "New video") 32 | assert String.contains?(content, "Choose a category") 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /rumbl/web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "rooms:*", Rumbl.RoomChannel 6 | 7 | ## Transports 8 | transport :websocket, Phoenix.Transports.WebSocket 9 | # transport :longpoll, Phoenix.Transports.LongPoll 10 | 11 | # Socket params are passed from the client and can 12 | # be used to verify and authenticate a user. After 13 | # verification, you can put default assigns into 14 | # the socket that will be set for all channels, ie 15 | # 16 | # {:ok, assign(socket, :user_id, verified_user_id)} 17 | # 18 | # To deny connection, return `:error`. 19 | # 20 | # See `Phoenix.Token` documentation for examples in 21 | # performing token verification on connect. 22 | def connect(_params, socket) do 23 | {:ok, socket} 24 | end 25 | 26 | # Socket id's are topics that allow you to identify all sockets for a given user: 27 | # 28 | # def id(socket), do: "users_socket:#{socket.assigns.user_id}" 29 | # 30 | # Would allow you to broadcast a "disconnect" event and terminate 31 | # all active sockets and channels for a given user: 32 | # 33 | # Rumbl.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{}) 34 | # 35 | # Returning `nil` makes this socket anonymous. 36 | def id(_socket), do: nil 37 | end 38 | -------------------------------------------------------------------------------- /rumbl/web/controllers/auth.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.Auth do 2 | import Plug.Conn 3 | import Comeonin.Bcrypt, only: [checkpw: 2, dummy_checkpw: 0] 4 | import Phoenix.Controller 5 | alias Rumbl.Router.Helpers 6 | alias Rumbl.User 7 | 8 | def init(opts) do 9 | Keyword.fetch!(opts, :repo) 10 | end 11 | 12 | def call(conn, repo) do 13 | user_id = get_session(conn, :user_id) 14 | cond do 15 | user = current_user(conn) -> 16 | conn 17 | user = user_id && repo.get(User, user_id) -> 18 | assign(conn, :current_user, user) 19 | true -> 20 | assign(conn, :current_user, nil) 21 | end 22 | end 23 | 24 | def current_user(conn) do 25 | conn.assigns[:current_user] 26 | end 27 | 28 | def authenticate_user(conn, _opts) do 29 | if current_user(conn) do 30 | conn 31 | else 32 | conn 33 | |> put_flash(:error, "You must be logged in to access that page") 34 | |> redirect(to: Helpers.page_path(conn, :index)) 35 | |> halt() 36 | end 37 | end 38 | 39 | def login_by_username_and_pass(conn, username, given_pass, opts) do 40 | repo = Keyword.fetch!(opts, :repo) 41 | user = repo.get_by(User, username: username) 42 | cond do 43 | user && checkpw(given_pass, user.password_hash) -> 44 | {:ok, login(conn, user)} 45 | user -> 46 | {:error, :unauthorized, conn} 47 | true -> 48 | dummy_checkpw() 49 | {:error, :not_found, conn} 50 | end 51 | end 52 | 53 | def login(conn, user = %User{id: user_id}) do 54 | conn 55 | |> assign(:current_user, user) 56 | |> put_session(:user_id, user_id) 57 | |> configure_session(renew: true) 58 | end 59 | 60 | def logout(conn) do 61 | conn 62 | |> delete_assign(:current_user) 63 | |> delete_session(:user_id) 64 | end 65 | 66 | defp delete_assign(conn, key) do 67 | Map.update!(conn, :assigns, &Map.delete(&1, key)) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /rumbl/web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.PageController do 2 | use Rumbl.Web, :controller 3 | 4 | def index(conn, _params) do 5 | render conn, "index.html" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /rumbl/web/controllers/session_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.SessionController do 2 | use Rumbl.Web, :controller 3 | plug :scrub_params, "session" when action in [:create] 4 | 5 | def create(conn, %{"session" => %{"username" => user, "password" => pass}}) do 6 | case Rumbl.Auth.login_by_username_and_pass(conn, user, pass, repo: Repo) do 7 | {:ok, conn} -> 8 | conn 9 | |> put_flash(:info, "Welcome back!") 10 | |> redirect(to: page_path(conn, :index)) 11 | {:error, _reason, conn} -> 12 | conn 13 | |> put_flash(:error, "Invalid username/password combination") 14 | |> render("new.html") 15 | end 16 | end 17 | 18 | def delete(conn, _params) do 19 | conn 20 | |> Rumbl.Auth.logout() 21 | |> put_flash(:info, "You have been logged out") 22 | |> redirect(to: page_path(conn, :index)) 23 | end 24 | 25 | def new(conn, _params) do 26 | render(conn, "new.html") 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /rumbl/web/controllers/user_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.UserController do 2 | use Rumbl.Web, :controller 3 | alias Rumbl.User 4 | plug :scrub_params, "user" when action in [:create, :update] 5 | plug :authenticate_user when action in [:index, :show] 6 | 7 | def create(conn, %{"user" => user_params}) do 8 | changeset = User.registration_changeset(%User{}, user_params) 9 | case Repo.insert(changeset) do 10 | {:ok, user} -> 11 | conn 12 | |> Rumbl.Auth.login(user) 13 | |> put_flash(:info, "#{user.name} created!") 14 | |> redirect(to: user_path(conn, :index)) 15 | {:error, changeset} -> 16 | render(conn, "new.html", changeset: changeset) 17 | end 18 | end 19 | 20 | def index(conn, _params) do 21 | users = Repo.all(User) 22 | render(conn, "index.html", users: users) 23 | end 24 | 25 | def new(conn, _params) do 26 | changeset = User.changeset(%User{}) 27 | render(conn, "new.html", changeset: changeset) 28 | end 29 | 30 | def show(conn, %{"username" => username}) do 31 | user = Repo.get_by(User, username: username) 32 | render(conn, "show.html", user: user) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /rumbl/web/controllers/video_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.VideoController do 2 | use Rumbl.Web, :controller 3 | alias Rumbl.Video 4 | alias Rumbl.Category 5 | plug :scrub_params, "video" when action in [:create, :update] 6 | plug :load_categories when action in [:create, :edit, :new, :update] 7 | 8 | def index(conn, _params) do 9 | videos = user_videos(conn) |> Repo.all |> Repo.preload(:category) 10 | render(conn, "index.html", videos: videos) 11 | end 12 | 13 | def new(conn, _params) do 14 | changeset = Video.changeset(%Video{}) 15 | render(conn, "new.html", changeset: changeset) 16 | end 17 | 18 | def create(conn, %{"video" => video_params}) do 19 | changeset = current_user(conn) 20 | |> build_assoc(:videos) 21 | |> Video.changeset(video_params) 22 | 23 | case Repo.insert(changeset) do 24 | {:ok, _video} -> 25 | conn 26 | |> put_flash(:info, "Video created successfully.") 27 | |> redirect(to: video_path(conn, :index)) 28 | {:error, changeset} -> 29 | render(conn, "new.html", changeset: changeset) 30 | end 31 | end 32 | 33 | def show(conn, %{"id" => id}) do 34 | video = get_from_user_videos!(conn, id) 35 | render(conn, "show.html", video: video) 36 | end 37 | 38 | def edit(conn, %{"id" => id}) do 39 | video = get_from_user_videos!(conn, id) 40 | changeset = Video.changeset(video) 41 | render(conn, "edit.html", video: video, changeset: changeset) 42 | end 43 | 44 | def update(conn, %{"id" => id, "video" => video_params}) do 45 | video = get_from_user_videos!(conn, id) 46 | changeset = Video.changeset(video, video_params) 47 | 48 | case Repo.update(changeset) do 49 | {:ok, video} -> 50 | conn 51 | |> put_flash(:info, "Video updated successfully.") 52 | |> redirect(to: video_path(conn, :show, video)) 53 | {:error, changeset} -> 54 | render(conn, "edit.html", video: video, changeset: changeset) 55 | end 56 | end 57 | 58 | def delete(conn, %{"id" => id}) do 59 | video = get_from_user_videos!(conn, id) 60 | Repo.delete!(video) 61 | 62 | conn 63 | |> put_flash(:info, "Video deleted successfully.") 64 | |> redirect(to: video_path(conn, :index)) 65 | end 66 | 67 | defp user_videos(conn) do 68 | assoc(current_user(conn), :videos) 69 | end 70 | 71 | defp get_from_user_videos!(conn, id) do 72 | Repo.get_by_uuid!(user_videos(conn), id) 73 | end 74 | 75 | defp load_categories(conn, _) do 76 | query = Category 77 | |> Category.alphabetical 78 | |> Category.names_and_ids 79 | categories = Repo.all(query) 80 | assign(conn, :categories, categories) 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /rumbl/web/controllers/watch_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.WatchController do 2 | use Rumbl.Web, :controller 3 | alias Rumbl.Video 4 | 5 | def show(conn, %{"id" => id}) do 6 | video = Repo.get_by_uuid!(Video, id) 7 | render(conn, "show.html", video: video) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /rumbl/web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](http://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import Rumbl.Gettext 9 | 10 | # Simple translation 11 | gettext "Here is the string to translate" 12 | 13 | # Plural translation 14 | ngettext "Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3 17 | 18 | # Domain-based translation 19 | dgettext "errors", "Here is the error message to translate" 20 | 21 | See the [Gettext Docs](http://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :rumbl 24 | end 25 | -------------------------------------------------------------------------------- /rumbl/web/graphql/schema.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.Schema do 2 | import Ecto.Query, only: [from: 2] 3 | alias Rumbl.Repo 4 | alias GraphQL.Schema 5 | alias GraphQL.Type.ObjectType 6 | alias GraphQL.Type.List 7 | alias GraphQL.Type.String 8 | alias GraphQL.Type.ID 9 | 10 | @moduledoc """ 11 | This module configures the Rumbl GraphQL implementation. 12 | 13 | example queries: 14 | { 15 | user(username:"sean") { 16 | name 17 | } 18 | 19 | user(id: "63254a3f-76fa-469a-9132-7224ffcbb9b1") { 20 | username 21 | } 22 | 23 | users { 24 | id, 25 | name, 26 | videos(category: "elixir") { 27 | title, 28 | user { 29 | id, 30 | name 31 | } 32 | } 33 | } 34 | 35 | video(id: "6076fed9-d2d0-43d3-965c-077359a65638") { 36 | id, 37 | title 38 | } 39 | 40 | videos { 41 | title 42 | } 43 | 44 | categories { 45 | id, 46 | name 47 | } 48 | } 49 | """ 50 | 51 | def schema do 52 | %Schema{ 53 | query: %ObjectType{ 54 | name: "API", 55 | fields: %{ 56 | user: user_query, 57 | users: users_query, 58 | video: video_query, 59 | videos: videos_query, 60 | categories: categories_query 61 | } 62 | } 63 | } 64 | end 65 | 66 | # QUERIES 67 | # ------- 68 | 69 | defp user_query do 70 | %{ 71 | type: user_type, 72 | args: %{ 73 | id: %{type: %String{}}, 74 | username: %{type: %String{}} 75 | }, 76 | resolve: {Rumbl.Schema, :user} 77 | } 78 | end 79 | 80 | defp users_query do 81 | %{ 82 | type: %List{ofType: user_type}, 83 | resolve: {Rumbl.Schema, :users} 84 | } 85 | end 86 | 87 | 88 | defp video_query do 89 | %{ 90 | type: video_type, 91 | args: %{ 92 | id: %{type: %String{}} 93 | }, 94 | resolve: {Rumbl.Schema, :video} 95 | } 96 | end 97 | 98 | defp videos_query do 99 | %{ 100 | type: %List{ofType: video_type}, 101 | args: %{ 102 | category: %{type: %String{}} 103 | }, 104 | resolve: {Rumbl.Schema, :videos} 105 | } 106 | end 107 | 108 | defp categories_query do 109 | %{ 110 | type: %List{ofType: category_type}, 111 | resolve: {Rumbl.Schema, :categories} 112 | } 113 | end 114 | 115 | # TYPES 116 | # ----- 117 | 118 | defp user_type do 119 | %ObjectType{ 120 | name: "User", 121 | fields: %{ 122 | id: %{type: %String{}}, 123 | username: %{type: %String{}}, 124 | name: %{type: %String{}}, 125 | videos: %{ 126 | args: %{ 127 | category: %{type: %String{}} 128 | }, 129 | type: %List{ofType: video_type}, 130 | resolve: {Rumbl.Schema, :videos} 131 | }, 132 | } 133 | } 134 | end 135 | 136 | defp video_type do 137 | %ObjectType{ 138 | name: "Video", 139 | fields: %{ 140 | id: %{type: %String{}}, 141 | url: %{type: %String{}}, 142 | title: %{type: %String{}}, 143 | user: %{ 144 | type: video_user_type, 145 | resolve: {Rumbl.Schema, :user} 146 | }, 147 | category: %{ 148 | type: %String{}, 149 | resolve: {Rumbl.Schema, :category} 150 | }, 151 | } 152 | } 153 | end 154 | 155 | defp video_user_type do 156 | %ObjectType{ 157 | name: "User", 158 | fields: %{ 159 | id: %{type: %String{}}, 160 | username: %{type: %String{}}, 161 | name: %{type: %String{}} 162 | } 163 | } 164 | end 165 | 166 | defp category_type do 167 | %ObjectType{ 168 | name: "Category", 169 | fields: %{ 170 | id: %{type: %String{}}, 171 | name: %{type: %String{}}, 172 | } 173 | } 174 | end 175 | 176 | # RESOLVERS 177 | # --------- 178 | 179 | def users(_, _, _) do 180 | Rumbl.Repo.all(Rumbl.User) 181 | end 182 | 183 | def user(video = %Rumbl.Video{}, _, _) do 184 | Repo.one Ecto.assoc(video, :user) 185 | end 186 | 187 | def user(_, %{id: user_id}, _) do 188 | Repo.get_by_uuid(Rumbl.User, user_id) 189 | end 190 | 191 | def user(_, %{username: username}, _) do 192 | Repo.one from u in Rumbl.User, 193 | where: ilike(u.username, ^username) 194 | end 195 | 196 | def videos(user = %Rumbl.User{}, %{category: category}, _) do 197 | Repo.all from v in Ecto.assoc(user, :videos), 198 | join: c in assoc(v, :category), 199 | where: ilike(c.name, ^category) 200 | end 201 | 202 | def videos(user = %Rumbl.User{}, _, _) do 203 | Repo.all Ecto.assoc(user, :videos) 204 | end 205 | 206 | def videos(_, %{category: category}, _) do 207 | Repo.all from v in Rumbl.Video, 208 | join: c in assoc(v, :category), 209 | where: ilike(c.name, ^category) 210 | end 211 | 212 | def videos(_, _, _) do 213 | Repo.all(Rumbl.Video) 214 | end 215 | 216 | def video(_, %{id: video_id}, _) do 217 | Repo.get_by_uuid(Rumbl.Video, video_id) 218 | end 219 | 220 | def categories(_, _, _) do 221 | Repo.all(Rumbl.Category) 222 | end 223 | 224 | def category(video = %Rumbl.Video{}, _, _) do 225 | Repo.one from c in Ecto.assoc(video, :category), 226 | select: c.name 227 | end 228 | end 229 | -------------------------------------------------------------------------------- /rumbl/web/models/category.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.Category do 2 | use Rumbl.Web, :model 3 | 4 | @primary_key {:id, :binary_id, autogenerate: true} 5 | 6 | schema "categories" do 7 | field :name, :string 8 | has_many :videos, Rumbl.Video 9 | 10 | timestamps 11 | end 12 | 13 | @required_fields ~w(name) 14 | @optional_fields ~w() 15 | 16 | def changeset(model, params \\ :empty) do 17 | model 18 | |> cast(params, @required_fields, @optional_fields) 19 | end 20 | 21 | def alphabetical(query) do 22 | from c in query, order_by: c.name 23 | end 24 | 25 | def names_and_ids(query) do 26 | from c in query, select: {c.name, c.id} 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /rumbl/web/models/user.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.User do 2 | use Rumbl.Web, :model 3 | 4 | @primary_key {:id, :binary_id, autogenerate: true} 5 | 6 | schema "users" do 7 | field :name, :string 8 | field :username, :string 9 | field :password, :string, virtual: true 10 | field :password_hash, :string 11 | has_many :videos, Rumbl.Video 12 | 13 | timestamps 14 | end 15 | 16 | def changeset(model, params \\ :empty) do 17 | model 18 | |> cast(params, ~w(name username), []) 19 | |> validate_length(:username, min: 3, max: 24) 20 | |> validate_exclusion(:username, ~w(new)) 21 | |> unique_constraint(:username) 22 | end 23 | 24 | def registration_changeset(model, params) do 25 | model 26 | |> changeset(params) 27 | |> cast(params, ~w(password), []) 28 | |> validate_length(:password, min: 6, max: 128) 29 | |> put_pass_hash() 30 | end 31 | 32 | defp put_pass_hash(changeset) do 33 | case changeset do 34 | %Ecto.Changeset{valid?: true, changes: %{password: pass}} -> 35 | put_change(changeset, :password_hash, Comeonin.Bcrypt.hashpwsalt(pass)) 36 | _ -> 37 | changeset 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /rumbl/web/models/video.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.Video do 2 | use Rumbl.Web, :model 3 | 4 | @primary_key {:id, :binary_id, autogenerate: true} 5 | 6 | schema "videos" do 7 | field :url, :string 8 | field :title, :string 9 | field :description, :string 10 | belongs_to :user, Rumbl.User, type: :binary_id 11 | belongs_to :category, Rumbl.Category, type: :binary_id 12 | 13 | timestamps 14 | end 15 | 16 | @required_fields ~w(url title description) 17 | @optional_fields ~w(category_id) 18 | 19 | def changeset(model, params \\ :empty) do 20 | model 21 | |> cast(params, @required_fields, @optional_fields) 22 | |> assoc_constraint(:category) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /rumbl/web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.Router do 2 | use Rumbl.Web, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_flash 8 | plug :protect_from_forgery 9 | plug :put_secure_browser_headers 10 | plug Rumbl.Auth, repo: Rumbl.Repo 11 | end 12 | 13 | pipeline :api do 14 | plug :accepts, ["json"] 15 | end 16 | 17 | scope "/", Rumbl do 18 | pipe_through :browser # Use the default browser stack 19 | 20 | get "/", PageController, :index 21 | resources "/users", UserController, only: [:create, :index, :new] 22 | get "/users/:username", UserController, :show 23 | resources "/sessions", SessionController, only: [:new, :create, :delete] 24 | get "/watch/:id", WatchController, :show 25 | end 26 | 27 | scope "/manage", Rumbl do 28 | pipe_through [:browser, :authenticate_user] 29 | 30 | resources "/videos", VideoController 31 | end 32 | 33 | scope "/api" do 34 | pipe_through :api 35 | 36 | forward "/", GraphQL.Plug, schema: {Rumbl.Schema, :schema} 37 | end 38 | 39 | # Other scopes may use custom stacks. 40 | # scope "/api", Rumbl do 41 | # pipe_through :api 42 | # end 43 | end 44 | -------------------------------------------------------------------------------- /rumbl/web/static/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauntedhost/programming-phoenix/37bf009a2649a245a6027ea7bcbb92d8e36169de/rumbl/web/static/assets/favicon.ico -------------------------------------------------------------------------------- /rumbl/web/static/assets/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauntedhost/programming-phoenix/37bf009a2649a245a6027ea7bcbb92d8e36169de/rumbl/web/static/assets/images/phoenix.png -------------------------------------------------------------------------------- /rumbl/web/static/assets/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /rumbl/web/static/js/app.js: -------------------------------------------------------------------------------- 1 | // Brunch automatically concatenates all files in your 2 | // watched paths. Those paths can be configured at 3 | // config.paths.watched in "brunch-config.js". 4 | // 5 | // However, those files will only be executed if 6 | // explicitly imported. The only exception are files 7 | // in vendor, which are never wrapped in imports and 8 | // therefore are always executed. 9 | 10 | // Import dependencies 11 | // 12 | // If you no longer want to use a dependency, remember 13 | // to also remove its path from "config.paths.watched". 14 | import "phoenix_html" 15 | 16 | // Import local files 17 | // 18 | // Local files can be imported directly using relative 19 | // paths "./socket" or full ones "web/static/js/socket". 20 | 21 | // import socket from "./socket" 22 | -------------------------------------------------------------------------------- /rumbl/web/static/js/socket.js: -------------------------------------------------------------------------------- 1 | // NOTE: The contents of this file will only be executed if 2 | // you uncomment its entry in "web/static/js/app.js". 3 | 4 | // To use Phoenix channels, the first step is to import Socket 5 | // and connect at the socket path in "lib/my_app/endpoint.ex": 6 | import {Socket} from "phoenix" 7 | 8 | let socket = new Socket("/socket", {params: {token: window.userToken}}) 9 | 10 | // When you connect, you'll often need to authenticate the client. 11 | // For example, imagine you have an authentication plug, `MyAuth`, 12 | // which authenticates the session and assigns a `:current_user`. 13 | // If the current user exists you can assign the user's token in 14 | // the connection for use in the layout. 15 | // 16 | // In your "web/router.ex": 17 | // 18 | // pipeline :browser do 19 | // ... 20 | // plug MyAuth 21 | // plug :put_user_token 22 | // end 23 | // 24 | // defp put_user_token(conn, _) do 25 | // if current_user = conn.assigns[:current_user] do 26 | // token = Phoenix.Token.sign(conn, "user socket", current_user.id) 27 | // assign(conn, :user_token, token) 28 | // else 29 | // conn 30 | // end 31 | // end 32 | // 33 | // Now you need to pass this token to JavaScript. You can do so 34 | // inside a script tag in "web/templates/layout/app.html.eex": 35 | // 36 | // 37 | // 38 | // You will need to verify the user token in the "connect/2" function 39 | // in "web/channels/user_socket.ex": 40 | // 41 | // def connect(%{"token" => token}, socket) do 42 | // # max_age: 1209600 is equivalent to two weeks in seconds 43 | // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do 44 | // {:ok, user_id} -> 45 | // {:ok, assign(socket, :user, user_id)} 46 | // {:error, reason} -> 47 | // :error 48 | // end 49 | // end 50 | // 51 | // Finally, pass the token on connect as below. Or remove it 52 | // from connect if you don't care about authentication. 53 | 54 | socket.connect() 55 | 56 | // Now that you are connected, you can join channels with a topic: 57 | let channel = socket.channel("topic:subtopic", {}) 58 | channel.join() 59 | .receive("ok", resp => { console.log("Joined successfully", resp) }) 60 | .receive("error", resp => { console.log("Unable to join", resp) }) 61 | 62 | export default socket 63 | -------------------------------------------------------------------------------- /rumbl/web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Hello Rumbl! 11 | "> 12 | 13 | 14 | 15 |
16 |
17 | 31 | 32 |
33 | 34 | 35 | 36 | 37 |
38 | <%= render @view_module, @view_template, assigns %> 39 |
40 | 41 |
42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /rumbl/web/templates/page/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |

Welcome to Rumbl.io!

3 |

Rumble out loud.

4 |
5 | -------------------------------------------------------------------------------- /rumbl/web/templates/session/new.html.eex: -------------------------------------------------------------------------------- 1 |

Login

2 | 3 | <%= form_for(@conn, session_path(@conn, :create), [as: :session], fn(f) -> %> 4 |
5 | <%= text_input(f, :username, placeholder: "Username", class: "form-control") %> 6 |
7 |
8 | <%= password_input(f, :password, placeholder: "Password", class: "form-control") %> 9 |
10 | <%= submit("Log in", class: "btn btn-primary") %> 11 | <% end) %> 12 | -------------------------------------------------------------------------------- /rumbl/web/templates/user/index.html.eex: -------------------------------------------------------------------------------- 1 |

Users:

2 | 3 | 4 | <%= for user <- @users do %> 5 | 6 | 9 | 14 | 17 | 18 | <% end %> 19 |
7 | <%= user.id %> 8 | 10 | 11 | <%= first_name(user) %> 12 | 13 | 15 | <%= link("View", to: user_path(@conn, :show, user.username)) %> 16 |
20 | -------------------------------------------------------------------------------- /rumbl/web/templates/user/new.html.eex: -------------------------------------------------------------------------------- 1 |

New User

2 | 3 | <%= if @changeset.action do %> 4 |
5 |

6 | Oops, something went wrong! Please check the errors below. 7 |

8 |
9 | <% end %> 10 | 11 | <%= form_for(@changeset, user_path(@conn, :create), fn(f) -> %> 12 |
13 | <%= text_input(f, :name, placeholder: "Name", class: "form-control") %> 14 | <%= error_tag(f, :name) %> 15 |
16 |
17 | <%= text_input(f, :username, placeholder: "Username", class: "form-control") %> 18 | <%= error_tag(f, :username) %> 19 |
20 |
21 | <%= password_input(f, :password, placeholder: "Password", class: "form-control") %> 22 | <%= error_tag(f, :password) %> 23 |
24 | 25 | <%= submit("Create User", class: "btn btn-primary")%> 26 | <% end) %> 27 | -------------------------------------------------------------------------------- /rumbl/web/templates/user/show.html.eex: -------------------------------------------------------------------------------- 1 |

User:

2 | <%= render("user.html", user: @user) %> 3 | -------------------------------------------------------------------------------- /rumbl/web/templates/user/user.html.eex: -------------------------------------------------------------------------------- 1 | Username: <%= @user.username %>
2 | Name: <%= @user.name %>
3 | -------------------------------------------------------------------------------- /rumbl/web/templates/video/edit.html.eex: -------------------------------------------------------------------------------- 1 |

Edit video

2 | 3 | <%= render "form.html", 4 | changeset: @changeset, 5 | categories: @categories, 6 | action: video_path(@conn, :update, @video) %> 7 | 8 | <%= link "Back", to: video_path(@conn, :index) %> 9 | -------------------------------------------------------------------------------- /rumbl/web/templates/video/form.html.eex: -------------------------------------------------------------------------------- 1 | <%= form_for @changeset, @action, fn f -> %> 2 | <%= if @changeset.action do %> 3 |
4 |

Oops, something went wrong! Please check the errors below.

5 |
6 | <% end %> 7 | 8 |
9 | <%= label(f, :url, class: "control-label") %> 10 | <%= text_input(f, :url, class: "form-control") %> 11 | <%= error_tag(f, :url) %> 12 |
13 | 14 |
15 | <%= label(f, :category_id, "Category", class: "control-label") %> 16 | <%= select(f, :category_id, @categories, 17 | class: "form-control", 18 | prompt: "Choose a category") %> 19 | <%= error_tag(f, :category) %> 20 |
21 | 22 |
23 | <%= label(f, :title, class: "control-label") %> 24 | <%= text_input(f, :title, class: "form-control") %> 25 | <%= error_tag(f, :title) %> 26 |
27 | 28 |
29 | <%= label(f, :description, class: "control-label") %> 30 | <%= textarea(f, :description, class: "form-control") %> 31 | <%= error_tag(f, :description) %> 32 |
33 | 34 |
35 | <%= submit("Submit", class: "btn btn-primary") %> 36 |
37 | <% end %> 38 | -------------------------------------------------------------------------------- /rumbl/web/templates/video/index.html.eex: -------------------------------------------------------------------------------- 1 |

Listing videos

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | <%= for video <- @videos do %> 13 | 14 | 15 | 16 | 29 | 30 | <% end %> 31 | 32 |
CategoryTitle
<%= category_name(video.category) %><%= video.title %> 17 | <%= link "Watch", 18 | to: watch_path(@conn, :show, video), 19 | class: "btn btn-default btn-xs" %> 20 | <%= link "Edit", 21 | to: video_path(@conn, :edit, video), 22 | class: "btn btn-default btn-xs" %> 23 | <%= link "Delete", 24 | to: video_path(@conn, :delete, video), 25 | method: :delete, 26 | data: [confirm: "Are you sure?"], 27 | class: "btn btn-danger btn-xs" %> 28 |
33 | 34 | <%= link "New video", to: video_path(@conn, :new) %> 35 | -------------------------------------------------------------------------------- /rumbl/web/templates/video/new.html.eex: -------------------------------------------------------------------------------- 1 |

New video

2 | 3 | <%= render "form.html", 4 | changeset: @changeset, 5 | categories: @categories, 6 | action: video_path(@conn, :create) %> 7 | 8 | <%= link "Back", to: video_path(@conn, :index) %> 9 | -------------------------------------------------------------------------------- /rumbl/web/templates/video/show.html.eex: -------------------------------------------------------------------------------- 1 |

Show video

2 | 3 | 26 | 27 | <%= link "Edit", to: video_path(@conn, :edit, @video) %> 28 | <%= link "Back", to: video_path(@conn, :index) %> 29 | -------------------------------------------------------------------------------- /rumbl/web/templates/watch/show.html.eex: -------------------------------------------------------------------------------- 1 |

<%= @video.title %>

2 |
3 |
4 | <%= content_tag :div, 5 | id: "video", 6 | data: [ 7 | id: @video.id, 8 | player_id: player_id(@video) 9 | ] do %> 10 | <% end %> 11 |
12 |
13 |
14 |
15 |

Annotations

16 |
17 |
18 |
19 | 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /rumbl/web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | if error = form.errors[field] do 13 | content_tag :span, translate_error(error), class: "help-block" 14 | end 15 | end 16 | 17 | @doc """ 18 | Translates an error message using gettext. 19 | """ 20 | def translate_error({msg, opts}) do 21 | # Because error messages were defined within Ecto, we must 22 | # call the Gettext module passing our Gettext backend. We 23 | # also use the "errors" domain as translations are placed 24 | # in the errors.po file. On your own code and templates, 25 | # this could be written simply as: 26 | # 27 | # dngettext "errors", "1 file", "%{count} files", count 28 | # 29 | Gettext.dngettext(Rumbl.Gettext, "errors", msg, msg, opts[:count], opts) 30 | end 31 | 32 | def translate_error(msg) do 33 | Gettext.dgettext(Rumbl.Gettext, "errors", msg) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /rumbl/web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.ErrorView do 2 | use Rumbl.Web, :view 3 | 4 | def render("404.html", _assigns) do 5 | "Page not found" 6 | end 7 | 8 | def render("500.html", _assigns) do 9 | "Server internal error" 10 | end 11 | 12 | # In case no render clause matches or no 13 | # template is found, let's render it as 500 14 | def template_not_found(_template, assigns) do 15 | render "500.html", assigns 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /rumbl/web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.LayoutView do 2 | use Rumbl.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /rumbl/web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.PageView do 2 | use Rumbl.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /rumbl/web/views/session_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.SessionView do 2 | use Rumbl.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /rumbl/web/views/user_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.UserView do 2 | use Rumbl.Web, :view 3 | alias Rumbl.User 4 | 5 | def first_name(%User{name: name}) do 6 | name 7 | |> String.split(" ") 8 | |> Enum.at(0) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /rumbl/web/views/video_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.VideoView do 2 | use Rumbl.Web, :view 3 | 4 | def category_name(%Rumbl.Category{name: name}) do 5 | {:safe, name} 6 | end 7 | 8 | def category_name(_) do 9 | {:safe, ""} 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /rumbl/web/views/watch_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.WatchView do 2 | use Rumbl.Web, :view 3 | 4 | def player_id(video) do 5 | ~r{^.*(?:youtu\.be/|\w+/|v=)(?[^#&?]*)} 6 | |> Regex.named_captures(video.url) 7 | |> get_in(["id"]) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /rumbl/web/web.ex: -------------------------------------------------------------------------------- 1 | defmodule Rumbl.Web do 2 | @moduledoc """ 3 | A module that keeps using definitions for controllers, 4 | views and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use Rumbl.Web, :controller 9 | use Rumbl.Web, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. 17 | """ 18 | 19 | def model do 20 | quote do 21 | use Ecto.Schema 22 | 23 | import Ecto 24 | import Ecto.Changeset 25 | import Ecto.Query, only: [from: 1, from: 2] 26 | end 27 | end 28 | 29 | def controller do 30 | quote do 31 | use Phoenix.Controller 32 | 33 | alias Rumbl.Repo 34 | import Ecto 35 | import Ecto.Query, only: [from: 1, from: 2] 36 | 37 | import Rumbl.Router.Helpers 38 | import Rumbl.Gettext 39 | import Rumbl.Auth, only: [authenticate_user: 2, current_user: 1] 40 | end 41 | end 42 | 43 | def view do 44 | quote do 45 | use Phoenix.View, root: "web/templates" 46 | 47 | # Import convenience functions from controllers 48 | import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] 49 | 50 | # Use all HTML functionality (forms, tags, etc) 51 | use Phoenix.HTML 52 | 53 | import Rumbl.Router.Helpers 54 | import Rumbl.ErrorHelpers 55 | import Rumbl.Gettext 56 | end 57 | end 58 | 59 | def router do 60 | quote do 61 | use Phoenix.Router 62 | 63 | import Rumbl.Auth, only: [authenticate_user: 2] 64 | end 65 | end 66 | 67 | def channel do 68 | quote do 69 | use Phoenix.Channel 70 | 71 | alias Rumbl.Repo 72 | import Ecto 73 | import Ecto.Query, only: [from: 1, from: 2] 74 | import Rumbl.Gettext 75 | end 76 | end 77 | 78 | @doc """ 79 | When used, dispatch to the appropriate controller/view/etc. 80 | """ 81 | defmacro __using__(which) when is_atom(which) do 82 | apply(__MODULE__, which, []) 83 | end 84 | end 85 | --------------------------------------------------------------------------------