From e152fe14652db4538c4e0983e86db2d44f40d997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20F=C3=B6hring?= Date: Sun, 21 Jun 2015 21:25:22 +0200 Subject: [PATCH] We come in peace :space_invader: --- .gitignore | 24 + LICENSE | 20 + README.md | 8 + brunch-config.js | 46 + config/config.exs | 24 + config/dev.exs | 35 + config/prod.exs | 51 + config/test.exs | 18 + lib/elixir_status.ex | 30 + lib/elixir_status/endpoint.ex | 35 + lib/elixir_status/repo.ex | 3 + mix.exs | 40 + mix.lock | 19 + package.json | 13 + priv/static/images/avatar_rrrene.jpg | Bin 0 -> 15091 bytes priv/static/images/phoenix.png | Bin 0 -> 13900 bytes priv/static/robots.txt | 5 + test/controllers/page_controller_test.exs | 8 + test/support/channel_case.ex | 41 + test/support/conn_case.ex | 43 + test/support/model_case.ex | 53 + test/test_helper.exs | 6 + test/views/error_view_test.exs | 21 + test/views/page_view_test.exs | 3 + web/controllers/github_auth_controller.ex | 51 + web/controllers/page_controller.ex | 9 + web/models/user.ex | 9 + web/oauth/git_hub_auth.ex | 42 + web/router.ex | 40 + web/static/css/1_poole.css | 427 +++++++ web/static/css/3_syntax.css | 66 + web/static/css/app.scss | 57 + web/static/js/app.js | 13 + web/static/vendor/phoenix.js | 1068 +++++++++++++++++ web/templates/layout/application.html.eex | 27 + web/templates/page/index.html.eex | 59 + web/templates/page/index_logged_in.html.eex | 24 + .../page/index_not_logged_in.html.eex | 8 + web/views/error_view.ex | 17 + web/views/layout_view.ex | 3 + web/views/page_view.ex | 3 + web/web.ex | 78 ++ 42 files changed, 2547 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 brunch-config.js create mode 100644 config/config.exs create mode 100644 config/dev.exs create mode 100644 config/prod.exs create mode 100644 config/test.exs create mode 100644 lib/elixir_status.ex create mode 100644 lib/elixir_status/endpoint.ex create mode 100644 lib/elixir_status/repo.ex create mode 100644 mix.exs create mode 100644 mix.lock create mode 100644 package.json create mode 100755 priv/static/images/avatar_rrrene.jpg create mode 100644 priv/static/images/phoenix.png create mode 100644 priv/static/robots.txt create mode 100644 test/controllers/page_controller_test.exs create mode 100644 test/support/channel_case.ex create mode 100644 test/support/conn_case.ex create mode 100644 test/support/model_case.ex create mode 100644 test/test_helper.exs create mode 100644 test/views/error_view_test.exs create mode 100644 test/views/page_view_test.exs create mode 100644 web/controllers/github_auth_controller.ex create mode 100644 web/controllers/page_controller.ex create mode 100644 web/models/user.ex create mode 100644 web/oauth/git_hub_auth.ex create mode 100644 web/router.ex create mode 100644 web/static/css/1_poole.css create mode 100644 web/static/css/3_syntax.css create mode 100644 web/static/css/app.scss create mode 100644 web/static/js/app.js create mode 100644 web/static/vendor/phoenix.js create mode 100644 web/templates/layout/application.html.eex create mode 100644 web/templates/page/index.html.eex create mode 100644 web/templates/page/index_logged_in.html.eex create mode 100644 web/templates/page/index_not_logged_in.html.eex create mode 100644 web/views/error_view.ex create mode 100644 web/views/layout_view.ex create mode 100644 web/views/page_view.ex create mode 100644 web/web.ex diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8076f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Mix artifacts +/_build +/deps +/*.ez + +# Generate on crash by the VM +erl_crash.dump + +# Static artifacts +/node_modules + +# Since we are building js and css from web/static, +# we ignore priv/static/{css,js}. You may want to +# comment this depending on your deployment strategy. +/priv/static/css +/priv/static/js + +# The config/prod.secret.exs file by default contains sensitive +# data and you should not commit it into version control. +# +# Alternatively, you may comment the line below and commit the +# secrets file as long as you replace its contents by environment +# variables. +/config/prod.secret.exs \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f826a47 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2015 René Föhring + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fc16481 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# ElixirStatus + +To start your new Phoenix application: + +1. Install dependencies with `mix deps.get` +2. Start Phoenix endpoint with `mix phoenix.server` + +Now you can visit `localhost:4000` from your browser. diff --git a/brunch-config.js b/brunch-config.js new file mode 100644 index 0000000..691b7d1 --- /dev/null +++ b/brunch-config.js @@ -0,0 +1,46 @@ +exports.config = { + // See http://brunch.io/#documentation for docs. + files: { + javascripts: { + joinTo: 'js/app.js' + // To use a separate vendor.js bundle, specify two files path + // https://github.com/brunch/brunch/blob/stable/docs/config.md#files + // joinTo: { + // 'js/app.js': /^(web\/static\/js)/, + // 'js/vendor.js': /^(web\/static\/vendor)/ + // } + // + // To change the order of concatenation of files, explictly mention here + // https://github.com/brunch/brunch/tree/stable/docs#concatenation + // order: { + // before: [ + // 'web/static/vendor/js/jquery-2.1.1.js', + // 'web/static/vendor/js/bootstrap.min.js' + // ] + // } + }, + stylesheets: { + joinTo: 'css/app.css' + }, + templates: { + joinTo: 'js/app.js' + } + }, + + // Phoenix paths configuration + paths: { + // Which directories to watch + watched: ["web/static", "test/static"], + + // Where to compile files to + public: "priv/static" + }, + + // Configure your plugins + plugins: { + babel: { + // Do not use ES6 compiler in vendor code + ignore: [/^(web\/static\/vendor)/] + } + } +}; diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..9157625 --- /dev/null +++ b/config/config.exs @@ -0,0 +1,24 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Mix.Config module. +# +# This configuration file is loaded before any dependency and +# is restricted to this project. +use Mix.Config + +# Configures the endpoint +config :elixir_status, ElixirStatus.Endpoint, + url: [host: "localhost"], + root: Path.dirname(__DIR__), + secret_key_base: "LstH3/4lI4FeMXhEDifTp4UP+EAUR8RsyElC9rzzC7uyR1+hJ9U94iTHiajgzEwQ", + debug_errors: false, + pubsub: [name: ElixirStatus.PubSub, + adapter: Phoenix.PubSub.PG2] + +# Configures Elixir's Logger +config :logger, :console, + format: "$time $metadata[$level] $message\n", + metadata: [:request_id] + +# Import environment specific config. This must remain at the bottom +# of this file so it overrides the configuration defined above. +import_config "#{Mix.env}.exs" diff --git a/config/dev.exs b/config/dev.exs new file mode 100644 index 0000000..9095b86 --- /dev/null +++ b/config/dev.exs @@ -0,0 +1,35 @@ +use Mix.Config + +# For development, we disable any cache and enable +# debugging and code reloading. +# +# The watchers configuration can be used to run external +# watchers to your application. For example, we use it +# with brunch.io to recompile .js and .css sources. +config :elixir_status, ElixirStatus.Endpoint, + http: [port: 4000], + debug_errors: true, + code_reloader: true, + cache_static_lookup: false, + watchers: [node: ["node_modules/brunch/bin/brunch", "watch"]] + +# Watch static and templates for browser reloading. +config :elixir_status, ElixirStatus.Endpoint, + live_reload: [ + patterns: [ + ~r{priv/static/.*(js|css|png|jpeg|jpg|gif)$}, + ~r{web/views/.*(ex)$}, + ~r{web/templates/.*(eex)$} + ] + ] + +# Do not include metadata nor timestamps in development logs +config :logger, :console, format: "[$level] $message\n" + +# Configure your database +config :elixir_status, ElixirStatus.Repo, + adapter: Ecto.Adapters.Postgres, + username: "postgres", + password: "postgres", + database: "elixir_status_dev", + size: 10 # The amount of database connections in the pool diff --git a/config/prod.exs b/config/prod.exs new file mode 100644 index 0000000..dc3a61d --- /dev/null +++ b/config/prod.exs @@ -0,0 +1,51 @@ +use Mix.Config + +# For production, we configure the host to read the PORT +# from the system environment. Therefore, you will need +# to set PORT=80 before running your server. +# +# You should also configure the url host to something +# meaningful, we use this information when generating URLs. +# +# Finally, we also include the path to a manifest +# containing the digested version of static files. This +# manifest is generated by the mix phoenix.digest task +# which you typically run after static files are built. +config :elixir_status, ElixirStatus.Endpoint, + http: [port: {:system, "PORT"}], + url: [host: "example.com"], + cache_static_manifest: "priv/static/manifest.json" + +# ## SSL Support +# +# To get SSL working, you will need to add the `https` key +# to the previous section: +# +# config :elixir_status, ElixirStatus.Endpoint, +# ... +# https: [port: 443, +# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), +# certfile: System.get_env("SOME_APP_SSL_CERT_PATH")] +# +# Where those two env variables point to a file on +# disk for the key and cert. + +# Do not print debug messages in production +config :logger, level: :info + +# ## Using releases +# +# If you are doing OTP releases, you need to instruct Phoenix +# to start the server for all endpoints: +# +# config :phoenix, :serve_endpoints, true +# +# Alternatively, you can configure exactly which server to +# start per endpoint: +# +# config :elixir_status, ElixirStatus.Endpoint, server: true +# + +# Finally import the config/prod.secret.exs +# which should be versioned separately. +import_config "prod.secret.exs" diff --git a/config/test.exs b/config/test.exs new file mode 100644 index 0000000..f052d8e --- /dev/null +++ b/config/test.exs @@ -0,0 +1,18 @@ +use Mix.Config + +# We don't run a server during test. If one is required, +# you can enable the server option below. +config :elixir_status, ElixirStatus.Endpoint, + http: [port: 4001], + server: false + +# Print only warnings and errors during test +config :logger, level: :warn + +# Configure your database +config :elixir_status, ElixirStatus.Repo, + adapter: Ecto.Adapters.Postgres, + username: "postgres", + password: "postgres", + database: "elixir_status_test", + size: 1 # Use a single connection for transactional tests diff --git a/lib/elixir_status.ex b/lib/elixir_status.ex new file mode 100644 index 0000000..e344bd2 --- /dev/null +++ b/lib/elixir_status.ex @@ -0,0 +1,30 @@ +defmodule ElixirStatus do + use Application + + # See http://elixir-lang.org/docs/stable/elixir/Application.html + # for more information on OTP Applications + def start(_type, _args) do + import Supervisor.Spec, warn: false + + children = [ + # Start the endpoint when the application starts + supervisor(ElixirStatus.Endpoint, []), + # Start the Ecto repository + worker(ElixirStatus.Repo, []), + # Here you could define other workers and supervisors as children + # worker(ElixirStatus.Worker, [arg1, arg2, arg3]), + ] + + # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: ElixirStatus.Supervisor] + Supervisor.start_link(children, opts) + end + + # Tell Phoenix to update the endpoint configuration + # whenever the application is updated. + def config_change(changed, _new, removed) do + ElixirStatus.Endpoint.config_change(changed, removed) + :ok + end +end diff --git a/lib/elixir_status/endpoint.ex b/lib/elixir_status/endpoint.ex new file mode 100644 index 0000000..dfe9494 --- /dev/null +++ b/lib/elixir_status/endpoint.ex @@ -0,0 +1,35 @@ +defmodule ElixirStatus.Endpoint do + use Phoenix.Endpoint, otp_app: :elixir_status + + # Serve at "/" the static files from "priv/static" directory. + # + # You should set gzip to true if you are running phoenix.digest + # when deploying your static files in production. + plug Plug.Static, + at: "/", from: :elixir_status, gzip: false, + only: ~w(css images js favicon.ico robots.txt) + + # Code reloading can be explicitly enabled under the + # :code_reloader configuration of your endpoint. + if code_reloading? do + plug Phoenix.LiveReloader + plug Phoenix.CodeReloader + end + + plug Plug.Logger + + plug Plug.Parsers, + parsers: [:urlencoded, :multipart, :json], + pass: ["*/*"], + json_decoder: Poison + + plug Plug.MethodOverride + plug Plug.Head + + plug Plug.Session, + store: :cookie, + key: "_elixir_status_key", + signing_salt: "XPXyl9lg" + + plug :router, ElixirStatus.Router +end diff --git a/lib/elixir_status/repo.ex b/lib/elixir_status/repo.ex new file mode 100644 index 0000000..7e98fac --- /dev/null +++ b/lib/elixir_status/repo.ex @@ -0,0 +1,3 @@ +defmodule ElixirStatus.Repo do + use Ecto.Repo, otp_app: :elixir_status +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..60d45a2 --- /dev/null +++ b/mix.exs @@ -0,0 +1,40 @@ +defmodule ElixirStatus.Mixfile do + use Mix.Project + + def project do + [app: :elixir_status, + version: "0.0.1", + elixir: "~> 1.0", + elixirc_paths: elixirc_paths(Mix.env), + compilers: [:phoenix] ++ Mix.compilers, + build_embedded: Mix.env == :prod, + start_permanent: Mix.env == :prod, + deps: deps] + end + + # Configuration for the OTP application + # + # Type `mix help compile.app` for more information + def application do + [mod: {ElixirStatus, []}, + applications: [:phoenix, :phoenix_html, :cowboy, :logger, + :phoenix_ecto, :postgrex, :oauth2]] + end + + # Specifies which paths to compile per environment + defp elixirc_paths(:test), do: ["lib", "web", "test/support"] + defp elixirc_paths(_), do: ["lib", "web"] + + # Specifies your project dependencies + # + # Type `mix help deps` for examples and options + defp deps do + [{:phoenix, "~> 0.13.1"}, + {:phoenix_ecto, "~> 0.4"}, + {:postgrex, ">= 0.0.0"}, + {:phoenix_html, "~> 1.0"}, + {:phoenix_live_reload, "~> 0.4", only: :dev}, + {:cowboy, "~> 1.0"}, + {:oauth2, "~> 0.1.0"}] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..5bf3a3b --- /dev/null +++ b/mix.lock @@ -0,0 +1,19 @@ +%{"cowboy": {:hex, :cowboy, "1.0.0"}, + "cowlib": {:hex, :cowlib, "1.0.1"}, + "decimal": {:hex, :decimal, "1.1.0"}, + "ecto": {:hex, :ecto, "0.12.0-rc"}, + "fs": {:hex, :fs, "0.9.2"}, + "hackney": {:hex, :hackney, "1.1.0"}, + "httpoison": {:hex, :httpoison, "0.7.0"}, + "idna": {:hex, :idna, "1.0.2"}, + "oauth2": {:hex, :oauth2, "0.1.1"}, + "phoenix": {:hex, :phoenix, "0.13.1"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "0.4.0"}, + "phoenix_html": {:hex, :phoenix_html, "1.2.1"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "0.4.1"}, + "plug": {:hex, :plug, "0.13.0"}, + "poison": {:hex, :poison, "1.4.0"}, + "poolboy": {:hex, :poolboy, "1.5.1"}, + "postgrex": {:hex, :postgrex, "0.8.2"}, + "ranch": {:hex, :ranch, "1.0.0"}, + "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5"}} diff --git a/package.json b/package.json new file mode 100644 index 0000000..dc440c9 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "repository": { + }, + "dependencies": { + "brunch": "^1.8.1", + "babel-brunch": "^5.1.1", + "clean-css-brunch": ">= 1.0 < 1.8", + "css-brunch": ">= 1.0 < 1.8", + "javascript-brunch": ">= 1.0 < 1.8", + "sass-brunch": "^1.8.10", + "uglify-js-brunch": ">= 1.0 < 1.8" + } +} diff --git a/priv/static/images/avatar_rrrene.jpg b/priv/static/images/avatar_rrrene.jpg new file mode 100755 index 0000000000000000000000000000000000000000..63908d2481f0ecd535997e42357d9ae7e37e6fda GIT binary patch literal 15091 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 E0boADdH?_b literal 0 HcmV?d00001 diff --git a/priv/static/images/phoenix.png b/priv/static/images/phoenix.png new file mode 100644 index 0000000000000000000000000000000000000000..9c81075f63d2151e6f40e9aa66f665749a87cc6a GIT binary patch literal 13900 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 UWxWXg`G1#+f~I_}taaG`2PLnS&Hw-a literal 0 HcmV?d00001 diff --git a/priv/static/robots.txt b/priv/static/robots.txt new file mode 100644 index 0000000..3c9c7c0 --- /dev/null +++ b/priv/static/robots.txt @@ -0,0 +1,5 @@ +# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-agent: * +# Disallow: / diff --git a/test/controllers/page_controller_test.exs b/test/controllers/page_controller_test.exs new file mode 100644 index 0000000..ce83bae --- /dev/null +++ b/test/controllers/page_controller_test.exs @@ -0,0 +1,8 @@ +defmodule ElixirStatus.PageControllerTest do + use ElixirStatus.ConnCase + + test "GET /" do + conn = get conn(), "/" + assert html_response(conn, 200) =~ "Welcome to Phoenix!" + end +end diff --git a/test/support/channel_case.ex b/test/support/channel_case.ex new file mode 100644 index 0000000..8ff4c3c --- /dev/null +++ b/test/support/channel_case.ex @@ -0,0 +1,41 @@ +defmodule ElixirStatus.ChannelCase do + @moduledoc """ + This module defines the test case to be used by + channel tests. + + Such tests rely on `Phoenix.ChannelTest` and also + imports other functionality to make it easier + to build and query models. + + Finally, if the test case interacts with the database, + it cannot be async. For this reason, every test runs + inside a transaction which is reset at the beginning + of the test unless the test case is marked as async. + """ + + use ExUnit.CaseTemplate + + using do + quote do + # Import conveniences for testing with channels + use Phoenix.ChannelTest + + # Alias the data repository and import query/model functions + alias ElixirStatus.Repo + import Ecto.Model + import Ecto.Query, only: [from: 2] + + + # The default endpoint for testing + @endpoint ElixirStatus.Endpoint + end + end + + setup tags do + unless tags[:async] do + Ecto.Adapters.SQL.restart_test_transaction(ElixirStatus.Repo, []) + end + + :ok + end +end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex new file mode 100644 index 0000000..7ec0178 --- /dev/null +++ b/test/support/conn_case.ex @@ -0,0 +1,43 @@ +defmodule ElixirStatus.ConnCase do + @moduledoc """ + This module defines the test case to be used by + tests that require setting up a connection. + + Such tests rely on `Phoenix.ConnTest` and also + imports other functionality to make it easier + to build and query models. + + Finally, if the test case interacts with the database, + it cannot be async. For this reason, every test runs + inside a transaction which is reset at the beginning + of the test unless the test case is marked as async. + """ + + use ExUnit.CaseTemplate + + using do + quote do + # Import conveniences for testing with connections + use Phoenix.ConnTest + + # Alias the data repository and import query/model functions + alias ElixirStatus.Repo + import Ecto.Model + import Ecto.Query, only: [from: 2] + + # Import URL helpers from the router + import ElixirStatus.Router.Helpers + + # The default endpoint for testing + @endpoint ElixirStatus.Endpoint + end + end + + setup tags do + unless tags[:async] do + Ecto.Adapters.SQL.restart_test_transaction(ElixirStatus.Repo, []) + end + + :ok + end +end diff --git a/test/support/model_case.ex b/test/support/model_case.ex new file mode 100644 index 0000000..b5f044b --- /dev/null +++ b/test/support/model_case.ex @@ -0,0 +1,53 @@ +defmodule ElixirStatus.ModelCase do + @moduledoc """ + This module defines the test case to be used by + model tests. + + You may define functions here to be used as helpers in + your model tests. See `errors_on/2`'s definition as reference. + + Finally, if the test case interacts with the database, + it cannot be async. For this reason, every test runs + inside a transaction which is reset at the beginning + of the test unless the test case is marked as async. + """ + + use ExUnit.CaseTemplate + + using do + quote do + # Alias the data repository and import query/model functions + alias ElixirStatus.Repo + import Ecto.Model + import Ecto.Query, only: [from: 2] + import ElixirStatus.ModelCase + end + end + + setup tags do + unless tags[:async] do + Ecto.Adapters.SQL.restart_test_transaction(ElixirStatus.Repo, []) + end + + :ok + end + + @doc """ + Helper for returning list of errors in model when passed certain data. + + ## Examples + + Given a User model that has validation for the presence of a value for the + `:name` field and validation that `:password` is "safe": + + iex> errors_on(%User{}, password: "password") + [{:password, "is unsafe"}, {:name, "is blank"}] + + You would then write your assertion like: + + assert {:password, "is unsafe"} in errors_on(%User{}, password: "password") + """ + def errors_on(model, data) do + model.__struct__.changeset(model, data).errors + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..a892c10 --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1,6 @@ +ExUnit.start + +# Create the database, run migrations, and start the test transaction. +Mix.Task.run "ecto.create", ["--quiet"] +Mix.Task.run "ecto.migrate", ["--quiet"] +Ecto.Adapters.SQL.begin_test_transaction(ElixirStatus.Repo) diff --git a/test/views/error_view_test.exs b/test/views/error_view_test.exs new file mode 100644 index 0000000..ee9e23b --- /dev/null +++ b/test/views/error_view_test.exs @@ -0,0 +1,21 @@ +defmodule ElixirStatus.ErrorViewTest do + use ElixirStatus.ConnCase, async: true + + # Bring render/3 and render_to_string/3 for testing custom views + import Phoenix.View + + test "renders 404.html" do + assert render_to_string(ElixirStatus.ErrorView, "404.html", []) == + "Page not found" + end + + test "render 500.html" do + assert render_to_string(ElixirStatus.ErrorView, "500.html", []) == + "Server internal error" + end + + test "render any other" do + assert render_to_string(ElixirStatus.ErrorView, "505.html", []) == + "Server internal error" + end +end diff --git a/test/views/page_view_test.exs b/test/views/page_view_test.exs new file mode 100644 index 0000000..f804105 --- /dev/null +++ b/test/views/page_view_test.exs @@ -0,0 +1,3 @@ +defmodule ElixirStatus.PageViewTest do + use ElixirStatus.ConnCase, async: true +end diff --git a/web/controllers/github_auth_controller.ex b/web/controllers/github_auth_controller.ex new file mode 100644 index 0000000..6e12f9b --- /dev/null +++ b/web/controllers/github_auth_controller.ex @@ -0,0 +1,51 @@ +defmodule ElixirStatus.GitHubAuthController do + use ElixirStatus.Web, :controller + + plug :action + + @doc """ + This action is reached via `/auth` and redirects to the OAuth2 provider + based on the chosen strategy. + """ + def sign_in(conn, _params) do + redirect conn, external: GitHubAuth.authorize_url! + end + + @doc """ + This action is reached via `/auth/sign_out` and redirects to the OAuth2 provider + based on the chosen strategy. + """ + def sign_out(conn, _params) do + conn + |> put_session(:current_user, nil) + |> put_session(:access_token, nil) + |> redirect(to: "/") + end + + @doc """ + This action is reached via `/auth/callback` is the the callback URL that + the OAuth2 provider will redirect the user back to with a `code` that will + be used to request an access token. The access token will then be used to + access protected resources on behalf of the user. + """ + def callback(conn, %{"code" => code}) do + # Exchange an auth code for an access token + token = GitHubAuth.get_token!(code: code) + + # Request the user's data with the access token + user = OAuth2.AccessToken.get!(token, "/user") + + # Store the user in the session under `:current_user` and redirect to /. + # In most cases, we'd probably just store the user's ID that can be used + # to fetch from the database. In this case, since this example app has no + # database, I'm just storing the user map. + # + # If you need to make additional resource requests, you may want to store + # the access token as well. + conn + |> put_session(:current_user, user) + |> put_session(:access_token, token.access_token) + |> redirect(to: "/") + end +end + diff --git a/web/controllers/page_controller.ex b/web/controllers/page_controller.ex new file mode 100644 index 0000000..073b1cc --- /dev/null +++ b/web/controllers/page_controller.ex @@ -0,0 +1,9 @@ +defmodule ElixirStatus.PageController do + use ElixirStatus.Web, :controller + + plug :action + + def index(conn, _params) do + render conn, "index.html" + end +end diff --git a/web/models/user.ex b/web/models/user.ex new file mode 100644 index 0000000..c39b377 --- /dev/null +++ b/web/models/user.ex @@ -0,0 +1,9 @@ +defmodule ElixirStatus.User do + @moduledoc """ + The `current_user` is stored in the connection as a map + with string keys. + """ + + def name(user), do: user["name"] + def email(user), do: user["email"] +end diff --git a/web/oauth/git_hub_auth.ex b/web/oauth/git_hub_auth.ex new file mode 100644 index 0000000..32dd523 --- /dev/null +++ b/web/oauth/git_hub_auth.ex @@ -0,0 +1,42 @@ +defmodule GitHubAuth do + @moduledoc """ + An OAuth2 strategy for GitHub. + """ + use OAuth2.Strategy + + alias OAuth2.Strategy.AuthCode + + # Public API + + def new do + OAuth2.new([ + strategy: __MODULE__, + client_id: System.get_env("CLIENT_ID"), + client_secret: System.get_env("CLIENT_SECRET"), + redirect_uri: System.get_env("REDIRECT_URI"), + site: "https://api.github.com", + authorize_url: "https://github.com/login/oauth/authorize", + token_url: "https://github.com/login/oauth/access_token" + ]) + end + + def authorize_url!(params \\ []) do + OAuth2.Client.authorize_url!(new(), params) + end + + def get_token!(params \\ [], headers \\ []) do + OAuth2.Client.get_token!(new(), params) + end + + # Strategy Callbacks + + def authorize_url(client, params) do + AuthCode.authorize_url(client, params) + end + + def get_token(client, params, headers) do + client + |> put_header("Accept", "application/json") + |> AuthCode.get_token(params, headers) + end +end diff --git a/web/router.ex b/web/router.ex new file mode 100644 index 0000000..c099fe4 --- /dev/null +++ b/web/router.ex @@ -0,0 +1,40 @@ +defmodule ElixirStatus.Router do + use ElixirStatus.Web, :router + + pipeline :browser do + plug :accepts, ["html"] + plug :fetch_session + plug :fetch_flash + plug :protect_from_forgery + plug :assign_current_user + end + + pipeline :api do + plug :accepts, ["json"] + end + + scope "/", ElixirStatus do + pipe_through :browser # Use the default browser stack + + get "/", PageController, :index + end + + scope "/auth", alias: ElixirStatus do + pipe_through :browser + get "/", GitHubAuthController, :sign_in + get "/sign_out", GitHubAuthController, :sign_out + get "/callback", GitHubAuthController, :callback + end + + # Fetch the current user from the session and add it to `conn.assigns`. This + # will allow you to have access to the current user in your views with + # `@current_user`. + defp assign_current_user(conn, _) do + assign(conn, :current_user, get_session(conn, :current_user)) + end + + # Other scopes may use custom stacks. + # scope "/api", ElixirStatus do + # pipe_through :api + # end +end diff --git a/web/static/css/1_poole.css b/web/static/css/1_poole.css new file mode 100644 index 0000000..5d137f6 --- /dev/null +++ b/web/static/css/1_poole.css @@ -0,0 +1,427 @@ +/* + * ___ + * /\_ \ + * _____ ___ ___\//\ \ __ + * /\ '__`\ / __`\ / __`\\ \ \ /'__`\ + * \ \ \_\ \/\ \_\ \/\ \_\ \\_\ \_/\ __/ + * \ \ ,__/\ \____/\ \____//\____\ \____\ + * \ \ \/ \/___/ \/___/ \/____/\/____/ + * \ \_\ + * \/_/ + * + * Designed, built, and released under MIT license by @mdo. Learn more at + * https://github.com/poole/poole. + */ + + +/* + * Contents + * + * Body resets + * Custom type + * Messages + * Container + * Masthead + * Posts and pages + * Pagination + * Reverse layout + * Themes + */ + + +/* + * Body resets + * + * Update the foundational and global aspects of the page. + */ + +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; +} + +html { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 1.5; +} +@media (min-width: 38em) { + html { + font-size: 20px; + } +} + +body { + color: #515151; + background-color: #fff; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +/* No `:visited` state is required by default (browsers will use `a`) */ +a { + color: #268bd2; + text-decoration: none; +} +a strong { + color: inherit; +} +/* `:focus` is linked to `:hover` for basic accessibility */ +a:hover, +a:focus { + text-decoration: underline; +} + +/* Headings */ +h1, h2, h3, h4, h5, h6 { + margin-bottom: .5rem; + font-weight: bold; + line-height: 1.25; + color: #313131; + text-rendering: optimizeLegibility; +} +h1 { + margin-top: 1rem; + font-size: 1.5rem; +} +h2 { + margin-top: 1.5rem; + font-size: 1.25rem; +} +h3, h4, h5, h6 { + margin-top: 1rem; + font-size: 1rem; +} + +/* Body text */ +p { + margin-top: 0; + margin-bottom: 1rem; +} + +strong { + color: #303030; +} + + +/* Lists */ +ul, ol, dl { + margin-top: 0; + margin-bottom: 1rem; +} + +dt { + font-weight: bold; +} +dd { + margin-bottom: .5rem; +} + +/* Misc */ +hr { + position: relative; + margin: 1.5rem 0; + border: 0; + border-top: 1px solid #eee; + border-bottom: 1px solid #fff; +} + +abbr { + font-size: 85%; + font-weight: bold; + color: #555; + text-transform: uppercase; +} +abbr[title] { + cursor: help; + border-bottom: 1px dotted #e5e5e5; +} + +/* Code */ +code, +pre { + font-family: Menlo, Monaco, "Courier New", monospace; +} +code { + padding: .25em .5em; + font-size: 85%; + color: #bf616a; + background-color: #f9f9f9; + border-radius: 3px; +} +pre { + display: block; + margin-top: 0; + margin-bottom: 1rem; + padding: 1rem; + font-size: .8rem; + line-height: 1.4; + white-space: pre; + white-space: pre-wrap; + word-break: break-all; + word-wrap: break-word; + background-color: #f9f9f9; +} +pre code { + padding: 0; + font-size: 100%; + color: inherit; + background-color: transparent; +} + +/* Pygments via Jekyll */ +.highlight { + margin-bottom: 1rem; + border-radius: 4px; +} +.highlight pre { + margin-bottom: 0; +} + +/* Gist via GitHub Pages */ +.gist .gist-file { + font-family: Menlo, Monaco, "Courier New", monospace !important; +} +.gist .markdown-body { + padding: 15px; +} +.gist pre { + padding: 0; + background-color: transparent; +} +.gist .gist-file .gist-data { + font-size: .8rem !important; + line-height: 1.4; +} +.gist code { + padding: 0; + color: inherit; + background-color: transparent; + border-radius: 0; +} + +/* Quotes */ +blockquote { + padding: .5rem 1rem; + margin: .8rem 0; + color: #7a7a7a; + border-left: .25rem solid #e5e5e5; +} +blockquote p:last-child { + margin-bottom: 0; +} +@media (min-width: 30em) { + blockquote { + padding-right: 4rem; + padding-left: 1.25rem; + } +} + +img { + display: block; + max-width: 100%; + margin: 0 0 1rem; + border-radius: 5px; +} + +/* Tables */ +table { + margin-bottom: 1rem; + width: 100%; + border: 1px solid #e5e5e5; + border-collapse: collapse; +} +td, +th { + padding: .25rem .5rem; + border: 1px solid #e5e5e5; +} +tbody tr:nth-child(odd) td, +tbody tr:nth-child(odd) th { + background-color: #f9f9f9; +} + + +/* + * Custom type + * + * Extend paragraphs with `.lead` for larger introductory text. + */ + +.lead { + font-size: 1.25rem; + font-weight: 300; +} + + +/* + * Messages + * + * Show alert messages to users. You may add it to single elements like a `

