Browse Source

init repo

Steven Jacobs 7 months ago
commit
3b8898532e
50 changed files with 12698 additions and 0 deletions
  1. 5 0
      .formatter.exs
  2. 42 0
      .gitignore
  3. 20 0
      README.md
  4. 5 0
      assets/.babelrc
  5. 3 0
      assets/css/app.css
  6. 134 0
      assets/css/phoenix.css
  7. 17 0
      assets/js/app.js
  8. 63 0
      assets/js/socket.js
  9. 11203 0
      assets/package-lock.json
  10. 24 0
      assets/package.json
  11. BIN
      assets/static/favicon.ico
  12. BIN
      assets/static/images/phoenix.png
  13. 5 0
      assets/static/robots.txt
  14. 41 0
      assets/webpack.config.js
  15. 34 0
      config/config.exs
  16. 75 0
      config/dev.exs
  17. 71 0
      config/prod.exs
  18. 18 0
      config/test.exs
  19. 18 0
      docker-compose.yml
  20. 9 0
      lib/incidentctl.ex
  21. 37 0
      lib/incidentctl/application.ex
  22. 5 0
      lib/incidentctl/repo.ex
  23. 64 0
      lib/incidentctl/slack.ex
  24. 69 0
      lib/incidentctl_web.ex
  25. 33 0
      lib/incidentctl_web/channels/user_socket.ex
  26. 7 0
      lib/incidentctl_web/controllers/page_controller.ex
  27. 46 0
      lib/incidentctl_web/endpoint.ex
  28. 24 0
      lib/incidentctl_web/gettext.ex
  29. 26 0
      lib/incidentctl_web/router.ex
  30. 30 0
      lib/incidentctl_web/templates/layout/app.html.eex
  31. 35 0
      lib/incidentctl_web/templates/page/index.html.eex
  32. 44 0
      lib/incidentctl_web/views/error_helpers.ex
  33. 16 0
      lib/incidentctl_web/views/error_view.ex
  34. 3 0
      lib/incidentctl_web/views/layout_view.ex
  35. 3 0
      lib/incidentctl_web/views/page_view.ex
  36. 64 0
      mix.exs
  37. 36 0
      mix.lock
  38. 97 0
      priv/gettext/en/LC_MESSAGES/errors.po
  39. 95 0
      priv/gettext/errors.pot
  40. 4 0
      priv/repo/migrations/.formatter.exs
  41. 11 0
      priv/repo/seeds.exs
  42. 4 0
      test/incidentctl/slack_test.exs
  43. 8 0
      test/incidentctl_web/controllers/page_controller_test.exs
  44. 14 0
      test/incidentctl_web/views/error_view_test.exs
  45. 3 0
      test/incidentctl_web/views/layout_view_test.exs
  46. 3 0
      test/incidentctl_web/views/page_view_test.exs
  47. 37 0
      test/support/channel_case.ex
  48. 38 0
      test/support/conn_case.ex
  49. 53 0
      test/support/data_case.ex
  50. 2 0
      test/test_helper.exs

+ 5 - 0
.formatter.exs

@@ -0,0 +1,5 @@
+[
+  import_deps: [:ecto, :phoenix],
+  inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
+  subdirectories: ["priv/*/migrations"]
+]

+ 42 - 0
.gitignore

@@ -0,0 +1,42 @@
+# The directory Mix will write compiled artifacts to.
+/_build/
+
+# If you run "mix test --cover", coverage assets end up here.
+/cover/
+
+# The directory Mix downloads your dependencies sources to.
+/deps/
+
+# Where 3rd-party dependencies like ExDoc output generated docs.
+/doc/
+
+# Ignore .fetch files in case you like to edit your project deps locally.
+/.fetch
+
+# If the VM crashes, it generates a dump, let's ignore it too.
+erl_crash.dump
+
+# Also ignore archive artifacts (built via "mix archive.build").
+*.ez
+
+# Ignore package tarball (built via "mix hex.build").
+incidentctl-*.tar
+
+# If NPM crashes, it generates a log, let's ignore it too.
+npm-debug.log
+
+# The directory NPM downloads your dependencies sources to.
+/assets/node_modules/
+
+# Since we are building assets from assets/,
+# we ignore priv/static. You may want to comment
+# this depending on your deployment strategy.
+/priv/static/
+
+# Files matching config/*.secret.exs pattern contain sensitive
+# data and you should not commit them into version control.
+#
+# Alternatively, you may comment the line below and commit the
+# secrets files as long as you replace their contents by environment
+# variables.
+/config/*.secret.exs

+ 20 - 0
README.md

@@ -0,0 +1,20 @@
+# Incidentctl
+
+To start your Phoenix server:
+
+  * Install dependencies with `mix deps.get`
+  * Create and migrate your database with `mix ecto.setup`
+  * Install Node.js dependencies with `cd assets && npm install`
+  * Start Phoenix endpoint with `mix phx.server`
+
+Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
+
+Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
+
+## Learn more
+
+  * Official website: http://www.phoenixframework.org/
+  * Guides: https://hexdocs.pm/phoenix/overview.html
+  * Docs: https://hexdocs.pm/phoenix
+  * Mailing list: http://groups.google.com/group/phoenix-talk
+  * Source: https://github.com/phoenixframework/phoenix

+ 5 - 0
assets/.babelrc

@@ -0,0 +1,5 @@
+{
+    "presets": [
+        "@babel/preset-env"
+    ]
+}

+ 3 - 0
assets/css/app.css

@@ -0,0 +1,3 @@
+/* This file is for your main application css. */
+
+@import "./phoenix.css";

