Initial done
This commit is contained in:
commit
8d0806f6f0
39 changed files with 2138 additions and 0 deletions
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
# Mix artifacts
|
||||
/_build
|
||||
/deps
|
||||
/*.ez
|
||||
|
||||
# Generate on crash by the VM
|
||||
erl_crash.dump
|
||||
|
||||
# Static artifacts
|
||||
/node_modules
|
||||
|
||||
# Since we are building js and css from web/static,
|
||||
# we ignore priv/static/{css,js}. You may want to
|
||||
# comment this depending on your deployment strategy.
|
||||
/priv/static/css
|
||||
/priv/static/js
|
8
README.md
Normal file
8
README.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Katso
|
||||
|
||||
To start your new Phoenix application:
|
||||
|
||||
1. Install dependencies with `mix deps.get`
|
||||
2. Start Phoenix endpoint with `mix phoenix.server`
|
||||
|
||||
Now you can visit `localhost:4000` from your browser.
|
39
brunch-config.js
Normal file
39
brunch-config.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
exports.config = {
|
||||
// See http://brunch.io/#documentation for docs.
|
||||
files: {
|
||||
javascripts: {
|
||||
joinTo: 'js/app.js'
|
||||
// To change the order of concatenation of files, explictly mention here
|
||||
// https://github.com/brunch/brunch/tree/stable/docs#concatenation
|
||||
// order: {
|
||||
// before: [
|
||||
// 'web/static/vendor/js/jquery-2.1.1.js',
|
||||
// 'web/static/vendor/js/bootstrap.min.js'
|
||||
// ]
|
||||
// }
|
||||
},
|
||||
stylesheets: {
|
||||
joinTo: 'css/app.css'
|
||||
},
|
||||
templates: {
|
||||
joinTo: 'js/app.js'
|
||||
}
|
||||
},
|
||||
|
||||
// Phoenix paths configuration
|
||||
paths: {
|
||||
// Which directories to watch
|
||||
watched: ["web/static", "test/static"],
|
||||
|
||||
// Where to compile files to
|
||||
public: "priv/static"
|
||||
},
|
||||
|
||||
// Configure your plugins
|
||||
plugins: {
|
||||
ES6to5: {
|
||||
// Do not use ES6 compiler in vendor code
|
||||
ignore: [/^(web\/static\/vendor)/]
|
||||
}
|
||||
}
|
||||
};
|
24
config/config.exs
Normal file
24
config/config.exs
Normal file
|
@ -0,0 +1,24 @@
|
|||
# This file is responsible for configuring your application
|
||||
# and its dependencies with the aid of the Mix.Config module.
|
||||
#
|
||||
# This configuration file is loaded before any dependency and
|
||||
# is restricted to this project.
|
||||
use Mix.Config
|
||||
|
||||
# Configures the endpoint
|
||||
config :katso, Katso.Endpoint,
|
||||
url: [host: "localhost"],
|
||||
root: Path.expand("..", __DIR__),
|
||||
secret_key_base: "AwltP5KYMmwNj+7/UP47rHeKMX16cxP7uv0Csr+PoIZEFN2o090mzVvLbcvJeld1",
|
||||
debug_errors: false,
|
||||
pubsub: [name: Katso.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"
|
35
config/dev.exs
Normal file
35
config/dev.exs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use Mix.Config
|
||||
|
||||
# For development, we disable any cache and enable
|
||||
# debugging and code reloading.
|
||||
#
|
||||
# The watchers configuration can be used to run external
|
||||
# watchers to your application. For example, we use it
|
||||
# with brunch.io to recompile .js and .css sources.
|
||||
config :katso, Katso.Endpoint,
|
||||
http: [port: 4000],
|
||||
debug_errors: true,
|
||||
code_reloader: true,
|
||||
cache_static_lookup: false,
|
||||
watchers: [node: ["node_modules/brunch/bin/brunch", "watch"]]
|
||||
|
||||
# Watch static and templates for browser reloading.
|
||||
config :katso, Katso.Endpoint,
|
||||
live_reload: [
|
||||
patterns: [
|
||||
~r{priv/static/.*(js|css|png|jpeg|jpg|gif)$},
|
||||
~r{web/views/.*(ex)$},
|
||||
~r{web/templates/.*(eex)$}
|
||||
]
|
||||
]
|
||||
|
||||
# Do not include metadata nor timestamps in development logs
|
||||
config :logger, :console, format: "[$level] $message\n"
|
||||
|
||||
# Configure your database
|
||||
config :katso, Katso.Repo,
|
||||
adapter: Ecto.Adapters.Postgres,
|
||||
username: "katso",
|
||||
password: "katso",
|
||||
database: "katso",
|
||||
hostname: "localhost"
|
45
config/prod.exs
Normal file
45
config/prod.exs
Normal file
|
@ -0,0 +1,45 @@
|
|||
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.
|
||||
config :katso, Katso.Endpoint,
|
||||
http: [port: {:system, "PORT"}],
|
||||
url: [host: "example.com"]
|
||||
|
||||
# ## SSL Support
|
||||
#
|
||||
# To get SSL working, you will need to add the `https` key
|
||||
# to the previous section:
|
||||
#
|
||||
# config:katso, Katso.Endpoint,
|
||||
# ...
|
||||
# https: [port: 443,
|
||||
# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
|
||||
# certfile: System.get_env("SOME_APP_SSL_CERT_PATH")]
|
||||
#
|
||||
# Where those two env variables point to a file on
|
||||
# disk for the key and cert.
|
||||
|
||||
# Do not print debug messages in production
|
||||
config :logger, level: :info
|
||||
|
||||
# ## Using releases
|
||||
#
|
||||
# If you are doing OTP releases, you need to instruct Phoenix
|
||||
# to start the server for all endpoints:
|
||||
#
|
||||
# config :phoenix, :serve_endpoints, true
|
||||
#
|
||||
# Alternatively, you can configure exactly which server to
|
||||
# start per endpoint:
|
||||
#
|
||||
# config :katso, Katso.Endpoint, server: true
|
||||
#
|
||||
|
||||
# Finally import the config/prod.secret.exs
|
||||
# which should be versioned separately.
|
||||
import_config "prod.secret.exs"
|
14
config/prod.secret.exs
Normal file
14
config/prod.secret.exs
Normal file
|
@ -0,0 +1,14 @@
|
|||
use Mix.Config
|
||||
|
||||
# In this file, we keep production configuration that
|
||||
# you likely want to automate and keep it away from
|
||||
# your version control system.
|
||||
config :katso, Katso.Endpoint,
|
||||
secret_key_base: "Pqf8JsBMaVsywaKVbsJFtsPPySK94cWLrvEgwENe37SBW5EiDO4J3F7AaZ5luBCY"
|
||||
|
||||
# Configure your database
|
||||
config :katso, Katso.Repo,
|
||||
adapter: Ecto.Adapters.Postgres,
|
||||
username: "postgres",
|
||||
password: "postgres",
|
||||
database: "katso_prod"
|
19
config/test.exs
Normal file
19
config/test.exs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use Mix.Config
|
||||
|
||||
# We don't run a server during test. If one is required,
|
||||
# you can enable the server option below.
|
||||
config :katso, Katso.Endpoint,
|
||||
http: [port: 4001],
|
||||
server: false
|
||||
|
||||
# Print only warnings and errors during test
|
||||
config :logger, level: :warn
|
||||
|
||||
# Configure your database
|
||||
config :katso, Katso.Repo,
|
||||
adapter: Ecto.Adapters.Postgres,
|
||||
username: "postgres",
|
||||
password: "postgres",
|
||||
database: "katso_test",
|
||||
size: 1,
|
||||
max_overflow: false
|
30
lib/katso.ex
Normal file
30
lib/katso.ex
Normal file
|
@ -0,0 +1,30 @@
|
|||
defmodule Katso do
|
||||
use Application
|
||||
|
||||
# See http://elixir-lang.org/docs/stable/elixir/Application.html
|
||||
# for more information on OTP Applications
|
||||
def start(_type, _args) do
|
||||
import Supervisor.Spec, warn: false
|
||||
|
||||
children = [
|
||||
# Start the endpoint when the application starts
|
||||
supervisor(Katso.Endpoint, []),
|
||||
# Start the Ecto repository
|
||||
worker(Katso.Repo, []),
|
||||
# Here you could define other workers and supervisors as children
|
||||
# worker(Katso.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: Katso.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
|
||||
Katso.Endpoint.config_change(changed, removed)
|
||||
:ok
|
||||
end
|
||||
end
|
33
lib/katso/endpoint.ex
Normal file
33
lib/katso/endpoint.ex
Normal file
|
@ -0,0 +1,33 @@
|
|||
defmodule Katso.Endpoint do
|
||||
use Phoenix.Endpoint, otp_app: :katso
|
||||
|
||||
# Serve at "/" the given assets from "priv/static" directory
|
||||
plug Plug.Static,
|
||||
at: "/", from: :katso,
|
||||
only: ~w(css images js favicon.ico robots.txt)
|
||||
|
||||
# Code reloading can be explicitly enabled under the
|
||||
# :code_reloader configuration of your endpoint.
|
||||
if code_reloading? do
|
||||
plug Phoenix.LiveReloader
|
||||
plug Phoenix.CodeReloader
|
||||
end
|
||||
|
||||
plug Plug.Logger
|
||||
|
||||
plug Plug.Parsers,
|
||||
parsers: [:urlencoded, :multipart, :json],
|
||||
pass: ["*/*"],
|
||||
json_decoder: Poison
|
||||
|
||||
plug Plug.MethodOverride
|
||||
plug Plug.Head
|
||||
|
||||
plug Plug.Session,
|
||||
store: :cookie,
|
||||
key: "_katso_key",
|
||||
signing_salt: "hybxwdCF",
|
||||
encryption_salt: "rGum4O3j"
|
||||
|
||||
plug :router, Katso.Router
|
||||
end
|
387
lib/katso/pageanalyzer.ex
Normal file
387
lib/katso/pageanalyzer.ex
Normal file
|
@ -0,0 +1,387 @@
|
|||
defmodule Katso.PageAnalyzer do
|
||||
@moduledoc """
|
||||
This module contains functionality to analyze a single page. First it scrapes the page using Scraper and then
|
||||
calculates the scores using TitleAnalyzer.
|
||||
"""
|
||||
|
||||
alias Katso.Repo
|
||||
alias Katso.Magazine
|
||||
alias Katso.Fetch
|
||||
alias Katso.FetchScore
|
||||
alias Katso.Title
|
||||
alias Katso.TitleScore
|
||||
|
||||
import Ecto.Query, only: [from: 2]
|
||||
|
||||
@sites %{
|
||||
iltalehti: %{
|
||||
name: "Iltalehti",
|
||||
url: "http://www.iltalehti.fi/",
|
||||
rules: [
|
||||
".otsikko"
|
||||
]
|
||||
},
|
||||
|
||||
iltasanomat: %{
|
||||
name: "Ilta-Sanomat",
|
||||
url: "http://www.iltasanomat.fi/",
|
||||
rules: [
|
||||
"h2",
|
||||
"h3",
|
||||
"a div p",
|
||||
"a div.content"
|
||||
]
|
||||
},
|
||||
|
||||
aamulehti: %{
|
||||
name: "Aamulehti",
|
||||
url: "http://www.aamulehti.fi/",
|
||||
rules: [
|
||||
"h2 a",
|
||||
"h3 a"
|
||||
]
|
||||
},
|
||||
|
||||
hs: %{
|
||||
name: "Helsingin Sanomat",
|
||||
url: "http://www.hs.fi/",
|
||||
rules: [
|
||||
"h2 a",
|
||||
"h3 a",
|
||||
"li div a"
|
||||
]
|
||||
},
|
||||
|
||||
ts: %{
|
||||
name: "Turun Sanomat",
|
||||
url: "http://www.ts.fi/",
|
||||
rules: [
|
||||
"h1"
|
||||
]
|
||||
},
|
||||
|
||||
ksml: %{
|
||||
name: "Keskisuomalainen",
|
||||
url: "http://www.ksml.fi/",
|
||||
rules: [
|
||||
"h1 a",
|
||||
"h2 a",
|
||||
"h3 a"
|
||||
]
|
||||
},
|
||||
|
||||
kymensanomat: %{
|
||||
name: "Kymen Sanomat",
|
||||
url: "http://www.kymensanomat.fi/",
|
||||
rules: [
|
||||
"h1 a",
|
||||
"h2 a",
|
||||
"div.news-title a"
|
||||
]
|
||||
},
|
||||
|
||||
etelasaimaa: %{
|
||||
name: "Etelä-Saimaa",
|
||||
url: "http://www.esaimaa.fi/",
|
||||
rules: [
|
||||
"h1 a",
|
||||
"h2 a",
|
||||
"div.news-title a"
|
||||
]
|
||||
},
|
||||
|
||||
kouvolansanomat: %{
|
||||
name: "Kouvolan Sanomat",
|
||||
url: "http://www.kouvolansanomat.fi/",
|
||||
rules: [
|
||||
"h1 a",
|
||||
"h2 a",
|
||||
"div.news-title a"
|
||||
]
|
||||
},
|
||||
|
||||
ess: %{
|
||||
name: "Etelä-Suomen Sanomat",
|
||||
url: "http://www.ess.fi/",
|
||||
rules: [
|
||||
"h1 a"
|
||||
]
|
||||
},
|
||||
|
||||
forssa: %{
|
||||
name: "Forssan Lehti",
|
||||
url: "http://www.forssanlehti.fi/",
|
||||
rules: [
|
||||
"h1 a",
|
||||
"h3 a"
|
||||
]
|
||||
},
|
||||
|
||||
hameensanomat: %{
|
||||
name: "Hämeen Sanomat",
|
||||
url: "http://www.hameensanomat.fi/",
|
||||
rules: [
|
||||
"h1 a"
|
||||
]
|
||||
},
|
||||
|
||||
lapinkansa: %{
|
||||
name: "Lapin Kansa",
|
||||
url: "http://www.lapinkansa.fi/",
|
||||
rules: [
|
||||
"h2 a",
|
||||
"li a"
|
||||
]
|
||||
},
|
||||
|
||||
yle: %{
|
||||
name: "Yle Uutiset",
|
||||
url: "http://yle.fi/uutiset/",
|
||||
rules: [
|
||||
"h1 a"
|
||||
]
|
||||
},
|
||||
|
||||
karjalainen: %{
|
||||
name: "Karjalainen",
|
||||
url: "http://www.karjalainen.fi/",
|
||||
rules: [
|
||||
"h1 a",
|
||||
"h2 a",
|
||||
"h3 a",
|
||||
"h4 a"
|
||||
]
|
||||
},
|
||||
|
||||
kangasalan_sanomat: %{
|
||||
name: "Kangasalan Sanomat",
|
||||
url: "http://kangasalansanomat.fi/",
|
||||
rules: [
|
||||
"h1",
|
||||
"h2"
|
||||
]
|
||||
},
|
||||
|
||||
kaleva: %{
|
||||
name: "Kaleva",
|
||||
url: "http://www.kaleva.fi/",
|
||||
rules: [
|
||||
"dd a",
|
||||
"h2"
|
||||
]
|
||||
},
|
||||
|
||||
mtv: %{
|
||||
name: "MTV Uutiset",
|
||||
url: "http://www.mtv.fi/uutiset",
|
||||
rules: [
|
||||
"h2",
|
||||
"div.related a",
|
||||
"li a p",
|
||||
"li a span",
|
||||
"p.headline"
|
||||
]
|
||||
},
|
||||
|
||||
uusisuomi: %{
|
||||
name: "Uusi Suomi",
|
||||
url: "http://www.uusisuomi.fi/",
|
||||
rules: [
|
||||
"h2 a",
|
||||
"h4 a"
|
||||
]
|
||||
},
|
||||
|
||||
ilkka: %{
|
||||
name: "Ilkka",
|
||||
url: "http://www.ilkka.fi/",
|
||||
rules: [
|
||||
"h1 a"
|
||||
]
|
||||
},
|
||||
|
||||
maaseuduntulevaisuus: %{
|
||||
name: "Maaseudun Tulevaisuus",
|
||||
url: "http://www.maaseuduntulevaisuus.fi/",
|
||||
rules: [
|
||||
"h2 a",
|
||||
"span.title"
|
||||
]
|
||||
},
|
||||
|
||||
savonsanomat: %{
|
||||
name: "Savon Sanomat",
|
||||
url: "http://www.savonsanomat.fi/",
|
||||
rules: [
|
||||
"h1 a",
|
||||
"h2 a",
|
||||
"div.media-body a"
|
||||
]
|
||||
},
|
||||
|
||||
# taloussanomat: %{
|
||||
# name: "Taloussanomat",
|
||||
# url: "http://www.taloussanomat.fi/",
|
||||
# rules: [
|
||||
# "h1 a",
|
||||
# "h2 a",
|
||||
# "h3 a"
|
||||
# ]
|
||||
# },
|
||||
|
||||
verkkouutiset: %{
|
||||
name: "Verkkouutiset",
|
||||
url: "http://www.verkkouutiset.fi/",
|
||||
rules: [
|
||||
"span.headline"
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
def analyze_all() do
|
||||
Map.keys(@sites)
|
||||
|> Enum.map(fn site_key -> Task.async Katso.PageAnalyzer, :analyze, [site_key] end)
|
||||
|> handle_responses
|
||||
|> calculate_scores
|
||||
|> reject_emptys
|
||||
|> print_scores
|
||||
|> store_data
|
||||
end
|
||||
|
||||
def analyze(site_key) do
|
||||
site = @sites[site_key]
|
||||
|
||||
data = Katso.Scraper.scrape(site)
|
||||
|> Enum.map(fn title -> {title, Katso.TitleAnalyzer.analyze title} end)
|
||||
|
||||
IO.puts "Analyzed " <> site.name
|
||||
|
||||
{site_key, data}
|
||||
end
|
||||
|
||||
def handle_responses(task_list, data \\ [])
|
||||
|
||||
def handle_responses([], data), do: data
|
||||
|
||||
def handle_responses(task_list, data) do
|
||||
receive do
|
||||
msg -> case Task.find task_list, msg do
|
||||
nil -> handle_responses task_list, data
|
||||
{result, task} ->
|
||||
data = [handle_response(result) | data]
|
||||
handle_responses List.delete(task_list, task), data
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def handle_response({site_key, result}) do
|
||||
initial_scores = %{
|
||||
relative_score: 0,
|
||||
total_score: 0,
|
||||
score_types: [],
|
||||
total_titles: 0,
|
||||
matches: []
|
||||
}
|
||||
|
||||
scores = result
|
||||
|> Enum.reduce(initial_scores, fn {title, title_scores}, total_scores ->
|
||||
title_scores
|
||||
|> Enum.reduce(total_scores, fn {score_key, _, score_amount}, acc ->
|
||||
acc
|
||||
|> Map.put(:score_types, Keyword.put(acc.score_types, score_key, Keyword.get(acc.score_types, score_key, 0) + score_amount))
|
||||
|> Map.put :total_score, acc.total_score + score_amount
|
||||
end)
|
||||
|> Map.put(:matches, [{title, title_scores} | total_scores.matches])
|
||||
|> Map.put :total_titles, total_scores.total_titles + 1
|
||||
end)
|
||||
|
||||
{site_key, scores}
|
||||
end
|
||||
|
||||
def calculate_scores(data) do
|
||||
Enum.map data, fn {site_key, scores} ->
|
||||
case scores.total_titles do
|
||||
0 -> nil
|
||||
_ -> {site_key, %{scores | relative_score: ((scores.total_score / scores.total_titles * 100) |> Float.round |> trunc) }}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def reject_emptys(data) do
|
||||
Enum.reject data, fn x -> x == nil end
|
||||
end
|
||||
|
||||
def print_scores(data) do
|
||||
Enum.map data, fn {site_key, scores} ->
|
||||
IO.puts Atom.to_string(site_key) <> ": " <> Integer.to_string scores.relative_score
|
||||
{site_key, scores}
|
||||
end
|
||||
end
|
||||
|
||||
def store_data(data) do
|
||||
Enum.each data, fn {site_key, scores} ->
|
||||
query = from m in Magazine,
|
||||
where: m.key == ^(Atom.to_string site_key)
|
||||
|
||||
magazine = case Repo.one query do
|
||||
nil -> create_magazine @sites[site_key], site_key
|
||||
m -> m
|
||||
end
|
||||
|
||||
fetch = create_fetch magazine, scores
|
||||
|
||||
Enum.each scores.score_types, fn score_type ->
|
||||
create_fetch_score fetch, score_type
|
||||
end
|
||||
|
||||
Enum.reject(scores.matches, fn {_, score_types} -> score_types == [] end)
|
||||
|> Enum.each fn {match, score_types} ->
|
||||
title = create_title fetch, {match, score_types}
|
||||
|
||||
Enum.each score_types, fn score_type ->
|
||||
create_title_score title, score_type
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp create_magazine(site, site_key) do
|
||||
Repo.insert Magazine.changeset %Magazine{}, %{
|
||||
name: site.name,
|
||||
key: Atom.to_string(site_key)
|
||||
}
|
||||
end
|
||||
|
||||
defp create_fetch(magazine, scores) do
|
||||
Repo.insert Fetch.changeset %Fetch{}, %{
|
||||
total_score: scores.total_score,
|
||||
total_titles: scores.total_titles,
|
||||
relative_score: scores.relative_score,
|
||||
magazine_id: magazine.id
|
||||
}
|
||||
end
|
||||
|
||||
defp create_title(fetch, {title, _}) do
|
||||
Repo.insert Title.changeset %Title{}, %{
|
||||
title: title,
|
||||
fetch_id: fetch.id
|
||||
}
|
||||
end
|
||||
|
||||
defp create_title_score(title, {score_type, score_words, score_amount}) do
|
||||
Repo.insert TitleScore.changeset %TitleScore{}, %{
|
||||
score_type: Atom.to_string(score_type),
|
||||
score_words: score_words,
|
||||
score_amount: score_amount,
|
||||
title_id: title.id
|
||||
}
|
||||
end
|
||||
|
||||
defp create_fetch_score(fetch, {score_type, score_amount}) do
|
||||
Repo.insert FetchScore.changeset %FetchScore{}, %{
|
||||
score_type: Atom.to_string(score_type),
|
||||
score_amount: score_amount,
|
||||
fetch_id: fetch.id
|
||||
}
|
||||
end
|
||||
end
|
3
lib/katso/repo.ex
Normal file
3
lib/katso/repo.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule Katso.Repo do
|
||||
use Ecto.Repo, otp_app: :katso
|
||||
end
|
48
lib/katso/scraper.ex
Normal file
48
lib/katso/scraper.ex
Normal file
|
@ -0,0 +1,48 @@
|
|||
defmodule Katso.Scraper do
|
||||
@moduledoc """
|
||||
This module stores the list of sites to scrape and their scraping rules.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
List of user agents to use, one will be picked randomly from this list.
|
||||
"""
|
||||
@uas [
|
||||
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A",
|
||||
"Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko",
|
||||
"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/4.0; InfoPath.2; SV1; .NET CLR 2.0.50727; WOW64)",
|
||||
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Zune 4.0; InfoPath.3; MS-RTC LM 8; .NET4.0C; .NET4.0E)",
|
||||
"Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0",
|
||||
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:24.0) Gecko/20100101 Firefox/24.0",
|
||||
"Opera/9.80 (X11; Linux i686; Ubuntu/14.10) Presto/2.12.388 Version/12.16"
|
||||
]
|
||||
|
||||
# Options for hackney
|
||||
@hackney follow_redirect: true
|
||||
|
||||
@doc """
|
||||
Scrape readable lines from the given site using the site's scraping rules.
|
||||
"""
|
||||
def scrape(site) do
|
||||
case HTTPoison.get site.url, [{:"User-Agent", Enum.at(@uas, :random.uniform(Enum.count(@uas)) - 1)}], [hackney: @hackney] do
|
||||
{:error, _} -> :error
|
||||
{:ok, %HTTPoison.Response{body: html}} -> scrape site, html
|
||||
end
|
||||
end
|
||||
|
||||
def scrape(site, html) do
|
||||
parse_tree = Floki.parse html
|
||||
|
||||
site.rules
|
||||
|> Enum.map_reduce([], fn rule, acc -> {nil, do_scrape(parse_tree, rule, acc)} end)
|
||||
|> Tuple.to_list
|
||||
|> Enum.at(1)
|
||||
|> Enum.map(fn elem -> Katso.Utils.convert_utf8 Floki.text elem end)
|
||||
|> Enum.reject(fn elem -> String.strip(elem) == "" end)
|
||||
end
|
||||
|
||||
def do_scrape(parse_tree, rule, results) do
|
||||
Floki.find(parse_tree, rule)
|
||||
|> Enum.concat results
|
||||
end
|
||||
end
|
136
lib/katso/titleanalyzer.ex
Normal file
136
lib/katso/titleanalyzer.ex
Normal file
|
@ -0,0 +1,136 @@
|
|||
defmodule Katso.TitleAnalyzer do
|
||||
@moduledoc """
|
||||
This module contains the tools for analyzing a single title or piece of text.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
The regex rules which are used to look up shitty stuff from text.
|
||||
Also includes explanations and point ratings for them.
|
||||
"""
|
||||
@rules %{
|
||||
vau: %{
|
||||
s: "Vau! Oho! Ja kaikenlainen muu ihmettely.",
|
||||
r: ~r/\b(?:vau|oho|ohh?oh?|hups(?:is)?|huh)\b/iu,
|
||||
p: 1
|
||||
},
|
||||
|
||||
nyt: %{
|
||||
s: "Nyt puhuu X! Nyt se on X!",
|
||||
r: ~r/\b(?:nyt se on|nyt puhu[uv])\b/iu,
|
||||
p: 1
|
||||
},
|
||||
|
||||
pronominit: %{
|
||||
s: "Epämääräiset pronominit joilla vältellään kertomasta, mistä puhutaan. Hän, tämä, nämä jne.",
|
||||
r: ~r/
|
||||
\b(?:
|
||||
tämä|tässä|tällä|tästä|tänne|tähän|tälle|tämän|tältä|tätä|tällaista|
|
||||
tämänlaiset|tämänlaisia|tämänlaista|
|
||||
nämä|näissä|näillä|näistä|näihin|näille|näiden|näiltä|näitä|näin|
|
||||
hän|hänessä|hänellä|hänestä|häneen|hänelle|hänen|häneltä|häntä|
|
||||
he|heissä|heillä|heistä|heihin|heille|heidän|heiltä|heitä|heistä
|
||||
)(?:kin)?\b
|
||||
/ixu,
|
||||
p: 1
|
||||
},
|
||||
|
||||
kysymys: %{
|
||||
s: "Kysymykset otsikoissa. Yleensä näihin vastaus on ”ei”.",
|
||||
r: ~r/\?/u,
|
||||
p: 1
|
||||
},
|
||||
|
||||
huuto: %{
|
||||
s: "Huonoja otsikoita tehostetaan usein huutomerkillä!",
|
||||
r: ~r/!/u,
|
||||
p: 1
|
||||
},
|
||||
|
||||
turhat_sanat: %{
|
||||
s: "Sanoja, jotka yleensä merkitsevät turhaa lööppiä ja joilla yritetään saada siitä dramaattisemman kuuloinen.",
|
||||
r: ~r/
|
||||
\b(?:
|
||||
kohu
|
||||
|raivostu
|
||||
|kauhunhetk
|
||||
|seksi
|
||||
|dramaatti
|
||||
|julkkis
|
||||
|bb\-
|
||||
|outo
|
||||
|rohke(?:at?|is)
|
||||
|paljast
|
||||
|raju
|
||||
|skandaal
|
||||
|mokat?\b
|
||||
|ällistyttä
|
||||
|mahtava
|
||||
|uskomat(?:t|o)
|
||||
|vihdoin
|
||||
|avautu
|
||||
|tilit(?:y|t)
|
||||
|hyytävi?ä
|
||||
|jäätävi?ä
|
||||
|et usko
|
||||
|kansa\b
|
||||
|testaa\b
|
||||
|arvaa
|
||||
|keksi(?:\b|tkö)
|
||||
|erikoi(?:s|n)
|
||||
|nolo(?:\b|a|i)
|
||||
|sensaatio
|
||||
|omitui
|
||||
)
|
||||
/iux,
|
||||
p: 1
|
||||
},
|
||||
|
||||
katso: %{
|
||||
s: "Kehotus katsomaan jotain lisäsisältöä, joka lähes poikkeuksetta on hyödytöntä.",
|
||||
r: ~r/\bkatso\b/u,
|
||||
p: 1
|
||||
},
|
||||
|
||||
some: %{
|
||||
s: "Sosiaalinen media on turhaa hömpötystä.",
|
||||
r: ~r/\bsome|twiitt|peukut(?:u|t)/,
|
||||
p: 1
|
||||
}
|
||||
}
|
||||
|
||||
def analyze(str) do
|
||||
str = convert_utf8 str
|
||||
|
||||
Map.keys(@rules)
|
||||
|> Enum.map(fn key ->
|
||||
rule = @rules[key]
|
||||
|
||||
case run_re str, rule do
|
||||
nil -> nil
|
||||
matches -> {key, matches, Enum.count(matches) * rule.p}
|
||||
end
|
||||
end)
|
||||
|> Enum.reject fn x -> x == nil end
|
||||
end
|
||||
|
||||
def run_re(str, %{r: r}) do
|
||||
case Regex.scan r, str do
|
||||
[] -> nil
|
||||
matches -> Enum.map matches, fn match -> Enum.at match, 0 end
|
||||
end
|
||||
end
|
||||
|
||||
def convert_utf8(str) do
|
||||
case String.valid? str do
|
||||
true -> str
|
||||
false ->
|
||||
String.codepoints(str)
|
||||
|> Enum.reduce "", fn codepoint, acc ->
|
||||
acc <> case codepoint do
|
||||
<<byte>> -> <<byte :: utf8>>
|
||||
char -> char
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
15
lib/katso/utils.ex
Normal file
15
lib/katso/utils.ex
Normal file
|
@ -0,0 +1,15 @@
|
|||
defmodule Katso.Utils do
|
||||
def convert_utf8(str) do
|
||||
case String.valid? str do
|
||||
true -> str
|
||||
false ->
|
||||
String.codepoints(str)
|
||||
|> Enum.reduce "", fn codepoint, acc ->
|
||||
acc <> case codepoint do
|
||||
<<byte>> -> <<byte :: utf8>>
|
||||
char -> char
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
41
mix.exs
Normal file
41
mix.exs
Normal file
|
@ -0,0 +1,41 @@
|
|||
defmodule Katso.Mixfile do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[app: :katso,
|
||||
version: "0.0.1",
|
||||
elixir: "~> 1.0",
|
||||
elixirc_paths: elixirc_paths(Mix.env),
|
||||
compilers: [:phoenix] ++ Mix.compilers,
|
||||
build_embedded: Mix.env == :prod,
|
||||
start_permanent: Mix.env == :prod,
|
||||
deps: deps]
|
||||
end
|
||||
|
||||
# Configuration for the OTP application
|
||||
#
|
||||
# Type `mix help compile.app` for more information
|
||||
def application do
|
||||
[mod: {Katso, []},
|
||||
applications: [:phoenix, :cowboy, :logger, :ecto, :httpoison]]
|
||||
end
|
||||
|
||||
# Specifies which paths to compile per environment
|
||||
defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
|
||||
defp elixirc_paths(_), do: ["lib", "web"]
|
||||
|
||||
# Specifies your project dependencies
|
||||
#
|
||||
# Type `mix help deps` for examples and options
|
||||
defp deps do
|
||||
[{:phoenix, "~> 0.11"},
|
||||
{:phoenix_ecto, "~> 0.3"},
|
||||
{:postgrex, ">= 0.0.0"},
|
||||
{:phoenix_live_reload, "~> 0.3"},
|
||||
{:cowboy, "~> 1.0"},
|
||||
{:excoder, "1.3.0", git: "git@bitbucket.org:Nicd/excoder.git"},
|
||||
{:floki, "~> 0.1"},
|
||||
{:httpoison, "~> 0.6"}
|
||||
]
|
||||
end
|
||||
end
|
21
mix.lock
Normal file
21
mix.lock
Normal file
|
@ -0,0 +1,21 @@
|
|||
%{"cowboy": {:hex, :cowboy, "1.0.0"},
|
||||
"cowlib": {:hex, :cowlib, "1.0.1"},
|
||||
"decimal": {:hex, :decimal, "1.1.0"},
|
||||
"ecto": {:hex, :ecto, "0.10.2"},
|
||||
"excoder": {:git, "git@bitbucket.org:Nicd/excoder.git", "274736c587c3b48fa4c86b86c94bf915977385d3", []},
|
||||
"floki": {:hex, :floki, "0.1.1"},
|
||||
"fs": {:hex, :fs, "0.9.1"},
|
||||
"hackney": {:hex, :hackney, "1.1.0"},
|
||||
"httpoison": {:hex, :httpoison, "0.6.2"},
|
||||
"iconv": {:git, "https://github.com/erylee/erlang-iconv.git", "bd9ed8cc16ba3595fc6993dc2e6bf97273ce7f6a", []},
|
||||
"idna": {:hex, :idna, "1.0.2"},
|
||||
"mochiweb": {:hex, :mochiweb, "2.12.2"},
|
||||
"phoenix": {:hex, :phoenix, "0.11.0"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "0.3.1"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "0.3.1"},
|
||||
"plug": {:hex, :plug, "0.11.3"},
|
||||
"poison": {:hex, :poison, "1.4.0"},
|
||||
"poolboy": {:hex, :poolboy, "1.4.2"},
|
||||
"postgrex": {:hex, :postgrex, "0.8.1"},
|
||||
"ranch": {:hex, :ranch, "1.0.0"},
|
||||
"ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.4"}}
|
13
package.json
Normal file
13
package.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"repository": {
|
||||
},
|
||||
"dependencies": {
|
||||
"brunch": "git://github.com/brunch/brunch#5176b6b4bf70cd8cb9dad0058dd3e83e8d983218",
|
||||
"babel-brunch": "^4.0.0",
|
||||
"clean-css-brunch": ">= 1.0 < 1.8",
|
||||
"css-brunch": ">= 1.0 < 1.8",
|
||||
"javascript-brunch": ">= 1.0 < 1.8",
|
||||
"sass-brunch": "git://github.com/brunch/sass-brunch.git#master",
|
||||
"uglify-js-brunch": ">= 1.0 < 1.8"
|
||||
}
|
||||
}
|
39
priv/repo/migrations/20150418171426_init.exs
Normal file
39
priv/repo/migrations/20150418171426_init.exs
Normal file
|
@ -0,0 +1,39 @@
|
|||
defmodule Katso.Repo.Migrations.Init do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:magazines) do
|
||||
add :name, :string
|
||||
add :key, :string
|
||||
end
|
||||
|
||||
create index(:magazines, [:key], unique: true)
|
||||
|
||||
create table(:fetches) do
|
||||
add :magazine_id, references(:magazines)
|
||||
add :total_score, :integer
|
||||
add :total_titles, :integer
|
||||
add :relative_score, :integer
|
||||
|
||||
timestamps
|
||||
end
|
||||
|
||||
create table(:fetch_scores) do
|
||||
add :fetch_id, references(:fetches)
|
||||
add :score_type, :string
|
||||
add :score_amount, :integer
|
||||
end
|
||||
|
||||
create table(:titles) do
|
||||
add :fetch_id, references(:fetches)
|
||||
add :title, :text
|
||||
end
|
||||
|
||||
create table(:title_scores) do
|
||||
add :title_id, references(:titles)
|
||||
add :score_type, :string
|
||||
add :score_amount, :integer
|
||||
add :score_words, {:array, :text}
|
||||
end
|
||||
end
|
||||
end
|
BIN
priv/static/images/phoenix.png
Normal file
BIN
priv/static/images/phoenix.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
8
test/controllers/page_controller_test.exs
Normal file
8
test/controllers/page_controller_test.exs
Normal file
|
@ -0,0 +1,8 @@
|
|||
defmodule Katso.PageControllerTest do
|
||||
use Katso.ConnCase
|
||||
|
||||
test "GET /" do
|
||||
conn = get conn(), "/"
|
||||
assert conn.resp_body =~ "Welcome to Phoenix!"
|
||||
end
|
||||
end
|
43
test/support/conn_case.ex
Normal file
43
test/support/conn_case.ex
Normal file
|
@ -0,0 +1,43 @@
|
|||
defmodule Katso.ConnCase do
|
||||
@moduledoc """
|
||||
This module defines the test case to be used by
|
||||
tests that require setting up a connection.
|
||||
|
||||
Such tests rely on `Phoenix.ConnTest` and also
|
||||
imports other functionalities to make it easier
|
||||
to build and query models.
|
||||
|
||||
Finally, if the test case interacts with the database,
|
||||
it cannot be async. For this reason, every test runs
|
||||
inside a transaction which is reset at the beginning
|
||||
of the test unless the test case is marked as async.
|
||||
"""
|
||||
|
||||
use ExUnit.CaseTemplate
|
||||
|
||||
using do
|
||||
quote do
|
||||
# Import conveniences for testing with connections
|
||||
use Phoenix.ConnTest
|
||||
|
||||
# Alias the data repository and import query/model functions
|
||||
alias Katso.Repo
|
||||
import Ecto.Model
|
||||
import Ecto.Query, only: [from: 2]
|
||||
|
||||
# Import URL helpers from the router
|
||||
import Katso.Router.Helpers
|
||||
|
||||
# The default endpoint for testing
|
||||
@endpoint Katso.Endpoint
|
||||
end
|
||||
end
|
||||
|
||||
setup tags do
|
||||
unless tags[:async] do
|
||||
Ecto.Adapters.SQL.restart_test_transaction(Katso.Repo, [])
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
6
test/test_helper.exs
Normal file
6
test/test_helper.exs
Normal file
|
@ -0,0 +1,6 @@
|
|||
ExUnit.start
|
||||
|
||||
# Create the database, run migrations, and start the test transaction.
|
||||
Mix.Task.run "ecto.create", ["--quiet"]
|
||||
Mix.Task.run "ecto.migrate", ["--quiet"]
|
||||
Ecto.Adapters.SQL.begin_test_transaction(Katso.Repo)
|
9
web/controllers/page_controller.ex
Normal file
9
web/controllers/page_controller.ex
Normal file
|
@ -0,0 +1,9 @@
|
|||
defmodule Katso.PageController do
|
||||
use Katso.Web, :controller
|
||||
|
||||
plug :action
|
||||
|
||||
def index(conn, _params) do
|
||||
render conn, "index.html"
|
||||
end
|
||||
end
|
23
web/models/fetch.ex
Normal file
23
web/models/fetch.ex
Normal file
|
@ -0,0 +1,23 @@
|
|||
defmodule Katso.Fetch do
|
||||
use Katso.Web, :model
|
||||
|
||||
schema "fetches" do
|
||||
field :total_score, :integer
|
||||
field :total_titles, :integer
|
||||
field :relative_score, :integer
|
||||
|
||||
timestamps
|
||||
|
||||
belongs_to :magazine, Katso.Magazine
|
||||
has_many :titles, Katso.Title
|
||||
has_many :fetch_scores, Katso.FetchScore
|
||||
end
|
||||
|
||||
@required_fields ~w(total_score total_titles relative_score magazine_id)
|
||||
@optional_fields ~w()
|
||||
|
||||
def changeset(model, params \\ nil) do
|
||||
model
|
||||
|> cast(params, @required_fields, @optional_fields)
|
||||
end
|
||||
end
|
18
web/models/fetch_score.ex
Normal file
18
web/models/fetch_score.ex
Normal file
|
@ -0,0 +1,18 @@
|
|||
defmodule Katso.FetchScore do
|
||||
use Katso.Web, :model
|
||||
|
||||
schema "fetch_scores" do
|
||||
field :score_type, :string
|
||||
field :score_amount, :integer
|
||||
|
||||
belongs_to :fetch, Katso.Fetch
|
||||
end
|
||||
|
||||
@required_fields ~w(score_type score_amount fetch_id)
|
||||
@optional_fields ~w()
|
||||
|
||||
def changeset(model, params \\ nil) do
|
||||
model
|
||||
|> cast(params, @required_fields, @optional_fields)
|
||||
end
|
||||
end
|
18
web/models/magazine.ex
Normal file
18
web/models/magazine.ex
Normal file
|
@ -0,0 +1,18 @@
|
|||
defmodule Katso.Magazine do
|
||||
use Katso.Web, :model
|
||||
|
||||
schema "magazines" do
|
||||
field :name, :string
|
||||
field :key, :string
|
||||
|
||||
has_many :fetches, Katso.Fetch
|
||||
end
|
||||
|
||||
@required_fields ~w(name key)
|
||||
@optional_fields ~w()
|
||||
|
||||
def changeset(model, params \\ nil) do
|
||||
model
|
||||
|> cast(params, @required_fields, @optional_fields)
|
||||
end
|
||||
end
|
18
web/models/title.ex
Normal file
18
web/models/title.ex
Normal file
|
@ -0,0 +1,18 @@
|
|||
defmodule Katso.Title do
|
||||
use Katso.Web, :model
|
||||
|
||||
schema "titles" do
|
||||
field :title, :string
|
||||
|
||||
belongs_to :fetch, Katso.Fetch
|
||||
has_many :title_scores, Katso.TitleScore
|
||||
end
|
||||
|
||||
@required_fields ~w(title fetch_id)
|
||||
@optional_fields ~w()
|
||||
|
||||
def changeset(model, params \\ nil) do
|
||||
model
|
||||
|> cast(params, @required_fields, @optional_fields)
|
||||
end
|
||||
end
|
19
web/models/title_score.ex
Normal file
19
web/models/title_score.ex
Normal file
|
@ -0,0 +1,19 @@
|
|||
defmodule Katso.TitleScore do
|
||||
use Katso.Web, :model
|
||||
|
||||
schema "title_scores" do
|
||||
field :score_type, :string
|
||||
field :score_amount, :integer
|
||||
field :score_words, {:array, :string}
|
||||
|
||||
belongs_to :title, Katso.Title
|
||||
end
|
||||
|
||||
@required_fields ~w(score_type score_amount score_words title_id)
|
||||
@optional_fields ~w()
|
||||
|
||||
def changeset(model, params \\ nil) do
|
||||
model
|
||||
|> cast(params, @required_fields, @optional_fields)
|
||||
end
|
||||
end
|
16
web/router.ex
Normal file
16
web/router.ex
Normal file
|
@ -0,0 +1,16 @@
|
|||
defmodule Katso.Router do
|
||||
use Phoenix.Router
|
||||
|
||||
pipeline :browser do
|
||||
plug :accepts, ["html"]
|
||||
plug :fetch_session
|
||||
plug :fetch_flash
|
||||
plug :protect_from_forgery
|
||||
end
|
||||
|
||||
scope "/", Katso do
|
||||
pipe_through :browser # Use the default browser stack
|
||||
|
||||
get "/", PageController, :index
|
||||
end
|
||||
end
|
81
web/static/css/app.scss
Normal file
81
web/static/css/app.scss
Normal file
File diff suppressed because one or more lines are too long
10
web/static/js/app.js
Normal file
10
web/static/js/app.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import {Socket} from "phoenix"
|
||||
|
||||
// let socket = new Socket("/ws")
|
||||
// socket.join("topic:subtopic", {}, chan => {
|
||||
// })
|
||||
|
||||
let App = {
|
||||
}
|
||||
|
||||
export default App
|
763
web/static/vendor/phoenix.js
vendored
Normal file
763
web/static/vendor/phoenix.js
vendored
Normal file
|
@ -0,0 +1,763 @@
|
|||
(function(/*! Brunch !*/) {
|
||||
'use strict';
|
||||
|
||||
var globals = typeof window !== 'undefined' ? window : global;
|
||||
if (typeof globals.require === 'function') return;
|
||||
|
||||
var modules = {};
|
||||
var cache = {};
|
||||
|
||||
var has = function(object, name) {
|
||||
return ({}).hasOwnProperty.call(object, name);
|
||||
};
|
||||
|
||||
var expand = function(root, name) {
|
||||
var results = [], parts, part;
|
||||
if (/^\.\.?(\/|$)/.test(name)) {
|
||||
parts = [root, name].join('/').split('/');
|
||||
} else {
|
||||
parts = name.split('/');
|
||||
}
|
||||
for (var i = 0, length = parts.length; i < length; i++) {
|
||||
part = parts[i];
|
||||
if (part === '..') {
|
||||
results.pop();
|
||||
} else if (part !== '.' && part !== '') {
|
||||
results.push(part);
|
||||
}
|
||||
}
|
||||
return results.join('/');
|
||||
};
|
||||
|
||||
var dirname = function(path) {
|
||||
return path.split('/').slice(0, -1).join('/');
|
||||
};
|
||||
|
||||
var localRequire = function(path) {
|
||||
return function(name) {
|
||||
var dir = dirname(path);
|
||||
var absolute = expand(dir, name);
|
||||
return globals.require(absolute, path);
|
||||
};
|
||||
};
|
||||
|
||||
var initModule = function(name, definition) {
|
||||
var module = {id: name, exports: {}};
|
||||
cache[name] = module;
|
||||
definition(module.exports, localRequire(name), module);
|
||||
return module.exports;
|
||||
};
|
||||
|
||||
var require = function(name, loaderPath) {
|
||||
var path = expand(name, '.');
|
||||
if (loaderPath == null) loaderPath = '/';
|
||||
|
||||
if (has(cache, path)) return cache[path].exports;
|
||||
if (has(modules, path)) return initModule(path, modules[path]);
|
||||
|
||||
var dirIndex = expand(path, './index');
|
||||
if (has(cache, dirIndex)) return cache[dirIndex].exports;
|
||||
if (has(modules, dirIndex)) return initModule(dirIndex, modules[dirIndex]);
|
||||
|
||||
throw new Error('Cannot find module "' + name + '" from '+ '"' + loaderPath + '"');
|
||||
};
|
||||
|
||||
var define = function(bundle, fn) {
|
||||
if (typeof bundle === 'object') {
|
||||
for (var key in bundle) {
|
||||
if (has(bundle, key)) {
|
||||
modules[key] = bundle[key];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
modules[bundle] = fn;
|
||||
}
|
||||
};
|
||||
|
||||
var list = function() {
|
||||
var result = [];
|
||||
for (var item in modules) {
|
||||
if (has(modules, item)) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
globals.require = require;
|
||||
globals.require.define = define;
|
||||
globals.require.register = define;
|
||||
globals.require.list = list;
|
||||
globals.require.brunch = true;
|
||||
})();
|
||||
require.define({'phoenix': function(exports, require, module){ "use strict";
|
||||
|
||||
var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } };
|
||||
|
||||
var SOCKET_STATES = { connecting: 0, open: 1, closing: 2, closed: 3 };
|
||||
var CHANNEL_EVENTS = {
|
||||
close: "phx_close",
|
||||
error: "phx_error",
|
||||
join: "phx_join",
|
||||
reply: "phx_reply",
|
||||
leave: "phx_leave"
|
||||
};
|
||||
|
||||
var Push = (function () {
|
||||
|
||||
// Initializes the Push
|
||||
//
|
||||
// chan - The Channel
|
||||
// event - The event, ie `"phx_join"`
|
||||
// payload - The payload, ie `{user_id: 123}`
|
||||
// mergePush - The optional `Push` to merge hooks from
|
||||
|
||||
function Push(chan, event, payload, mergePush) {
|
||||
var _this = this;
|
||||
|
||||
_classCallCheck(this, Push);
|
||||
|
||||
this.chan = chan;
|
||||
this.event = event;
|
||||
this.payload = payload || {};
|
||||
this.receivedResp = null;
|
||||
this.afterHooks = [];
|
||||
this.recHooks = {};
|
||||
this.sent = false;
|
||||
if (mergePush) {
|
||||
mergePush.afterHooks.forEach(function (hook) {
|
||||
return _this.after(hook.ms, hook.callback);
|
||||
});
|
||||
for (var status in mergePush.recHooks) {
|
||||
if (mergePush.recHooks.hasOwnProperty(status)) {
|
||||
this.receive(status, mergePush.recHooks[status]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Push.prototype.send = function send() {
|
||||
var _this = this;
|
||||
|
||||
var ref = this.chan.socket.makeRef();
|
||||
var refEvent = this.chan.replyEventName(ref);
|
||||
|
||||
this.chan.on(refEvent, function (payload) {
|
||||
_this.receivedResp = payload;
|
||||
_this.matchReceive(payload);
|
||||
_this.chan.off(refEvent);
|
||||
_this.cancelAfters();
|
||||
});
|
||||
|
||||
this.startAfters();
|
||||
this.sent = true;
|
||||
this.chan.socket.push({
|
||||
topic: this.chan.topic,
|
||||
event: this.event,
|
||||
payload: this.payload,
|
||||
ref: ref
|
||||
});
|
||||
};
|
||||
|
||||
Push.prototype.receive = function receive(status, callback) {
|
||||
if (this.receivedResp && this.receivedResp.status === status) {
|
||||
callback(this.receivedResp.response);
|
||||
}
|
||||
this.recHooks[status] = callback;
|
||||
return this;
|
||||
};
|
||||
|
||||
Push.prototype.after = function after(ms, callback) {
|
||||
var timer = null;
|
||||
if (this.sent) {
|
||||
timer = setTimeout(callback, ms);
|
||||
}
|
||||
this.afterHooks.push({ ms: ms, callback: callback, timer: timer });
|
||||
return this;
|
||||
};
|
||||
|
||||
// private
|
||||
|
||||
Push.prototype.matchReceive = function matchReceive(_ref) {
|
||||
var status = _ref.status;
|
||||
var response = _ref.response;
|
||||
var ref = _ref.ref;
|
||||
|
||||
var callback = this.recHooks[status];
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.event === CHANNEL_EVENTS.join) {
|
||||
callback(this.chan);
|
||||
} else {
|
||||
callback(response);
|
||||
}
|
||||
};
|
||||
|
||||
Push.prototype.cancelAfters = function cancelAfters() {
|
||||
this.afterHooks.forEach(function (hook) {
|
||||
clearTimeout(hook.timer);
|
||||
hook.timer = null;
|
||||
});
|
||||
};
|
||||
|
||||
Push.prototype.startAfters = function startAfters() {
|
||||
this.afterHooks.map(function (hook) {
|
||||
if (!hook.timer) {
|
||||
hook.timer = setTimeout(function () {
|
||||
return hook.callback();
|
||||
}, hook.ms);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return Push;
|
||||
})();
|
||||
|
||||
var Channel = exports.Channel = (function () {
|
||||
function Channel(topic, message, callback, socket) {
|
||||
_classCallCheck(this, Channel);
|
||||
|
||||
this.topic = topic;
|
||||
this.message = message;
|
||||
this.callback = callback;
|
||||
this.socket = socket;
|
||||
this.bindings = [];
|
||||
this.afterHooks = [];
|
||||
this.recHooks = {};
|
||||
this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.message);
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
||||
Channel.prototype.after = function after(ms, callback) {
|
||||
this.joinPush.after(ms, callback);
|
||||
return this;
|
||||
};
|
||||
|
||||
Channel.prototype.receive = function receive(status, callback) {
|
||||
this.joinPush.receive(status, callback);
|
||||
return this;
|
||||
};
|
||||
|
||||
Channel.prototype.rejoin = function rejoin() {
|
||||
this.reset();
|
||||
this.joinPush.send();
|
||||
};
|
||||
|
||||
Channel.prototype.onClose = function onClose(callback) {
|
||||
this.on(CHANNEL_EVENTS.close, callback);
|
||||
};
|
||||
|
||||
Channel.prototype.onError = function onError(callback) {
|
||||
var _this = this;
|
||||
|
||||
this.on(CHANNEL_EVENTS.error, function (reason) {
|
||||
callback(reason);
|
||||
_this.trigger(CHANNEL_EVENTS.close, "error");
|
||||
});
|
||||
};
|
||||
|
||||
Channel.prototype.reset = function reset() {
|
||||
var _this = this;
|
||||
|
||||
this.bindings = [];
|
||||
var newJoinPush = new Push(this, CHANNEL_EVENTS.join, this.message, this.joinPush);
|
||||
this.joinPush = newJoinPush;
|
||||
this.onError(function (reason) {
|
||||
setTimeout(function () {
|
||||
return _this.rejoin();
|
||||
}, _this.socket.reconnectAfterMs);
|
||||
});
|
||||
this.on(CHANNEL_EVENTS.reply, function (payload) {
|
||||
_this.trigger(_this.replyEventName(payload.ref), payload);
|
||||
});
|
||||
};
|
||||
|
||||
Channel.prototype.on = function on(event, callback) {
|
||||
this.bindings.push({ event: event, callback: callback });
|
||||
};
|
||||
|
||||
Channel.prototype.isMember = function isMember(topic) {
|
||||
return this.topic === topic;
|
||||
};
|
||||
|
||||
Channel.prototype.off = function off(event) {
|
||||
this.bindings = this.bindings.filter(function (bind) {
|
||||
return bind.event !== event;
|
||||
});
|
||||
};
|
||||
|
||||
Channel.prototype.trigger = function trigger(triggerEvent, msg) {
|
||||
this.bindings.filter(function (bind) {
|
||||
return bind.event === triggerEvent;
|
||||
}).map(function (bind) {
|
||||
return bind.callback(msg);
|
||||
});
|
||||
};
|
||||
|
||||
Channel.prototype.push = function push(event, payload) {
|
||||
var pushEvent = new Push(this, event, payload);
|
||||
pushEvent.send();
|
||||
|
||||
return pushEvent;
|
||||
};
|
||||
|
||||
Channel.prototype.replyEventName = function replyEventName(ref) {
|
||||
return "chan_reply_" + ref;
|
||||
};
|
||||
|
||||
Channel.prototype.leave = function leave() {
|
||||
var _this = this;
|
||||
|
||||
return this.push(CHANNEL_EVENTS.leave).receive("ok", function () {
|
||||
_this.socket.leave(_this);
|
||||
chan.reset();
|
||||
});
|
||||
};
|
||||
|
||||
return Channel;
|
||||
})();
|
||||
|
||||
var Socket = exports.Socket = (function () {
|
||||
|
||||
// Initializes the Socket
|
||||
//
|
||||
// endPoint - The string WebSocket endpoint, ie, "ws://example.com/ws",
|
||||
// "wss://example.com"
|
||||
// "/ws" (inherited host & protocol)
|
||||
// opts - Optional configuration
|
||||
// transport - The Websocket Transport, ie WebSocket, Phoenix.LongPoller.
|
||||
// Defaults to WebSocket with automatic LongPoller fallback.
|
||||
// heartbeatIntervalMs - The millisec interval to send a heartbeat message
|
||||
// reconnectAfterMs - The millisec interval to reconnect after connection loss
|
||||
// logger - The optional function for specialized logging, ie:
|
||||
// `logger: function(msg){ console.log(msg) }`
|
||||
// longpoller_timeout - The maximum timeout of a long poll AJAX request.
|
||||
// Defaults to 20s (double the server long poll timer).
|
||||
//
|
||||
// For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim)
|
||||
//
|
||||
|
||||
function Socket(endPoint) {
|
||||
var opts = arguments[1] === undefined ? {} : arguments[1];
|
||||
|
||||
_classCallCheck(this, Socket);
|
||||
|
||||
this.states = SOCKET_STATES;
|
||||
this.stateChangeCallbacks = { open: [], close: [], error: [], message: [] };
|
||||
this.flushEveryMs = 50;
|
||||
this.reconnectTimer = null;
|
||||
this.channels = [];
|
||||
this.sendBuffer = [];
|
||||
this.ref = 0;
|
||||
this.transport = opts.transport || window.WebSocket || LongPoller;
|
||||
this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 30000;
|
||||
this.reconnectAfterMs = opts.reconnectAfterMs || 5000;
|
||||
this.logger = opts.logger || function () {}; // noop
|
||||
this.longpoller_timeout = opts.longpoller_timeout || 20000;
|
||||
this.endPoint = this.expandEndpoint(endPoint);
|
||||
|
||||
this.resetBufferTimer();
|
||||
}
|
||||
|
||||
Socket.prototype.protocol = function protocol() {
|
||||
return location.protocol.match(/^https/) ? "wss" : "ws";
|
||||
};
|
||||
|
||||
Socket.prototype.expandEndpoint = function expandEndpoint(endPoint) {
|
||||
if (endPoint.charAt(0) !== "/") {
|
||||
return endPoint;
|
||||
}
|
||||
if (endPoint.charAt(1) === "/") {
|
||||
return "" + this.protocol() + ":" + endPoint;
|
||||
}
|
||||
|
||||
return "" + this.protocol() + "://" + location.host + "" + endPoint;
|
||||
};
|
||||
|
||||
Socket.prototype.disconnect = function disconnect(callback, code, reason) {
|
||||
if (this.conn) {
|
||||
this.conn.onclose = function () {}; // noop
|
||||
if (code) {
|
||||
this.conn.close(code, reason || "");
|
||||
} else {
|
||||
this.conn.close();
|
||||
}
|
||||
this.conn = null;
|
||||
}
|
||||
callback && callback();
|
||||
};
|
||||
|
||||
Socket.prototype.connect = function connect() {
|
||||
var _this = this;
|
||||
|
||||
this.disconnect(function () {
|
||||
_this.conn = new _this.transport(_this.endPoint);
|
||||
_this.conn.timeout = _this.longpoller_timeout;
|
||||
_this.conn.onopen = function () {
|
||||
return _this.onConnOpen();
|
||||
};
|
||||
_this.conn.onerror = function (error) {
|
||||
return _this.onConnError(error);
|
||||
};
|
||||
_this.conn.onmessage = function (event) {
|
||||
return _this.onConnMessage(event);
|
||||
};
|
||||
_this.conn.onclose = function (event) {
|
||||
return _this.onConnClose(event);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
Socket.prototype.resetBufferTimer = function resetBufferTimer() {
|
||||
var _this = this;
|
||||
|
||||
clearTimeout(this.sendBufferTimer);
|
||||
this.sendBufferTimer = setTimeout(function () {
|
||||
return _this.flushSendBuffer();
|
||||
}, this.flushEveryMs);
|
||||
};
|
||||
|
||||
// Logs the message. Override `this.logger` for specialized logging. noops by default
|
||||
|
||||
Socket.prototype.log = function log(msg) {
|
||||
this.logger(msg);
|
||||
};
|
||||
|
||||
// Registers callbacks for connection state change events
|
||||
//
|
||||
// Examples
|
||||
//
|
||||
// socket.onError function(error){ alert("An error occurred") }
|
||||
//
|
||||
|
||||
Socket.prototype.onOpen = function onOpen(callback) {
|
||||
this.stateChangeCallbacks.open.push(callback);
|
||||
};
|
||||
|
||||
Socket.prototype.onClose = function onClose(callback) {
|
||||
this.stateChangeCallbacks.close.push(callback);
|
||||
};
|
||||
|
||||
Socket.prototype.onError = function onError(callback) {
|
||||
this.stateChangeCallbacks.error.push(callback);
|
||||
};
|
||||
|
||||
Socket.prototype.onMessage = function onMessage(callback) {
|
||||
this.stateChangeCallbacks.message.push(callback);
|
||||
};
|
||||
|
||||
Socket.prototype.onConnOpen = function onConnOpen() {
|
||||
var _this = this;
|
||||
|
||||
clearInterval(this.reconnectTimer);
|
||||
if (!this.conn.skipHeartbeat) {
|
||||
clearInterval(this.heartbeatTimer);
|
||||
this.heartbeatTimer = setInterval(function () {
|
||||
return _this.sendHeartbeat();
|
||||
}, this.heartbeatIntervalMs);
|
||||
}
|
||||
this.rejoinAll();
|
||||
this.stateChangeCallbacks.open.forEach(function (callback) {
|
||||
return callback();
|
||||
});
|
||||
};
|
||||
|
||||
Socket.prototype.onConnClose = function onConnClose(event) {
|
||||
var _this = this;
|
||||
|
||||
this.log("WS close:");
|
||||
this.log(event);
|
||||
clearInterval(this.reconnectTimer);
|
||||
clearInterval(this.heartbeatTimer);
|
||||
this.reconnectTimer = setInterval(function () {
|
||||
return _this.connect();
|
||||
}, this.reconnectAfterMs);
|
||||
this.stateChangeCallbacks.close.forEach(function (callback) {
|
||||
return callback(event);
|
||||
});
|
||||
};
|
||||
|
||||
Socket.prototype.onConnError = function onConnError(error) {
|
||||
this.log("WS error:");
|
||||
this.log(error);
|
||||
this.stateChangeCallbacks.error.forEach(function (callback) {
|
||||
return callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
Socket.prototype.connectionState = function connectionState() {
|
||||
switch (this.conn && this.conn.readyState) {
|
||||
case this.states.connecting:
|
||||
return "connecting";
|
||||
case this.states.open:
|
||||
return "open";
|
||||
case this.states.closing:
|
||||
return "closing";
|
||||
default:
|
||||
return "closed";
|
||||
}
|
||||
};
|
||||
|
||||
Socket.prototype.isConnected = function isConnected() {
|
||||
return this.connectionState() === "open";
|
||||
};
|
||||
|
||||
Socket.prototype.rejoinAll = function rejoinAll() {
|
||||
this.channels.forEach(function (chan) {
|
||||
return chan.rejoin();
|
||||
});
|
||||
};
|
||||
|
||||
Socket.prototype.join = function join(topic, message, callback) {
|
||||
var chan = new Channel(topic, message, callback, this);
|
||||
this.channels.push(chan);
|
||||
if (this.isConnected()) {
|
||||
chan.rejoin();
|
||||
}
|
||||
return chan;
|
||||
};
|
||||
|
||||
Socket.prototype.leave = function leave(chan) {
|
||||
this.channels = this.channels.filter(function (c) {
|
||||
return !c.isMember(chan.topic);
|
||||
});
|
||||
};
|
||||
|
||||
Socket.prototype.push = function push(data) {
|
||||
var _this = this;
|
||||
|
||||
var callback = function () {
|
||||
return _this.conn.send(JSON.stringify(data));
|
||||
};
|
||||
if (this.isConnected()) {
|
||||
callback();
|
||||
} else {
|
||||
this.sendBuffer.push(callback);
|
||||
}
|
||||
};
|
||||
|
||||
// Return the next message ref, accounting for overflows
|
||||
|
||||
Socket.prototype.makeRef = function makeRef() {
|
||||
var newRef = this.ref + 1;
|
||||
if (newRef === this.ref) {
|
||||
this.ref = 0;
|
||||
} else {
|
||||
this.ref = newRef;
|
||||
}
|
||||
|
||||
return this.ref.toString();
|
||||
};
|
||||
|
||||
Socket.prototype.sendHeartbeat = function sendHeartbeat() {
|
||||
this.push({ topic: "phoenix", event: "heartbeat", payload: {}, ref: this.makeRef() });
|
||||
};
|
||||
|
||||
Socket.prototype.flushSendBuffer = function flushSendBuffer() {
|
||||
if (this.isConnected() && this.sendBuffer.length > 0) {
|
||||
this.sendBuffer.forEach(function (callback) {
|
||||
return callback();
|
||||
});
|
||||
this.sendBuffer = [];
|
||||
}
|
||||
this.resetBufferTimer();
|
||||
};
|
||||
|
||||
Socket.prototype.onConnMessage = function onConnMessage(rawMessage) {
|
||||
this.log("message received:");
|
||||
this.log(rawMessage);
|
||||
|
||||
var _JSON$parse = JSON.parse(rawMessage.data);
|
||||
|
||||
var topic = _JSON$parse.topic;
|
||||
var event = _JSON$parse.event;
|
||||
var payload = _JSON$parse.payload;
|
||||
|
||||
this.channels.filter(function (chan) {
|
||||
return chan.isMember(topic);
|
||||
}).forEach(function (chan) {
|
||||
return chan.trigger(event, payload);
|
||||
});
|
||||
this.stateChangeCallbacks.message.forEach(function (callback) {
|
||||
callback(topic, event, payload);
|
||||
});
|
||||
};
|
||||
|
||||
return Socket;
|
||||
})();
|
||||
|
||||
var LongPoller = exports.LongPoller = (function () {
|
||||
function LongPoller(endPoint) {
|
||||
_classCallCheck(this, LongPoller);
|
||||
|
||||
this.retryInMs = 5000;
|
||||
this.endPoint = null;
|
||||
this.token = null;
|
||||
this.sig = null;
|
||||
this.skipHeartbeat = true;
|
||||
this.onopen = function () {}; // noop
|
||||
this.onerror = function () {}; // noop
|
||||
this.onmessage = function () {}; // noop
|
||||
this.onclose = function () {}; // noop
|
||||
this.states = SOCKET_STATES;
|
||||
this.upgradeEndpoint = this.normalizeEndpoint(endPoint);
|
||||
this.pollEndpoint = this.upgradeEndpoint + (/\/$/.test(endPoint) ? "poll" : "/poll");
|
||||
this.readyState = this.states.connecting;
|
||||
|
||||
this.poll();
|
||||
}
|
||||
|
||||
LongPoller.prototype.normalizeEndpoint = function normalizeEndpoint(endPoint) {
|
||||
return endPoint.replace("ws://", "http://").replace("wss://", "https://");
|
||||
};
|
||||
|
||||
LongPoller.prototype.endpointURL = function endpointURL() {
|
||||
return this.pollEndpoint + ("?token=" + encodeURIComponent(this.token) + "&sig=" + encodeURIComponent(this.sig));
|
||||
};
|
||||
|
||||
LongPoller.prototype.closeAndRetry = function closeAndRetry() {
|
||||
this.close();
|
||||
this.readyState = this.states.connecting;
|
||||
};
|
||||
|
||||
LongPoller.prototype.ontimeout = function ontimeout() {
|
||||
this.onerror("timeout");
|
||||
this.closeAndRetry();
|
||||
};
|
||||
|
||||
LongPoller.prototype.poll = function poll() {
|
||||
var _this = this;
|
||||
|
||||
if (!(this.readyState === this.states.open || this.readyState === this.states.connecting)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Ajax.request("GET", this.endpointURL(), "application/json", null, this.timeout, this.ontimeout.bind(this), function (resp) {
|
||||
if (resp) {
|
||||
var status = resp.status;
|
||||
var token = resp.token;
|
||||
var sig = resp.sig;
|
||||
var messages = resp.messages;
|
||||
|
||||
_this.token = token;
|
||||
_this.sig = sig;
|
||||
} else {
|
||||
var status = 0;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case 200:
|
||||
messages.forEach(function (msg) {
|
||||
return _this.onmessage({ data: JSON.stringify(msg) });
|
||||
});
|
||||
_this.poll();
|
||||
break;
|
||||
case 204:
|
||||
_this.poll();
|
||||
break;
|
||||
case 410:
|
||||
_this.readyState = _this.states.open;
|
||||
_this.onopen();
|
||||
_this.poll();
|
||||
break;
|
||||
case 0:
|
||||
case 500:
|
||||
_this.onerror();
|
||||
_this.closeAndRetry();
|
||||
break;
|
||||
default:
|
||||
throw "unhandled poll status " + status;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
LongPoller.prototype.send = function send(body) {
|
||||
var _this = this;
|
||||
|
||||
Ajax.request("POST", this.endpointURL(), "application/json", body, this.timeout, this.onerror.bind(this, "timeout"), function (resp) {
|
||||
if (!resp || resp.status !== 200) {
|
||||
_this.onerror(status);
|
||||
_this.closeAndRetry();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
LongPoller.prototype.close = function close(code, reason) {
|
||||
this.readyState = this.states.closed;
|
||||
this.onclose();
|
||||
};
|
||||
|
||||
return LongPoller;
|
||||
})();
|
||||
|
||||
var Ajax = exports.Ajax = (function () {
|
||||
function Ajax() {
|
||||
_classCallCheck(this, Ajax);
|
||||
}
|
||||
|
||||
Ajax.request = function request(method, endPoint, accept, body, timeout, ontimeout, callback) {
|
||||
if (window.XDomainRequest) {
|
||||
var req = new XDomainRequest(); // IE8, IE9
|
||||
this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback);
|
||||
} else {
|
||||
var req = window.XMLHttpRequest ? new XMLHttpRequest() : // IE7+, Firefox, Chrome, Opera, Safari
|
||||
new ActiveXObject("Microsoft.XMLHTTP"); // IE6, IE5
|
||||
this.xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback);
|
||||
}
|
||||
};
|
||||
|
||||
Ajax.xdomainRequest = function xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback) {
|
||||
var _this = this;
|
||||
|
||||
req.timeout = timeout;
|
||||
req.open(method, endPoint);
|
||||
req.onload = function () {
|
||||
var response = _this.parseJSON(req.responseText);
|
||||
callback && callback(response);
|
||||
};
|
||||
if (ontimeout) {
|
||||
req.ontimeout = ontimeout;
|
||||
}
|
||||
|
||||
// Work around bug in IE9 that requires an attached onprogress handler
|
||||
req.onprogress = function () {};
|
||||
|
||||
req.send(body);
|
||||
};
|
||||
|
||||
Ajax.xhrRequest = function xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback) {
|
||||
var _this = this;
|
||||
|
||||
req.timeout = timeout;
|
||||
req.open(method, endPoint, true);
|
||||
req.setRequestHeader("Content-Type", accept);
|
||||
req.onerror = function () {
|
||||
callback && callback(null);
|
||||
};
|
||||
req.onreadystatechange = function () {
|
||||
if (req.readyState === _this.states.complete && callback) {
|
||||
var response = _this.parseJSON(req.responseText);
|
||||
callback(response);
|
||||
}
|
||||
};
|
||||
if (ontimeout) {
|
||||
req.ontimeout = ontimeout;
|
||||
}
|
||||
|
||||
req.send(body);
|
||||
};
|
||||
|
||||
Ajax.parseJSON = function parseJSON(resp) {
|
||||
return resp && resp !== "" ? JSON.parse(resp) : null;
|
||||
};
|
||||
|
||||
return Ajax;
|
||||
})();
|
||||
|
||||
Ajax.states = { complete: 4 };
|
||||
exports.__esModule = true;
|
||||
}});
|
||||
if(typeof(window) === 'object' && !window.Phoenix){ window.Phoenix = require('phoenix') };
|
35
web/templates/layout/application.html.eex
Normal file
35
web/templates/layout/application.html.eex
Normal file
|
@ -0,0 +1,35 @@
|
|||
<!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">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
|
||||
<title>Oho! Katso kuvat ja tilastot! Arvaatko, mikä lehti on surkein?</title>
|
||||
<link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>
|
||||
Oho! Katso kuvat!
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<p class="alert alert-info"><%= get_flash(@conn, :info) %></p>
|
||||
<p class="alert alert-danger"><%= get_flash(@conn, :error) %></p>
|
||||
|
||||
<%= @inner %>
|
||||
|
||||
<div class="footer">
|
||||
<p>© Mikko Ahlroth 2015</p>
|
||||
</div>
|
||||
|
||||
</div> <!-- /container -->
|
||||
<script src="<%= static_path(@conn, "/js/app.js") %>"></script>
|
||||
<script>require("web/static/js/app")</script>
|
||||
</body>
|
||||
</html>
|
10
web/templates/page/index.html.eex
Normal file
10
web/templates/page/index.html.eex
Normal file
|
@ -0,0 +1,10 @@
|
|||
<div class="jumbotron">
|
||||
<h2>Onko tämä Internetin paras sivusto?</h2>
|
||||
<p class="lead">Nyt se on tutkittu! Tämä outo sivusto selvittää, mikä nykyjournalismissa on vikana. Vai onko?</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<h1>Tämän hetken lööpeimmät</h1>
|
||||
</div>
|
||||
</div>
|
17
web/views/error_view.ex
Normal file
17
web/views/error_view.ex
Normal file
|
@ -0,0 +1,17 @@
|
|||
defmodule Katso.ErrorView do
|
||||
use Katso.Web, :view
|
||||
|
||||
def render("404.html", _assigns) do
|
||||
"Page not found - 404"
|
||||
end
|
||||
|
||||
def render("500.html", _assigns) do
|
||||
"Server internal error - 500"
|
||||
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
|
3
web/views/layout_view.ex
Normal file
3
web/views/layout_view.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule Katso.LayoutView do
|
||||
use Katso.Web, :view
|
||||
end
|
3
web/views/page_view.ex
Normal file
3
web/views/page_view.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule Katso.PageView do
|
||||
use Katso.Web, :view
|
||||
end
|
72
web/web.ex
Normal file
72
web/web.ex
Normal file
|
@ -0,0 +1,72 @@
|
|||
defmodule Katso.Web do
|
||||
@moduledoc """
|
||||
A module that keeps using definitions for controllers,
|
||||
views and so on.
|
||||
|
||||
This can be used in your application as:
|
||||
|
||||
use Katso.Web, :controller
|
||||
use Katso.Web, :view
|
||||
|
||||
The definitions below will be executed for every view,
|
||||
controller, etc, so keep them short and clean, focused
|
||||
on imports, uses and aliases.
|
||||
|
||||
Do NOT define functions inside the quoted expressions
|
||||
below.
|
||||
"""
|
||||
|
||||
def model do
|
||||
quote do
|
||||
use Ecto.Model
|
||||
end
|
||||
end
|
||||
|
||||
def controller do
|
||||
quote do
|
||||
use Phoenix.Controller
|
||||
|
||||
# Alias the data repository and import query/model functions
|
||||
alias Katso.Repo
|
||||
import Ecto.Model
|
||||
import Ecto.Query, only: [from: 2]
|
||||
|
||||
# Import URL helpers from the router
|
||||
import Katso.Router.Helpers
|
||||
end
|
||||
end
|
||||
|
||||
def view do
|
||||
quote do
|
||||
use Phoenix.View, root: "web/templates"
|
||||
|
||||
# Import convenience functions from controllers
|
||||
import Phoenix.Controller, only: [get_flash: 2]
|
||||
|
||||
# Import URL helpers from the router
|
||||
import Katso.Router.Helpers
|
||||
|
||||
# Use all HTML functionality (forms, tags, etc)
|
||||
use Phoenix.HTML
|
||||
end
|
||||
end
|
||||
|
||||
def channel do
|
||||
quote do
|
||||
use Phoenix.Channel
|
||||
|
||||
# Alias the data repository and import query/model functions
|
||||
alias Katso.Repo
|
||||
import Ecto.Model
|
||||
import Ecto.Query, only: [from: 2]
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
When used, dispatch to the appropriate controller/view/etc.
|
||||
"""
|
||||
defmacro __using__(which) when is_atom(which) do
|
||||
apply(__MODULE__, which, [])
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue