Compare commits

...
This repository has been archived on 2024-06-24. You can view files and clone it, but cannot push or open issues or pull requests.

16 commits

Author SHA1 Message Date
James Harton
41db949fcd Use an access token because it avoids the rate limit. 2018-04-10 18:16:17 +12:00
James Harton
873cdfe6a4 update go.sh 2018-04-10 12:43:15 +12:00
James Harton
7497392301 Add go.sh 2018-04-10 12:41:39 +12:00
James Harton
b974f1449f Update readme. 2018-04-09 13:03:58 +12:00
James Harton
2705e0b6b8 Fix wrapping. 2018-04-08 17:14:01 +12:00
James Harton
f3bee7434d Update README. 2018-04-08 12:43:50 +12:00
James Harton
69f2bdbb75 Get Absinthe working with list and creating people. 2018-04-08 12:42:50 +12:00
James Harton
707452b649 Fix timestamps to always create as :utc_datetime. 2018-04-08 12:25:31 +12:00
James Harton
d7c98d6a47 Ready for step 3. 2018-04-08 11:24:08 +12:00
James Harton
9075cfedf5 Allow users to add a github username via the UI. 2018-04-08 11:17:58 +12:00
James Harton
4a3670f676 Working face gallery app with server rendered page. 2018-04-08 10:47:22 +12:00
James Harton
7ec94ec686 Add face importing from github. 2018-04-08 10:06:11 +12:00
James Harton
d38a867de1 Add very simple context for face gallery. 2018-04-08 09:29:25 +12:00
James Harton
be7f3a2981 Empty Phoenix application. 2018-04-08 09:20:53 +12:00
James Harton
9edfb03977 Initial readme. 2018-04-08 09:17:28 +12:00
James Harton
b4a66b9c79 Update readme for step 1. 2018-04-08 09:08:06 +12:00
55 changed files with 5123 additions and 11 deletions

28
.gitignore vendored Normal file
View file

@ -0,0 +1,28 @@
# App artifacts
/_build
/db
/deps
/*.ez
.envrc
# Generated on crash by the VM
erl_crash.dump
# Generated on crash by NPM
npm-debug.log
# Static artifacts
/assets/node_modules
# Since we are building assets from assets/,
# we ignore priv/static. You may want to comment
# this depending on your deployment strategy.
/priv/static/
# Files matching config/*.secret.exs pattern contain sensitive
# data and you should not commit them into version control.
#
# Alternatively, you may comment the line below and commit the
# secrets files as long as you replace their contents by environment
# variables.
/config/*.secret.exs

View file

@ -1,15 +1,19 @@
# Wellington Elixir Meetup GraphQL Lightning Talk
## Because I counldn't think of a snappier title
# Getting started with Absinthe
This is a very quick overview of what GraphQL is and what it's useful for. A lot of this info can be gleaned directly from the [Absinthe](https://absinthe-graphql.org/) documentation.
[Absinthe](https://absinthe-graphql.org/) is an open source implementation of the GraphQL server specification for Elixir. Note that I said "Elixir" there and not "Phoenix". Absinthe doesn't need Phoenix, but we're going to use it in this example because my guess is that not many people are making purely GraphQL services and most of us are probably bolting it on to existing sites or services.
Things we're going to cover in this presentation:
Absinthe has [truly amazing docs](https://hexdocs.pm/absinthe/overview.html). Seriously. They're so great. You should check them out.
* What the heck is GraphQL anyway?
* Setting it up with Phoenix
* Handling Queries
* Handling Mutations
* Handling Subscriptions
* Extra for experts
In this branch we've added Absinthe to our project, and configured a type and a schema for our list of people, and a mutation which can import new people.
To get started checkout the `step-1` branch.
Look at:
* `mix.exs`
* `lib/faces_web/router.ex`
* `lib/faces_web/schema/schema.ex`
* `lib/faces_web/schema/person.ex`
* `lib/faces_web/resolvers/people.ex`
[Demo](http://localhost:4000/graphiql)
Next, move on to `step-4` to see how to use this from the client-side.

3
assets/.eslintrc.js Normal file
View file

@ -0,0 +1,3 @@
module.exports = {
"extends": "standard"
};

62
assets/brunch-config.js Normal file
View file

@ -0,0 +1,62 @@
exports.config = {
// See http://brunch.io/#documentation for docs.
files: {
javascripts: {
joinTo: "js/app.js"
// To use a separate vendor.js bundle, specify two files path
// http://brunch.io/docs/config#-files-
// joinTo: {
// "js/app.js": /^js/,
// "js/vendor.js": /^(?!js)/
// }
//
// To change the order of concatenation of files, explicitly mention here
// order: {
// before: [
// "vendor/js/jquery-2.1.1.js",
// "vendor/js/bootstrap.min.js"
// ]
// }
},
stylesheets: {
joinTo: "css/app.css"
},
templates: {
joinTo: "js/app.js"
}
},
conventions: {
// This option sets where we should place non-css and non-js assets in.
// By default, we set this to "/assets/static". Files in this directory
// will be copied to `paths.public`, which is "priv/static" by default.
assets: /^(static)/
},
// Phoenix paths configuration
paths: {
// Dependencies and current project directories to watch
watched: ["static", "css", "js", "vendor"],
// Where to compile files to
public: "../priv/static"
},
// Configure your plugins
plugins: {
babel: {
// Do not use ES6 compiler in vendor code
ignore: [/vendor/]
}
},
modules: {
autoRequire: {
"js/app.js": ["js/app"]
}
},
npm: {
enabled: true
}
};

1
assets/css/app.css Normal file
View file

@ -0,0 +1 @@
/* This file is for your main application css. */

