Compare commits

..

4 commits

Author SHA1 Message Date
James Harton
3568c50db5 update gitignore, 2018-04-10 18:19:57 +12:00
James Harton
48d4cb732d Update go.sh 2018-04-10 12:37:40 +12:00
James Harton
6ff0e26b01 Add go.sh 2018-04-10 12:33:42 +12:00
James Harton
524e0fc663 Update READme and include gitignore. 2018-04-08 16:28:29 +12:00
66 changed files with 20 additions and 5833 deletions

View file

@ -1,6 +0,0 @@
{
"files.associations": {
"*.js": "javascriptreact"
},
"eslint.packageManager": "yarn"
}

View file

@ -1,7 +1,15 @@
# All done.
# Wellington Elixir Meetup GraphQL Lightning Talk
## Because I couldn't think of a snappier title
I hope you've enjoyed our little trip around GraphQL.
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.
All code for this talk is available at [gitlab.com/jimsy/graphql-lightning-talk](https://gitlab.com/jimsy/graphql-lightning-talk).
Things we're going to cover in this presentation:
I'm `@jimsy` on the [AU/NZ Elixir Slack](http://aunz-elixir.herokuapp.com/) - feel free to ask me questions there.
* What the heck is GraphQL anyway?
* Setting it up with Phoenix
* Handling Queries
* Handling Mutations
* Handling Subscriptions
* Extra for experts
To get started checkout the `step-1` branch.

View file

@ -1,36 +0,0 @@
module.exports = {
"env": {
"browser": true,
"commonjs": true,
"es6": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
},
"sourceType": "module"
},
"plugins": [
"react"
],
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
]
}
};

View file

@ -1,64 +0,0 @@
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: {
presets: ["es2015", "react"],
// Do not use ES6 compiler in vendor code
ignore: [/vendor/]
}
},
modules: {
autoRequire: {
"js/app.js": ["js/app"]
}
},
npm: {
enabled: true,
whitelist: ["phoenix", "phoenix_html", "react", "react-dom"]
}
};

View file

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

View file

@ -1,7 +0,0 @@
import * as AbsintheSocket from "@absinthe/socket";
import { createAbsintheSocketLink } from "@absinthe/socket-apollo-link";
import { Socket as PhoenixSocket } from "phoenix";
export default createAbsintheSocketLink(AbsintheSocket.create(
new PhoenixSocket("ws://localhost:4000/socket")
));

View file

@ -1,43 +0,0 @@
// 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"
import React from "react";
import ReactDOM from "react-dom";
import { ApolloProvider } from 'react-apollo';
import Header from "./components/Header";
import Gallery from "./components/Gallery";
import client from "./client";
class App extends React.Component {
render() {
return (
<ApolloProvider client={client}>
<div className="container">
<Header />
<Gallery />
</div>
</ApolloProvider>
);
}
}
ReactDOM.render(<App />, document.getElementById("face-gallery"));

View file

@ -1,10 +0,0 @@
import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import absintheSocketLink from "./absinthe-socket-link";
const client = new ApolloClient({
link: absintheSocketLink,
cache: new InMemoryCache(),
});
export default client;

View file

@ -1,65 +0,0 @@
import React from "react";
import gql from "graphql-tag";
import { Mutation } from "react-apollo";
import IMPORT_PERSON from "../queries/import_person";
import LIST_PEOPLE from "../queries/list_people";
class AddFace extends React.Component {
// I extracted the cache updating logic from the JSX because there were too
// many nested braces and it was making me confused.
cacheUpdater(cache, { data: { importPerson } }) {
let { people } = cache.readQuery({ query: LIST_PEOPLE });
// If the person already exists in the cache then replace them with the new data.
let existingPersonIndex = people.findIndex(person => person.username === importPerson.username);
if (existingPersonIndex >= 0) {
people.splice(existingPersonIndex, 1, importPerson);
}
// Otherwise add them to the cache.
else {
people = people.concat([importPerson]);
}
cache.writeQuery({
query: LIST_PEOPLE,
data: { people: people }
});
}
render() {
let input;
return (
<Mutation
mutation={IMPORT_PERSON}
update={this.cacheUpdater}>
{(importPerson, { data }) => (
<div className="col-sm-3 mb-3">
<div className="card">
<div className="card-header">
<h5 className="card-title">Add Face</h5>
</div>
<div className="card-body">
<form onSubmit={e => {
e.preventDefault();
importPerson({ variables: { username: input.value } });
input.value = "";
}}>
<div className="form-group">
<label htmlFor="username">Github Username</label>
<input type="text" name="username" className="form-control" ref={node => input = node} />
</div>
<input type="submit" className="btn btn-primary" value="Add" />
</form>
</div>
</div>
</div>
)}
</Mutation>
);
}
}
export default AddFace;

View file