`, + * or to a parent if there are multiple elements to show. + */ + +.message { + margin-bottom: 1rem; + padding: 1rem; + color: #717171; + background-color: #f9f9f9; +} + + +/* + * Container + * + * Center the page content. + */ + +.container { + max-width: 38rem; + padding-left: 1rem; + padding-right: 1rem; + margin-left: auto; + margin-right: auto; +} + + +/* + * Masthead + * + * Super small header above the content for site name and short description. + */ + +.masthead { + padding-top: 1rem; + padding-bottom: 1rem; + margin-bottom: 3rem; +} +.masthead-title { + margin-top: 0; + margin-bottom: 0; + color: #505050; +} +.masthead-title a { + color: #505050; +} +.masthead-title small { + font-size: 75%; + font-weight: 400; + color: #c0c0c0; + letter-spacing: 0; +} + + +/* + * Posts and pages + * + * Each post is wrapped in `.post` and is used on default and post layouts. Each + * page is wrapped in `.page` and is only used on the page layout. + */ + +.page, +.post { + margin-bottom: 2em; +} + +/* Blog post or page title */ +.page-title, +.post-title, +.post-title a { + color: #303030; +} +.page-title, +.post-title { + margin-top: 0; +} + +/* Meta data line below post title */ +.post-date { + display: block; + margin-top: -.5rem; + margin-bottom: 1rem; + color: #9a9a9a; +} + +/* Related posts */ +.related { + padding-top: 2rem; + padding-bottom: 2rem; + border-top: 1px solid #eee; +} +.related-posts { + padding-left: 0; + list-style: none; +} +.related-posts h3 { + margin-top: 0; +} +.related-posts li small { + font-size: 75%; + color: #999; +} +.related-posts li a:hover { + color: #268bd2; + text-decoration: none; +} +.related-posts li a:hover small { + color: inherit; +} + + +/* + * Pagination + * + * Super lightweight (HTML-wise) blog pagination. `span`s are provide for when + * there are no more previous or next posts to show. + */ + +.pagination { + overflow: hidden; /* clearfix */ + margin-left: -1rem; + margin-right: -1rem; + font-family: "PT Sans", Helvetica, Arial, sans-serif; + color: #ccc; + text-align: center; +} + +/* Pagination items can be `span`s or `a`s */ +.pagination-item { + display: block; + padding: 1rem; + border: 1px solid #eee; +} +.pagination-item:first-child { + margin-bottom: -1px; +} + +/* Only provide a hover state for linked pagination items */ +a.pagination-item:hover { + background-color: #f5f5f5; +} + +@media (min-width: 30em) { + .pagination { + margin: 3rem 0; + } + .pagination-item { + float: left; + width: 50%; + } + .pagination-item:first-child { + margin-bottom: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + } + .pagination-item:last-child { + margin-left: -1px; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + } +} \ No newline at end of file diff --git a/web/static/css/3_syntax.css b/web/static/css/3_syntax.css new file mode 100644 index 0000000..1264b87 --- /dev/null +++ b/web/static/css/3_syntax.css @@ -0,0 +1,66 @@ +.hll { background-color: #ffffcc } + /*{ background: #f0f3f3; }*/ +.c { color: #999; } /* Comment */ +.err { color: #AA0000; background-color: #FFAAAA } /* Error */ +.k { color: #006699; } /* Keyword */ +.o { color: #555555 } /* Operator */ +.cm { color: #0099FF; font-style: italic } /* Comment.Multiline */ +.cp { color: #009999 } /* Comment.Preproc */ +.c1 { color: #999; } /* Comment.Single */ +.cs { color: #999; } /* Comment.Special */ +.gd { background-color: #FFCCCC; border: 1px solid #CC0000 } /* Generic.Deleted */ +.ge { font-style: italic } /* Generic.Emph */ +.gr { color: #FF0000 } /* Generic.Error */ +.gh { color: #003300; } /* Generic.Heading */ +.gi { background-color: #CCFFCC; border: 1px solid #00CC00 } /* Generic.Inserted */ +.go { color: #AAAAAA } /* Generic.Output */ +.gp { color: #000099; } /* Generic.Prompt */ +.gs { } /* Generic.Strong */ +.gu { color: #003300; } /* Generic.Subheading */ +.gt { color: #99CC66 } /* Generic.Traceback */ +.kc { color: #006699; } /* Keyword.Constant */ +.kd { color: #006699; } /* Keyword.Declaration */ +.kn { color: #006699; } /* Keyword.Namespace */ +.kp { color: #006699 } /* Keyword.Pseudo */ +.kr { color: #006699; } /* Keyword.Reserved */ +.kt { color: #007788; } /* Keyword.Type */ +.m { color: #FF6600 } /* Literal.Number */ +.s { color: #d44950 } /* Literal.String */ +.na { color: #4f9fcf } /* Name.Attribute */ +.nb { color: #336666 } /* Name.Builtin */ +.nc { color: #00AA88; } /* Name.Class */ +.no { color: #336600 } /* Name.Constant */ +.nd { color: #9999FF } /* Name.Decorator */ +.ni { color: #999999; } /* Name.Entity */ +.ne { color: #CC0000; } /* Name.Exception */ +.nf { color: #CC00FF } /* Name.Function */ +.nl { color: #9999FF } /* Name.Label */ +.nn { color: #00CCFF; } /* Name.Namespace */ +.nt { color: #2f6f9f; } /* Name.Tag */ +.nv { color: #003333 } /* Name.Variable */ +.ow { color: #000000; } /* Operator.Word */ +.w { color: #bbbbbb } /* Text.Whitespace */ +.mf { color: #FF6600 } /* Literal.Number.Float */ +.mh { color: #FF6600 } /* Literal.Number.Hex */ +.mi { color: #FF6600 } /* Literal.Number.Integer */ +.mo { color: #FF6600 } /* Literal.Number.Oct */ +.sb { color: #CC3300 } /* Literal.String.Backtick */ +.sc { color: #CC3300 } /* Literal.String.Char */ +.sd { color: #CC3300; font-style: italic } /* Literal.String.Doc */ +.s2 { color: #CC3300 } /* Literal.String.Double */ +.se { color: #CC3300; } /* Literal.String.Escape */ +.sh { color: #CC3300 } /* Literal.String.Heredoc */ +.si { color: #AA0000 } /* Literal.String.Interpol */ +.sx { color: #CC3300 } /* Literal.String.Other */ +.sr { color: #33AAAA } /* Literal.String.Regex */ +.s1 { color: #CC3300 } /* Literal.String.Single */ +.ss { color: #FFCC33 } /* Literal.String.Symbol */ +.bp { color: #336666 } /* Name.Builtin.Pseudo */ +.vc { color: #003333 } /* Name.Variable.Class */ +.vg { color: #003333 } /* Name.Variable.Global */ +.vi { color: #003333 } /* Name.Variable.Instance */ +.il { color: #FF6600 } /* Literal.Number.Integer.Long */ + +.css .o, +.css .o + .nt, +.css .nt + .nt { color: #999; } diff --git a/web/static/css/app.scss b/web/static/css/app.scss new file mode 100644 index 0000000..e946c33 --- /dev/null +++ b/web/static/css/app.scss @@ -0,0 +1,57 @@ +.post-author { + position: absolute; + margin-left: -80px; + + .avatar { + background-size: 100% 100%; + width: 64px; + height: 64px; + border-radius: 32px; + } +} + +footer, +footer a { + color: #aaa; + font-size: 0.8rem; +} + + +.btn-primary { + color: #fff; + background-color: #428bca; + border-color: #357ebd; +} +.btn { + display: inline-block; + margin-bottom: 0; + font-weight: 400; + text-align: center; + vertical-align: middle; + cursor: pointer; + background-image: none; + border: 1px solid transparent; + white-space: nowrap; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + border-radius: 4px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.btn { + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} +.btn-primary:hover { + color: #fff; + background-color: #286090; + border-color: #204d74; +} +.btn.focus, .btn:focus, .btn:hover { + text-decoration: none; +} \ No newline at end of file diff --git a/web/static/js/app.js b/web/static/js/app.js new file mode 100644 index 0000000..f88a60a --- /dev/null +++ b/web/static/js/app.js @@ -0,0 +1,13 @@ +import {Socket} from "phoenix" + +// let socket = new Socket("/ws") +// socket.connect() +// let chan = socket.chan("topic:subtopic", {}) +// chan.join().receive("ok", chan => { +// console.log("Success!") +// }) + +let App = { +} + +export default App diff --git a/web/static/vendor/phoenix.js b/web/static/vendor/phoenix.js new file mode 100644 index 0000000..ff8420a --- /dev/null +++ b/web/static/vendor/phoenix.js @@ -0,0 +1,1068 @@ +(function(/*! Brunch !*/) { + 'use strict'; + + var globals = typeof window !== 'undefined' ? window : global; + if (typeof globals.require === 'function') return; + + var modules = {}; + var cache = {}; + + var has = function(object, name) { + return ({}).hasOwnProperty.call(object, name); + }; + + var expand = function(root, name) { + var results = [], parts, part; + if (/^\.\.?(\/|$)/.test(name)) { + parts = [root, name].join('/').split('/'); + } else { + parts = name.split('/'); + } + for (var i = 0, length = parts.length; i < length; i++) { + part = parts[i]; + if (part === '..') { + results.pop(); + } else if (part !== '.' && part !== '') { + results.push(part); + } + } + return results.join('/'); + }; + + var dirname = function(path) { + return path.split('/').slice(0, -1).join('/'); + }; + + var localRequire = function(path) { + return function(name) { + var dir = dirname(path); + var absolute = expand(dir, name); + return globals.require(absolute, path); + }; + }; + + var initModule = function(name, definition) { + var module = {id: name, exports: {}}; + cache[name] = module; + definition(module.exports, localRequire(name), module); + return module.exports; + }; + + var require = function(name, loaderPath) { + var path = expand(name, '.'); + if (loaderPath == null) loaderPath = '/'; + + if (has(cache, path)) return cache[path].exports; + if (has(modules, path)) return initModule(path, modules[path]); + + var dirIndex = expand(path, './index'); + if (has(cache, dirIndex)) return cache[dirIndex].exports; + if (has(modules, dirIndex)) return initModule(dirIndex, modules[dirIndex]); + + throw new Error('Cannot find module "' + name + '" from '+ '"' + loaderPath + '"'); + }; + + var define = function(bundle, fn) { + if (typeof bundle === 'object') { + for (var key in bundle) { + if (has(bundle, key)) { + modules[key] = bundle[key]; + } + } + } else { + modules[bundle] = fn; + } + }; + + var list = function() { + var result = []; + for (var item in modules) { + if (has(modules, item)) { + result.push(item); + } + } + return result; + }; + + globals.require = require; + globals.require.define = define; + globals.require.register = define; + globals.require.list = list; + globals.require.brunch = true; +})(); +require.define({'phoenix': function(exports, require, module){ "use strict"; + +var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + +// Phoenix Channels JavaScript client +// +// ## Socket Connection +// +// A single connection is established to the server and +// channels are mulitplexed over the connection. +// Connect to the server using the `Socket` class: +// +// let socket = new Socket("/ws") +// socket.connect() +// +// The `Socket` constructor takes the mount point of the socket +// as well as options that can be found in the Socket docs, +// such as configuring the `LongPoller` transport, and heartbeat. +// +// +// ## Channels +// +// Channels are isolated, concurrent processes on the server that +// subscribe to topics and broker events between the client and server. +// To join a channel, you must provide the topic, and channel params for +// authorization. Here's an example chat room example where `"new_msg"` +// events are listened for, messages are pushed to the server, and +// the channel is joined with ok/error matches, and `after` hook: +// +// let chan = socket.chan("rooms:123", {token: roomToken}) +// chan.on("new_msg", msg => console.log("Got message", msg) ) +// $input.onEnter( e => { +// chan.push("new_msg", {body: e.target.val}) +// .receive("ok", (message) => console.log("created message", message) ) +// .receive("error", (reasons) => console.log("create failed", reasons) ) +// .after(10000, () => console.log("Networking issue. Still waiting...") ) +// }) +// chan.join() +// .receive("ok", ({messages}) => console.log("catching up", messages) ) +// .receive("error", ({reason}) => console.log("failed join", reason) ) +// .after(10000, () => console.log("Networking issue. Still waiting...") ) +// +// +// ## Joining +// +// Joining a channel with `chan.join(topic, params)`, binds the params to +// `chan.params`. Subsequent rejoins will send up the modified params for +// updating authorization params, or passing up last_message_id information. +// Successful joins receive an "ok" status, while unsuccessful joins +// receive "error". +// +// +// ## Pushing Messages +// +// From the prevoius example, we can see that pushing messages to the server +// can be done with `chan.push(eventName, payload)` and we can optionally +// receive responses from the push. Additionally, we can use +// `after(millsec, callback)` to abort waiting for our `receive` hooks and +// take action after some period of waiting. +// +// +// ## Socket Hooks +// +// Lifecycle events of the multiplexed connection can be hooked into via +// `socket.onError()` and `socket.onClose()` events, ie: +// +// socket.onError( () => console.log("there was an error with the connection!") ) +// socket.onClose( () => console.log("the connection dropped") ) +// +// +// ## Channel Hooks +// +// For each joined channel, you can bind to `onError` and `onClose` events +// to monitor the channel lifecycle, ie: +// +// chan.onError( () => console.log("there was an error!") ) +// chan.onClose( () => console.log("the channel has gone away gracefully") ) +// +// ### onError hooks +// +// `onError` hooks are invoked if the socket connection drops, or the channel +// crashes on the server. In either case, a channel rejoin is attemtped +// automatically in an exponential backoff manner. +// +// ### onClose hooks +// +// `onClose` hooks are invoked only in two cases. 1) the channel explicitly +// closed on the server, or 2). The client explicitly closed, by calling +// `chan.leave()` +// + +var SOCKET_STATES = { connecting: 0, open: 1, closing: 2, closed: 3 }; +var CHAN_STATES = { + closed: "closed", + errored: "errored", + joined: "joined", + joining: "joining" }; +var CHAN_EVENTS = { + close: "phx_close", + error: "phx_error", + join: "phx_join", + reply: "phx_reply", + leave: "phx_leave" +}; + +var Push = (function () { + + // Initializes the Push + // + // chan - The Channel + // event - The event, ie `"phx_join"` + // payload - The payload, ie `{user_id: 123}` + // + + function Push(chan, event, payload) { + _classCallCheck(this, Push); + + this.chan = chan; + this.event = event; + this.payload = payload || {}; + this.receivedResp = null; + this.afterHook = null; + this.recHooks = []; + this.sent = false; + } + + _prototypeProperties(Push, null, { + send: { + value: function send() { + var _this = this; + + var ref = this.chan.socket.makeRef(); + this.refEvent = this.chan.replyEventName(ref); + this.receivedResp = null; + this.sent = false; + + this.chan.on(this.refEvent, function (payload) { + _this.receivedResp = payload; + _this.matchReceive(payload); + _this.cancelRefEvent(); + _this.cancelAfter(); + }); + + this.startAfter(); + this.sent = true; + this.chan.socket.push({ + topic: this.chan.topic, + event: this.event, + payload: this.payload, + ref: ref + }); + }, + writable: true, + configurable: true + }, + receive: { + value: function receive(status, callback) { + if (this.receivedResp && this.receivedResp.status === status) { + callback(this.receivedResp.response); + } + + this.recHooks.push({ status: status, callback: callback }); + return this; + }, + writable: true, + configurable: true + }, + after: { + value: function after(ms, callback) { + if (this.afterHook) { + throw "only a single after hook can be applied to a push"; + } + var timer = null; + if (this.sent) { + timer = setTimeout(callback, ms); + } + this.afterHook = { ms: ms, callback: callback, timer: timer }; + return this; + }, + writable: true, + configurable: true + }, + matchReceive: { + + // private + + value: function matchReceive(_ref) { + var status = _ref.status; + var response = _ref.response; + var ref = _ref.ref; + + this.recHooks.filter(function (h) { + return h.status === status; + }).forEach(function (h) { + return h.callback(response); + }); + }, + writable: true, + configurable: true + }, + cancelRefEvent: { + value: function cancelRefEvent() { + this.chan.off(this.refEvent); + }, + writable: true, + configurable: true + }, + cancelAfter: { + value: function cancelAfter() { + if (!this.afterHook) { + return; + } + clearTimeout(this.afterHook.timer); + this.afterHook.timer = null; + }, + writable: true, + configurable: true + }, + startAfter: { + value: function startAfter() { + var _this = this; + + if (!this.afterHook) { + return; + } + var callback = function () { + _this.cancelRefEvent(); + _this.afterHook.callback(); + }; + this.afterHook.timer = setTimeout(callback, this.afterHook.ms); + }, + writable: true, + configurable: true + } + }); + + return Push; +})(); + +var Channel = exports.Channel = (function () { + function Channel(topic, params, socket) { + var _this = this; + + _classCallCheck(this, Channel); + + this.state = CHAN_STATES.closed; + this.topic = topic; + this.params = params || {}; + this.socket = socket; + this.bindings = []; + this.joinedOnce = false; + this.joinPush = new Push(this, CHAN_EVENTS.join, this.params); + this.pushBuffer = []; + + this.joinPush.receive("ok", function () { + _this.state = CHAN_STATES.joined; + }); + this.onClose(function () { + _this.state = CHAN_STATES.closed; + _this.socket.remove(_this); + }); + this.onError(function (reason) { + _this.state = CHAN_STATES.errored; + setTimeout(function () { + return _this.rejoinUntilConnected(); + }, _this.socket.reconnectAfterMs); + }); + this.on(CHAN_EVENTS.reply, function (payload, ref) { + _this.trigger(_this.replyEventName(ref), payload); + }); + } + + _prototypeProperties(Channel, null, { + rejoinUntilConnected: { + value: function rejoinUntilConnected() { + var _this = this; + + if (this.state !== CHAN_STATES.errored) { + return; + } + if (this.socket.isConnected()) { + this.rejoin(); + } else { + setTimeout(function () { + return _this.rejoinUntilConnected(); + }, this.socket.reconnectAfterMs); + } + }, + writable: true, + configurable: true + }, + join: { + value: function join() { + if (this.joinedOnce) { + throw "tried to join mulitple times. 'join' can only be called a singe time per channel instance"; + } else { + this.joinedOnce = true; + } + this.sendJoin(); + return this.joinPush; + }, + writable: true, + configurable: true + }, + onClose: { + value: function onClose(callback) { + this.on(CHAN_EVENTS.close, callback); + }, + writable: true, + configurable: true + }, + onError: { + value: function onError(callback) { + this.on(CHAN_EVENTS.error, function (reason) { + return callback(reason); + }); + }, + writable: true, + configurable: true + }, + on: { + value: function on(event, callback) { + this.bindings.push({ event: event, callback: callback }); + }, + writable: true, + configurable: true + }, + off: { + value: function off(event) { + this.bindings = this.bindings.filter(function (bind) { + return bind.event !== event; + }); + }, + writable: true, + configurable: true + }, + canPush: { + value: function canPush() { + return this.socket.isConnected() && this.state === CHAN_STATES.joined; + }, + writable: true, + configurable: true + }, + push: { + value: function push(event, payload) { + if (!this.joinedOnce) { + throw "tried to push '" + event + "' to '" + this.topic + "' before joining. Use chan.join() before pushing events"; + } + var pushEvent = new Push(this, event, payload); + if (this.canPush()) { + pushEvent.send(); + } else { + this.pushBuffer.push(pushEvent); + } + + return pushEvent; + }, + writable: true, + configurable: true + }, + leave: { + + // Leaves the channel + // + // Unsubscribes from server events, and + // instructs channel to terminate on server + // + // Triggers onClose() hooks + // + // To receive leave acknowledgements, use the a `receive` + // hook to bind to the server ack, ie: + // + // chan.leave().receive("ok", () => alert("left!") ) + // + + value: function leave() { + var _this = this; + + return this.push(CHAN_EVENTS.leave).receive("ok", function () { + _this.trigger(CHAN_EVENTS.close, "leave"); + }); + }, + writable: true, + configurable: true + }, + isMember: { + + // private + + value: function isMember(topic) { + return this.topic === topic; + }, + writable: true, + configurable: true + }, + sendJoin: { + value: function sendJoin() { + this.state = CHAN_STATES.joining; + this.joinPush.send(); + }, + writable: true, + configurable: true + }, + rejoin: { + value: function rejoin() { + this.sendJoin(); + this.pushBuffer.forEach(function (pushEvent) { + return pushEvent.send(); + }); + this.pushBuffer = []; + }, + writable: true, + configurable: true + }, + trigger: { + value: function trigger(triggerEvent, payload, ref) { + this.bindings.filter(function (bind) { + return bind.event === triggerEvent; + }).map(function (bind) { + return bind.callback(payload, ref); + }); + }, + writable: true, + configurable: true + }, + replyEventName: { + value: function replyEventName(ref) { + return "chan_reply_" + ref; + }, + writable: true, + configurable: true + } + }); + + return Channel; +})(); + +var Socket = exports.Socket = (function () { + + // Initializes the Socket + // + // endPoint - The string WebSocket endpoint, ie, "ws://example.com/ws", + // "wss://example.com" + // "/ws" (inherited host & protocol) + // opts - Optional configuration + // transport - The Websocket Transport, ie WebSocket, Phoenix.LongPoller. + // Defaults to WebSocket with automatic LongPoller fallback. + // heartbeatIntervalMs - The millisec interval to send a heartbeat message + // reconnectAfterMs - The millisec interval to reconnect after connection loss + // logger - The optional function for specialized logging, ie: + // `logger: function(msg){ console.log(msg) }` + // longpoller_timeout - The maximum timeout of a long poll AJAX request. + // Defaults to 20s (double the server long poll timer). + // + // For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim) + // + + function Socket(endPoint) { + var opts = arguments[1] === undefined ? {} : arguments[1]; + + _classCallCheck(this, Socket); + + this.stateChangeCallbacks = { open: [], close: [], error: [], message: [] }; + this.reconnectTimer = null; + this.channels = []; + this.sendBuffer = []; + this.ref = 0; + this.transport = opts.transport || window.WebSocket || LongPoller; + this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 30000; + this.reconnectAfterMs = opts.reconnectAfterMs || 5000; + this.logger = opts.logger || function () {}; // noop + this.longpoller_timeout = opts.longpoller_timeout || 20000; + this.endPoint = this.expandEndpoint(endPoint); + } + + _prototypeProperties(Socket, null, { + protocol: { + value: function protocol() { + return location.protocol.match(/^https/) ? "wss" : "ws"; + }, + writable: true, + configurable: true + }, + expandEndpoint: { + value: function expandEndpoint(endPoint) { + if (endPoint.charAt(0) !== "/") { + return endPoint; + } + if (endPoint.charAt(1) === "/") { + return "" + this.protocol() + ":" + endPoint; + } + + return "" + this.protocol() + "://" + location.host + "" + endPoint; + }, + writable: true, + configurable: true + }, + disconnect: { + value: function disconnect(callback, code, reason) { + if (this.conn) { + this.conn.onclose = function () {}; // noop + if (code) { + this.conn.close(code, reason || ""); + } else { + this.conn.close(); + } + this.conn = null; + } + callback && callback(); + }, + writable: true, + configurable: true + }, + connect: { + value: function connect() { + var _this = this; + + this.disconnect(function () { + _this.conn = new _this.transport(_this.endPoint); + _this.conn.timeout = _this.longpoller_timeout; + _this.conn.onopen = function () { + return _this.onConnOpen(); + }; + _this.conn.onerror = function (error) { + return _this.onConnError(error); + }; + _this.conn.onmessage = function (event) { + return _this.onConnMessage(event); + }; + _this.conn.onclose = function (event) { + return _this.onConnClose(event); + }; + }); + }, + writable: true, + configurable: true + }, + log: { + + // Logs the message. Override `this.logger` for specialized logging. noops by default + + value: function log(msg) { + this.logger(msg); + }, + writable: true, + configurable: true + }, + onOpen: { + + // Registers callbacks for connection state change events + // + // Examples + // + // socket.onError(function(error){ alert("An error occurred") }) + // + + value: function onOpen(callback) { + this.stateChangeCallbacks.open.push(callback); + }, + writable: true, + configurable: true + }, + onClose: { + value: function onClose(callback) { + this.stateChangeCallbacks.close.push(callback); + }, + writable: true, + configurable: true + }, + onError: { + value: function onError(callback) { + this.stateChangeCallbacks.error.push(callback); + }, + writable: true, + configurable: true + }, + onMessage: { + value: function onMessage(callback) { + this.stateChangeCallbacks.message.push(callback); + }, + writable: true, + configurable: true + }, + onConnOpen: { + value: function onConnOpen() { + var _this = this; + + this.flushSendBuffer(); + clearInterval(this.reconnectTimer); + if (!this.conn.skipHeartbeat) { + clearInterval(this.heartbeatTimer); + this.heartbeatTimer = setInterval(function () { + return _this.sendHeartbeat(); + }, this.heartbeatIntervalMs); + } + this.stateChangeCallbacks.open.forEach(function (callback) { + return callback(); + }); + }, + writable: true, + configurable: true + }, + onConnClose: { + value: function onConnClose(event) { + var _this = this; + + this.log("WS close:"); + this.log(event); + this.triggerChanError(); + clearInterval(this.reconnectTimer); + clearInterval(this.heartbeatTimer); + this.reconnectTimer = setInterval(function () { + return _this.connect(); + }, this.reconnectAfterMs); + this.stateChangeCallbacks.close.forEach(function (callback) { + return callback(event); + }); + }, + writable: true, + configurable: true + }, + onConnError: { + value: function onConnError(error) { + this.log("WS error:"); + this.log(error); + this.triggerChanError(); + this.stateChangeCallbacks.error.forEach(function (callback) { + return callback(error); + }); + }, + writable: true, + configurable: true + }, + triggerChanError: { + value: function triggerChanError() { + this.channels.forEach(function (chan) { + return chan.trigger(CHAN_EVENTS.error); + }); + }, + writable: true, + configurable: true + }, + connectionState: { + value: function connectionState() { + switch (this.conn && this.conn.readyState) { + case SOCKET_STATES.connecting: + return "connecting"; + case SOCKET_STATES.open: + return "open"; + case SOCKET_STATES.closing: + return "closing"; + default: + return "closed"; + } + }, + writable: true, + configurable: true + }, + isConnected: { + value: function isConnected() { + return this.connectionState() === "open"; + }, + writable: true, + configurable: true + }, + remove: { + value: function remove(chan) { + this.channels = this.channels.filter(function (c) { + return !c.isMember(chan.topic); + }); + }, + writable: true, + configurable: true + }, + chan: { + value: function chan(topic, params) { + var chan = new Channel(topic, params, this); + this.channels.push(chan); + return chan; + }, + writable: true, + configurable: true + }, + push: { + value: function push(data) { + var _this = this; + + var callback = function () { + return _this.conn.send(JSON.stringify(data)); + }; + if (this.isConnected()) { + callback(); + } else { + this.sendBuffer.push(callback); + } + }, + writable: true, + configurable: true + }, + makeRef: { + + // Return the next message ref, accounting for overflows + + value: function makeRef() { + var newRef = this.ref + 1; + if (newRef === this.ref) { + this.ref = 0; + } else { + this.ref = newRef; + } + + return this.ref.toString(); + }, + writable: true, + configurable: true + }, + sendHeartbeat: { + value: function sendHeartbeat() { + this.push({ topic: "phoenix", event: "heartbeat", payload: {}, ref: this.makeRef() }); + }, + writable: true, + configurable: true + }, + flushSendBuffer: { + value: function flushSendBuffer() { + if (this.isConnected() && this.sendBuffer.length > 0) { + this.sendBuffer.forEach(function (callback) { + return callback(); + }); + this.sendBuffer = []; + } + }, + writable: true, + configurable: true + }, + onConnMessage: { + value: function onConnMessage(rawMessage) { + this.log("message received:"); + this.log(rawMessage); + var msg = JSON.parse(rawMessage.data); + var topic = msg.topic; + var event = msg.event; + var payload = msg.payload; + var ref = msg.ref; + + this.channels.filter(function (chan) { + return chan.isMember(topic); + }).forEach(function (chan) { + return chan.trigger(event, payload, ref); + }); + this.stateChangeCallbacks.message.forEach(function (callback) { + return callback(msg); + }); + }, + writable: true, + configurable: true + } + }); + + return Socket; +})(); + +var LongPoller = exports.LongPoller = (function () { + function LongPoller(endPoint) { + _classCallCheck(this, LongPoller); + + this.retryInMs = 5000; + this.endPoint = null; + this.token = null; + this.sig = null; + this.skipHeartbeat = true; + this.onopen = function () {}; // noop + this.onerror = function () {}; // noop + this.onmessage = function () {}; // noop + this.onclose = function () {}; // noop + this.upgradeEndpoint = this.normalizeEndpoint(endPoint); + this.pollEndpoint = this.upgradeEndpoint + (/\/$/.test(endPoint) ? "poll" : "/poll"); + this.readyState = SOCKET_STATES.connecting; + + this.poll(); + } + + _prototypeProperties(LongPoller, null, { + normalizeEndpoint: { + value: function normalizeEndpoint(endPoint) { + return endPoint.replace("ws://", "http://").replace("wss://", "https://"); + }, + writable: true, + configurable: true + }, + endpointURL: { + value: function endpointURL() { + return this.pollEndpoint + ("?token=" + encodeURIComponent(this.token) + "&sig=" + encodeURIComponent(this.sig)); + }, + writable: true, + configurable: true + }, + closeAndRetry: { + value: function closeAndRetry() { + this.close(); + this.readyState = SOCKET_STATES.connecting; + }, + writable: true, + configurable: true + }, + ontimeout: { + value: function ontimeout() { + this.onerror("timeout"); + this.closeAndRetry(); + }, + writable: true, + configurable: true + }, + poll: { + value: function poll() { + var _this = this; + + if (!(this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting)) { + return; + } + + Ajax.request("GET", this.endpointURL(), "application/json", null, this.timeout, this.ontimeout.bind(this), function (resp) { + if (resp) { + var status = resp.status; + var token = resp.token; + var sig = resp.sig; + var messages = resp.messages; + + _this.token = token; + _this.sig = sig; + } else { + var status = 0; + } + + switch (status) { + case 200: + messages.forEach(function (msg) { + return _this.onmessage({ data: JSON.stringify(msg) }); + }); + _this.poll(); + break; + case 204: + _this.poll(); + break; + case 410: + _this.readyState = SOCKET_STATES.open; + _this.onopen(); + _this.poll(); + break; + case 0: + case 500: + _this.onerror(); + _this.closeAndRetry(); + break; + default: + throw "unhandled poll status " + status; + } + }); + }, + writable: true, + configurable: true + }, + send: { + value: function send(body) { + var _this = this; + + Ajax.request("POST", this.endpointURL(), "application/json", body, this.timeout, this.onerror.bind(this, "timeout"), function (resp) { + if (!resp || resp.status !== 200) { + _this.onerror(status); + _this.closeAndRetry(); + } + }); + }, + writable: true, + configurable: true + }, + close: { + value: function close(code, reason) { + this.readyState = SOCKET_STATES.closed; + this.onclose(); + }, + writable: true, + configurable: true + } + }); + + return LongPoller; +})(); + +var Ajax = exports.Ajax = (function () { + function Ajax() { + _classCallCheck(this, Ajax); + } + + _prototypeProperties(Ajax, { + request: { + value: function request(method, endPoint, accept, body, timeout, ontimeout, callback) { + if (window.XDomainRequest) { + var req = new XDomainRequest(); // IE8, IE9 + this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback); + } else { + var req = window.XMLHttpRequest ? new XMLHttpRequest() : // IE7+, Firefox, Chrome, Opera, Safari + new ActiveXObject("Microsoft.XMLHTTP"); // IE6, IE5 + this.xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback); + } + }, + writable: true, + configurable: true + }, + xdomainRequest: { + value: function xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback) { + var _this = this; + + req.timeout = timeout; + req.open(method, endPoint); + req.onload = function () { + var response = _this.parseJSON(req.responseText); + callback && callback(response); + }; + if (ontimeout) { + req.ontimeout = ontimeout; + } + + // Work around bug in IE9 that requires an attached onprogress handler + req.onprogress = function () {}; + + req.send(body); + }, + writable: true, + configurable: true + }, + xhrRequest: { + value: function xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback) { + var _this = this; + + req.timeout = timeout; + req.open(method, endPoint, true); + req.setRequestHeader("Content-Type", accept); + req.onerror = function () { + callback && callback(null); + }; + req.onreadystatechange = function () { + if (req.readyState === _this.states.complete && callback) { + var response = _this.parseJSON(req.responseText); + callback(response); + } + }; + if (ontimeout) { + req.ontimeout = ontimeout; + } + + req.send(body); + }, + writable: true, + configurable: true + }, + parseJSON: { + value: function parseJSON(resp) { + return resp && resp !== "" ? JSON.parse(resp) : null; + }, + writable: true, + configurable: true + } + }); + + return Ajax; +})(); + +Ajax.states = { complete: 4 }; +Object.defineProperty(exports, "__esModule", { + value: true +}); + }}); +if(typeof(window) === 'object' && !window.Phoenix){ window.Phoenix = require('phoenix') }; \ No newline at end of file diff --git a/web/templates/layout/application.html.eex b/web/templates/layout/application.html.eex new file mode 100644 index 0000000..8ab7880 --- /dev/null +++ b/web/templates/layout/application.html.eex @@ -0,0 +1,27 @@ + + + + + + + + + + Hello Phoenix! + "> + + + + +

+
+

+

+
+ <%= @inner %> +
+ + + + + diff --git a/web/templates/page/index.html.eex b/web/templates/page/index.html.eex new file mode 100644 index 0000000..0dedb16 --- /dev/null +++ b/web/templates/page/index.html.eex @@ -0,0 +1,59 @@ +
+
+ +

+ + elixirstatus.com + +

+ + + +
+

+ Hi there, +

+

+ I'm @rrrene, maintainer of Inch CI and InchEx. +

+

+ This page exists because I think we need a site where people can post what they made in Elixir, simliar to Rubyflow for Ruby. + A site that is build in Elixir, that is Open Source and integrates nicely with the existing ecosystem. +

+

+ I think a site like that, if done right, will eventually be of great use to encourage people to contribute to the Elixir community. + You can read the full reasoning behind this in the project proposal. +

+

+ Ah, nearly forgot: I want to build this site, right here. +

+
+
+ +
+ + <%= if @current_user do %> + + <%= render ElixirStatus.PageView, "index_logged_in.html", current_user: @current_user, conn: @conn %> + + <% else %> + + <%= render ElixirStatus.PageView, "index_not_logged_in.html", conn: @conn %> + + <% end %> + +
+ + + +
diff --git a/web/templates/page/index_logged_in.html.eex b/web/templates/page/index_logged_in.html.eex new file mode 100644 index 0000000..068025b --- /dev/null +++ b/web/templates/page/index_logged_in.html.eex @@ -0,0 +1,24 @@ +
+
+ +

+ Welcome, <%= @current_user["login"] %>! +

+ +
+

+ You successfully signed in and will be among the first to know when this site becomes useful! + Let's get the word out: +

+

+

ElixirStatus, an upcoming community site for Elixir: elixirstatus.com
+

+

+ Tweet this now! +

+
+
+ +
diff --git a/web/templates/page/index_not_logged_in.html.eex b/web/templates/page/index_not_logged_in.html.eex new file mode 100644 index 0000000..2232a4a --- /dev/null +++ b/web/templates/page/index_not_logged_in.html.eex @@ -0,0 +1,8 @@ + +

+ By signing in, you show that you would like this site to exist. +

+ + + Sign in with GitHub + diff --git a/web/views/error_view.ex b/web/views/error_view.ex new file mode 100644 index 0000000..72d9d46 --- /dev/null +++ b/web/views/error_view.ex @@ -0,0 +1,17 @@ +defmodule ElixirStatus.ErrorView do + use ElixirStatus.Web, :view + + def render("404.html", _assigns) do + "Page not found" + end + + def render("500.html", _assigns) do + "Server internal error" + end + + # In case no render clause matches or no + # template is found, let's render it as 500 + def template_not_found(_template, assigns) do + render "500.html", assigns + end +end diff --git a/web/views/layout_view.ex b/web/views/layout_view.ex new file mode 100644 index 0000000..595c5f3 --- /dev/null +++ b/web/views/layout_view.ex @@ -0,0 +1,3 @@ +defmodule ElixirStatus.LayoutView do + use ElixirStatus.Web, :view +end diff --git a/web/views/page_view.ex b/web/views/page_view.ex new file mode 100644 index 0000000..608fdac --- /dev/null +++ b/web/views/page_view.ex @@ -0,0 +1,3 @@ +defmodule ElixirStatus.PageView do + use ElixirStatus.Web, :view +end diff --git a/web/web.ex b/web/web.ex new file mode 100644 index 0000000..1d6a87a --- /dev/null +++ b/web/web.ex @@ -0,0 +1,78 @@ +defmodule ElixirStatus.Web do + @moduledoc """ + A module that keeps using definitions for controllers, + views and so on. + + This can be used in your application as: + + use ElixirStatus.Web, :controller + use ElixirStatus.Web, :view + + The definitions below will be executed for every view, + controller, etc, so keep them short and clean, focused + on imports, uses and aliases. + + Do NOT define functions inside the quoted expressions + below. + """ + + def model do + quote do + use Ecto.Model + end + end + + def controller do + quote do + use Phoenix.Controller + + # Alias the data repository and import query/model functions + alias ElixirStatus.Repo + import Ecto.Model + import Ecto.Query, only: [from: 2] + + # Import URL helpers from the router + import ElixirStatus.Router.Helpers + end + end + + def view do + quote do + use Phoenix.View, root: "web/templates" + + # Import convenience functions from controllers + import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] + + # Import URL helpers from the router + import ElixirStatus.Router.Helpers + + # Use all HTML functionality (forms, tags, etc) + use Phoenix.HTML + end + end + + def router do + quote do + use Phoenix.Router + end + end + + def channel do + quote do + use Phoenix.Channel + + # Alias the data repository and import query/model functions + alias ElixirStatus.Repo + import Ecto.Model + import Ecto.Query, only: [from: 2] + + end + end + + @doc """ + When used, dispatch to the appropriate controller/view/etc. + """ + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end +end