21
assets/js/app.js Normal file
View file

@ -0,0 +1,21 @@
// Brunch automatically concatenates all files in your
// watched paths. Those paths can be configured at
// config.paths.watched in "brunch-config.js".
//
// However, those files will only be executed if
// explicitly imported. The only exception are files
// in vendor, which are never wrapped in imports and
// therefore are always executed.
// Import dependencies
//
// If you no longer want to use a dependency, remember
// to also remove its path from "config.paths.watched".
import "phoenix_html"
// Import local files
//
// Local files can be imported directly using relative
// paths "./socket" or full ones "web/static/js/socket".
// import socket from "./socket"

62
assets/js/socket.js Normal file
View file

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

25
assets/package.json Normal file
View file

@ -0,0 +1,25 @@
{
"repository": {},
"license": "MIT",
"scripts": {
"deploy": "brunch build --production",
"watch": "brunch watch --stdin"
},
"dependencies": {
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html",
"yarn": "^1.5.1"
},
"devDependencies": {
"babel-brunch": "6.1.1",
"brunch": "2.10.9",
"clean-css-brunch": "2.10.0",
"eslint": "^4.19.1",
"eslint-config-standard": "^11.0.0",
"eslint-plugin-import": "^2.10.0",
"eslint-plugin-node": "^6.0.1",
"eslint-plugin-promise": "^3.7.0",
"eslint-plugin-standard": "^3.0.1",
"uglify-js-brunch": "2.10.0"
}
}

BIN
assets/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

5
assets/static/robots.txt Normal file
View file

@ -0,0 +1,5 @@
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
#
# To ban all spiders from the entire site uncomment the next two lines:
# User-agent: *
# Disallow: /

3603
assets/yarn.lock Normal file

File diff suppressed because it is too large Load diff

29
config/config.exs Normal file
View file

@ -0,0 +1,29 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
use Mix.Config
# General application configuration
config :faces, ecto_repos: [Faces.Repo]
# Configures the endpoint
config :faces, FacesWeb.Endpoint,
url: [host: "localhost"],
secret_key_base: "ThLNldzPhX4I3g7eg79qoWrA1dj48zfsJBwbivAgNXq3XxhIo8tYh7jtteExdh0N",
render_errors: [view: FacesWeb.ErrorView, accepts: ~w(html json)],
pubsub: [name: Faces.PubSub, adapter: Phoenix.PubSub.PG2]
# Configures Elixir's Logger
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:user_id]
config :faces, Faces.Repo, migration_timestamps: [type: :utc_datetime]
config :faces, Faces.Gallery.GithubUserData, access_token: System.get_env("GITHUB_ACCESS_TOKEN")
# 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"

58
config/dev.exs Normal file
View file