@ -1,49 +0,0 @@
import React from "react";
import gql from "graphql-tag";
import { Query } from "react-apollo";
import Person from "./Person";
import AddFace from "./AddFace";
import LIST_PEOPLE from "../queries/list_people";
import PERSON_ADDED from "../queries/person_added";
class Gallery extends React.Component {
// Uses Apollo's `subscribeToMore` callback to subscribe to the `personAdded`
// subscription and update our local data with the new data.
doSubscription(subscribeToMore) {
subscribeToMore({
document: PERSON_ADDED,
updateQuery: ({ people }, { subscriptionData }) => {
let newPerson = subscriptionData.data.personAdded;
let existingPersonIndex = people.findIndex(person => person.username === newPerson.username);
if (existingPersonIndex >= 0) {
people.splice(existingPersonIndex, 1, newPerson);
}
else {
people = people.concat([newPerson]);
}
return { people };
}
});
}
render() {
return (
<div className="row">
<Query query={LIST_PEOPLE}>
{({ subscribeToMore, loading, data }) => {
if (loading) return null;
this.doSubscription(subscribeToMore);
return data.people.map((person, key) => (<Person person={person} key={key} />));
}}
</Query>
<AddFace />
</div>
);
}
}
export default Gallery;

View file

@ -1,15 +0,0 @@
import React from "react";
class Header extends React.Component {
render() {
return (
<div className="row">
<div className="col-sm">
<h1>Face Gallery</h1>
</div>
</div>
);
}
}
export default Header;

View file

@ -1,22 +0,0 @@
import React from "react";
class Person extends React.Component {
render() {
const person = this.props.person;
const githubUrl = `https://github.com/${person.username}`;
return (
<div className="col-sm-3 mb-3">
<div className="card">
<img className="card-img-top" src={person.avatarUrl} alt={person.name} />
<div className="card-body">
<a href={githubUrl}><h5 className="card-title">{person.name}</h5></a>
<p className="card-text">{person.location}</p>
</div>
</div>
</div>
);
}
}
export default Person;

View file

@ -1,12 +0,0 @@
import gql from "graphql-tag";
export default gql `
mutation importPerson($username: String!) {
importPerson(username: $username) {
username,
name,
location,
avatarUrl
}
}
`;

View file

@ -1,12 +0,0 @@
import gql from "graphql-tag";
export default gql `
query {
people{
username,
name,
location,
avatarUrl
}
}
`;

View file

@ -1,12 +0,0 @@
import gql from "graphql-tag";
export default gql`
subscription personAdded {
personAdded {
username,
name,
location,
avatarUrl
}
}
`;

View file

@ -1,62 +0,0 @@
// 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

View file

@ -1,34 +0,0 @@
{
"repository": {},
"license": "MIT",
"scripts": {
"deploy": "brunch build --production",
"watch": "brunch watch --stdin"
},
"dependencies": {
"@absinthe/socket-apollo-link": "^0.1.11",
"apollo-client-preset": "^1.0.8",
"babel-preset-react": "^6.24.1",
"graphql": "^0.13.2",
"graphql-tag": "^2.8.0",
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html",
"react": "^16.3.1",
"react-apollo": "^2.1.3",
"react-dom": "^16.3.1",
"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-react": "^7.7.0",
"eslint-plugin-standard": "^3.0.1",
"uglify-js-brunch": "2.10.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View file

@ -1,5 +0,0 @@
# 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: /

File diff suppressed because it is too large Load diff

View file

@ -1,29 +0,0 @@
# 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"

View file

@ -1,58 +0,0 @@
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

View file

@ -1,64 +0,0 @@
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"

View file

@ -1,19 +0,0 @@
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

9
go.sh
View file

@ -1,3 +1,10 @@
#!/bin/sh
set -e
echo "All done."
open -a /Applications/Marked\ 2.app README.md
code .
read -p "Press enter to continue to next step"
git checkout step-1
exec ./go.sh

View file

@ -1,9 +0,0 @@
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

View file

@ -1,33 +0,0 @@
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]),
supervisor(Absinthe.Subscription, [FacesWeb.Endpoint]),
worker(Faces.Gallery.EventListener, [])
]
# 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

@ -1,57 +0,0 @@
defmodule Faces.Gallery.EventListener do
use GenServer
require Logger
alias Absinthe.Subscription
@notification_channel "people_changes"
def start_link, do: GenServer.start_link(__MODULE__, [])
def init(_opts) do
pg_config = Faces.Repo.config()
{:ok, pid} = Postgrex.Notifications.start_link(pg_config)
{:ok, ref} = Postgrex.Notifications.listen(pid, @notification_channel)
Logger.debug(fn -> "#{__MODULE__} subscribed to channel #{@notification_channel}" end)
{:ok, {pid, @notification_channel, ref}}
end
def handle_info({:notification, _pid, _ref, @notification_channel, payload}, _state) do
with {:ok, event} <- Poison.decode(payload),
{:ok, person} <- strip_person_from_event(event),
:ok <- publish_via_absinthe(person) do
{:noreply, :event_handled}
else
_error -> {:noreply, :event_received}
end
end
def handle_info(_, _state), do: {:noreply, :event_received}
defp strip_person_from_event(%{"new_row_data" => row_data}) do
person =
row_data
|> Map.take(["id", "username", "name", "location", "avatar_url"])
|> Map.put("inserted_at", cooerce_time(Map.get(row_data, "inserted_at")))
|> Map.put("updated_at", cooerce_time(Map.get(row_data, "updated_at")))
|> Enum.reduce(%{}, fn {key, value}, map -> Map.put(map, String.to_atom(key), value) end)
{:ok, person}
end
defp strip_person_from_event(_), do: {:error, "Invalid event data"}
defp publish_via_absinthe(person) do
Subscription.publish(FacesWeb.Endpoint, person, person_added: "person_added")
end
defp cooerce_time(nil), do: DateTime.utc_now()
defp cooerce_time(iso8601) do
with {:ok, ndt} <- NaiveDateTime.from_iso8601(iso8601),
{:ok, dt} <- DateTime.from_naive(ndt, "Etc/UTC") do
dt
else
{:error, reason} -> {:error, reason}
end
end
end

