Initial commit
This commit is contained in:
commit
026b0ea2ae
|
@ -0,0 +1,5 @@
|
|||
[
|
||||
import_deps: [:ecto, :phoenix],
|
||||
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
|
||||
subdirectories: ["priv/*/migrations"]
|
||||
]
|
|
@ -0,0 +1,36 @@
|
|||
# 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").
|
||||
frenzy-*.tar
|
||||
|
||||
# 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
|
|
@ -0,0 +1,19 @@
|
|||
# Frenzy
|
||||
|
||||
To start your Phoenix server:
|
||||
|
||||
* Install dependencies with `mix deps.get`
|
||||
* Create and migrate your database with `mix ecto.setup`
|
||||
* 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
|
|
@ -0,0 +1,30 @@
|
|||
# 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 :frenzy,
|
||||
ecto_repos: [Frenzy.Repo]
|
||||
|
||||
# Configures the endpoint
|
||||
config :frenzy, FrenzyWeb.Endpoint,
|
||||
url: [host: "localhost"],
|
||||
secret_key_base: "OHENOE79+KzItVyWbScSSn2yPhZToFpNriYZm3gj2sGYR0hbKEiSLvcVvTK1zxo8",
|
||||
render_errors: [view: FrenzyWeb.ErrorView, accepts: ~w(html json)],
|
||||
pubsub: [name: Frenzy.PubSub, adapter: Phoenix.PubSub.PG2]
|
||||
|
||||
# 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"
|
|
@ -0,0 +1,69 @@
|
|||
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 :frenzy, FrenzyWeb.Endpoint,
|
||||
http: [port: 4000],
|
||||
debug_errors: true,
|
||||
code_reloader: true,
|
||||
check_origin: false,
|
||||
watchers: []
|
||||
|
||||
# ## 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 :frenzy, FrenzyWeb.Endpoint,
|
||||
live_reload: [
|
||||
patterns: [
|
||||
~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
|
||||
~r{priv/gettext/.*(po)$},
|
||||
~r{lib/frenzy_web/views/.*(ex)$},
|
||||
~r{lib/frenzy_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 :frenzy, Frenzy.Repo,
|
||||
username: "postgres",
|
||||
password: "postgres",
|
||||
database: "frenzy_dev",
|
||||
hostname: "localhost",
|
||||
pool_size: 10
|
||||
|
||||
import_config "dev.secret.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 :frenzy, FrenzyWeb.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 :frenzy, FrenzyWeb.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 :frenzy, FrenzyWeb.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 :frenzy, FrenzyWeb.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"
|
|
@ -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 :frenzy, FrenzyWeb.Endpoint,
|
||||
http: [port: 4002],
|
||||
server: false
|
||||
|
||||
# Print only warnings and errors during test
|
||||
config :logger, level: :warn
|
||||
|
||||
# Configure your database
|
||||
config :frenzy, Frenzy.Repo,
|
||||
username: "postgres",
|
||||
password: "postgres",
|
||||
database: "frenzy_test",
|
||||
hostname: "localhost",
|
||||
pool: Ecto.Adapters.SQL.Sandbox
|
|
@ -0,0 +1,9 @@
|
|||
defmodule Frenzy do
|
||||
@moduledoc """
|
||||
Frenzy 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
|
|
@ -0,0 +1,32 @@
|
|||
defmodule Frenzy.Application do
|
||||
# See https://hexdocs.pm/elixir/Application.html
|
||||
# for more information on OTP Applications
|
||||
@moduledoc false
|
||||
|
||||
use Application
|
||||
|
||||
def start(_type, _args) do
|
||||
# List all child processes to be supervised
|
||||
children = [
|
||||
# Start the Ecto repository
|
||||
Frenzy.Repo,
|
||||
# Start the endpoint when the application starts
|
||||
FrenzyWeb.Endpoint,
|
||||
# Starts a worker by calling: Frenzy.Worker.start_link(arg)
|
||||
# {Frenzy.Worker, arg},
|
||||
{Frenzy.UpdateFeeds, name: Frenzy.UpdateFeeds}
|
||||
]
|
||||
|
||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||
# for other strategies and supported options
|
||||
opts = [strategy: :one_for_one, name: Frenzy.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
|
||||
FrenzyWeb.Endpoint.config_change(changed, removed)
|
||||
:ok
|
||||
end
|
||||
end
|
|
@ -0,0 +1,34 @@
|
|||
defmodule Frenzy.Feed do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
def to_fever(feed) do
|
||||
%{
|
||||
id: feed.id,
|
||||
favicon_id: feed.id,
|
||||
title: feed.title,
|
||||
url: feed.feed_url,
|
||||
site_url: feed.site_url,
|
||||
last_updated_on_time: Timex.to_unix(feed.last_updated),
|
||||
is_spark: false
|
||||
}
|
||||
end
|
||||
|
||||
schema "feeds" do
|
||||
field :feed_url, :string
|
||||
field :last_updated, :utc_datetime
|
||||
field :site_url, :string
|
||||
field :title, :string
|
||||
|
||||
has_many :items, Frenzy.Item, on_delete: :delete_all
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(feed, attrs) do
|
||||
feed
|
||||
|> cast(attrs, [:title, :feed_url, :site_url, :last_updated])
|
||||
|> validate_required([:feed_url])
|
||||
end
|
||||
end
|
|
@ -0,0 +1,40 @@
|
|||
defmodule Frenzy.Item do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
def to_fever(item) do
|
||||
%{
|
||||
id: item.id,
|
||||
feed_id: item.feed_id,
|
||||
title: item.title,
|
||||
author: item.creator,
|
||||
html: item.content,
|
||||
url: item.url,
|
||||
is_saved: 0,
|
||||
is_read: (if item.read, do: 1, else: 0),
|
||||
created_on_time: Timex.to_unix(item.date)
|
||||
}
|
||||
end
|
||||
|
||||
schema "items" do
|
||||
field :content, :string
|
||||
field :date, :utc_datetime
|
||||
field :creator, :string
|
||||
field :guid, :string
|
||||
field :url, :string
|
||||
field :read, :boolean, default: false
|
||||
field :read_date, :utc_datetime
|
||||
field :title, :string
|
||||
|
||||
belongs_to :feed, Frenzy.Feed
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(item, attrs) do
|
||||
item
|
||||
|> cast(attrs, [:guid, :title, :url, :creator, :date, :content, :read, :read_date])
|
||||
|> validate_required([:guid, :title, :url, :date, :content, :feed])
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
defmodule Frenzy.Repo do
|
||||
use Ecto.Repo,
|
||||
otp_app: :frenzy,
|
||||
adapter: Ecto.Adapters.Postgres
|
||||
end
|
|
@ -0,0 +1,136 @@
|
|||
defmodule Frenzy.UpdateFeeds do
|
||||
use GenServer
|
||||
alias Frenzy.{Repo, Feed, Item}
|
||||
import Ecto.Query
|
||||
require Logger
|
||||
|
||||
def start_link(state) do
|
||||
GenServer.start_link(__MODULE__, :ok, state)
|
||||
end
|
||||
|
||||
def refresh(pid, feed) do
|
||||
GenServer.call(pid, {:refresh, feed})
|
||||
end
|
||||
|
||||
def init(state) do
|
||||
update_feeds()
|
||||
schedule_update()
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def handle_call({:refresh, feed}, _from, state) do
|
||||
update_feed(feed)
|
||||
new_feed = Feed |> Repo.get(feed.id) |> Repo.preload(:items)
|
||||
{:reply, new_feed, state}
|
||||
end
|
||||
|
||||
def handle_info(:update_feeds, state) do
|
||||
update_feeds()
|
||||
schedule_update()
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp schedule_update() do
|
||||
Process.send_after(self(), :update_feeds, 15 * 60 * 1000) # 15 minutes
|
||||
# Process.send_after(self(), :update_feeds, 60 * 1000) # 1 minutes
|
||||
end
|
||||
|
||||
defp update_feeds() do
|
||||
Logger.info("Updating all feeds")
|
||||
|
||||
Enum.map(Repo.all(Feed), &update_feed/1)
|
||||
prune_old_items()
|
||||
end
|
||||
|
||||
defp prune_old_items() do
|
||||
{count, _} = Repo.delete_all(from i in Item, where: i.read, where: i.read_date <= from_now(-1, "week"))
|
||||
Logger.info("Removed #{count} read items")
|
||||
end
|
||||
|
||||
defp update_feed(feed) do
|
||||
Logger.debug("Updating #{feed.feed_url}")
|
||||
|
||||
case HTTPoison.get(feed.feed_url) do
|
||||
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
|
||||
case Fiet.parse(body) do
|
||||
{:ok, rss} ->
|
||||
update_feed_from_rss(feed, rss)
|
||||
end
|
||||
{:ok, %HTTPoison.Response{status_code: 404}} ->
|
||||
Logger.warn("RSS feed #{feed.feed_url} not found")
|
||||
{:error, %HTTPoison.Error{reason: reason}} ->
|
||||
Logger.error("Couldn't load RSS feed: #{reason}")
|
||||
end
|
||||
end
|
||||
|
||||
defp update_feed_from_rss(feed, rss) do
|
||||
changeset = Feed.changeset(feed, %{
|
||||
title: rss.title,
|
||||
site_url: rss.link.href,
|
||||
last_updated: parse_date(rss.updated_at)
|
||||
})
|
||||
Repo.update(changeset)
|
||||
|
||||
feed = Repo.preload(feed, :items)
|
||||
|
||||
Enum.map(rss.items, fn entry ->
|
||||
if !Enum.any?(feed.items, fn item -> item.guid == entry.id end) do
|
||||
create_item(feed, entry)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp create_item(feed, entry) do
|
||||
Logger.debug("Creating item for:")
|
||||
IO.inspect(entry)
|
||||
|
||||
url = get_real_url(entry)
|
||||
|
||||
changeset = Ecto.build_assoc(feed, :items, %{
|
||||
guid: entry.id,
|
||||
title: entry.title,
|
||||
url: url,
|
||||
date: parse_date(entry.published_at),
|
||||
creator: "",
|
||||
content: get_article_content(url)
|
||||
})
|
||||
|
||||
Repo.insert(changeset)
|
||||
end
|
||||
|
||||
defp parse_date(str) do
|
||||
case Timex.parse(str, "{RFC1123}") do
|
||||
{:ok, date} ->
|
||||
Timex.Timezone.convert(date, :utc)
|
||||
_ ->
|
||||
{:ok, date, _} = DateTime.from_iso8601(str)
|
||||
Timex.Timezone.convert(date, :utc)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_real_url(entry) do
|
||||
links = Enum.reject(entry.links, fn l -> l.rel == "shorturl" end)
|
||||
case Enum.find(links, fn l -> l.rel == "related" end) do
|
||||
nil ->
|
||||
case Enum.find(links, fn l -> l.rel == "alternate" end) do
|
||||
nil -> Enum.fetch!(links, 0).href
|
||||
link -> link.href
|
||||
end
|
||||
link -> link.href
|
||||
end
|
||||
end
|
||||
|
||||
defp get_article_content(url) do
|
||||
Logger.debug("Getting article from #{url}")
|
||||
|
||||
case HTTPoison.get(url) do
|
||||
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
|
||||
article = Readability.article(body)
|
||||
Readability.readable_html(article)
|
||||
{:ok, %HTTPoison.Response{status_code: 404}} ->
|
||||
Logger.warn("Article #{url} not found")
|
||||
{:error, %HTTPoison.Error{reason: reason}} ->
|
||||
Logger.error("Couldn't load article: #{reason}")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,69 @@
|
|||
defmodule FrenzyWeb 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 FrenzyWeb, :controller
|
||||
use FrenzyWeb, :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: FrenzyWeb
|
||||
|
||||
import Plug.Conn
|
||||
import FrenzyWeb.Gettext
|
||||
alias FrenzyWeb.Router.Helpers, as: Routes
|
||||
end
|
||||
end
|
||||
|
||||
def view do
|
||||
quote do
|
||||
use Phoenix.View,
|
||||
root: "lib/frenzy_web/templates",
|
||||
namespace: FrenzyWeb
|
||||
|
||||
# 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 FrenzyWeb.ErrorHelpers
|
||||
import FrenzyWeb.Gettext
|
||||
alias FrenzyWeb.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 FrenzyWeb.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
|
|
@ -0,0 +1,33 @@
|
|||
defmodule FrenzyWeb.UserSocket do
|
||||
use Phoenix.Socket
|
||||
|
||||
## Channels
|
||||
# channel "room:*", FrenzyWeb.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:
|
||||
#
|
||||
# FrenzyWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
|
||||
#
|
||||
# Returning `nil` makes this socket anonymous.
|
||||
def id(_socket), do: nil
|
||||
end
|
|
@ -0,0 +1,43 @@
|
|||
defmodule FrenzyWeb.FeedController do
|
||||
use FrenzyWeb, :controller
|
||||
alias Frenzy.{Repo, Feed, Item}
|
||||
alias FrenzyWeb.Router.Helpers, as: Routes
|
||||
alias FrenzyWeb.Endpoint
|
||||
import Ecto.Query
|
||||
|
||||
def index(conn, _params) do
|
||||
render(conn, "index.html", feeds: Repo.all(Feed))
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
feed = Repo.get(Feed, id)
|
||||
items = Repo.all(from Item, where: [feed_id: ^id], order_by: [desc: :date])
|
||||
render(conn, "show.html", %{
|
||||
feed: feed,
|
||||
items: items
|
||||
})
|
||||
end
|
||||
|
||||
def new(conn, _params) do
|
||||
changeset = Feed.changeset(%Feed{}, %{})
|
||||
render(conn, "new.html", changeset: changeset)
|
||||
end
|
||||
|
||||
def create(conn, %{"feed" => feed}) do
|
||||
changeset = Feed.changeset(%Feed{}, feed)
|
||||
{:ok, feed} = Repo.insert(changeset)
|
||||
redirect(conn, to: Routes.feed_path(Endpoint, :index))
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => id}) do
|
||||
feed = Repo.get(Feed, id)
|
||||
{:ok, _} = Repo.delete(feed)
|
||||
redirect(conn, to: Routes.feed_path(Endpoint, :index))
|
||||
end
|
||||
|
||||
def refresh(conn, %{"id" => id}) do
|
||||
feed = Repo.get(Feed, id)
|
||||
feed = Frenzy.UpdateFeeds.refresh(Frenzy.UpdateFeeds, feed)
|
||||
redirect(conn, to: Routes.feed_path(Endpoint, :show, feed.id))
|
||||
end
|
||||
end
|
|
@ -0,0 +1,149 @@
|
|||
defmodule FrenzyWeb.FeverController do
|
||||
use FrenzyWeb, :controller
|
||||
alias Frenzy.{Repo, Feed, Item}
|
||||
import Ecto.Query
|
||||
|
||||
plug :api_check
|
||||
|
||||
def api_check(conn, _) do
|
||||
if Map.has_key?(conn.params, "api") do
|
||||
conn
|
||||
else
|
||||
conn |> resp(400, "Invalid request") |> halt()
|
||||
end
|
||||
end
|
||||
|
||||
def get(conn, params) do
|
||||
json(conn, %{api_version: 2, auth: 0})
|
||||
end
|
||||
|
||||
def post(conn, %{"api_key" => api_key} = params) do
|
||||
case validate_key(api_key) do
|
||||
:invalid ->
|
||||
resp(conn, 401, "Invalid API key")
|
||||
:ok ->
|
||||
json(conn, fever_response(params))
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_key(api_key) do
|
||||
auth = Application.get_env(:frenzy, :auth)
|
||||
username = auth[:username]
|
||||
password = auth[:password]
|
||||
expected = :crypto.hash(:md5, "#{username}:#{password}") |> Base.encode16(case: :lower)
|
||||
case api_key |> String.downcase do
|
||||
^expected -> :ok
|
||||
_ -> :invalid
|
||||
end
|
||||
end
|
||||
|
||||
defp fever_response(params) do
|
||||
res = %{api_version: 2, auth: 1}
|
||||
|> mark(params)
|
||||
|> unread_recently_read(params)
|
||||
|> groups(params)
|
||||
|> feeds(params)
|
||||
|> favicons(params)
|
||||
|> links(params)
|
||||
|> unread(params)
|
||||
|> saved(params)
|
||||
|> items(params)
|
||||
end
|
||||
|
||||
defp mark(res, %{"mark" => "item", "id" => id, "as" => as} = params) do
|
||||
item = Repo.get(Item, id) |> Repo.preload(:feed)
|
||||
diff = case as do
|
||||
"read" ->
|
||||
%{read: true, read_date: Timex.now}
|
||||
"unread" ->
|
||||
%{read: false, read_date: nil}
|
||||
_ ->
|
||||
%{}
|
||||
end
|
||||
changeset = Item.changeset(item, diff)
|
||||
Repo.update(changeset)
|
||||
res
|
||||
end
|
||||
defp mark(res, _), do: res
|
||||
|
||||
defp unread_recently_read(res, %{"unread_recently_read" => 1}) do
|
||||
Repo.all(from i in Item, where: i.read, where: i.read_date >= from_now(-1, "hour"))
|
||||
|> Enum.map(fn i -> Item.changeset(i, %{read: false, read_date: nil}) end)
|
||||
|> Enum.map(&Repo.update/1)
|
||||
res
|
||||
end
|
||||
defp unread_recently_read(res, _), do: res
|
||||
|
||||
defp groups(res, %{"groups" => _}) do
|
||||
res
|
||||
|> Map.put(:groups, [])
|
||||
|> Map.put(:feeds_groups, [])
|
||||
end
|
||||
defp groups(res, _), do: res
|
||||
|
||||
defp feeds(res, %{"feeds" => _}) do
|
||||
feeds = Repo.all(Feed)
|
||||
|> Enum.map(&Feed.to_fever/1)
|
||||
res
|
||||
|> Map.put(:feeds, feeds)
|
||||
|> Map.put(:feeds_groups, [])
|
||||
end
|
||||
defp feeds(res, _), do: res
|
||||
|
||||
defp favicons(res, %{"favicons" => _}) do
|
||||
res
|
||||
|> Map.put(:favicons, [])
|
||||
end
|
||||
defp favicons(res, _), do: res
|
||||
|
||||
defp links(res, %{"links" => _}) do
|
||||
res
|
||||
|> Map.put(:links, [])
|
||||
end
|
||||
defp links(res, _), do: res
|
||||
|
||||
defp unread(res, %{"unread_item_ids" => _}) do
|
||||
unread = Repo.all(from Item, where: [read: false])
|
||||
|> Enum.map(fn item -> item.id end)
|
||||
|> Enum.join(",")
|
||||
res
|
||||
|> Map.put(:unread_item_ids, unread)
|
||||
end
|
||||
defp unread(res, _), do: res
|
||||
|
||||
defp saved(res, %{"saved_item_ids" => _}) do
|
||||
res
|
||||
|> Map.put(:saved_item_ids, "")
|
||||
end
|
||||
defp saved(res, _), do: res
|
||||
|
||||
defp items(res, %{"items" => _} = params) do
|
||||
items = cond do
|
||||
Map.has_key?(params, "with_ids") ->
|
||||
params["with_ids"]
|
||||
|> String.split(",")
|
||||
|> Enum.map(fn id ->
|
||||
{id, _} = id |> String.trim |> Integer.parse
|
||||
Repo.get(Item, id)
|
||||
end)
|
||||
Map.has_key?(params, "since_id") ->
|
||||
since = Repo.get(Item, params["since_id"])
|
||||
{since, _} = Integer.parse(since)
|
||||
Repo.all(from i in Item, where: i.inserted_at > ^since.inserted_at, order_by: [asc: :id], limit: 50)
|
||||
Map.has_key?(params, "max_id") ->
|
||||
max = Repo.get(Item, params["max_id"])
|
||||
{max, _} = Integer.parse(max)
|
||||
Repo.all(from i in Item, where: i.inserted_at < ^max.inserted_at, order_by: [desc: :id], limit: 50)
|
||||
true ->
|
||||
[]
|
||||
end
|
||||
items = items
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Enum.map(&Item.to_fever/1)
|
||||
res
|
||||
|> Map.put(:items, items)
|
||||
|> Map.put(:total_items, Enum.count(items))
|
||||
end
|
||||
defp items(res, _), do: res
|
||||
|
||||
end
|
|
@ -0,0 +1,36 @@
|
|||
defmodule FrenzyWeb.ItemController do
|
||||
use FrenzyWeb, :controller
|
||||
alias Frenzy.{Repo, Feed, Item}
|
||||
alias FrenzyWeb.Router.Helpers, as: Routes
|
||||
alias FrenzyWeb.Endpoint
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
item = Repo.get(Item, id)
|
||||
feed = Repo.get(Feed, item.feed_id)
|
||||
render(conn, "show.html", %{
|
||||
item: item,
|
||||
feed: feed
|
||||
})
|
||||
end
|
||||
|
||||
def read(conn, %{"id" => id}) do
|
||||
item = Repo.get(Item, id) |> Repo.preload(:feed)
|
||||
changeset = Item.changeset(item, %{
|
||||
read: true,
|
||||
read_date: Timex.now
|
||||
})
|
||||
Repo.update(changeset)
|
||||
redirect(conn, to: Routes.item_path(Endpoint, :show, id))
|
||||
end
|
||||
|
||||
def unread(conn, %{"id" => id}) do
|
||||
item = Repo.get(Item, id) |> Repo.preload(:feed)
|
||||
changeset = Item.changeset(item, %{
|
||||
read: false,
|
||||
read_date: nil
|
||||
})
|
||||
Repo.update(changeset)
|
||||
redirect(conn, to: Routes.item_path(Endpoint, :show, id))
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
defmodule FrenzyWeb.PageController do
|
||||
use FrenzyWeb, :controller
|
||||
|
||||
def index(conn, _params) do
|
||||
render(conn, "index.html")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,46 @@
|
|||
defmodule FrenzyWeb.Endpoint do
|
||||
use Phoenix.Endpoint, otp_app: :frenzy
|
||||
|
||||
socket "/socket", FrenzyWeb.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: :frenzy,
|
||||
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: "_frenzy_key",
|
||||
signing_salt: "bL/djGKI"
|
||||
|
||||
plug FrenzyWeb.Router
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
defmodule FrenzyWeb.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 FrenzyWeb.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: :frenzy
|
||||
end
|
|
@ -0,0 +1,45 @@
|
|||
defmodule FrenzyWeb.Router do
|
||||
use FrenzyWeb, :router
|
||||
|
||||
pipeline :browser do
|
||||
plug :accepts, ["html"]
|
||||
plug :fetch_session
|
||||
plug :fetch_flash
|
||||
plug :protect_from_forgery
|
||||
plug :put_secure_browser_headers
|
||||
plug BasicAuth, use_config: {:frenzy, :auth}
|
||||
end
|
||||
|
||||
pipeline :api do
|
||||
plug :accepts, ["json"]
|
||||
|
||||
end
|
||||
|
||||
scope "/", FrenzyWeb do
|
||||
pipe_through :browser
|
||||
|
||||
# get "/", PageController, :index
|
||||
|
||||
get "/", FeedController, :index
|
||||
resources "/feeds", FeedController, except: [:edit, :update]
|
||||
post "/feeds/:id/refresh", FeedController, :refresh
|
||||
|
||||
resources "/items", ItemController, only: [:show]
|
||||
post "/items/:id/read", ItemController, :read
|
||||
post "/items/:id/unread", ItemController, :unread
|
||||
end
|
||||
|
||||
scope "/", FrenzyWeb do
|
||||
pipe_through :api
|
||||
|
||||
get "/fever", FeverController, :get
|
||||
get "/api/fever.php", FeverController, :get
|
||||
post "/fever", FeverController, :post
|
||||
post "/api/fever.php", FeverController, :post
|
||||
end
|
||||
|
||||
# Other scopes may use custom stacks.
|
||||
# scope "/api", FrenzyWeb do
|
||||
# pipe_through :api
|
||||
# end
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
<a href="/feeds/new" class="button">Add Feed</a>
|
||||
|
||||
<table>
|
||||
<%= for feed <- @feeds do %>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/feeds/<%= feed.id %>"><%= feed.feed_url %></a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="<%= feed.site_url %>"><%= feed.title %></a>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
|
@ -0,0 +1,9 @@
|
|||
<%= form_for @changeset, Routes.feed_path(@conn, :create), fn form -> %>
|
||||
<div class="form-group">
|
||||
<label for="feed_url">Feed URL</label>
|
||||
<%= text_input form, :feed_url %>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<%= submit "Create Feed" %>
|
||||
</div>
|
||||
<% end %>
|
|
@ -0,0 +1,21 @@
|
|||
<%= form_tag Routes.feed_path(@conn, :refresh, @feed.id), method: :post do %>
|
||||
<%= submit "Refresh Feed" %>
|
||||
<% end %>
|
||||
|
||||
<%= form_tag Routes.feed_path(@conn, :delete, @feed.id), method: :delete do %>
|
||||
<%= submit "Delete Feed" %>
|
||||
<% end %>
|
||||
|
||||
<table>
|
||||
<%= for item <- @items do %>
|
||||
<tr <%= if item.read do %>class="item-read"<% end %>>
|
||||
<td>
|
||||
<a href="<%= Routes.item_path(@conn, :show, item.id) %>"><%= item.title %></a>
|
||||
</td>
|
||||
<td>
|
||||
<% {:ok, date} = Timex.format(item.date, "{YYYY}-{M}-{D} {h12}:{m} {AM}") %>
|
||||
<%= date %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
|
@ -0,0 +1,17 @@
|
|||
<%= if @item.read do %>
|
||||
<%= form_tag Routes.item_path(@conn, :unread, @item.id), method: :post do %>
|
||||
<%= submit "Mark as Unread" %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= form_tag Routes.item_path(@conn, :read, @item.id), method: :post do %>
|
||||
<%= submit "Mark as Read" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<a href="<%= Routes.feed_path(@conn, :show, @feed.id) %>"><%= @feed.title %></a>
|
||||
<h1>
|
||||
<a href="<%= @item.url %>"><%= @item.title %></a>
|
||||
</h1>
|
||||
<article>
|
||||
<%= raw(@item.content) %>
|
||||
</article>
|
|
@ -0,0 +1,27 @@
|
|||
<!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>Frenzy · 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="/">Frenzy</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</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>
|
|
@ -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 and maintainability.</p>
|
||||
</section>
|
||||
|
||||
<section class="row">
|
||||
<article class="column">
|
||||
<h2>Resources</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://hexdocs.pm/phoenix/overview.html">Guides & 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>
|
|
@ -0,0 +1,44 @@
|
|||
defmodule FrenzyWeb.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(FrenzyWeb.Gettext, "errors", msg, msg, count, opts)
|
||||
else
|
||||
Gettext.dgettext(FrenzyWeb.Gettext, "errors", msg, opts)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
defmodule FrenzyWeb.ErrorView do
|
||||
use FrenzyWeb, :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
|
|
@ -0,0 +1,3 @@
|
|||
defmodule FrenzyWeb.FeedView do
|
||||
use FrenzyWeb, :view
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
defmodule FrenzyWeb.ItemView do
|
||||
use FrenzyWeb, :view
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
defmodule FrenzyWeb.LayoutView do
|
||||
use FrenzyWeb, :view
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
defmodule FrenzyWeb.PageView do
|
||||
use FrenzyWeb, :view
|
||||
end
|
|
@ -0,0 +1,67 @@
|
|||
defmodule Frenzy.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :frenzy,
|
||||
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: {Frenzy.Application, []},
|
||||
extra_applications: [:logger, :runtime_tools, :httpoison, :readability]
|
||||
]
|
||||
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.0"},
|
||||
{: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"},
|
||||
{:httpoison, "~> 1.4"},
|
||||
{:fiet, git: "https://github.com/shadowfacts/fiet.git", branch: "master"},
|
||||
{:timex, "~> 3.0"},
|
||||
{:readability, git: "https://github.com/shadowfacts/readability.git", branch: "master"},
|
||||
{:basic_auth, "~> 2.2.2"}
|
||||
]
|
||||
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
|
|
@ -0,0 +1,43 @@
|
|||
%{
|
||||
"basic_auth": {:hex, :basic_auth, "2.2.4", "d8c748237870dd1df3bc5c0f1ab4f1fad6270c75472d7e62b19302ec59e92a79", [:mix], [{:plug, "~> 0.14 or ~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"},
|
||||
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
|
||||
"cowboy": {:hex, :cowboy, "2.6.0", "dc1ff5354c89e36a3e3ef8d10433396dcff0dcbb1d4223b58c64c2d51a6d88d9", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"cowlib": {:hex, :cowlib, "2.7.0", "3ef16e77562f9855a2605900cedb15c1462d76fb1be6a32fc3ae91973ee543d2", [:rebar3], [], "hexpm"},
|
||||
"db_connection": {:hex, :db_connection, "2.0.2", "440c05518b0bdca0469dafaf45403597430448c1281def14ef9ccaa41833ea1e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"decimal": {:hex, :decimal, "1.6.0", "bfd84d90ff966e1f5d4370bdd3943432d8f65f07d3bab48001aebd7030590dcc", [:mix], [], "hexpm"},
|
||||
"ecto": {:hex, :ecto, "3.0.3", "018a3df0956636f84eb3033d807485a7d3dea8474f47b90da5cb8073444c4384", [:mix], [{:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.0.2", "0e04cbc183b91ea0085c502226befcd237a4ac31c204fd4be8d4db6676b5f10d", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.2.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"fiet": {:git, "https://github.com/shadowfacts/fiet.git", "bf117bc30a6355a189d05a562127cfaf9e0187ae", [branch: "master"]},
|
||||
"file_system": {:hex, :file_system, "0.2.6", "fd4dc3af89b9ab1dc8ccbcc214a0e60c41f34be251d9307920748a14bf41f1d3", [:mix], [], "hexpm"},
|
||||
"floki": {:hex, :floki, "0.20.4", "be42ac911fece24b4c72f3b5846774b6e61b83fe685c2fc9d62093277fb3bc86", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"gettext": {:hex, :gettext, "0.16.1", "e2130b25eebcbe02bb343b119a07ae2c7e28bd4b146c4a154da2ffb2b3507af2", [:mix], [], "hexpm"},
|
||||
"hackney": {:hex, :hackney, "1.14.3", "b5f6f5dcc4f1fba340762738759209e21914516df6be440d85772542d4a5e412", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm"},
|
||||
"httpoison": {:hex, :httpoison, "1.4.0", "e0b3c2ad6fa573134e42194d13e925acfa8f89d138bc621ffb7b1989e6d22e73", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
|
||||
"mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"},
|
||||
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
|
||||
"mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
|
||||
"phoenix": {:hex, :phoenix, "1.4.0", "56fe9a809e0e735f3e3b9b31c1b749d4b436e466d8da627b8d82f90eaae714d2", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "2.12.0", "1fb3c2e48b4b66d75564d8d63df6d53655469216d6b553e7e14ced2b46f97622", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.0", "3bb31a9fbd40ffe8652e60c8660dffd72dd231efcdf49b744fb75b9ef7db5dd2", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.1", "6668d787e602981f24f17a5fbb69cc98f8ab085114ebfac6cc36e10a90c8e93c", [:mix], [], "hexpm"},
|
||||
"plug": {:hex, :plug, "1.7.1", "8516d565fb84a6a8b2ca722e74e2cd25ca0fc9d64f364ec9dbec09d33eb78ccd", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.0.0", "ab0c92728f2ba43c544cce85f0f220d8d30fc0c90eaa1e6203683ab039655062", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"},
|
||||
"postgrex": {:hex, :postgrex, "0.14.0", "f3d6ffea1ca8a156e0633900a5338a3d17b00435227726baed8982718232b694", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"ranch": {:hex, :ranch, "1.7.0", "9583f47160ca62af7f8d5db11454068eaa32b56eeadf984d4f46e61a076df5f2", [:rebar3], [], "hexpm"},
|
||||
"readability": {:git, "https://github.com/shadowfacts/readability.git", "71fa17caaf8103ef213e2c7dde4b447a48669122", [branch: "master"]},
|
||||
"saxy": {:hex, :saxy, "0.6.0", "cdb2f2fcd8133d1f3f8b0cf6a131ee1ca348dca613de266e9a239db850c4a093", [:mix], [], "hexpm"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"},
|
||||
"telemetry": {:hex, :telemetry, "0.2.0", "5b40caa3efe4deb30fb12d7cd8ed4f556f6d6bd15c374c2366772161311ce377", [:mix], [], "hexpm"},
|
||||
"timex": {:hex, :timex, "3.4.2", "d74649c93ad0e12ce5b17cf5e11fbd1fb1b24a3d114643e86dba194b64439547", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"tzdata": {:hex, :tzdata, "0.5.19", "7962a3997bf06303b7d1772988ede22260f3dae1bf897408ebdac2b4435f4e6a", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},
|
||||
}
|
|
@ -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 ""
|
|
@ -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 ""
|
|
@ -0,0 +1,4 @@
|
|||
[
|
||||
import_deps: [:ecto_sql],
|
||||
inputs: ["*.exs"]
|
||||
]
|
|
@ -0,0 +1,15 @@
|
|||
defmodule Frenzy.Repo.Migrations.CreateFeeds do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:feeds) do
|
||||
add :title, :string
|
||||
add :feed_url, :string
|
||||
add :site_url, :string
|
||||
add :last_updated, :naive_datetime
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
defmodule Frenzy.Repo.Migrations.CreateItems do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:items) do
|
||||
add :guid, :string
|
||||
add :title, :string
|
||||
add :url, :string
|
||||
add :creator, :string
|
||||
add :date, :utc_datetime
|
||||
add :content, :text
|
||||
add :read, :boolean, default: false, null: false
|
||||
add :read_date, :utc_datetime
|
||||
add :feed_id, references(:feeds)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
# create index(:items, [:feed])
|
||||
end
|
||||
end
|
|
@ -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:
|
||||
#
|
||||
# Frenzy.Repo.insert!(%Frenzy.SomeSchema{})
|
||||
#
|
||||
# We recommend using the bang functions (`insert!`, `update!`
|
||||
# and so on) as they will fail if something goes wrong.
|
|
@ -0,0 +1,8 @@
|
|||
defmodule FrenzyWeb.PageControllerTest do
|
||||
use FrenzyWeb.ConnCase
|
||||
|
||||
test "GET /", %{conn: conn} do
|
||||
conn = get(conn, "/")
|
||||
assert html_response(conn, 200) =~ "Welcome to Phoenix!"
|
||||
end
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
defmodule FrenzyWeb.ErrorViewTest do
|
||||
use FrenzyWeb.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(FrenzyWeb.ErrorView, "404.html", []) == "Not Found"
|
||||
end
|
||||
|
||||
test "renders 500.html" do
|
||||
assert render_to_string(FrenzyWeb.ErrorView, "500.html", []) == "Internal Server Error"
|
||||
end
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
defmodule FrenzyWeb.LayoutViewTest do
|
||||
use FrenzyWeb.ConnCase, async: true
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
defmodule FrenzyWeb.PageViewTest do
|
||||
use FrenzyWeb.ConnCase, async: true
|
||||
end
|
|
@ -0,0 +1,37 @@
|
|||
defmodule FrenzyWeb.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 FrenzyWeb.Endpoint
|
||||
end
|
||||
end
|
||||
|
||||
setup tags do
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Frenzy.Repo)
|
||||
|
||||
unless tags[:async] do
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Frenzy.Repo, {:shared, self()})
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
|
@ -0,0 +1,38 @@
|
|||
defmodule FrenzyWeb.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 FrenzyWeb.Router.Helpers, as: Routes
|
||||
|
||||
# The default endpoint for testing
|
||||
@endpoint FrenzyWeb.Endpoint
|
||||
end
|
||||
end
|
||||
|
||||
setup tags do
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Frenzy.Repo)
|
||||
|
||||
unless tags[:async] do
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Frenzy.Repo, {:shared, self()})
|
||||
end
|
||||
|
||||
{:ok, conn: Phoenix.ConnTest.build_conn()}
|
||||
end
|
||||
end
|
|
@ -0,0 +1,53 @@
|
|||
defmodule Frenzy.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 Frenzy.Repo
|
||||
|
||||
import Ecto
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
import Frenzy.DataCase
|
||||
end
|
||||
end
|
||||
|
||||
setup tags do
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Frenzy.Repo)
|
||||
|
||||
unless tags[:async] do
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Frenzy.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
|
|
@ -0,0 +1,2 @@
|
|||
ExUnit.start()
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Frenzy.Repo, :manual)
|
Loading…
Reference in New Issue