diff --git a/.gitignore b/.gitignore index 8d000f2..95e92be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,28 +1,28 @@ # Editor artifacts .*.swp # App artifacts /_build /db /deps /*.ez Mnesia* # Generated on crash by the VM erl_crash.dump # Static artifacts -/node_modules +/assets/node_modules # Since we are building assets from web/static, # we ignore priv/static. You may want to comment # this depending on your deployment strategy. /priv/static/ # 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 diff --git a/brunch-config.js b/assets/brunch-config.js similarity index 84% rename from brunch-config.js rename to assets/brunch-config.js index f4ed4ad..53f537c 100644 --- a/brunch-config.js +++ b/assets/brunch-config.js @@ -1,77 +1,74 @@ 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 // http://brunch.io/docs/config#-files- // joinTo: { // "js/app.js": /^(web\/static\/js)/, // "js/vendor.js": /^(web\/static\/vendor)|(deps)/ // } // // To change the order of concatenation of files, explicitly mention here // order: { // before: [ // "web/static/vendor/js/jquery-2.1.1.js", // "web/static/vendor/js/bootstrap.min.js" // ] // } }, stylesheets: { joinTo: "css/app.css", order: { after: ["web/static/css/app.css"] // concat app.css last } }, templates: { joinTo: "js/app.js" } }, conventions: { // This option sets where we should place non-css and non-js assets in. - // By default, we set this to "/web/static/assets". Files in this directory + // By default, we set this to "/assets/static". Files in this directory // will be copied to `paths.public`, which is "priv/static" by default. - assets: /^(web\/static\/assets)/ + assets: /^(static)/ }, // Phoenix paths configuration paths: { // Dependencies and current project directories to watch - watched: [ - "web/static", - "test/static" - ], + watched: ["static", "css", "js", "js/widgets", "vendor"], // Where to compile files to - public: "priv/static" + public: "../priv/static" }, // Configure your plugins plugins: { babel: { // Do not use ES6 compiler in vendor code - ignore: [/web\/static\/vendor/] + ignore: [/vendor/] }, copycat: { fonts: ["node_modules/bootstrap/fonts"] } }, modules: { autoRequire: { - "js/app.js": ["web/static/js/app"] + "js/app.js": ["js/app"] } }, npm: { enabled: true, globals: { $: "jquery", jQuery: "jquery", bootstrap: "bootstrap" } } }; diff --git a/web/static/css/app.css b/assets/css/app.css similarity index 100% rename from web/static/css/app.css rename to assets/css/app.css diff --git a/web/static/css/phoenix.css b/assets/css/phoenix.css similarity index 100% rename from web/static/css/phoenix.css rename to assets/css/phoenix.css diff --git a/web/static/css/widgets.css b/assets/css/widgets.css similarity index 100% rename from web/static/css/widgets.css rename to assets/css/widgets.css diff --git a/web/static/js/api.js b/assets/js/api.js similarity index 100% rename from web/static/js/api.js rename to assets/js/api.js diff --git a/web/static/js/app.js b/assets/js/app.js similarity index 100% rename from web/static/js/app.js rename to assets/js/app.js diff --git a/web/static/js/widgets/chatinput.js b/assets/js/widgets/chatinput.js similarity index 100% rename from web/static/js/widgets/chatinput.js rename to assets/js/widgets/chatinput.js diff --git a/web/static/js/widgets/chatroom.js b/assets/js/widgets/chatroom.js similarity index 100% rename from web/static/js/widgets/chatroom.js rename to assets/js/widgets/chatroom.js diff --git a/web/static/js/widgets/userlist.js b/assets/js/widgets/userlist.js similarity index 100% rename from web/static/js/widgets/userlist.js rename to assets/js/widgets/userlist.js diff --git a/web/static/js/widgets/userstatus.js b/assets/js/widgets/userstatus.js similarity index 100% rename from web/static/js/widgets/userstatus.js rename to assets/js/widgets/userstatus.js diff --git a/package.json b/assets/package.json similarity index 79% rename from package.json rename to assets/package.json index 0af9f90..a5bc2ea 100644 --- a/package.json +++ b/assets/package.json @@ -1,23 +1,23 @@ { "repository": {}, "license": "MIT", "scripts": { "deploy": "brunch build --production", "watch": "brunch watch --stdin" }, "dependencies": { - "phoenix": "file:deps/phoenix", - "phoenix_html": "file:deps/phoenix_html", + "phoenix": "file:../deps/phoenix", + "phoenix_html": "file:../deps/phoenix_html", "jquery": ">=2.1", "bootstrap": "~3.3.7" }, "devDependencies": { "babel-brunch": "~6.0.0", - "brunch": "2.7.4", + "brunch": "2.10.9", "clean-css-brunch": "~2.0.0", "css-brunch": "~2.0.0", "javascript-brunch": "~2.0.0", "uglify-js-brunch": "~2.0.1", "copycat-brunch": "~1.1.0" } } diff --git a/web/static/assets/favicon.ico b/assets/static/favicon.ico similarity index 100% rename from web/static/assets/favicon.ico rename to assets/static/favicon.ico diff --git a/web/static/assets/images/kolab-logo.png b/assets/static/images/kolab-logo.png similarity index 100% rename from web/static/assets/images/kolab-logo.png rename to assets/static/images/kolab-logo.png diff --git a/web/static/assets/robots.txt b/assets/static/robots.txt similarity index 100% rename from web/static/assets/robots.txt rename to assets/static/robots.txt diff --git a/config/config.exs b/config/config.exs index 53e09d0..1380390 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,28 +1,28 @@ # 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 # General application configuration config :kolab_chat, salts: [session_signing: "M7HpCp6W", session_encryption: nil] # Configures the endpoint -config :kolab_chat, KolabChat.Endpoint, +config :kolab_chat, KolabChat.Web.Endpoint, url: [host: "localhost"], secret_key_base: "XCVqlNuOTjBK3GB4lPKKdoTk9149ftPIJmytpQnYxI4qpGwjJbR47bYdzOAggBii", - render_errors: [view: KolabChat.ErrorView, accepts: ~w(html json)], + render_errors: [view: KolabChat.Web.ErrorView, accepts: ~w(html json)], pubsub: [name: KolabChat.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 index b4886aa..79718cd 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,34 +1,34 @@ 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 :kolab_chat, KolabChat.Endpoint, +config :kolab_chat, KolabChat.Web.Endpoint, http: [port: 4000], debug_errors: true, code_reloader: true, check_origin: false, watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin", - cd: Path.expand("../", __DIR__)]] + cd: Path.expand("../assets", __DIR__)]] # Watch static and templates for browser reloading. -config :kolab_chat, KolabChat.Endpoint, +config :kolab_chat, KolabChat.Web.Endpoint, live_reload: [ patterns: [ ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, ~r{priv/gettext/.*(po)$}, - ~r{web/views/.*(ex)$}, - ~r{web/templates/.*(eex)$} + ~r{lib/kolab_chat/web/views/.*(ex)$}, + ~r{lib/kolab_chat/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 diff --git a/config/prod.exs b/config/prod.exs index 746a707..13da2ca 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -1,63 +1,63 @@ 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 :kolab_chat, KolabChat.Endpoint, +config :kolab_chat, KolabChat.Web.Endpoint, http: [port: {:system, "PORT"}], url: [host: "example.com", port: 80], - cache_static_manifest: "priv/static/manifest.json" + cache_static_manifest: "priv/static/cache_manifest.json" # Do not print debug messages in production config :logger, level: :info, compile_time_purge_level: :warn # ## 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 :kolab_chat, KolabChat.Endpoint, +# config :kolab_chat, KolabChat.Web.Endpoint, # ... # url: [host: "example.com", port: 443], # 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 return an absolute path to # the key and cert in disk or a relative path inside priv, # for example "priv/ssl/server.key". # # We also recommend setting `force_ssl`, ensuring no data is # ever sent via http, always redirecting to https: # -# config :kolab_chat, KolabChat.Endpoint, +# config :kolab_chat, KolabChat.Web.Endpoint, # force_ssl: [hsts: true] # # Check `Plug.SSL` for all available options in `force_ssl`. # ## 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 :kolab_chat, KolabChat.Endpoint, server: true +# config :kolab_chat, KolabChat.Web.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 index 4172c66..230ed95 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,10 +1,10 @@ use Mix.Config # We don't run a server during test. If one is required, # you can enable the server option below. -config :kolab_chat, KolabChat.Endpoint, +config :kolab_chat, KolabChat.Web.Endpoint, http: [port: 4001], server: false # Print only warnings and errors during test config :logger, level: :warn diff --git a/lib/kolab_chat.ex b/lib/kolab_chat.ex index 69aff4d..26d6447 100644 --- a/lib/kolab_chat.ex +++ b/lib/kolab_chat.ex @@ -1,31 +1,24 @@ defmodule KolabChat 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 # Define workers and child supervisors to be supervised children = [ # Start the endpoint when the application starts - supervisor(KolabChat.Endpoint, []), + supervisor(KolabChat.Web.Endpoint, []), # Start phoenix presence module - supervisor(KolabChat.Presence, []), + supervisor(KolabChat.Web.Presence, []), # Start your own worker by calling: KolabChat.Worker.start_link(arg1, arg2, arg3) # worker(KolabChat.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: KolabChat.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 - KolabChat.Endpoint.config_change(changed, removed) - :ok - end end diff --git a/web/channels/presence.ex b/lib/kolab_chat/web/channels/presence.ex similarity index 97% rename from web/channels/presence.ex rename to lib/kolab_chat/web/channels/presence.ex index 3492c97..d51fceb 100644 --- a/web/channels/presence.ex +++ b/lib/kolab_chat/web/channels/presence.ex @@ -1,77 +1,77 @@ -defmodule KolabChat.Presence do +defmodule KolabChat.Web.Presence do @moduledoc """ Provides presence tracking to channels and processes. See the [`Phoenix.Presence`](http://hexdocs.pm/phoenix/Phoenix.Presence.html) docs for more details. ## Usage Presences can be tracked in your channel after joining: defmodule KolabChat.MyChannel do use KolabChat.Web, :channel - alias KolabChat.Presence + alias KolabChat.Web.Presence def join("some:topic", _params, socket) do send(self, :after_join) {:ok, assign(socket, :user_id, ...)} end def handle_info(:after_join, socket) do {:ok, _} = Presence.track(socket, socket.assigns.user_id, %{ online_at: inspect(System.system_time(:seconds)) }) push socket, "presence_state", Presence.list(socket) {:noreply, socket} end end In the example above, `Presence.track` is used to register this channel's process as a presence for the socket's user ID, with a map of metadata. Next, the current presence list for the socket's topic is pushed to the client as a `"presence_state"` event. Finally, a diff of presence join and leave events will be sent to the client as they happen in real-time with the "presence_diff" event. See `Phoenix.Presence.list/2` for details on the presence datastructure. ## Fetching Presence Information The `fetch/2` callback is triggered when using `list/1` and serves as a mechanism to fetch presence information a single time, before broadcasting the information to all channel subscribers. This prevents N query problems and gives you a single place to group isolated data fetching to extend presence metadata. The function receives a topic and map of presences and must return a map of data matching the Presence datastructure: %{"123" => %{metas: [%{status: "away", phx_ref: ...}], "456" => %{metas: [%{status: "online", phx_ref: ...}]} The `:metas` key must be kept, but you can extend the map of information to include any additional information. For example: def fetch(_topic, entries) do query = from u in User, where: u.id in ^Map.keys(entries), select: {u.id, u} users = query |> Repo.all |> Enum.into(%{}) for {key, %{metas: metas}} <- entries, into: %{} do {key, %{metas: metas, user: users[key]}} end end The function above fetches all users from the database who have registered presences for the given topic. The fetched information is then extended with a `:user` key of the user's information, while maintaining the required `:metas` field from the original presence data. """ use Phoenix.Presence, otp_app: :kolab_chat, pubsub_server: KolabChat.PubSub end diff --git a/web/channels/room_channel.ex b/lib/kolab_chat/web/channels/room_channel.ex similarity index 92% rename from web/channels/room_channel.ex rename to lib/kolab_chat/web/channels/room_channel.ex index f590455..0779f94 100644 --- a/web/channels/room_channel.ex +++ b/lib/kolab_chat/web/channels/room_channel.ex @@ -1,14 +1,14 @@ -defmodule KolabChat.RoomChannel do +defmodule KolabChat.Web.RoomChannel do use KolabChat.Web, :channel @spec join(topic :: binary(), args :: map(), socket :: pid()) :: {:ok, socket :: pid()} def join("room:lobby", _, socket) do {:ok, socket} end @spec handle_in(topic :: binary, args :: map(), socket :: pid()) :: {:noreply, socket :: pid()} def handle_in("new:message", message, socket) do broadcast! socket, "new:message", %{user: message["user"], body: message["body"]} {:noreply, socket} end end diff --git a/web/channels/system_channel.ex b/lib/kolab_chat/web/channels/system_channel.ex similarity index 98% rename from web/channels/system_channel.ex rename to lib/kolab_chat/web/channels/system_channel.ex index 6011fd7..965a022 100644 --- a/web/channels/system_channel.ex +++ b/lib/kolab_chat/web/channels/system_channel.ex @@ -1,108 +1,108 @@ -defmodule KolabChat.SystemChannel do +defmodule KolabChat.Web.SystemChannel do use KolabChat.Web, :channel @status [ # user is available for chat :online, :away, # user is connected and visible, but not available :busy, :unavailable, # user is shown as offline :invisible, :offline ] @spec join(topic :: binary(), args :: map(), socket :: pid()) :: {:ok, socket :: pid()} def join("system", %{"context" => context}, socket) do perform_join(context, socket) end def join("system", _args, socket) do perform_join("default", socket) end @spec handle_info(:after_join, socket :: pid()) :: {:noreply, socket :: pid()} def handle_info(:after_join, socket) do push socket, "presence_state", Presence.list(socket) push socket, "info", %{user: socket.assigns.user.username} Presence.track(socket, socket.assigns.user.username, %{ status: get_user_status(socket), context: socket.assigns.context }) {:noreply, socket} end @spec handle_in(topic :: binary, args :: map(), socket :: pid()) :: {:noreply, socket :: pid()} def handle_in("set-status", %{"status" => status}, socket) do status = check_status(status) socket |> update_presence_status(status) |> set_user_status(status) {:noreply, socket} end defp perform_join(context, socket) do socket = assign(socket, :context, context) send self(), :after_join {:ok, socket} end defp update_presence_status(socket, :invalid), do: socket defp update_presence_status(socket, status) do Presence.update(socket, socket.assigns.user.username, %{ status: status, context: socket.assigns.context }) socket end # Makes sure the provided status name is supported # Returns status name as an atom defp check_status(status) do status = String.to_atom(status) if Enum.member?(@status, status) do status else :invalid end end # Get the last user/context status from the database defp get_user_status(socket) do require Amnesia require Amnesia.Helper key = socket.assigns.user.username <> ":" <> socket.assigns.context Amnesia.transaction do case Database.Status.read(key) do # use last status %Database.Status{status: status} -> status # otherwise set status to online _ -> :online end end end # Save the current user/context status to the database defp set_user_status(socket, :invalid), do: socket defp set_user_status(socket, status) do require Amnesia require Amnesia.Helper key = socket.assigns.user.username <> ":" <> socket.assigns.context Amnesia.transaction do Database.Status.write(%Database.Status{key: key, status: status}) end socket end end diff --git a/web/channels/user_socket.ex b/lib/kolab_chat/web/channels/user_socket.ex similarity index 90% rename from web/channels/user_socket.ex rename to lib/kolab_chat/web/channels/user_socket.ex index a731204..d1f03db 100644 --- a/web/channels/user_socket.ex +++ b/lib/kolab_chat/web/channels/user_socket.ex @@ -1,41 +1,41 @@ -defmodule KolabChat.UserSocket do +defmodule KolabChat.Web.UserSocket do use Phoenix.Socket alias KolabChat.Database ## Channels - channel "room:*", KolabChat.RoomChannel - channel "system", KolabChat.SystemChannel + channel "room:*", KolabChat.Web.RoomChannel + channel "system", KolabChat.Web.SystemChannel ## Transports transport :websocket, Phoenix.Transports.WebSocket transport :longpoll, Phoenix.Transports.LongPoll # 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`. def connect(%{"token" => token}, socket) do case Phoenix.Token.verify(socket, "user", token, max_age: 86_400) do {:ok, user_id} -> socket = assign(socket, :user, Database.User.read!(user_id)) {:ok, socket} {:error, _} -> :error end end # Socket id's are topics that allow you to identify all sockets for a given user: # # def id(socket), do: "users_socket:#{socket.assigns.user_id}" # # Would allow you to broadcast a "disconnect" event and terminate # all active sockets and channels for a given user: # # KolabChat.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{}) # # Returning `nil` makes this socket anonymous. def id(_socket), do: nil end diff --git a/web/controllers/auth_controller.ex b/lib/kolab_chat/web/controllers/auth_controller.ex similarity index 96% rename from web/controllers/auth_controller.ex rename to lib/kolab_chat/web/controllers/auth_controller.ex index 5327c7c..fcf556a 100644 --- a/web/controllers/auth_controller.ex +++ b/lib/kolab_chat/web/controllers/auth_controller.ex @@ -1,55 +1,55 @@ -defmodule KolabChat.AuthController do +defmodule KolabChat.Web.AuthController do use KolabChat.Web, :controller @doc """ Handler for the default logon form """ def default_callback(conn, params) do %{"logon" => %{"password" => _pass, "username" => username}} = params signin(conn, username) end defp signin(conn, username) when is_nil(username) or username == "" do conn |> put_flash(:error, gettext("Invalid username!")) |> redirect(to: "/") end defp signin(conn, username) do case insert_or_update_user(username) do {:ok, user} -> conn |> put_flash(:info, gettext("Signed in!")) |> put_session(:user_id, user.id) |> redirect(to: "/") {:error, _reason} -> conn |> put_flash(:error, gettext("Error signing in")) |> redirect(to: "/") end end defp insert_or_update_user(username) do require Amnesia require Amnesia.Helper case Database.User.find(username) do nil -> user = Amnesia.transaction do Database.User.write!(%Database.User{username: username}) end {:ok, user} user -> {:ok, user} end end @doc """ Handler for logout action """ def logout(conn, _params) do conn |> configure_session(drop: true) |> redirect(to: "/") end end diff --git a/web/controllers/chat_controller.ex b/lib/kolab_chat/web/controllers/chat_controller.ex similarity index 81% rename from web/controllers/chat_controller.ex rename to lib/kolab_chat/web/controllers/chat_controller.ex index 9fb4fad..47958af 100644 --- a/web/controllers/chat_controller.ex +++ b/lib/kolab_chat/web/controllers/chat_controller.ex @@ -1,11 +1,11 @@ -defmodule KolabChat.ChatController do +defmodule KolabChat.Web.ChatController do use KolabChat.Web, :controller plug :put_layout, "chat.html" def index(conn, %{"room" => room} = _params) do conn |> assign(:room, room) |> render("index.html") end end diff --git a/web/controllers/page_controller.ex b/lib/kolab_chat/web/controllers/page_controller.ex similarity index 71% rename from web/controllers/page_controller.ex rename to lib/kolab_chat/web/controllers/page_controller.ex index ca5625b..66dfe02 100644 --- a/web/controllers/page_controller.ex +++ b/lib/kolab_chat/web/controllers/page_controller.ex @@ -1,7 +1,7 @@ -defmodule KolabChat.PageController do +defmodule KolabChat.Web.PageController do use KolabChat.Web, :controller def index(conn, _params) do render conn, "index.html" end end diff --git a/web/controllers/plugs/locale.ex b/lib/kolab_chat/web/controllers/plugs/locale.ex similarity index 87% rename from web/controllers/plugs/locale.ex rename to lib/kolab_chat/web/controllers/plugs/locale.ex index 2bc795c..fa5eb84 100644 --- a/web/controllers/plugs/locale.ex +++ b/lib/kolab_chat/web/controllers/plugs/locale.ex @@ -1,38 +1,38 @@ defmodule KolabChat.Plugs.Locale do import Plug.Conn def init(opts), do: opts def call(conn, _opts) do case get_session(conn, :locale) || client_locale(conn) do nil -> conn locale -> - Gettext.put_locale(KolabChat.Gettext, locale) + Gettext.put_locale(KolabChat.Web.Gettext, locale) put_session(conn, :locale, locale) end end # Gets supported locale code from the client # Uses 'locale' parameter or Accept-Language header defp client_locale(conn) do - supported = Gettext.known_locales(KolabChat.Gettext) + supported = Gettext.known_locales(KolabChat.Web.Gettext) locale = conn.params["locale"] || get_req_header(conn, "accept-language") locale |> to_string() |> String.split(",") |> Enum.map(&parse_locale/1) |> Enum.filter(fn(x) -> Enum.member?(supported, x) end) |> Enum.at(0) end # Extracts locale code from an element of Accept-Language header defp parse_locale(locale) do locale |> String.split(";") |> Enum.at(0) |> String.trim() |> String.replace("-", "_") end end diff --git a/web/controllers/plugs/set_user.ex b/lib/kolab_chat/web/controllers/plugs/set_user.ex similarity index 100% rename from web/controllers/plugs/set_user.ex rename to lib/kolab_chat/web/controllers/plugs/set_user.ex diff --git a/lib/kolab_chat/endpoint.ex b/lib/kolab_chat/web/endpoint.ex similarity index 92% rename from lib/kolab_chat/endpoint.ex rename to lib/kolab_chat/web/endpoint.ex index aca8631..8e113f5 100644 --- a/lib/kolab_chat/endpoint.ex +++ b/lib/kolab_chat/web/endpoint.ex @@ -1,43 +1,43 @@ -defmodule KolabChat.Endpoint do +defmodule KolabChat.Web.Endpoint do use Phoenix.Endpoint, otp_app: :kolab_chat - socket "/socket", KolabChat.UserSocket + socket "/socket", KolabChat.Web.UserSocket # 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: :kolab_chat, 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: Poison 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: "_kolab_chat_key", signing_salt: Keyword.get(Application.get_env(:kolab_chat, :salts), :session_signing), encryption_salt: Keyword.get(Application.get_env(:kolab_chat, :salts), :session_encryption) - plug KolabChat.Router + plug KolabChat.Web.Router end diff --git a/web/gettext.ex b/lib/kolab_chat/web/gettext.ex similarity index 95% rename from web/gettext.ex rename to lib/kolab_chat/web/gettext.ex index 24deb2b..1fa9da4 100644 --- a/web/gettext.ex +++ b/lib/kolab_chat/web/gettext.ex @@ -1,24 +1,24 @@ -defmodule KolabChat.Gettext do +defmodule KolabChat.Web.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 KolabChat.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: :kolab_chat end diff --git a/web/router.ex b/lib/kolab_chat/web/router.ex similarity index 85% rename from web/router.ex rename to lib/kolab_chat/web/router.ex index e67850f..e65a8fb 100644 --- a/web/router.ex +++ b/lib/kolab_chat/web/router.ex @@ -1,42 +1,42 @@ -defmodule KolabChat.Router do +defmodule KolabChat.Web.Router do use KolabChat.Web, :router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers plug KolabChat.Plugs.Locale plug KolabChat.Plugs.SetUser end pipeline :api do plug :accepts, ["json"] end - scope "/", KolabChat do + scope "/", KolabChat.Web do pipe_through :browser get "/", PageController, :index end - scope "/chat", KolabChat do + scope "/chat", KolabChat.Web do pipe_through :browser get "/", ChatController, :index get "/:room", ChatController, :index end - scope "/auth", KolabChat do + scope "/auth", KolabChat.Web do pipe_through :browser post "/default/callback", AuthController, :default_callback get "/logout", AuthController, :logout end # Other scopes may use custom stacks. # scope "/api", KolabChat do # pipe_through :api # end end diff --git a/web/templates/chat/index.html.eex b/lib/kolab_chat/web/templates/chat/index.html.eex similarity index 100% rename from web/templates/chat/index.html.eex rename to lib/kolab_chat/web/templates/chat/index.html.eex diff --git a/web/templates/layout/app.html.eex b/lib/kolab_chat/web/templates/layout/app.html.eex similarity index 100% rename from web/templates/layout/app.html.eex rename to lib/kolab_chat/web/templates/layout/app.html.eex diff --git a/web/templates/layout/chat.html.eex b/lib/kolab_chat/web/templates/layout/chat.html.eex similarity index 100% rename from web/templates/layout/chat.html.eex rename to lib/kolab_chat/web/templates/layout/chat.html.eex diff --git a/web/templates/page/index.html.eex b/lib/kolab_chat/web/templates/page/index.html.eex similarity index 100% rename from web/templates/page/index.html.eex rename to lib/kolab_chat/web/templates/page/index.html.eex diff --git a/lib/kolab_chat/web/views/chat_view.ex b/lib/kolab_chat/web/views/chat_view.ex new file mode 100644 index 0000000..d0939ef --- /dev/null +++ b/lib/kolab_chat/web/views/chat_view.ex @@ -0,0 +1,3 @@ +defmodule KolabChat.Web.ChatView do + use KolabChat.Web, :view +end diff --git a/web/views/error_helpers.ex b/lib/kolab_chat/web/views/error_helpers.ex similarity index 96% rename from web/views/error_helpers.ex rename to lib/kolab_chat/web/views/error_helpers.ex index 40b4c0c..610d43c 100644 --- a/web/views/error_helpers.ex +++ b/lib/kolab_chat/web/views/error_helpers.ex @@ -1,40 +1,40 @@ -defmodule KolabChat.ErrorHelpers do +defmodule KolabChat.Web.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 if error = form.errors[field] do content_tag :span, translate_error(error), class: "help-block" end end @doc """ Translates an error message using gettext. """ def translate_error({msg, opts}) do # Because error messages were defined within Ecto, we must # call the Gettext module passing our Gettext backend. We # also use the "errors" domain as translations are placed # in the errors.po file. # Ecto will pass the :count keyword if the error message is # meant to be pluralized. # On your own code and templates, depending on whether you # need the message to be pluralized or not, this could be # written simply as: # # dngettext "errors", "1 file", "%{count} files", count # dgettext "errors", "is invalid" # if count = opts[:count] do Gettext.dngettext(KolabChat.Gettext, "errors", msg, msg, count, opts) else Gettext.dgettext(KolabChat.Gettext, "errors", msg, opts) end end end diff --git a/web/views/error_view.ex b/lib/kolab_chat/web/views/error_view.ex similarity index 90% rename from web/views/error_view.ex rename to lib/kolab_chat/web/views/error_view.ex index d0c0f09..f4dfffa 100644 --- a/web/views/error_view.ex +++ b/lib/kolab_chat/web/views/error_view.ex @@ -1,17 +1,17 @@ -defmodule KolabChat.ErrorView do +defmodule KolabChat.Web.ErrorView do use KolabChat.Web, :view def render("404.html", _assigns) do "Page not found" end def render("500.html", _assigns) do "Internal server 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/lib/kolab_chat/web/views/layout_view.ex b/lib/kolab_chat/web/views/layout_view.ex new file mode 100644 index 0000000..563a97c --- /dev/null +++ b/lib/kolab_chat/web/views/layout_view.ex @@ -0,0 +1,3 @@ +defmodule KolabChat.Web.LayoutView do + use KolabChat.Web, :view +end diff --git a/lib/kolab_chat/web/views/page_view.ex b/lib/kolab_chat/web/views/page_view.ex new file mode 100644 index 0000000..dc52629 --- /dev/null +++ b/lib/kolab_chat/web/views/page_view.ex @@ -0,0 +1,3 @@ +defmodule KolabChat.Web.PageView do + use KolabChat.Web, :view +end diff --git a/web/web.ex b/lib/kolab_chat/web/web.ex similarity index 72% rename from web/web.ex rename to lib/kolab_chat/web/web.ex index 74ce923..5fc0a31 100644 --- a/web/web.ex +++ b/lib/kolab_chat/web/web.ex @@ -1,69 +1,70 @@ defmodule KolabChat.Web do @moduledoc """ A module that keeps using definitions for controllers, views and so on. This can be used in your application as: use KolabChat.Web, :controller use KolabChat.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 controller do quote do - use Phoenix.Controller + use Phoenix.Controller, namespace: KolabChat.Web alias KolabChat.Database - import KolabChat.Router.Helpers - import KolabChat.Gettext + import KolabChat.Web.Router.Helpers + import KolabChat.Web.Gettext end end def view do quote do - use Phoenix.View, root: "web/templates" + use Phoenix.View, root: "lib/kolab_chat/web/templates", + namespace: KolabChat.Web # Import convenience functions from controllers import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] # Use all HTML functionality (forms, tags, etc) use Phoenix.HTML - import KolabChat.Router.Helpers - import KolabChat.ErrorHelpers - import KolabChat.Gettext + import KolabChat.Web.Router.Helpers + import KolabChat.Web.ErrorHelpers + import KolabChat.Web.Gettext end end def router do quote do use Phoenix.Router end end def channel do quote do use Phoenix.Channel alias KolabChat.Database - alias KolabChat.Presence + alias KolabChat.Web.Presence - import KolabChat.Gettext + import KolabChat.Web.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 diff --git a/mix.exs b/mix.exs index 48bdc3e..ebeb3a5 100644 --- a/mix.exs +++ b/mix.exs @@ -1,53 +1,53 @@ defmodule KolabChat.Mixfile do use Mix.Project def project do [app: :kolab_chat, version: "0.0.1", elixir: "~> 1.2", elixirc_paths: elixirc_paths(Mix.env), compilers: [:phoenix, :gettext] ++ Mix.compilers, build_embedded: Mix.env == :prod, 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: {KolabChat, []}, applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext, :amnesia]] end # Specifies which paths to compile per environment. - defp elixirc_paths(:test), do: ["lib", "web", "test/support"] - defp elixirc_paths(_), do: ["lib", "web"] + 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.2.1"}, + [{:phoenix, "~> 1.3.0-rc"}, {:phoenix_pubsub, "~> 1.0"}, {:phoenix_html, "~> 2.6"}, {:phoenix_live_reload, "~> 1.0", only: :dev}, {:gettext, "~> 0.11"}, {:amnesia, "~> 0.2.0"}, {:cowboy, "~> 1.0"}, {:credo, "~> 0.5", only: [:dev, :test]} ] 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 [] end end diff --git a/mix.lock b/mix.lock index f3d86b9..8ad867e 100644 --- a/mix.lock +++ b/mix.lock @@ -1,16 +1,16 @@ -%{"amnesia": {:hex, :amnesia, "0.2.5", "3202e0b01e380671274caea32fbe78bbd1989e1215215f41a2d97583e5c7d163", [], [{:exquisite, "~> 0.1.6", [hex: :exquisite, optional: false]}]}, +%{"amnesia": {:hex, :amnesia, "0.2.5", "3202e0b01e380671274caea32fbe78bbd1989e1215215f41a2d97583e5c7d163", [:mix], [{:exquisite, "~> 0.1.6", [hex: :exquisite, optional: false]}]}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], []}, "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, optional: false]}]}, - "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [], []}, + "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []}, "credo": {:hex, :credo, "0.6.1", "a941e2591bd2bd2055dc92b810c174650b40b8290459c89a835af9d59ac4a5f8", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, optional: false]}]}, - "exquisite": {:hex, :exquisite, "0.1.8", "ee8f56aae477287ce5e7dfcbc163a420cccbb73e680a6d80a09203e9ef514fa4", [], []}, - "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [], []}, - "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [], []}, + "exquisite": {:hex, :exquisite, "0.1.8", "ee8f56aae477287ce5e7dfcbc163a420cccbb73e680a6d80a09203e9ef514fa4", [:mix], []}, + "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []}, + "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], []}, "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], []}, - "phoenix": {:hex, :phoenix, "1.2.1", "6dc592249ab73c67575769765b66ad164ad25d83defa3492dc6ae269bd2a68ab", [], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, optional: false]}, {:plug, "~> 1.1", [hex: :plug, optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}]}, - "phoenix_html": {:hex, :phoenix_html, "2.9.3", "1b5a2122cbf743aa242f54dced8a4f1cc778b8bd304f4b4c0043a6250c58e258", [], [{:plug, "~> 1.0", [hex: :plug, optional: false]}]}, - "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.8", "4333f9c74190f485a74866beff2f9304f069d53f047f5fbb0fb8d1ee4c495f73", [], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2-rc", [hex: :phoenix, optional: false]}]}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.1", "c10ddf6237007c804bf2b8f3c4d5b99009b42eca3a0dfac04ea2d8001186056a", [], []}, - "plug": {:hex, :plug, "1.3.3", "d9be189924379b4e9d470caef87380d09549aea1ceafe6a0d41292c8c317c923", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]}, - "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [], []}, + "phoenix": {:hex, :phoenix, "1.3.0-rc.1", "0d04948a4bd24823f101024c07b6a4d35e58f1fd92a465c1bc75dd37acd1041a", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, optional: false]}]}, + "phoenix_html": {:hex, :phoenix_html, "2.9.3", "1b5a2122cbf743aa242f54dced8a4f1cc778b8bd304f4b4c0043a6250c58e258", [:mix], [{:plug, "~> 1.0", [hex: :plug, optional: false]}]}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.8", "4333f9c74190f485a74866beff2f9304f069d53f047f5fbb0fb8d1ee4c495f73", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2-rc", [hex: :phoenix, optional: false]}]}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.1", "c10ddf6237007c804bf2b8f3c4d5b99009b42eca3a0dfac04ea2d8001186056a", [:mix], []}, + "plug": {:hex, :plug, "1.3.4", "b4ef3a383f991bfa594552ded44934f2a9853407899d47ecc0481777fb1906f6", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]}, + "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], []}, "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], []}} diff --git a/test/controllers/page_controller_test.exs b/test/controllers/page_controller_test.exs index dfda05d..3529eec 100644 --- a/test/controllers/page_controller_test.exs +++ b/test/controllers/page_controller_test.exs @@ -1,8 +1,8 @@ -defmodule KolabChat.PageControllerTest do - use KolabChat.ConnCase +defmodule KolabChat.Web.PageControllerTest do + use KolabChat.Web.ConnCase test "GET /", %{conn: conn} 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 index bd99ed8..a28ae2a 100644 --- a/test/support/channel_case.ex +++ b/test/support/channel_case.ex @@ -1,43 +1,43 @@ -defmodule KolabChat.ChannelCase do +defmodule KolabChat.Web.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 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 KolabChat.Repo import Ecto import Ecto.Changeset import Ecto.Query # The default endpoint for testing - @endpoint KolabChat.Endpoint + @endpoint KolabChat.Web.Endpoint end end setup tags do :ok = Ecto.Adapters.SQL.Sandbox.checkout(KolabChat.Repo) unless tags[:async] do Ecto.Adapters.SQL.Sandbox.mode(KolabChat.Repo, {:shared, self()}) end :ok end end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 5fb21fa..4fc8cad 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -1,44 +1,44 @@ -defmodule KolabChat.ConnCase do +defmodule KolabChat.Web.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 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 KolabChat.Repo import Ecto import Ecto.Changeset import Ecto.Query - import KolabChat.Router.Helpers + import KolabChat.Web.Router.Helpers # The default endpoint for testing - @endpoint KolabChat.Endpoint + @endpoint KolabChat.Web.Endpoint end end setup tags do :ok = Ecto.Adapters.SQL.Sandbox.checkout(KolabChat.Repo) unless tags[:async] do Ecto.Adapters.SQL.Sandbox.mode(KolabChat.Repo, {:shared, self()}) end {:ok, conn: Phoenix.ConnTest.build_conn()} end end diff --git a/test/views/error_view_test.exs b/test/views/error_view_test.exs index bc7aaa8..469633e 100644 --- a/test/views/error_view_test.exs +++ b/test/views/error_view_test.exs @@ -1,21 +1,21 @@ -defmodule KolabChat.ErrorViewTest do - use KolabChat.ConnCase, async: true +defmodule KolabChat.Web.ErrorViewTest do + use KolabChat.Web.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(KolabChat.ErrorView, "404.html", []) == + assert render_to_string(KolabChat.Web.ErrorView, "404.html", []) == "Page not found" end test "render 500.html" do - assert render_to_string(KolabChat.ErrorView, "500.html", []) == + assert render_to_string(KolabChat.Web.ErrorView, "500.html", []) == "Internal server error" end test "render any other" do - assert render_to_string(KolabChat.ErrorView, "505.html", []) == + assert render_to_string(KolabChat.Web.ErrorView, "505.html", []) == "Internal server error" end end diff --git a/test/views/layout_view_test.exs b/test/views/layout_view_test.exs index 97a527b..b5f41e2 100644 --- a/test/views/layout_view_test.exs +++ b/test/views/layout_view_test.exs @@ -1,3 +1,3 @@ -defmodule KolabChat.LayoutViewTest do - use KolabChat.ConnCase, async: true +defmodule KolabChat.Web.LayoutViewTest do + use KolabChat.Web.ConnCase, async: true end diff --git a/test/views/page_view_test.exs b/test/views/page_view_test.exs index 8f993a8..996b8ce 100644 --- a/test/views/page_view_test.exs +++ b/test/views/page_view_test.exs @@ -1,3 +1,3 @@ -defmodule KolabChat.PageViewTest do - use KolabChat.ConnCase, async: true +defmodule KolabChat.Web.PageViewTest do + use KolabChat.Web.ConnCase, async: true end diff --git a/web/views/chat_view.ex b/web/views/chat_view.ex deleted file mode 100644 index b6b3261..0000000 --- a/web/views/chat_view.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule KolabChat.ChatView do - use KolabChat.Web, :view -end diff --git a/web/views/layout_view.ex b/web/views/layout_view.ex deleted file mode 100644 index 131a259..0000000 --- a/web/views/layout_view.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule KolabChat.LayoutView do - use KolabChat.Web, :view -end diff --git a/web/views/page_view.ex b/web/views/page_view.ex deleted file mode 100644 index 7d6c7a4..0000000 --- a/web/views/page_view.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule KolabChat.PageView do - use KolabChat.Web, :view -end