View file

@ -1,23 +0,0 @@
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

@ -1,57 +0,0 @@
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

@ -1,23 +0,0 @@
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

@ -1,22 +0,0 @@
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

View file

@ -1,11 +0,0 @@
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

View file

@ -1,67 +0,0 @@
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

@ -1,38 +0,0 @@
defmodule FacesWeb.UserSocket do
use Phoenix.Socket
use Absinthe.Phoenix.Socket, schema: FacesWeb.Schema
## 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

@ -1,8 +0,0 @@
defmodule FacesWeb.FaceController do
use FacesWeb, :controller
alias Faces.Gallery
def index(conn, _params) do
render(conn, "index.html")
end
end

View file

@ -1,65 +0,0 @@
defmodule FacesWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :faces
use Absinthe.Phoenix.Endpoint
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

View file

@ -1,24 +0,0 @@
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

@ -1,30 +0,0 @@
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

View file

@ -1,29 +0,0 @@
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)
get("/", FaceController, :index)
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

@ -1,27 +0,0 @@
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

@ -1,33 +0,0 @@
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
subscription do
@desc "New person added"
field :person_added, :person do
config(fn _, _ ->
{:ok, topic: "person_added"}
end)
trigger(:import_person, topic: fn _ -> "person_added" end)
end
end
end

View file

@ -1 +0,0 @@
<div id="face-gallery"></div>

View file

@ -1,23 +0,0 @@
<!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>
<%= render @view_module, @view_template, assigns %>
<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

@ -1,44 +0,0 @@
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

@ -1,16 +0,0 @@
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

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

View file

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

64
mix.exs
View file

@ -1,64 +0,0 @@
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", override: true},
{: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},
{:absinthe_phoenix, "~> 1.4.0"}
]
end
# Aliases are shortcuts or tasks specific to the current project.
# For example, to create, migrate and run the seeds file at once:
#
# $ mix ecto.setup
#
# See the documentation for `Mix` for more info on aliases.
defp aliases do
[
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate", "test"]
]
end
end

View file

@ -1,36 +0,0 @@
%{
"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_phoenix": {:hex, :absinthe_phoenix, "1.4.2", "cb84c81b94103fdfbbdd8b83b7b4b70a850ab7d3be6a1e56b96de2bb854b09b6", [:mix], [{:absinthe, "~> 1.4.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.4.0", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.2", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.10.5", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:poison, "~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "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

@ -1,97 +0,0 @@
## `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 ""

View file

@ -1,95 +0,0 @@
## 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

@ -1,16 +0,0 @@
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

View file

@ -1,47 +0,0 @@
defmodule Faces.Repo.Migrations.BroadcastPeopleTableChanges do
use Ecto.Migration
def up do
# Create a function that broadcasts row changes
execute("
CREATE OR REPLACE FUNCTION broadcast_changes()
RETURNS trigger AS $$
DECLARE
current_row RECORD;
BEGIN
IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
current_row := NEW;
ELSE
current_row := OLD;
END IF;
IF (TG_OP = 'INSERT') THEN
OLD := NEW;
END IF;
PERFORM pg_notify(
'people_changes',
json_build_object(
'table', TG_TABLE_NAME,
'type', TG_OP,
'id', current_row.id,
'new_row_data', row_to_json(NEW),
'old_row_data', row_to_json(OLD)
)::text
);
RETURN current_row;
END;
$$ LANGUAGE plpgsql;")
# Create a trigger links the people table to the broadcast function
execute("
CREATE TRIGGER notify_people_changes_trigger
AFTER INSERT OR UPDATE
ON people
FOR EACH ROW
EXECUTE PROCEDURE broadcast_changes();")
end
def down do
execute("DROP TRIGGER IF EXISTS notify_people_changes_trigger ON people;")
execute("DROP FUNCTION IF EXISTS broadcast_changes;")
end
end

View file

@ -1,17 +0,0 @@
# 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

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

View file

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

View file

@ -1,8 +0,0 @@
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

@ -1,16 +0,0 @@
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

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

View file

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

View file

@ -1,37 +0,0 @@
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

View file

@ -1,38 +0,0 @@
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

View file

@ -1,53 +0,0 @@
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

View file

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