@ -0,0 +1,58 @@
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 :faces, FacesWeb.Endpoint,
http: [port: 4000],
debug_errors: true,
code_reloader: true,
check_origin: false,
watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin",
cd: Path.expand("../assets", __DIR__)]]
# ## SSL Support
#
# In order to use HTTPS in development, a self-signed
# certificate can be generated by running the following
# command from your terminal:
#
# openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout priv/server.key -out priv/server.pem
#
# The `http:` config above can be replaced with:
#
# https: [port: 4000, keyfile: "priv/server.key", certfile: "priv/server.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 :faces, FacesWeb.Endpoint,
live_reload: [
patterns: [
~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
~r{priv/gettext/.*(po)$},
~r{lib/faces_web/views/.*(ex)$},
~r{lib/faces_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
# Configure your database
config :faces, Faces.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "faces_dev",
hostname: "localhost",
pool_size: 10

64
config/prod.exs Normal file
View file

@ -0,0 +1,64 @@
use Mix.Config
# For production, we often load configuration from external
# sources, such as your system environment. For this reason,
# you won't find the :http configuration below, but set inside
# FacesWeb.Endpoint.init/2 when load_from_system_env is
# true. Any dynamic configuration should be done there.
#
# Don't forget to configure the url host to something meaningful,
# Phoenix uses this information when generating URLs.
#
# Finally, 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 typically run after static files are built.
config :faces, FacesWeb.Endpoint,
load_from_system_env: true,
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 :faces, FacesWeb.Endpoint,
# ...
# url: [host: "example.com", port: 443],
# https: [:inet6,
# port: 443,
# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
# certfile: System.get_env("SOME_APP_SSL_CERT_PATH")]
#
# Where those two env variables return an absolute path to
# the key and cert in disk or a relative path inside priv,
# for example "priv/ssl/server.key".
#
# We also recommend setting `force_ssl`, ensuring no data is
# ever sent via http, always redirecting to https:
#
# config :faces, FacesWeb.Endpoint,
# force_ssl: [hsts: true]
#
# Check `Plug.SSL` for all available options in `force_ssl`.
# ## Using releases
#
# If you are doing OTP releases, you need to instruct Phoenix
# to start the server for all endpoints:
#
# config :phoenix, :serve_endpoints, true
#
# Alternatively, you can configure exactly which server to
# start per endpoint:
#
# config :faces, FacesWeb.Endpoint, server: true
#
# Finally import the config/prod.secret.exs
# which should be versioned separately.
import_config "prod.secret.exs"

19
config/test.exs Normal file
View 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 :faces, FacesWeb.Endpoint,
http: [port: 4001],
server: false
# Print only warnings and errors during test
config :logger, level: :warn
# Configure your database
config :faces, Faces.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "faces_test",
hostname: "localhost",
pool: Ecto.Adapters.SQL.Sandbox

11
go.sh Executable file
View file

@ -0,0 +1,11 @@
#!/bin/sh
set -e
(cd assets && yarn install)
mix deps.get
mix phx.server
read -p "Press enter to continue to next step"
git checkout step-4
exec ./go.sh

9
lib/faces.ex Normal file
View file

@ -0,0 +1,9 @@
defmodule Faces do
@moduledoc """
Faces 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

31
lib/faces/application.ex Normal file
View file

@ -0,0 +1,31 @@
defmodule Faces.Application do
use Application
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
import Supervisor.Spec
# Define workers and child supervisors to be supervised
children = [
# Start the Ecto repository
supervisor(Faces.Repo, []),
# Start the endpoint when the application starts
supervisor(FacesWeb.Endpoint, []),
# Start your own worker by calling: Faces.Worker.start_link(arg1, arg2, arg3)
# worker(Faces.Worker, [arg1, arg2, arg3]),
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Faces.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
FacesWeb.Endpoint.config_change(changed, removed)
:ok
end
end

View file

@ -0,0 +1,23 @@
defmodule Faces.Gallery do
@moduledoc """
The Gallery context.
"""
alias Faces.Repo
alias Faces.Gallery.{Person, Importer}
@doc """
Returns the list of people.
## Examples
iex> list_people()
[%Person{}, ...]
"""
def list_people do
Repo.all(Person)
end
def import_user(username), do: Importer.import(username)
end

View file

@ -0,0 +1,57 @@
defmodule Faces.Gallery.GithubUserData do
alias Tentacat.{Client, Users}
@doc """
Retrieves a user's information from GitHub.
## Examples
iex> GitHubUserData.get("jamesotron")
{:ok, %{
"avatar_url" => "https://avatars2.githubusercontent.com/u/59449?v=4",
"location" => "Wellington, New Zealand",
"name" => "James Harton",
"username" => "jamesotron"
}}
iex> GitHubUserData.get("thisUserReallyDoesntExist")
{:error, "404 while retrieving \"thisUserReallyDoesntExist\" from Github: Not Found"}
"""
def get(username) do
with {200, user_data, _} <- get_user_from_github(username),
{:ok, user_data} <- just_the_facts(user_data),
{:ok, user_data} <- add_username(username, user_data) do
{:ok, user_data}
else
{:error, reason} ->
{:error, reason}
{i, %{"message" => message}, _} when is_integer(i) ->
{:error, "#{i} while retrieving #{inspect(username)} from Github: #{message}"}
{i, _, _} when is_integer(i) ->
{:error, "#{i} while retrieving #{inspect(username)} from Github"}
end
end
defp get_user_from_github(username) do
username
|> Users.find(github_client())
end
defp github_client, do: Client.new(%{access_token: github_access_token()})
defp github_access_token do
:faces
|> Application.get_env(__MODULE__, [])
|> Keyword.get(:access_token)
end
defp just_the_facts(user_data) do
{:ok, Map.take(user_data, ["avatar_url", "name", "location"])}
end
defp add_username(username, user_data) do
{:ok, Map.put(user_data, "username", username)}
end
end

View file

@ -0,0 +1,23 @@
defmodule Faces.Gallery.Importer do
alias Faces.Gallery.{Person, GithubUserData}
alias Faces.Repo
def import(username) do
with {:ok, user_data} <- GithubUserData.get(username),
{:ok, changeset} <- generate_changeset(user_data),
{:ok, person} <- upsert(changeset) do
{:ok, person}
else
{:error, reason} -> {:error, reason}
end
end
defp generate_changeset(user_data) do
{:ok, Person.changeset(%Person{}, user_data)}
end
defp upsert(changeset) do
changeset
|> Repo.insert(on_conflict: :replace_all, conflict_target: [:username])
end
end

View file

@ -0,0 +1,22 @@
defmodule Faces.Gallery.Person do
use Ecto.Schema
import Ecto.Changeset
@timestamps_opts [type: :utc_datetime, usec: true]
schema "people" do
field(:username, :string)
field(:avatar_url, :string)
field(:location, :string)
field(:name, :string)
timestamps()
end
@doc false
def changeset(person, attrs) do
person
|> cast(attrs, [:username, :name, :location, :avatar_url])
|> validate_required([:username, :name, :avatar_url])
end
end

11
lib/faces/repo.ex Normal file
View file

@ -0,0 +1,11 @@
defmodule Faces.Repo do
use Ecto.Repo, otp_app: :faces
@doc """
Dynamically loads the repository url from the
DATABASE_URL environment variable.
"""
def init(_, opts) do
{:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))}
end
end

67
lib/faces_web.ex Normal file
View file

@ -0,0 +1,67 @@
defmodule FacesWeb 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 FacesWeb, :controller
use FacesWeb, :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: FacesWeb
import Plug.Conn
import FacesWeb.Router.Helpers
import FacesWeb.Gettext
end
end
def view do
quote do
use Phoenix.View, root: "lib/faces_web/templates",
namespace: FacesWeb
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_flash: 2, view_module: 1]
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
import FacesWeb.Router.Helpers
import FacesWeb.ErrorHelpers
import FacesWeb.Gettext
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 FacesWeb.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

View file

@ -0,0 +1,37 @@
defmodule FacesWeb.UserSocket do
use Phoenix.Socket
## Channels
# channel "room:*", FacesWeb.RoomChannel
## Transports
transport :websocket, Phoenix.Transports.WebSocket
# transport :longpoll, Phoenix.Transports.LongPoll
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
# verification, you can put default assigns into
# the socket that will be set for all channels, ie
#
# {:ok, assign(socket, :user_id, verified_user_id)}
#
# To deny connection, return `:error`.
#
# See `Phoenix.Token` documentation for examples in
# performing token verification on connect.
def connect(_params, socket) 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:
#
# FacesWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
#
# Returning `nil` makes this socket anonymous.
def id(_socket), do: nil
end

View file

@ -0,0 +1,20 @@
defmodule FacesWeb.FaceController do
use FacesWeb, :controller
alias Faces.Gallery
def index(conn, _params) do
render(conn, "index.html", people: Gallery.list_people())
end
def create(conn, %{"username" => username}) do
case Gallery.import_user(username) do
{:ok, _user} ->
render(conn, "index.html", people: Gallery.list_people())
{:error, reason} ->
conn
|> put_flash(:error, reason)
|> render("index.html", people: Gallery.list_people())
end
end
end

56
lib/faces_web/endpoint.ex Normal file
View file

@ -0,0 +1,56 @@
defmodule FacesWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :faces
socket "/socket", FacesWeb.UserSocket
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phoenix.digest
# when deploying your static files in production.
plug Plug.Static,
at: "/", from: :faces, 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.Logger
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Poison
plug Plug.MethodOverride
plug Plug.Head
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
plug Plug.Session,
store: :cookie,
key: "_faces_key",
signing_salt: "SBTuT23J"
plug FacesWeb.Router
@doc """
Callback invoked for dynamically configuring the endpoint.
It receives the endpoint configuration and checks if
configuration should be loaded from the system environment.
"""
def init(_key, config) do
if config[:load_from_system_env] do
port = System.get_env("PORT") || raise "expected the PORT environment variable to be set"
{:ok, Keyword.put(config, :http, [:inet6, port: port])}
else
{:ok, config}
end
end
end

24
lib/faces_web/gettext.ex Normal file
View file

@ -0,0 +1,24 @@
defmodule FacesWeb.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 FacesWeb.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: :faces
end

View file

@ -0,0 +1,30 @@
defmodule FacesWeb.Resolvers.People do
alias Faces.Gallery
@doc """
This is the resolver callback for Absinthe to find a list of all people.
The arguments are:
* `parent`, any parent object which Absinthe things we're related to.
* `args`, any arguments passed to the query.
* `resolution`,
"""
def list_people(_parent, _args, _resolution) do
{:ok, Gallery.list_people()}
end
@doc """
This is the resolver callback Absinthe uses to create a person.
The arguments are:
* `parent` any parent object which Absinthe things we're related to.
* `args` a map of arguments passed to the query.
* `context` a context object which can be used for things like
authentication, etc.
"""
def create_person(_parent, %{username: username}, _context) do
Gallery.import_user(username)
end
end

30
lib/faces_web/router.ex Normal file
View file

@ -0,0 +1,30 @@
defmodule FacesWeb.Router do
use FacesWeb, :router
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(:fetch_flash)
plug(:protect_from_forgery)
plug(:put_secure_browser_headers)
end
pipeline :api do
plug(:accepts, ["json"])
end
scope "/", FacesWeb do
# Use the default browser stack
pipe_through(:browser)
resources("/", FaceController, only: [:index, :create])
end
# Other scopes may use custom stacks.
# scope "/api", FacesWeb do
# pipe_through :api
# end
forward("/api", Absinthe.Plug, schema: FacesWeb.Schema)
forward("/graphiql", Absinthe.Plug.GraphiQL, schema: FacesWeb.Schema)
end

View file

@ -0,0 +1,27 @@
defmodule FacesWeb.Schema.Person do
use Absinthe.Schema.Notation
@desc "A person whose face we want to see"
object :person do
@desc "A unique identifier for this person"
field(:id, :id)
@desc "The person's Github username"
field(:username, :string)
@desc "The person's name as per Github"
field(:name, :string)
@desc "The person's location as per Github"
field(:location, :string)
@desc "The URL of the person's Github avatar image"
field(:avatar_url, :string)
@desc "When this user was first imported into the faces app"
field(:inserted_at, :datetime)
@desc "When this user was last updated in the faces app"
field(:updated_at, :datetime)
end
end

View file

@ -0,0 +1,22 @@
defmodule FacesWeb.Schema do
use Absinthe.Schema
alias FacesWeb.Resolvers
import_types(Absinthe.Type.Custom)
import_types(FacesWeb.Schema.Person)
query do
@desc "List all people"
field :people, list_of(:person) do
resolve(&Resolvers.People.list_people/3)
end
end
mutation do
@desc "Import a user from Github"
field :import_person, type: :person do
arg(:username, non_null(:string))
resolve(&Resolvers.People.create_person/3)
end
end
end

View file

@ -0,0 +1,41 @@
<div class="row">
<div class="col-sm">
<h1>Face Gallery</h1>
<%= if get_flash(@conn, :error) do %>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<% end %>
</div>
</div>
<div class="row" id="face-gallery">
<%= for person <- @people do %>
<div class="col-sm-3 mb-3">
<div class="card">
<img class="card-img-top" src="<%= person.avatar_url %>" alt="<%= person.name %>">
<div class="card-body">
<a href="https://github.com/<%= person.username %>"><h5 class="card-title"><%= person.name %></h5></a>
<p class="card-text"><%= person.location %></p>
</div>
</div>
</div>
<% end %>
<div class="col-sm-3 mb-3">
<div class="card">
<div class="card-header">
<h5 class="card-title">Add Face</h5>
</div>
<div class="card-body">
<%= form_for @conn, face_path(@conn, :create), fn f -> %>
<div class="form-group">
<label for="username">Github Username</label>
<%= text_input f, :username, class: "form-control" %>
</div>
<%= submit "Add", class: "btn btn-primary" %>
<% end %>
</div>
</div>
</div>
</div>

View file

@ -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">
<meta name="description" content="">
<meta name="author" content="">
<title>Hello Faces!</title>
<link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
</head>
<body>
<div class="container">
<main role="main">
<%= render @view_module, @view_template, assigns %>
</main>
</div> <!-- /container -->
<script src="<%= static_path(@conn, "/js/app.js") %>"></script>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
</body>
</html>

View file

@ -0,0 +1,44 @@
defmodule FacesWeb.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(FacesWeb.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(FacesWeb.Gettext, "errors", msg, opts)
end
end
end

View file

@ -0,0 +1,16 @@
defmodule FacesWeb.ErrorView do
use FacesWeb, :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

View file

@ -0,0 +1,3 @@
defmodule FacesWeb.FaceView do
use FacesWeb, :view
end

View file

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

63
mix.exs Normal file
View file

@ -0,0 +1,63 @@
defmodule Faces.Mixfile do
use Mix.Project
def project do
[
app: :faces,
version: "0.0.1",
elixir: "~> 1.4",
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: {Faces.Application, []},
extra_applications: [:logger, :runtime_tools]
]
end
# Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
defp deps do
[
{:phoenix, "~> 1.3.2"},
{:phoenix_pubsub, "~> 1.0"},
{:phoenix_ecto, "~> 3.2"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 2.10"},
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:gettext, "~> 0.11"},
{:cowboy, "~> 1.0"},
{:tentacat, "~> 0.9.0"},
{:absinthe_plug, "~> 1.4"},
{:poison, "~> 2.1.0", override: true}
]
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

35
mix.lock Normal file
View file

@ -0,0 +1,35 @@
%{
"absinthe": {:hex, :absinthe, "1.4.10", "9f8d0c34dfcfd0030d3a3f123c7501e99ab59651731387289dad5885047ebb2a", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"absinthe_plug": {:hex, :absinthe_plug, "1.4.2", "01bf16f0a637869bcc0a1919935f08ff853501004e7549ddaa3a7788deb48965", [:mix], [{:absinthe, "~> 1.4", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
"cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
"cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"},
"db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
"decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"},
"ecto": {:hex, :ecto, "2.2.9", "031d55df9bb430cb118e6f3026a87408d9ce9638737bda3871e5d727a3594aae", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
"exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"},
"file_system": {:hex, :file_system, "0.2.4", "f0bdda195c0e46e987333e986452ec523aed21d784189144f647c43eaf307064", [:mix], [], "hexpm"},
"gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"},
"hackney": {:hex, :hackney, "1.12.1", "8bf2d0e11e722e533903fe126e14d6e7e94d9b7983ced595b75f532e04b7fdc7", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.1", [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.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"httpoison": {:hex, :httpoison, "0.13.0", "bfaf44d9f133a6599886720f3937a7699466d23bb0cd7a88b6ba011f53c6f562", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"idna": {:hex, :idna, "5.1.1", "cbc3b2fa1645113267cc59c760bafa64b2ea0334635ef06dbac8801e42f7279c", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
"jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
"mime": {:hex, :mime, "1.2.0", "78adaa84832b3680de06f88f0997e3ead3b451a440d183d688085be2d709b534", [:mix], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"},
"phoenix": {:hex, :phoenix, "1.3.2", "2a00d751f51670ea6bc3f2ba4e6eb27ecb8a2c71e7978d9cd3e5de5ccf7378bd", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_ecto": {:hex, :phoenix_ecto, "3.3.0", "702f6e164512853d29f9d20763493f2b3bcfcb44f118af2bc37bb95d0801b480", [:mix], [{:ecto, "~> 2.1", [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.11.1", "77b6f7fbd252168c6ec4f573de648d37cc5258cda13266ef001fbf99267eb6f3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.1.3", "1d178429fc8950b12457d09c6afec247bfe1fcb6f36209e18fbb0221bdfe4d41", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], [], "hexpm"},
"plug": {:hex, :plug, "1.5.0", "224b25b4039bedc1eac149fb52ed456770b9678bbf0349cdd810460e1e09195b", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
"poison": {:hex, :poison, "2.1.0", "f583218ced822675e484648fa26c933d621373f01c6c76bd00005d7bd4b82e27", [:mix], [], "hexpm"},
"poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"},
"postgrex": {:hex, :postgrex, "0.13.5", "3d931aba29363e1443da167a4b12f06dcd171103c424de15e5f3fc2ba3e6d9c5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"},
"ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"},
"tentacat": {:hex, :tentacat, "0.9.0", "c773d6d3def1a37296330c2787549c0f0f507f45b3a580d32d7d8aa3fdd56d3f", [:mix], [{:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:httpoison, "~> 0.8", [hex: :httpoison, repo: "hexpm", optional: false]}], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"},
}

View file

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

95
priv/gettext/errors.pot Normal file
View file

@ -0,0 +1,95 @@
## This file 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 as 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 ""

View file

@ -0,0 +1,16 @@
defmodule Faces.Repo.Migrations.CreatePeople do
use Ecto.Migration
def change do
create table(:people) do
add(:username, :string)
add(:name, :string)
add(:location, :string)
add(:avatar_url, :string)
timestamps()
end
create(index(:people, [:username], unique: true))
end
end

17
priv/repo/seeds.exs Normal file
View file

@ -0,0 +1,17 @@
# 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:
#
# Faces.Repo.insert!(%Faces.SomeSchema{})
#
# We recommend using the bang functions (`insert!`, `update!`
# and so on) as they will fail if something goes wrong.
alias Faces.Gallery
Gallery.import_user("jamesotron")
Gallery.import_user("terrcin")
Gallery.import_user("edgurgel")

View file

@ -0,0 +1,4 @@
defmodule Faces.GalleryTest do
use Faces.DataCase
alias Faces.Gallery
end

View file

@ -0,0 +1,5 @@
defmodule FacesGalleryGithubUserDataTest do
use ExUnit.Case
alias Faces.Gallery.GithubUserData
doctest Faces.Gallery.GithubUserData
end

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,37 @@
defmodule FacesWeb.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 datastructures 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 FacesWeb.Endpoint
end
end
setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Faces.Repo)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(Faces.Repo, {:shared, self()})
end
:ok
end
end

38
test/support/conn_case.ex Normal file
View file

@ -0,0 +1,38 @@
defmodule FacesWeb.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 datastructures 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
import FacesWeb.Router.Helpers
# The default endpoint for testing
@endpoint FacesWeb.Endpoint
end
end
setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Faces.Repo)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(Faces.Repo, {:shared, self()})
end
{:ok, conn: Phoenix.ConnTest.build_conn()}
end
end

53
test/support/data_case.ex Normal file
View file

@ -0,0 +1,53 @@
defmodule Faces.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 Faces.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query
import Faces.DataCase
end
end
setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Faces.Repo)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(Faces.Repo, {:shared, self()})
end
:ok
end
@doc """
A helper that transform changeset errors to 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

4
test/test_helper.exs Normal file
View file

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