File diff suppressed because it is too large
+ 134 - 0
assets/css/phoenix.css


+ 17 - 0
assets/js/app.js

@@ -0,0 +1,17 @@
+// We need to import the CSS so that webpack will load it.
+// The MiniCssExtractPlugin is used to separate it out into
+// its own CSS file.
+import css from "../css/app.css"
+
+// webpack automatically bundles all modules in your
+// entry points. Those entry points can be configured
+// in "webpack.config.js".
+//
+// Import dependencies
+//
+import "phoenix_html"
+
+// Import local files
+//
+// Local files can be imported directly using relative paths, for example:
+// import socket from "./socket"

+ 63 - 0
assets/js/socket.js

@@ -0,0 +1,63 @@
+// NOTE: The contents of this file will only be executed if
+// you uncomment its entry in "assets/js/app.js".
+
+// To use Phoenix channels, the first step is to import Socket,
+// and connect at the socket path in "lib/web/endpoint.ex".
+//
+// Pass the token on params as below. Or remove it
+// from the params if you are not using authentication.
+import {Socket} from "phoenix"
+
+let socket = new Socket("/socket", {params: {token: window.userToken}})
+
+// When you connect, you'll often need to authenticate the client.
+// For example, imagine you have an authentication plug, `MyAuth`,
+// which authenticates the session and assigns a `:current_user`.
+// If the current user exists you can assign the user's token in
+// the connection for use in the layout.
+//
+// In your "lib/web/router.ex":
+//
+//     pipeline :browser do
+//       ...
+//       plug MyAuth
+//       plug :put_user_token
+//     end
+//
+//     defp put_user_token(conn, _) do
+//       if current_user = conn.assigns[:current_user] do
+//         token = Phoenix.Token.sign(conn, "user socket", current_user.id)
+//         assign(conn, :user_token, token)
+//       else
+//         conn
+//       end
+//     end
+//
+// Now you need to pass this token to JavaScript. You can do so
+// inside a script tag in "lib/web/templates/layout/app.html.eex":
+//
+//     <script>window.userToken = "<%= assigns[:user_token] %>";</script>
+//
+// You will need to verify the user token in the "connect/3" function
+// in "lib/web/channels/user_socket.ex":
+//
+//     def connect(%{"token" => token}, socket, _connect_info) do
+//       # max_age: 1209600 is equivalent to two weeks in seconds
+//       case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
+//         {:ok, user_id} ->
+//           {:ok, assign(socket, :user, user_id)}
+//         {:error, reason} ->
+//           :error
+//       end
+//     end
+//
+// Finally, connect to the socket:
+socket.connect()
+
+// Now that you are connected, you can join channels with a topic:
+let channel = socket.channel("topic:subtopic", {})
+channel.join()
+  .receive("ok", resp => { console.log("Joined successfully", resp) })
+  .receive("error", resp => { console.log("Unable to join", resp) })
+
+export default socket

File diff suppressed because it is too large
+ 11203 - 0
assets/package-lock.json


+ 24 - 0
assets/package.json

@@ -0,0 +1,24 @@
+{
+  "repository": {},
+  "license": "MIT",
+  "scripts": {
+    "deploy": "webpack --mode production",
+    "watch": "webpack --mode development --watch"
+  },
+  "dependencies": {
+    "phoenix": "file:../deps/phoenix",
+    "phoenix_html": "file:../deps/phoenix_html"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.0.0",
+    "@babel/preset-env": "^7.0.0",
+    "babel-loader": "^8.0.0",
+    "copy-webpack-plugin": "^4.5.0",
+    "css-loader": "^2.1.1",
+    "mini-css-extract-plugin": "^0.4.0",
+    "optimize-css-assets-webpack-plugin": "^4.0.0",
+    "uglifyjs-webpack-plugin": "^1.2.4",
+    "webpack": "4.4.0",
+    "webpack-cli": "^2.0.10"
+  }
+}

BIN
assets/static/favicon.ico


BIN
assets/static/images/phoenix.png


+ 5 - 0
assets/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: /

+ 41 - 0
assets/webpack.config.js

@@ -0,0 +1,41 @@
+const path = require('path');
+const glob = require('glob');
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
+const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
+const CopyWebpackPlugin = require('copy-webpack-plugin');
+
+module.exports = (env, options) => ({
+  optimization: {
+    minimizer: [
+      new UglifyJsPlugin({ cache: true, parallel: true, sourceMap: false }),
+      new OptimizeCSSAssetsPlugin({})
+    ]
+  },
+  entry: {
+      './js/app.js': ['./js/app.js'].concat(glob.sync('./vendor/**/*.js'))
+  },
+  output: {
+    filename: 'app.js',
+    path: path.resolve(__dirname, '../priv/static/js')
+  },
+  module: {
+    rules: [
+      {
+        test: /\.js$/,
+        exclude: /node_modules/,
+        use: {
+          loader: 'babel-loader'
+        }
+      },
+      {
+        test: /\.css$/,
+        use: [MiniCssExtractPlugin.loader, 'css-loader']
+      }
+    ]
+  },
+  plugins: [
+    new MiniCssExtractPlugin({ filename: '../css/app.css' }),
+    new CopyWebpackPlugin([{ from: 'static/', to: '../' }])
+  ]
+});

+ 34 - 0
config/config.exs

