Compare commits
4 commits
Author | SHA1 | Date | |
---|---|---|---|
|
3568c50db5 | ||
|
48d4cb732d | ||
|
6ff0e26b01 | ||
|
524e0fc663 |
61 changed files with 14 additions and 5566 deletions
24
README.md
24
README.md
|
@ -1,17 +1,15 @@
|
||||||
# Client-side GraphQL
|
# Wellington Elixir Meetup GraphQL Lightning Talk
|
||||||
|
## Because I couldn't think of a snappier title
|
||||||
|
|
||||||
So, a GraphQL API isn't much use without the ability to use it from the browser, so here's where we implement the faces gallery as a client-side app. Whilst it's possible to do progressive enhancement I've removed all functionality from the faces controller and templates to prove that all the data loading and changing is happening via 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.
|
||||||
|
|
||||||
I've used [React](https://reactjs.org/) and [Apollo client](https://www.apollographql.com/client) via [react-apollo](https://www.apollographql.com/docs/react/).
|
Things we're going to cover in this presentation:
|
||||||
|
|
||||||
Things to look at:
|
* What the heck is GraphQL anyway?
|
||||||
|
* Setting it up with Phoenix
|
||||||
|
* Handling Queries
|
||||||
|
* Handling Mutations
|
||||||
|
* Handling Subscriptions
|
||||||
|
* Extra for experts
|
||||||
|
|
||||||
* `assets/js/app.js`
|
To get started checkout the `step-1` branch.
|
||||||
* `assets/js/components/Gallery.js`
|
|
||||||
* `assets/js/components/AddFace.js`
|
|
||||||
* `assets/js/queries/list_people.js`
|
|
||||||
* `assets/js/queries/import_person.js`
|
|
||||||
|
|
||||||
[Demo](http://localhost:4000)
|
|
||||||
|
|
||||||
Next, let's flip over to `step-5` to see some magic.
|
|
||||||
|
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -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"]
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1 +0,0 @@
|
||||||
/* This file is for your main application css. */
|
|
|
@ -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"));
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { ApolloClient } from 'apollo-client';
|
|
||||||
import { HttpLink } from 'apollo-link-http';
|
|
||||||
import { InMemoryCache } from 'apollo-cache-inmemory';
|
|
||||||
|
|
||||||
const client = new ApolloClient({
|
|
||||||
link: new HttpLink({
|
|
||||||
uri: "/api"
|
|
||||||
}),
|
|
||||||
cache: new InMemoryCache(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default client;
|
|
|
@ -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;
|
|
|
@ -1,27 +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";
|
|
||||||
|
|
||||||
class Gallery extends React.Component {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="row">
|
|
||||||
<Query query={LIST_PEOPLE}>
|
|
||||||
{({ loading, error, data }) => {
|
|
||||||
if (loading) return null;
|
|
||||||
if (error) return `Error!: ${error}`;
|
|
||||||
|
|
||||||
return data.people.map((person, key) => (<Person person={person} key={key} />));
|
|
||||||
}}
|
|
||||||
</Query>
|
|
||||||
<AddFace />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Gallery;
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -1,12 +0,0 @@
|
||||||
import gql from "graphql-tag";
|
|
||||||
|
|
||||||
export default gql `
|
|
||||||
mutation importPerson($username: String!) {
|
|
||||||
importPerson(username: $username) {
|
|
||||||
username,
|
|
||||||
name,
|
|
||||||
location,
|
|
||||||
avatarUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
|
@ -1,12 +0,0 @@
|
||||||
import gql from "graphql-tag";
|
|
||||||
|
|
||||||
export default gql `
|
|
||||||
query {
|
|
||||||
people{
|
|
||||||
username,
|
|
||||||
name,
|
|
||||||
location,
|
|
||||||
avatarUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
|
@ -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
|
|
|
@ -1,33 +0,0 @@
|
||||||
{
|
|
||||||
"repository": {},
|
|
||||||
"license": "MIT",
|
|
||||||
"scripts": {
|
|
||||||
"deploy": "brunch build --production",
|
|
||||||
"watch": "brunch watch --stdin"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"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 |
|
@ -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: /
|
|
3910
assets/yarn.lock
3910
assets/yarn.lock
File diff suppressed because it is too large
Load diff
|
@ -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"
|
|
|
@ -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
|
|
|
@ -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"
|
|
|
@ -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
|
|
7
go.sh
7
go.sh
|
@ -2,10 +2,9 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
(cd assets && yarn install)
|
open -a /Applications/Marked\ 2.app README.md
|
||||||
mix deps.get
|
code .
|
||||||
mix phx.server
|
|
||||||
|
|
||||||
read -p "Press enter to continue to next step"
|
read -p "Press enter to continue to next step"
|
||||||
git checkout step-5
|
git checkout step-1
|
||||||
exec ./go.sh
|
exec ./go.sh
|
||||||
|
|
|
@ -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
|
|
|
@ -1,31 +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]),
|
|
||||||
]
|
|
||||||
|
|
||||||
# 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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -1,37 +0,0 @@
|
||||||
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
|
|
|
@ -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
|
|
|
@ -1,56 +0,0 @@
|
||||||
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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -1,22 +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
|
|
||||||
end
|
|
|
@ -1 +0,0 @@
|
||||||
<div id="face-gallery"></div>
|
|
|
@ -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>
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -1,3 +0,0 @@
|
||||||
defmodule FacesWeb.FaceView do
|
|
||||||
use FacesWeb, :view
|
|
||||||
end
|
|
|
@ -1,3 +0,0 @@
|
||||||
defmodule FacesWeb.LayoutView do
|
|
||||||
use FacesWeb, :view
|
|
||||||
end
|
|
63
mix.exs
63
mix.exs
|
@ -1,63 +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"},
|
|
||||||
{: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
35
mix.lock
|
@ -1,35 +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_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"},
|
|
||||||
}
|
|
|
@ -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 ""
|
|
|
@ -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 ""
|
|
|
@ -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
|
|
|
@ -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")
|
|
|
@ -1,4 +0,0 @@
|
||||||
defmodule Faces.GalleryTest do
|
|
||||||
use Faces.DataCase
|
|
||||||
alias Faces.Gallery
|
|
||||||
end
|
|
|
@ -1,5 +0,0 @@
|
||||||
defmodule FacesGalleryGithubUserDataTest do
|
|
||||||
use ExUnit.Case
|
|
||||||
alias Faces.Gallery.GithubUserData
|
|
||||||
doctest Faces.Gallery.GithubUserData
|
|
||||||
end
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -1,3 +0,0 @@
|
||||||
defmodule FacesWeb.LayoutViewTest do
|
|
||||||
use FacesWeb.ConnCase, async: true
|
|
||||||
end
|
|
|
@ -1,3 +0,0 @@
|
||||||
defmodule FacesWeb.PageViewTest do
|
|
||||||
use FacesWeb.ConnCase, async: true
|
|
||||||
end
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -1,4 +0,0 @@
|
||||||
ExUnit.start()
|
|
||||||
|
|
||||||
Ecto.Adapters.SQL.Sandbox.mode(Faces.Repo, :manual)
|
|
||||||
|
|
Reference in a new issue