@@ -0,0 +1,34 @@
+# 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.
+
+# General application configuration
+use Mix.Config
+
+config :incidentctl,
+  ecto_repos: [Incidentctl.Repo]
+
+# Configures the endpoint
+config :incidentctl, IncidentctlWeb.Endpoint,
+  url: [host: "localhost"],
+  secret_key_base: "GxjuQK1OKwS3CLWQQWqW91YUEhb9JTEb7dnzeRMePbqldl/kooOjgvtXhadws785",
+  render_errors: [view: IncidentctlWeb.ErrorView, accepts: ~w(html json)],
+  pubsub: [name: Incidentctl.PubSub, adapter: Phoenix.PubSub.PG2]
+
+config :incidentctl, Incidentctl.Slack, token: System.get_env("SLACK_BOT_TOKEN")
+
+config :slack, api_token: System.get_env("SLACK_OAUTH_TOKEN")
+
+# Configures Elixir's Logger
+config :logger, :console,
+  format: "$time $metadata[$level] $message\n",
+  metadata: [:request_id]
+
+# Use Jason for JSON parsing in Phoenix
+config :phoenix, :json_library, Jason
+
+# 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"

+ 75 - 0
config/dev.exs

@@ -0,0 +1,75 @@
+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 webpack to recompile .js and .css sources.
+config :incidentctl, IncidentctlWeb.Endpoint,
+  http: [port: 4000],
+  debug_errors: true,
+  code_reloader: true,
+  check_origin: false,
+  watchers: [
+    node: [
+      "node_modules/webpack/bin/webpack.js",
+      "--mode",
+      "development",
+      "--watch-stdin",
+      cd: Path.expand("../assets", __DIR__)
+    ]
+  ]
+
+# ## SSL Support
+#
+# In order to use HTTPS in development, a self-signed
+# certificate can be generated by running the following
+# Mix task:
+#
+#     mix phx.gen.cert
+#
+# Note that this task requires Erlang/OTP 20 or later.
+# Run `mix help phx.gen.cert` for more information.
+#
+# The `http:` config above can be replaced with:
+#
+#     https: [
+#       port: 4001,
+#       cipher_suite: :strong,
+#       keyfile: "priv/cert/selfsigned_key.pem",
+#       certfile: "priv/cert/selfsigned.pem"
+#     ],
+#
+# If desired, both `http:` and `https:` keys can be
+# configured to run both http and https servers on
+# different ports.
+
+# Watch static and templates for browser reloading.
+config :incidentctl, IncidentctlWeb.Endpoint,
+  live_reload: [
+    patterns: [
+      ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
+      ~r"priv/gettext/.*(po)$",
+      ~r"lib/incidentctl_web/{live,views}/.*(ex)$",
+      ~r"lib/incidentctl_web/templates/.*(eex)$"
+    ]
+  ]
+
+# Do not include metadata nor timestamps in development logs
+config :logger, :console, format: "[$level] $message\n"
+
+# Set a higher stacktrace during development. Avoid configuring such
+# in production as building large stacktraces may be expensive.
+config :phoenix, :stacktrace_depth, 20
+
+# Initialize plugs at runtime for faster development compilation
+config :phoenix, :plug_init_mode, :runtime
+
+# Configure your database
+config :incidentctl, Incidentctl.Repo,
+  username: "postgres",
+  password: "postgres",
+  database: "incidentctl_dev",
+  hostname: "localhost",
+  pool_size: 10

+ 71 - 0
config/prod.exs

@@ -0,0 +1,71 @@
+use Mix.Config
+
+# For production, don't forget to configure the url host
+# to something meaningful, Phoenix uses this information
+# when generating URLs.
+#
+# Note we also include the path to a cache manifest
+# containing the digested version of static files. This
+# manifest is generated by the `mix phx.digest` task,
+# which you should run after static files are built and
+# before starting your production server.
+config :incidentctl, IncidentctlWeb.Endpoint,
+  http: [:inet6, port: System.get_env("PORT") || 4000],
+  url: [host: "example.com", port: 80],
+  cache_static_manifest: "priv/static/cache_manifest.json"
+
+# Do not print debug messages in production
+config :logger, level: :info
+
+# ## SSL Support
+#
+# To get SSL working, you will need to add the `https` key
+# to the previous section and set your `:url` port to 443:
+#
+#     config :incidentctl, IncidentctlWeb.Endpoint,
+#       ...
+#       url: [host: "example.com", port: 443],
+#       https: [
+#         :inet6,
+#         port: 443,
+#         cipher_suite: :strong,
+#         keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
+#         certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
+#       ]
+#
+# The `cipher_suite` is set to `:strong` to support only the
+# latest and more secure SSL ciphers. This means old browsers
+# and clients may not be supported. You can set it to
+# `:compatible` for wider support.
+#
+# `:keyfile` and `:certfile` expect an absolute path to the key
+# and cert in disk or a relative path inside priv, for example
+# "priv/ssl/server.key". For all supported SSL configuration
+# options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
+#
+# We also recommend setting `force_ssl` in your endpoint, ensuring
+# no data is ever sent via http, always redirecting to https:
+#
+#     config :incidentctl, IncidentctlWeb.Endpoint,
+#       force_ssl: [hsts: true]
+#
+# Check `Plug.SSL` for all available options in `force_ssl`.
+
+# ## Using releases (distillery)
+#
+# 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 :incidentctl, IncidentctlWeb.Endpoint, server: true
+#
+# Note you can't rely on `System.get_env/1` when using releases.
+# See the releases documentation accordingly.
+
+# Finally import the config/prod.secret.exs which should be versioned
+# separately.
+import_config "prod.secret.exs"

+ 18 - 0
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 :incidentctl, IncidentctlWeb.Endpoint,
+  http: [port: 4002],
+  server: false
+
+# Print only warnings and errors during test
+config :logger, level: :warn
+
+# Configure your database
+config :incidentctl, Incidentctl.Repo,
+  username: "postgres",
+  password: "postgres",
+  database: "incidentctl_test",
+  hostname: "localhost",
+  pool: Ecto.Adapters.SQL.Sandbox

+ 18 - 0
docker-compose.yml

@@ -0,0 +1,18 @@
+# Use postgres/example user/password credentials
+version: '3.1'
+
+services:
+
+  db:
+    image: postgres
+    restart: always
+    ports:
+      - 5432:5432
+    environment:
+      POSTGRES_PASSWORD: postgres
+
+  adminer:
+    image: adminer
+    restart: always
+    ports:
+      - 8080:8080

+ 9 - 0
lib/incidentctl.ex

@@ -0,0 +1,9 @@
+defmodule Incidentctl do
+  @moduledoc """
+  Incidentctl keeps the contexts that define your domain
+  and business logic.
+
+  Contexts are also responsible for managing your data, regardless
+  if it comes from the database, an external API or others.
+  """
+end

+ 37 - 0
lib/incidentctl/application.ex

@@ -0,0 +1,37 @@
+defmodule Incidentctl.Application do
+  # See https://hexdocs.pm/elixir/Application.html
+  # for more information on OTP Applications
+  @moduledoc false
+
+  use Application
+
+  def start(_type, _args) do
+    import Supervisor.Spec, warn: false
+    # List all child processes to be supervised
+    children = [
+      # Start the Ecto repository
+      Incidentctl.Repo,
+      # Start the endpoint when the application starts
+      IncidentctlWeb.Endpoint,
+      # Starts a worker by calling: Incidentctl.Worker.start_link(arg)
+      # {Incidentctl.Worker, arg},
+      worker(Slack.Bot, [
+        Incidentctl.Slack,
+        [],
+        Application.get_env(:incidentctl, Incidentctl.Slack)[:token]
+      ])
+    ]
+
+    # See https://hexdocs.pm/elixir/Supervisor.html
+    # for other strategies and supported options
+    opts = [strategy: :one_for_one, name: Incidentctl.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
+    IncidentctlWeb.Endpoint.config_change(changed, removed)
+    :ok
+  end
+end

+ 5 - 0
lib/incidentctl/repo.ex

@@ -0,0 +1,5 @@
+defmodule Incidentctl.Repo do
+  use Ecto.Repo,
+    otp_app: :incidentctl,
+    adapter: Ecto.Adapters.Postgres
+end

+ 64 - 0
lib/incidentctl/slack.ex

@@ -0,0 +1,64 @@
+defmodule Incidentctl.Slack do
+  use Slack
+
+  def handle_connect(slack, state) do
+    IO.puts("Connected as #{slack.me.name}")
+    {:ok, state}
+  end
+
+  def handle_event(message = %{type: "message"}, slack, state) do
+    if message_for_me?(message.text, slack.me.id) do
+      random_number = Integer.to_string(:rand.uniform(999_999))
+      channel_name = "inc-#{random_number}-190418"
+
+      %{"channel" => %{"id" => channel_id}} = create_channel(channel_name)
+
+      send_message("<##{channel_id}> created.", message.channel, slack)
+    end
+
+    {:ok, state}
+  end
+
+  def handle_event(_, _, state), do: {:ok, state}
+
+  def handle_info({:message, text, channel}, slack, state) do
+    IO.puts("Sending your message, captain!")
+
+    send_message(text, channel, slack)
+
+    {:ok, state}
+  end
+
+  def handle_info(_, _, state), do: {:ok, state}
+
+  @doc """
+  Returns true if all of the following are true:
+  - The message is for the user id provided
+  - The user id does not included in a blockquote
+
+  ## Examples
+      iex> message_match = "<@123456> do you like cheese?"
+      iex> Incidentctl.Slack.message_for_me?(message_match, 123456)
+      true
+
+      iex> message_non_match_quote = "> <@123456> I like cheese."
+      iex> Incidentctl.Slack.message_for_me?(message_non_match_quote, 123456)
+      false
+
+      iex> message_non_match_middle = "I heard <@123456> favorite cheese is gouda."
+      iex> Incidentctl.Slack.message_for_me?(message_non_match_middle, 123456)
+      false
+  """
+  def message_for_me?(message_text, id) do
+    message_text
+    |> String.split("\n")
+    |> Enum.filter(&String.match?(&1, ~r/^(?!&gt;)/))
+    |> Enum.filter(&String.match?(&1, ~r/^(<@#{id}>)/))
+    |> length > 0
+  end
+
+  def create_channel(name) do
+    name
+    |> Slack.Web.Channels.create()
+  end
+end

+ 69 - 0
lib/incidentctl_web.ex

@@ -0,0 +1,69 @@
+defmodule IncidentctlWeb do
+  @moduledoc """
+  The entrypoint for defining your web interface, such
+  as controllers, views, channels and so on.
+
+  This can be used in your application as:
+
+      use IncidentctlWeb, :controller
+      use IncidentctlWeb, :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. Instead, define any helper function in modules
+  and import those modules here.
+  """
+
+  def controller do
+    quote do
+      use Phoenix.Controller, namespace: IncidentctlWeb
+
+      import Plug.Conn
+      import IncidentctlWeb.Gettext
+      alias IncidentctlWeb.Router.Helpers, as: Routes
+    end
+  end
+
+  def view do
+    quote do
+      use Phoenix.View,
+        root: "lib/incidentctl_web/templates",
+        namespace: IncidentctlWeb
+
+      # Import convenience functions from controllers
+      import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1]
+
+      # Use all HTML functionality (forms, tags, etc)
+      use Phoenix.HTML
+
+      import IncidentctlWeb.ErrorHelpers
+      import IncidentctlWeb.Gettext
+      alias IncidentctlWeb.Router.Helpers, as: Routes
+    end
+  end
+
+  def router do
+    quote do
+      use Phoenix.Router
+      import Plug.Conn
+      import Phoenix.Controller
+    end
+  end
+
+  def channel do
+    quote do
+      use Phoenix.Channel
+      import IncidentctlWeb.Gettext
+    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

+ 33 - 0
lib/incidentctl_web/channels/user_socket.ex

@@ -0,0 +1,33 @@
+defmodule IncidentctlWeb.UserSocket do
+  use Phoenix.Socket
+
+  ## Channels
+  # channel "room:*", IncidentctlWeb.RoomChannel
+
+  # Socket params are passed from the client and can
+  # be used to verify and authenticate a user. After
+  # verification, you can put default assigns into
+  # the socket that will be set for all channels, ie
+  #
+  #     {:ok, assign(socket, :user_id, verified_user_id)}
+  #
+  # To deny connection, return `:error`.
+  #
+  # See `Phoenix.Token` documentation for examples in
+  # performing token verification on connect.
+  def connect(_params, socket, _connect_info) do
+    {:ok, socket}
+  end
+
+  # Socket id's are topics that allow you to identify all sockets for a given user:
+  #
+  #     def id(socket), do: "user_socket:#{socket.assigns.user_id}"
+  #
+  # Would allow you to broadcast a "disconnect" event and terminate
+  # all active sockets and channels for a given user:
+  #
+  #     IncidentctlWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
+  #
+  # Returning `nil` makes this socket anonymous.
+  def id(_socket), do: nil
+end

+ 7 - 0
lib/incidentctl_web/controllers/page_controller.ex

@@ -0,0 +1,7 @@
+defmodule IncidentctlWeb.PageController do
+  use IncidentctlWeb, :controller
+
+  def index(conn, _params) do
+    render(conn, "index.html")
+  end
+end

+ 46 - 0
lib/incidentctl_web/endpoint.ex

@@ -0,0 +1,46 @@
+defmodule IncidentctlWeb.Endpoint do
+  use Phoenix.Endpoint, otp_app: :incidentctl
+
+  socket "/socket", IncidentctlWeb.UserSocket,
+    websocket: true,
+    longpoll: false
+
+  # Serve at "/" the static files from "priv/static" directory.
+  #
+  # You should set gzip to true if you are running phx.digest
+  # when deploying your static files in production.
+  plug Plug.Static,
+    at: "/",
+    from: :incidentctl,
+    gzip: false,
+    only: ~w(css fonts images js favicon.ico robots.txt)
+
+  # Code reloading can be explicitly enabled under the
+  # :code_reloader configuration of your endpoint.
+  if code_reloading? do
+    socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
+    plug Phoenix.LiveReloader
+    plug Phoenix.CodeReloader
+  end
+
+  plug Plug.RequestId
+  plug Plug.Logger
+
+  plug Plug.Parsers,
+    parsers: [:urlencoded, :multipart, :json],
+    pass: ["*/*"],
+    json_decoder: Phoenix.json_library()
+
+  plug Plug.MethodOverride
+  plug Plug.Head
+
+  # The session will be stored in the cookie and signed,
+  # this means its contents can be read but not tampered with.
+  # Set :encryption_salt if you would also like to encrypt it.
+  plug Plug.Session,
+    store: :cookie,
+    key: "_incidentctl_key",
+    signing_salt: "zE7z7DlA"
+
+  plug IncidentctlWeb.Router
+end

+ 24 - 0
lib/incidentctl_web/gettext.ex

@@ -0,0 +1,24 @@
+defmodule IncidentctlWeb.Gettext do
+  @moduledoc """
+  A module providing Internationalization with a gettext-based API.
+
+  By using [Gettext](https://hexdocs.pm/gettext),
+  your module gains a set of macros for translations, for example:
+
+      import IncidentctlWeb.Gettext
+
+      # Simple translation
+      gettext("Here is the string to translate")
+
+      # Plural translation
+      ngettext("Here is the string to translate",
+               "Here are the strings to translate",
+               3)
+
+      # Domain-based translation
+      dgettext("errors", "Here is the error message to translate")
+
+  See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
+  """
+  use Gettext, otp_app: :incidentctl
+end

+ 26 - 0
lib/incidentctl_web/router.ex

@@ -0,0 +1,26 @@
+defmodule IncidentctlWeb.Router do
+  use IncidentctlWeb, :router
+
+  pipeline :browser do
+    plug :accepts, ["html"]
+    plug :fetch_session
+    plug :fetch_flash
+    plug :protect_from_forgery
+    plug :put_secure_browser_headers
+  end
+
+  pipeline :api do
+    plug :accepts, ["json"]
+  end
+
+  scope "/", IncidentctlWeb do
+    pipe_through :browser
+
+    get "/", PageController, :index
+  end
+
+  # Other scopes may use custom stacks.
+  # scope "/api", IncidentctlWeb do
+  #   pipe_through :api
+  # end
+end

+ 30 - 0
lib/incidentctl_web/templates/layout/app.html.eex

@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8"/>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+    <title>Incidentctl · Phoenix Framework</title>
+    <link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
+  </head>
+  <body>
+    <header>
+      <section class="container">
+        <nav role="navigation">
+          <ul>
+            <li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
+          </ul>
+        </nav>
+        <a href="http://phoenixframework.org/" class="phx-logo">
+          <img src="<%= Routes.static_path(@conn, "/images/phoenix.png") %>" alt="Phoenix Framework Logo"/>
+        </a>
+      </section>
+    </header>
+    <main role="main" class="container">
+      <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
+      <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
+      <%= render @view_module, @view_template, assigns %>
+    </main>
+    <script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
+  </body>
+</html>

+ 35 - 0
lib/incidentctl_web/templates/page/index.html.eex

@@ -0,0 +1,35 @@
+<section class="phx-hero">
+  <h1><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h1>
+  <p>A productive web framework that<br/>does not compromise speed or maintainability.</p>
+</section>
+
+<section class="row">
+  <article class="column">
+    <h2>Resources</h2>
+    <ul>
+      <li>
+        <a href="https://hexdocs.pm/phoenix/overview.html">Guides &amp; Docs</a>
+      </li>
+      <li>
+        <a href="https://github.com/phoenixframework/phoenix">Source</a>
+      </li>
+      <li>
+        <a href="https://github.com/phoenixframework/phoenix/blob/v1.4/CHANGELOG.md">v1.4 Changelog</a>
+      </li>
+    </ul>
+  </article>
+  <article class="column">
+    <h2>Help</h2>
+    <ul>
+      <li>
+        <a href="https://elixirforum.com/c/phoenix-forum">Forum</a>
+      </li>
+      <li>
+        <a href="https://webchat.freenode.net/?channels=elixir-lang">#elixir-lang on Freenode IRC</a>
+      </li>
+      <li>
+        <a href="https://twitter.com/elixirphoenix">Twitter @elixirphoenix</a>
+      </li>
+    </ul>
+  </article>
+</section>

+ 44 - 0
lib/incidentctl_web/views/error_helpers.ex

@@ -0,0 +1,44 @@
+defmodule IncidentctlWeb.ErrorHelpers do
+  @moduledoc """
+  Conveniences for translating and building error messages.
+  """
+
+  use Phoenix.HTML
+
+  @doc """
+  Generates tag for inlined form input errors.
+  """
+  def error_tag(form, field) do
+    Enum.map(Keyword.get_values(form.errors, field), fn error ->
+      content_tag(:span, translate_error(error), class: "help-block")
+    end)
+  end
+
+  @doc """
+  Translates an error message using gettext.
+  """
+  def translate_error({msg, opts}) do
+    # When using gettext, we typically pass the strings we want
+    # to translate as a static argument:
+    #
+    #     # Translate "is invalid" in the "errors" domain
+    #     dgettext("errors", "is invalid")
+    #
+    #     # Translate the number of files with plural rules
+    #     dngettext("errors", "1 file", "%{count} files", count)
+    #
+    # Because the error messages we show in our forms and APIs
+    # are defined inside Ecto, we need to translate them dynamically.
+    # This requires us to call the Gettext module passing our gettext
+    # backend as first argument.
+    #
+    # Note we use the "errors" domain, which means translations
+    # should be written to the errors.po file. The :count option is
+    # set by Ecto and indicates we should also apply plural rules.
+    if count = opts[:count] do
+      Gettext.dngettext(IncidentctlWeb.Gettext, "errors", msg, msg, count, opts)
+    else
+      Gettext.dgettext(IncidentctlWeb.Gettext, "errors", msg, opts)
+    end
+  end
+end

+ 16 - 0
lib/incidentctl_web/views/error_view.ex

@@ -0,0 +1,16 @@
+defmodule IncidentctlWeb.ErrorView do
+  use IncidentctlWeb, :view
+
+  # If you want to customize a particular status code
+  # for a certain format, you may uncomment below.
+  # def render("500.html", _assigns) do
+  #   "Internal Server Error"
+  # end
+
+  # By default, Phoenix returns the status message from
+  # the template name. For example, "404.html" becomes
+  # "Not Found".
+  def template_not_found(template, _assigns) do
+    Phoenix.Controller.status_message_from_template(template)
+  end
+end

+ 3 - 0
lib/incidentctl_web/views/layout_view.ex

@@ -0,0 +1,3 @@
+defmodule IncidentctlWeb.LayoutView do
+  use IncidentctlWeb, :view
+end

+ 3 - 0
lib/incidentctl_web/views/page_view.ex

@@ -0,0 +1,3 @@
+defmodule IncidentctlWeb.PageView do
+  use IncidentctlWeb, :view
+end

+ 64 - 0
mix.exs

@@ -0,0 +1,64 @@
+defmodule Incidentctl.MixProject do
+  use Mix.Project
+
+  def project do
+    [
+      app: :incidentctl,
+      version: "0.1.0",
+      elixir: "~> 1.5",
+      elixirc_paths: elixirc_paths(Mix.env()),
+      compilers: [:phoenix, :gettext] ++ Mix.compilers(),
+      start_permanent: Mix.env() == :prod,
+      aliases: aliases(),
+      deps: deps()
+    ]
+  end
+
+  # Configuration for the OTP application.
+  #
+  # Type `mix help compile.app` for more information.
+  def application do
+    [
+      mod: {Incidentctl.Application, []},
+      extra_applications: [:logger, :runtime_tools]
+    ]
+  end
+
+  # Specifies which paths to compile per environment.
+  defp elixirc_paths(:test), do: ["lib", "test/support"]
+  defp elixirc_paths(_), do: ["lib"]
+
+  # Specifies your project dependencies.
+  #
+  # Type `mix help deps` for examples and options.
+  defp deps do
+    [
+      {:phoenix, "~> 1.4.3"},
+      {:phoenix_pubsub, "~> 1.1"},
+      {:phoenix_ecto, "~> 4.0"},
+      {:ecto_sql, "~> 3.0"},
+      {:postgrex, ">= 0.0.0"},
+      {:phoenix_html, "~> 2.11"},
+      {:phoenix_live_reload, "~> 1.2", only: :dev},
+      {:gettext, "~> 0.11"},
+      {:jason, "~> 1.0"},
+      {:plug_cowboy, "~> 2.0"},
+      {:slack, github: "BlakeWilliams/Elixir-Slack", ref: "4812cf8"}
+      # {:slack, "~> 0.18.0"}
+    ]
+  end
+
+  # Aliases are shortcuts or tasks specific to the current project.
+  # For example, to create, migrate and run the seeds file at once:
+  #
+  #     $ mix ecto.setup
+  #
+  # See the documentation for `Mix` for more info on aliases.
+  defp aliases do
+    [
+      "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
+      "ecto.reset": ["ecto.drop", "ecto.setup"],
+      test: ["ecto.create --quiet", "ecto.migrate", "test"]
+    ]
+  end
+end

File diff suppressed because it is too large
+ 36 - 0
mix.lock


+ 97 - 0
priv/gettext/en/LC_MESSAGES/errors.po

@@ -0,0 +1,97 @@
+## `msgid`s in this file come from POT (.pot) files.
+##
+## Do not add, change, or remove `msgid`s manually here as
+## they're tied to the ones in the corresponding POT file
+## (with the same domain).
+##
+## Use `mix gettext.extract --merge` or `mix gettext.merge`
+## to merge POT files into PO files.
+msgid ""
+msgstr ""
+"Language: en\n"
+
+## From Ecto.Changeset.cast/4
+msgid "can't be blank"
+msgstr ""
+
+## From Ecto.Changeset.unique_constraint/3
+msgid "has already been taken"
+msgstr ""
+
+## From Ecto.Changeset.put_change/3
+msgid "is invalid"
+msgstr ""
+
+## From Ecto.Changeset.validate_acceptance/3
+msgid "must be accepted"
+msgstr ""
+
+## From Ecto.Changeset.validate_format/3
+msgid "has invalid format"
+msgstr ""
+
+## From Ecto.Changeset.validate_subset/3
+msgid "has an invalid entry"
+msgstr ""
+
+## From Ecto.Changeset.validate_exclusion/3
+msgid "is reserved"
+msgstr ""
+
+## From Ecto.Changeset.validate_confirmation/3
+msgid "does not match confirmation"
+msgstr ""
+
+## From Ecto.Changeset.no_assoc_constraint/3
+msgid "is still associated with this entry"
+msgstr ""
+
+msgid "are still associated with this entry"
+msgstr ""
+
+## From Ecto.Changeset.validate_length/3
+msgid "should be %{count} character(s)"
+msgid_plural "should be %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have %{count} item(s)"
+msgid_plural "should have %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at least %{count} character(s)"
+msgid_plural "should be at least %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have at least %{count} item(s)"
+msgid_plural "should have at least %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at most %{count} character(s)"
+msgid_plural "should be at most %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have at most %{count} item(s)"
+msgid_plural "should have at most %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+## From Ecto.Changeset.validate_number/3
+msgid "must be less than %{number}"
+msgstr ""
+
+msgid "must be greater than %{number}"
+msgstr ""
+
+msgid "must be less than or equal to %{number}"
+msgstr ""
+
+msgid "must be greater than or equal to %{number}"
+msgstr ""
+
+msgid "must be equal to %{number}"
+msgstr ""

+ 95 - 0
priv/gettext/errors.pot

@@ -0,0 +1,95 @@
+## This is a PO Template file.
+##
+## `msgid`s here are often extracted from source code.
+## Add new translations manually only if they're dynamic
+## translations that can't be statically extracted.
+##
+## Run `mix gettext.extract` to bring this file up to
+## date. Leave `msgstr`s empty as changing them here has no
+## effect: edit them in PO (`.po`) files instead.
+
+## From Ecto.Changeset.cast/4
+msgid "can't be blank"
+msgstr ""
+
+## From Ecto.Changeset.unique_constraint/3
+msgid "has already been taken"
+msgstr ""
+
+## From Ecto.Changeset.put_change/3
+msgid "is invalid"
+msgstr ""
+
+## From Ecto.Changeset.validate_acceptance/3
+msgid "must be accepted"
+msgstr ""
+
+## From Ecto.Changeset.validate_format/3
+msgid "has invalid format"
+msgstr ""
+
+## From Ecto.Changeset.validate_subset/3
+msgid "has an invalid entry"
+msgstr ""
+
+## From Ecto.Changeset.validate_exclusion/3
+msgid "is reserved"
+msgstr ""
+
+## From Ecto.Changeset.validate_confirmation/3
+msgid "does not match confirmation"
+msgstr ""
+
+## From Ecto.Changeset.no_assoc_constraint/3
+msgid "is still associated with this entry"
+msgstr ""
+
+msgid "are still associated with this entry"
+msgstr ""
+
+## From Ecto.Changeset.validate_length/3
+msgid "should be %{count} character(s)"
+msgid_plural "should be %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have %{count} item(s)"
+msgid_plural "should have %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at least %{count} character(s)"
+msgid_plural "should be at least %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have at least %{count} item(s)"
+msgid_plural "should have at least %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at most %{count} character(s)"
+msgid_plural "should be at most %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have at most %{count} item(s)"
+msgid_plural "should have at most %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+## From Ecto.Changeset.validate_number/3
+msgid "must be less than %{number}"
+msgstr ""
+
+msgid "must be greater than %{number}"
+msgstr ""
+
+msgid "must be less than or equal to %{number}"
+msgstr ""
+
+msgid "must be greater than or equal to %{number}"
+msgstr ""
+
+msgid "must be equal to %{number}"
+msgstr ""

+ 4 - 0
priv/repo/migrations/.formatter.exs

@@ -0,0 +1,4 @@
+[
+  import_deps: [:ecto_sql],
+  inputs: ["*.exs"]
+]

+ 11 - 0
priv/repo/seeds.exs

@@ -0,0 +1,11 @@
+# Script for populating the database. You can run it as:
+#
+#     mix run priv/repo/seeds.exs
+#
+# Inside the script, you can read and write to any of your
+# repositories directly:
+#
+#     Incidentctl.Repo.insert!(%Incidentctl.SomeSchema{})
+#
+# We recommend using the bang functions (`insert!`, `update!`
+# and so on) as they will fail if something goes wrong.

+ 4 - 0
test/incidentctl/slack_test.exs

@@ -0,0 +1,4 @@
+defmodule Incidentctl.SlackTest do
+  use ExUnit.Case, async: true
+  doctest Incidentctl.Slack
+end

+ 8 - 0
test/incidentctl_web/controllers/page_controller_test.exs

@@ -0,0 +1,8 @@
+defmodule IncidentctlWeb.PageControllerTest do
+  use IncidentctlWeb.ConnCase
+
+  test "GET /", %{conn: conn} do
+    conn = get(conn, "/")
+    assert html_response(conn, 200) =~ "Welcome to Phoenix!"
+  end
+end

+ 14 - 0
test/incidentctl_web/views/error_view_test.exs

@@ -0,0 +1,14 @@
+defmodule IncidentctlWeb.ErrorViewTest do
+  use IncidentctlWeb.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(IncidentctlWeb.ErrorView, "404.html", []) == "Not Found"
+  end
+
+  test "renders 500.html" do
+    assert render_to_string(IncidentctlWeb.ErrorView, "500.html", []) == "Internal Server Error"
+  end
+end

+ 3 - 0
test/incidentctl_web/views/layout_view_test.exs

@@ -0,0 +1,3 @@
+defmodule IncidentctlWeb.LayoutViewTest do
+  use IncidentctlWeb.ConnCase, async: true
+end

+ 3 - 0
test/incidentctl_web/views/page_view_test.exs

@@ -0,0 +1,3 @@
+defmodule IncidentctlWeb.PageViewTest do
+  use IncidentctlWeb.ConnCase, async: true
+end

+ 37 - 0
test/support/channel_case.ex

@@ -0,0 +1,37 @@
+defmodule IncidentctlWeb.ChannelCase do
+  @moduledoc """
+  This module defines the test case to be used by
+  channel tests.
+
+  Such tests rely on `Phoenix.ChannelTest` and also
+  import other functionality to make it easier
+  to build common data structures and query the data layer.
+
+  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
+
+      # The default endpoint for testing
+      @endpoint IncidentctlWeb.Endpoint
+    end
+  end
+
+  setup tags do
+    :ok = Ecto.Adapters.SQL.Sandbox.checkout(Incidentctl.Repo)
+
+    unless tags[:async] do
+      Ecto.Adapters.SQL.Sandbox.mode(Incidentctl.Repo, {:shared, self()})
+    end
+
+    :ok
+  end
+end

+ 38 - 0
test/support/conn_case.ex

@@ -0,0 +1,38 @@
+defmodule IncidentctlWeb.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
+  import other functionality to make it easier
+  to build common data structures and query the data layer.
+
+  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 IncidentctlWeb.Router.Helpers, as: Routes
+
+      # The default endpoint for testing
+      @endpoint IncidentctlWeb.Endpoint
+    end
+  end
+
+  setup tags do
+    :ok = Ecto.Adapters.SQL.Sandbox.checkout(Incidentctl.Repo)
+
+    unless tags[:async] do
+      Ecto.Adapters.SQL.Sandbox.mode(Incidentctl.Repo, {:shared, self()})
+    end
+
+    {:ok, conn: Phoenix.ConnTest.build_conn()}
+  end
+end

+ 53 - 0
test/support/data_case.ex

@@ -0,0 +1,53 @@
+defmodule Incidentctl.DataCase do
+  @moduledoc """
+  This module defines the setup for tests requiring
+  access to the application's data layer.
+
+  You may define functions here to be used as helpers in
+  your tests.
+
+  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 Incidentctl.Repo
+
+      import Ecto
+      import Ecto.Changeset
+      import Ecto.Query
+      import Incidentctl.DataCase
+    end
+  end
+
+  setup tags do
+    :ok = Ecto.Adapters.SQL.Sandbox.checkout(Incidentctl.Repo)
+
+    unless tags[:async] do
+      Ecto.Adapters.SQL.Sandbox.mode(Incidentctl.Repo, {:shared, self()})
+    end
+
+    :ok
+  end
+
+  @doc """
+  A helper that transforms changeset errors into a map of messages.
+
+      assert {:error, changeset} = Accounts.create_user(%{password: "short"})
+      assert "password is too short" in errors_on(changeset).password
+      assert %{password: ["password is too short"]} = errors_on(changeset)
+
+  """
+  def errors_on(changeset) do
+    Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
+      Enum.reduce(opts, message, fn {key, value}, acc ->
+        String.replace(acc, "%{#{key}}", to_string(value))
+      end)
+    end)
+  end
+end

+ 2 - 0
test/test_helper.exs

@@ -0,0 +1,2 @@
+ExUnit.start()
+Ecto.Adapters.SQL.Sandbox.mode(Incidentctl.Repo, :manual)