├── Procfile ├── test ├── test_helper.exs ├── plugs │ └── url_format_test.exs ├── models │ ├── user_test.exs │ ├── session_test.exs │ ├── resource_test.exs │ ├── episode_guest_test.exs │ ├── show_note_test.exs │ ├── person_test.exs │ ├── linked_account_test.exs │ └── episode_test.exs ├── support │ ├── oauth │ │ └── github_stub.ex │ ├── channel_case.ex │ ├── model_case.ex │ ├── factory.ex │ └── conn_case.ex └── controllers │ ├── feed_controller_test.exs │ ├── session_controller_test.exs │ ├── person_controller_test.exs │ ├── resource_controller_test.exs │ ├── show_note_controller_test.exs │ └── episode_controller_test.exs ├── elixir_buildpack.config ├── lib ├── ember_weekend_api │ ├── repo.ex │ ├── web │ │ ├── views │ │ │ ├── user_view.ex │ │ │ ├── error_view.ex │ │ │ ├── session_view.ex │ │ │ ├── changeset_view.ex │ │ │ ├── episode_view.ex │ │ │ ├── resource_view.ex │ │ │ ├── show_note_view.ex │ │ │ ├── error_helpers.ex │ │ │ ├── person_view.ex │ │ │ ├── episode_show_view.ex │ │ │ └── feed_view.ex │ │ ├── controllers │ │ │ ├── feed_controller.ex │ │ │ ├── person_controller.ex │ │ │ ├── show_note_controller.ex │ │ │ ├── resource_controller.ex │ │ │ ├── session_controller.ex │ │ │ └── episode_controller.ex │ │ ├── models │ │ │ ├── session.ex │ │ │ ├── episode_guest.ex │ │ │ ├── resource_author.ex │ │ │ ├── show_note.ex │ │ │ ├── user.ex │ │ │ ├── linked_account.ex │ │ │ ├── resource.ex │ │ │ ├── person.ex │ │ │ └── episode.ex │ │ ├── gettext.ex │ │ ├── router.ex │ │ ├── endpoint.ex │ │ ├── channels │ │ │ └── user_socket.ex │ │ ├── web.ex │ │ └── templates │ │ │ └── feed │ │ │ └── index.xml.eex │ ├── auth.ex │ ├── plugs │ │ ├── auth.ex │ │ └── url_format.ex │ ├── controller_errors.ex │ ├── oauth │ │ └── github_http_client.ex │ └── seeder.ex └── ember_weekend_api.ex ├── .travis.yml ├── priv ├── repo │ ├── migrations │ │ ├── 20160330015551_add_bio_to_people.exs │ │ ├── 20160330013523_add_tagline_to_people.exs │ │ ├── 20170314070102_add_length_to_episodes.exs │ │ ├── 20160223041228_create_user.exs │ │ ├── 20160303023449_create_resource.exs │ │ ├── 20160325181223_add_number_to_episodes.exs │ │ ├── 20160326163616_add_username_to_linked_accounts.exs │ │ ├── 20170424224656_add_note_to_show_note.exs │ │ ├── 20160227054711_create_person.exs │ │ ├── 20160223051953_create_session.exs │ │ ├── 20160222005644_create_episode.exs │ │ ├── 20170320000547_add_published_to_episode.exs │ │ ├── 20160223175047_create_linked_account.exs │ │ ├── 20160328221802_create_episode_guest.exs │ │ ├── 20160303024319_create_resource_author.exs │ │ └── 20160315221037_create_show_note.exs │ └── seeds.exs └── gettext │ ├── errors.pot │ └── en │ └── LC_MESSAGES │ └── errors.po ├── .gitignore ├── README.md ├── config ├── test.exs ├── prod.exs ├── config.exs └── dev.exs ├── .projections.json ├── mix.exs └── mix.lock /Procfile: -------------------------------------------------------------------------------- 1 | web: mix phoenix.server 2 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | {:ok, _} = Application.ensure_all_started(:ex_machina) 2 | ExUnit.start 3 | 4 | -------------------------------------------------------------------------------- /elixir_buildpack.config: -------------------------------------------------------------------------------- 1 | erlang_version=23.3.1 2 | elixir_version=1.11.4 3 | always_rebuild=false 4 | pre_compile="pwd" 5 | post_compile="pwd" 6 | runtime_path=/app 7 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Repo do 2 | use Ecto.Repo, 3 | otp_app: :ember_weekend_api, 4 | adapter: Ecto.Adapters.Postgres 5 | end 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | services: 3 | - postgresql 4 | elixir: 5 | - 1.4.2 6 | otp_release: 7 | - 19.3 8 | script: 9 | - mix ecto.create 10 | - mix test --trace 11 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/views/user_view.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.UserView do 2 | use EmberWeekendApi.Web, :view 3 | use JaSerializer.PhoenixView 4 | attributes [:name, :username] 5 | def type, do: "users" 6 | end 7 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160330015551_add_bio_to_people.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Repo.Migrations.AddBioToPeople do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:people) do 6 | add :bio, :text 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160330013523_add_tagline_to_people.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Repo.Migrations.AddTaglineToPeople do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:people) do 6 | add :tagline, :text 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20170314070102_add_length_to_episodes.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Repo.Migrations.AddLengthToEpisodes do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:episodes) do 6 | add :length, :integer 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160223041228_create_user.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Repo.Migrations.CreateUser do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:users) do 6 | add :name, :string 7 | add :username, :string 8 | 9 | timestamps() 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160303023449_create_resource.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Repo.Migrations.CreateResource do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:resources) do 6 | add :title, :string 7 | add :url, :string 8 | 9 | timestamps() 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160325181223_add_number_to_episodes.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Repo.Migrations.AddNumberToEpisodes do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:episodes) do 6 | add :number, :integer 7 | end 8 | create index(:episodes, [:number], unique: true) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/plugs/url_format_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.URLFormatTest do 2 | use EmberWeekendApi.Web.ConnCase 3 | 4 | test "adds json-api headers to *.json-api GET requests", %{conn: conn} do 5 | conn = get conn, "#{episode_path(conn, :index)}.json-api" 6 | assert conn.status == 200 7 | assert json_api_response(conn)["data"] == [] 8 | end 9 | 10 | end 11 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160326163616_add_username_to_linked_accounts.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Repo.Migrations.AddUsernameToLinkedAccounts do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:linked_accounts) do 6 | add :username, :string 7 | end 8 | create index(:linked_accounts, [:username,:provider], unique: true) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/repo/migrations/20170424224656_add_note_to_show_note.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Repo.Migrations.AddNoteToShowNote do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:show_notes) do 6 | add :note, :string 7 | end 8 | end 9 | 10 | def down do 11 | alter table(:show_notes) do 12 | remove(:note) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160227054711_create_person.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Repo.Migrations.CreatePerson do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:people) do 6 | add :name, :string 7 | add :handle, :string 8 | add :url, :string 9 | add :avatar_url, :string 10 | 11 | timestamps() 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160223051953_create_session.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Repo.Migrations.CreateSession do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:sessions) do 6 | add :token, :string 7 | add :user_id, references(:users, on_delete: :nothing) 8 | 9 | timestamps() 10 | end 11 | create index(:sessions, [:user_id]) 12 | 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160222005644_create_episode.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Repo.Migrations.CreateEpisode do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:episodes) do 6 | add :title, :string 7 | add :description, :string 8 | add :slug, :string 9 | add :release_date, :date 10 | add :filename, :string 11 | add :duration, :string 12 | 13 | timestamps() 14 | end 15 | 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /priv/repo/migrations/20170320000547_add_published_to_episode.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Repo.Migrations.AddPublishedToEpisode do 2 | use Ecto.Migration 3 | 4 | def up do 5 | alter table(:episodes) do 6 | add :published, :boolean, default: false, null: false 7 | end 8 | execute("UPDATE episodes SET published = true") 9 | end 10 | 11 | def down do 12 | alter table(:episodes) do 13 | remove(:published) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | 7 | # Generate on crash by the VM 8 | erl_crash.dump 9 | 10 | # The config/prod.secret.exs file by default contains sensitive 11 | # data and you should not commit it into version control. 12 | # 13 | # Alternatively, you may comment the line below and commit the 14 | # secrets file as long as you replace its contents by environment 15 | # variables. 16 | /config/prod.secret.exs 17 | /config/dev.secret.exs 18 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160223175047_create_linked_account.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Repo.Migrations.CreateLinkedAccount do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:linked_accounts) do 6 | add :provider, :string 7 | add :access_token, :string 8 | add :provider_id, :string 9 | add :user_id, references(:users, on_delete: :nothing) 10 | 11 | timestamps() 12 | end 13 | create index(:linked_accounts, [:user_id]) 14 | 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # EmberWeekendApi.Repo.insert!(%EmberWeekendApi.SomeModel{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | 13 | unless Mix.env == :test do 14 | EmberWeekendApi.Seeder.seed 15 | end 16 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160328221802_create_episode_guest.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Repo.Migrations.CreateEpisodeGuest do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:episode_guest) do 6 | add :episode_id, references(:episodes, on_delete: :nothing) 7 | add :guest_id, references(:people, on_delete: :nothing) 8 | 9 | timestamps() 10 | end 11 | create index(:episode_guest, [:episode_id]) 12 | create index(:episode_guest, [:guest_id]) 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160303024319_create_resource_author.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Repo.Migrations.CreateResourceAuthor do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:resource_authors) do 6 | add :author_id, references(:people, on_delete: :nothing) 7 | add :resource_id, references(:resources, on_delete: :nothing) 8 | 9 | timestamps() 10 | end 11 | create index(:resource_authors, [:author_id]) 12 | create index(:resource_authors, [:resource_id]) 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160315221037_create_show_note.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Repo.Migrations.CreateShowNote do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:show_notes) do 6 | add :time_stamp, :string 7 | add :resource_id, references(:resources, on_delete: :nothing) 8 | add :episode_id, references(:episodes, on_delete: :nothing) 9 | 10 | timestamps() 11 | end 12 | create index(:show_notes, [:resource_id]) 13 | create index(:show_notes, [:episode_id]) 14 | 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/models/user_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.UserTest do 2 | use EmberWeekendApi.Web.ModelCase 3 | 4 | alias EmberWeekendApi.Web.User 5 | 6 | @valid_attrs %{name: "some content", username: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = User.changeset(%User{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = User.changeset(%User{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/session_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.SessionTest do 2 | use EmberWeekendApi.Web.ModelCase 3 | 4 | alias EmberWeekendApi.Web.Session 5 | 6 | @valid_attrs %{token: "some content", user_id: 1} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Session.changeset(%Session{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Session.changeset(%Session{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/resource_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.ResourceTest do 2 | use EmberWeekendApi.Web.ModelCase 3 | 4 | alias EmberWeekendApi.Web.Resource 5 | 6 | @valid_attrs %{title: "some content", url: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Resource.changeset(%Resource{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Resource.changeset(%Resource{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/episode_guest_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.EpisodeGuestTest do 2 | use EmberWeekendApi.Web.ModelCase 3 | 4 | alias EmberWeekendApi.Web.EpisodeGuest 5 | 6 | @valid_attrs %{episode_id: 1, guest_id: 2} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = EpisodeGuest.changeset(%EpisodeGuest{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = EpisodeGuest.changeset(%EpisodeGuest{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/show_note_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.ShowNoteTest do 2 | use EmberWeekendApi.Web.ModelCase 3 | 4 | alias EmberWeekendApi.Web.ShowNote 5 | 6 | @valid_attrs %{time_stamp: "some content", episode_id: 1, resource_id: 1} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = ShowNote.changeset(%ShowNote{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = ShowNote.changeset(%ShowNote{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.ErrorView do 2 | 3 | def render("404.json-api", _assigns) do 4 | %{errors: [%{detail: "Route not found"}]} 5 | end 6 | 7 | def render("500.json-api", _assigns) do 8 | %{errors: [%{detail: "Internal server error"}]} 9 | end 10 | 11 | def render("errors.json-api", %{errors: errors}) do 12 | %{errors: errors} 13 | end 14 | 15 | def render("500.html", _assigns) do 16 | "Internal server error" 17 | end 18 | 19 | def template_not_found(_template, assigns) do 20 | render "500.json-api", assigns 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/views/session_view.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.SessionView do 2 | use EmberWeekendApi.Web, :view 3 | use JaSerializer.PhoenixView 4 | alias EmberWeekendApi.Web.UserView 5 | attributes [:token] 6 | 7 | has_one :user, 8 | type: "users", 9 | serializer: UserView, 10 | include: false 11 | 12 | def type, do: "sessions" 13 | 14 | def user(model, _conn) do 15 | case model.user do 16 | %Ecto.Association.NotLoaded{} -> 17 | model 18 | |> Ecto.assoc(:user) 19 | |> EmberWeekendApi.Repo.one 20 | other -> other 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/controllers/feed_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.FeedController do 2 | use EmberWeekendApi.Web, :controller 3 | alias EmberWeekendApi.Web.Episode 4 | 5 | def index(conn, _params) do 6 | episodes = from(e in Episode.published(Episode), 7 | order_by: [desc: e.number], 8 | left_join: show_notes in assoc(e, :show_notes), 9 | left_join: resource in assoc(show_notes, :resource), 10 | preload: [show_notes: {show_notes, resource: resource }] 11 | ) 12 | |> Repo.all 13 | render(conn, "index.xml", episodes: episodes) 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /test/models/person_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.PersonTest do 2 | use EmberWeekendApi.Web.ModelCase 3 | 4 | alias EmberWeekendApi.Web.Person 5 | 6 | @valid_attrs %{ 7 | avatar_url: "some content", 8 | tagline: "some content", 9 | handle: "some content", 10 | name: "some content", 11 | url: "some content" 12 | } 13 | @invalid_attrs %{} 14 | 15 | test "changeset with valid attributes" do 16 | changeset = Person.changeset(%Person{}, @valid_attrs) 17 | assert changeset.valid? 18 | end 19 | 20 | test "changeset with invalid attributes" do 21 | changeset = Person.changeset(%Person{}, @invalid_attrs) 22 | refute changeset.valid? 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/views/changeset_view.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.ChangesetView do 2 | use EmberWeekendApi.Web, :view 3 | 4 | @doc """ 5 | Traverses and translates changeset errors. 6 | 7 | See `Ecto.Changeset.traverse_errors/2` and 8 | `EmberWeekendApi.Web.ErrorHelpers.translate_error/1` for more details. 9 | """ 10 | def translate_errors(changeset) do 11 | Ecto.Changeset.traverse_errors(changeset, &translate_error/1) 12 | end 13 | 14 | def render("error.json", %{changeset: changeset}) do 15 | # When encoded, the changeset returns its errors 16 | # as a JSON object. So we just pass it forward. 17 | %{errors: translate_errors(changeset)} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ember Weekend API [![Build Status](https://travis-ci.org/ember-weekend/ember-weekend-api.svg?branch=master)](https://travis-ci.org/tsubery/ember-weekend) 2 | 3 | 4 | To start your Phoenix app: 5 | 6 | * Install dependencies with `mix deps.get` 7 | * Create and migrate your database with `mix ecto.create && mix ecto.migrate` 8 | * Start Phoenix endpoint with `mix phoenix.server` 9 | 10 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 11 | 12 | ## Postgres 13 | 14 | ``` 15 | $ docker run --rm --name ember-weekend-api -p 5432:5432 -e POSTGRES_PASSWORD=postgres -e POSTGRES_USER=postgres postgres 16 | ``` 17 | 18 | ``` 19 | $ mix run priv/repo/seeds.exs 20 | ``` 21 | -------------------------------------------------------------------------------- /test/models/linked_account_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.LinkedAccountTest do 2 | use EmberWeekendApi.Web.ModelCase 3 | 4 | alias EmberWeekendApi.Web.LinkedAccount 5 | 6 | @valid_attrs %{ 7 | username: "some_username", 8 | access_token: "some content", 9 | provider: "some content", 10 | provider_id: "some content", 11 | user_id: 1 12 | } 13 | @invalid_attrs %{} 14 | 15 | test "changeset with valid attributes" do 16 | changeset = LinkedAccount.changeset(%LinkedAccount{}, @valid_attrs) 17 | assert changeset.valid? 18 | end 19 | 20 | test "changeset with invalid attributes" do 21 | changeset = LinkedAccount.changeset(%LinkedAccount{}, @invalid_attrs) 22 | refute changeset.valid? 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/models/session.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.Session do 2 | use EmberWeekendApi.Web, :model 3 | 4 | schema "sessions" do 5 | field :token, :string 6 | belongs_to :user, EmberWeekendApi.Web.User 7 | 8 | timestamps([type: :utc_datetime_usec]) 9 | end 10 | 11 | @required_fields ~w(token user_id)a 12 | @optional_fields ~w()a 13 | 14 | @doc """ 15 | Creates a changeset based on the `struct` and `params`. 16 | 17 | If no params are provided, an invalid changeset is returned 18 | with no validation performed. 19 | """ 20 | def changeset(struct, params \\ %{}) do 21 | struct 22 | |> cast(params, @optional_fields ++ @required_fields) 23 | |> validate_required(@required_fields) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/support/oauth/github_stub.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Github.Stub do 2 | @name "Rick Sanchez" 3 | @username "tinyrick" 4 | @provider_id "1" 5 | 6 | def username(), do: @username 7 | def name(), do: @name 8 | def provider_id(), do: @provider_id 9 | 10 | def get_access_token(code, _) do 11 | case code do 12 | "valid_code" -> 13 | {:ok, %{ access_token: "valid_token" }} 14 | _ -> 15 | {:error, %{ message: "Invalid code" }} 16 | end 17 | end 18 | 19 | def get_user_data(access_token) do 20 | case access_token do 21 | "valid_token" -> 22 | {:ok, %{name: name(), username: username(), provider_id: provider_id()}} 23 | _ -> 24 | {:error, %{message: "Invalid token"}} 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/models/episode_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.EpisodeTest do 2 | use EmberWeekendApi.Web.ModelCase 3 | 4 | alias EmberWeekendApi.Web.Episode 5 | 6 | @valid_attrs %{number: 1, description: "some content", 7 | duration: "some content", filename: "some content", 8 | release_date: "2010-04-17", slug: "some content", 9 | title: "some content", published: true, length: 1} 10 | @invalid_attrs %{} 11 | 12 | test "changeset with valid attributes" do 13 | changeset = Episode.changeset(%Episode{}, @valid_attrs) 14 | assert changeset.errors == [] 15 | assert changeset.valid? 16 | end 17 | 18 | test "changeset with invalid attributes" do 19 | changeset = Episode.changeset(%Episode{}, @invalid_attrs) 20 | refute changeset.valid? 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/models/episode_guest.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.EpisodeGuest do 2 | use EmberWeekendApi.Web, :model 3 | 4 | schema "episode_guest" do 5 | belongs_to :episode, EmberWeekendApi.Web.Episode 6 | belongs_to :guest, EmberWeekendApi.Web.Person 7 | 8 | timestamps([type: :utc_datetime_usec]) 9 | end 10 | 11 | @required_fields ~w(episode_id guest_id)a 12 | @optional_fields ~w()a 13 | 14 | @doc """ 15 | Creates a changeset based on the `struct` and `params`. 16 | 17 | If no params are provided, an invalid changeset is returned 18 | with no validation performed. 19 | """ 20 | def changeset(struct, params \\ %{}) do 21 | struct 22 | |> cast(params, @optional_fields ++ @required_fields) 23 | |> validate_required(@required_fields) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/models/resource_author.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.ResourceAuthor do 2 | use EmberWeekendApi.Web, :model 3 | 4 | schema "resource_authors" do 5 | belongs_to :author, EmberWeekendApi.Web.Person 6 | belongs_to :resource, EmberWeekendApi.Web.Resource 7 | 8 | timestamps([type: :utc_datetime_usec]) 9 | end 10 | 11 | @required_fields ~w(author_id resource_id)a 12 | @optional_fields ~w()a 13 | 14 | @doc """ 15 | Creates a changeset based on the `struct` and `params`. 16 | 17 | If no params are provided, an invalid changeset is returned 18 | with no validation performed. 19 | """ 20 | def changeset(struct, params \\ %{}) do 21 | struct 22 | |> cast(params, @optional_fields ++ @required_fields) 23 | |> validate_required(@required_fields) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](http://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import EmberWeekendApi.Web.Gettext 9 | 10 | # Simple translation 11 | gettext "Here is the string to translate" 12 | 13 | # Plural translation 14 | ngettext "Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3 17 | 18 | # Domain-based translation 19 | dgettext "errors", "Here is the error message to translate" 20 | 21 | See the [Gettext Docs](http://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :ember_weekend_api 24 | end 25 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/models/show_note.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.ShowNote do 2 | use EmberWeekendApi.Web, :model 3 | 4 | schema "show_notes" do 5 | field :time_stamp, :string 6 | field :note, :string 7 | belongs_to :resource, EmberWeekendApi.Web.Resource 8 | belongs_to :episode, EmberWeekendApi.Web.Episode 9 | 10 | timestamps([type: :utc_datetime_usec]) 11 | end 12 | 13 | @required_fields ~w(time_stamp episode_id)a 14 | @optional_fields ~w(note resource_id)a 15 | 16 | @doc """ 17 | Creates a changeset based on the `struct` and `params`. 18 | 19 | If no params are provided, an invalid changeset is returned 20 | with no validation performed. 21 | """ 22 | def changeset(struct, params \\ %{}) do 23 | struct 24 | |> cast(params, @optional_fields ++ @required_fields) 25 | |> validate_required(@required_fields) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/models/user.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.User do 2 | use EmberWeekendApi.Web, :model 3 | alias EmberWeekendApi.Web.LinkedAccount 4 | alias EmberWeekendApi.Web.Session 5 | 6 | schema "users" do 7 | field :name, :string 8 | field :username, :string 9 | has_many :linked_accounts, LinkedAccount 10 | has_many :sessions, Session 11 | 12 | timestamps([type: :utc_datetime_usec]) 13 | end 14 | 15 | @required_fields ~w(name username)a 16 | @optional_fields ~w()a 17 | 18 | @doc """ 19 | Creates a changeset based on the `struct` and `params`. 20 | 21 | If no params are provided, an invalid changeset is returned 22 | with no validation performed. 23 | """ 24 | def changeset(struct, params \\ %{}) do 25 | struct 26 | |> cast(params, @optional_fields ++ @required_fields) 27 | |> validate_required(@required_fields) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/models/linked_account.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.LinkedAccount do 2 | use EmberWeekendApi.Web, :model 3 | 4 | schema "linked_accounts" do 5 | field :username, :string 6 | field :provider, :string 7 | field :access_token, :string 8 | field :provider_id, :string 9 | belongs_to :user, EmberWeekendApi.Web.User 10 | 11 | timestamps([type: :utc_datetime_usec]) 12 | end 13 | 14 | @required_fields ~w(provider access_token provider_id user_id username)a 15 | @optional_fields ~w()a 16 | 17 | @doc """ 18 | Creates a changeset based on the `struct` and `params`. 19 | 20 | If no params are provided, an invalid changeset is returned 21 | with no validation performed. 22 | """ 23 | def changeset(struct, params \\ %{}) do 24 | struct 25 | |> cast(params, @optional_fields ++ @required_fields) 26 | |> validate_required(@required_fields) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/models/resource.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.Resource do 2 | use EmberWeekendApi.Web, :model 3 | alias EmberWeekendApi.Web.ResourceAuthor 4 | alias EmberWeekendApi.Web.ShowNote 5 | 6 | schema "resources" do 7 | field :title, :string 8 | field :url, :string 9 | has_many :resource_authors, ResourceAuthor 10 | has_many :authors, through: [:resource_authors, :author] 11 | has_many :show_notes, ShowNote 12 | 13 | timestamps([type: :utc_datetime_usec]) 14 | end 15 | 16 | @required_fields ~w(title url)a 17 | @optional_fields ~w()a 18 | 19 | @doc """ 20 | Creates a changeset based on the `model` and `params`. 21 | 22 | If no params are provided, an invalid changeset is returned 23 | with no validation performed. 24 | """ 25 | def changeset(model, params \\ %{}) do 26 | model 27 | |> cast(params, @optional_fields ++ @required_fields) 28 | |> validate_required(@required_fields) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.Router do 2 | use EmberWeekendApi.Web, :router 3 | 4 | pipeline :api do 5 | plug :accepts, ["json-api"] 6 | plug JaSerializer.ContentTypeNegotiation 7 | plug JaSerializer.Deserializer 8 | plug EmberWeekendApi.Plugs.Auth 9 | end 10 | 11 | scope "/api", EmberWeekendApi.Web do 12 | pipe_through :api 13 | 14 | resources "/episodes", EpisodeController, only: [:index, :show, :update, :delete, :create] 15 | resources "/people", PersonController, only: [:index, :show, :update, :delete, :create] 16 | resources "/resources", ResourceController, only: [:index, :show, :update, :delete, :create] 17 | resources "/show-notes", ShowNoteController, only: [:index, :show, :update, :delete, :create] 18 | delete "/sessions/:token", SessionController, :delete 19 | post "/sessions", SessionController, :create 20 | end 21 | 22 | get "/feed.xml", EmberWeekendApi.Web.FeedController, :index 23 | end 24 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/views/episode_view.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.EpisodeView do 2 | use EmberWeekendApi.Web, :view 3 | use JaSerializer.PhoenixView 4 | 5 | attributes [ 6 | :number, :title, :description, :slug, 7 | :release_date, :filename, :duration, :published, :length 8 | ] 9 | 10 | def type, do: "episodes" 11 | 12 | def release_date(episode, _conn) do 13 | date = episode.release_date 14 | Enum.join [date.year, date.month, date.day], "-" 15 | end 16 | 17 | def show_notes(model, _conn) do 18 | case model.show_notes do 19 | %Ecto.Association.NotLoaded{} -> 20 | model 21 | |> Ecto.assoc(:show_notes) 22 | |> EmberWeekendApi.Repo.all 23 | other -> other 24 | end 25 | end 26 | 27 | def guests(model, _conn) do 28 | case model.guests do 29 | %Ecto.Association.NotLoaded{} -> 30 | model 31 | |> Ecto.assoc(:guests) 32 | |> EmberWeekendApi.Repo.all 33 | other -> other 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :ember_weekend_api 3 | 4 | socket "/socket", EmberWeekendApi.Web.UserSocket 5 | 6 | # Code reloading can be explicitly enabled under the 7 | # :code_reloader configuration of your endpoint. 8 | if code_reloading? do 9 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 10 | plug Phoenix.LiveReloader 11 | plug Phoenix.CodeReloader 12 | end 13 | 14 | plug Plug.RequestId 15 | plug Plug.Logger 16 | 17 | plug EmberWeekendApi.Plugs.URLFormat 18 | plug Plug.Parsers, 19 | parsers: [:urlencoded, :multipart, :json], 20 | pass: ["*/*"], 21 | json_decoder: Poison 22 | 23 | plug Plug.MethodOverride 24 | plug Plug.Head 25 | 26 | plug Plug.Session, 27 | store: :cookie, 28 | key: "_ember_weekend_api_key", 29 | signing_salt: "Z63alZTm" 30 | 31 | plug Corsica, origins: "*", allow_headers: ["accept", "content-type", "authorization"] 32 | plug EmberWeekendApi.Web.Router 33 | end 34 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :ember_weekend_api, EmberWeekendApi.Web.Endpoint, 6 | secret_key_base: "iSFpZGaE90m7xArjQY/hjEBAxC9Wy8NXMYAQQ+OaSKEd4epRi4VJxQXtCxBODwOy", 7 | http: [port: 4001], 8 | server: false 9 | 10 | # Print only warnings and errors during test 11 | config :logger, level: :warn 12 | 13 | # Configure your database 14 | if Map.has_key?(System.get_env(), "DATABASE_URL") do 15 | config :ember_weekend_api, EmberWeekendApi.Repo, 16 | adapter: Ecto.Adapters.Postgres, 17 | url: System.get_env("DATABASE_URL"), 18 | pool: Ecto.Adapters.SQL.Sandbox 19 | else 20 | config :ember_weekend_api, EmberWeekendApi.Repo, 21 | adapter: Ecto.Adapters.Postgres, 22 | database: "ember_weekend_api_test", 23 | hostname: "localhost", 24 | username: "postgres", 25 | password: "postgres", 26 | pool: Ecto.Adapters.SQL.Sandbox 27 | end 28 | 29 | config :ember_weekend_api, :github_api, EmberWeekendApi.Github.Stub 30 | config :ember_weekend_api, :admins, ["tinyrick"] 31 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/auth.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Auth do 2 | alias EmberWeekendApi.Web.ControllerErrors 3 | 4 | def admin?(conn) do 5 | case conn.assigns[:current_user] do 6 | nil -> false 7 | user -> user 8 | |> linked_accounts 9 | |> Enum.map(fn(account) -> account.username end) 10 | |> Enum.map_reduce(false, fn(username, admin) -> 11 | is_admin = Enum.member?(admin_usernames(), username) 12 | { is_admin, admin || is_admin } 13 | end) 14 | |> elem(1) 15 | end 16 | end 17 | 18 | def admin_usernames do 19 | Application.get_env(:ember_weekend_api, :admins) 20 | end 21 | 22 | def linked_accounts(user) do 23 | case user.linked_accounts do 24 | %Ecto.Association.NotLoaded{} -> 25 | user 26 | |> Ecto.assoc(:linked_accounts) 27 | |> EmberWeekendApi.Repo.all 28 | other -> other 29 | end 30 | end 31 | 32 | def authenticate(conn, :admin) do 33 | if admin?(conn) do 34 | conn 35 | else 36 | ControllerErrors.unauthorized(conn) 37 | end 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/views/resource_view.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.ResourceView do 2 | use EmberWeekendApi.Web, :view 3 | use JaSerializer.PhoenixView 4 | alias EmberWeekendApi.Web.PersonView 5 | alias EmberWeekendApi.Web.ShowNoteView 6 | 7 | location "/api/resources/:id" 8 | attributes [:title, :url] 9 | 10 | has_many :authors, 11 | type: "people", 12 | serializer: PersonView, 13 | include: false 14 | 15 | has_many :show_notes, 16 | type: "show-notes", 17 | serializer: ShowNoteView, 18 | include: false 19 | 20 | def type, do: "resources" 21 | 22 | def authors(model, _conn) do 23 | case model.authors do 24 | %Ecto.Association.NotLoaded{} -> 25 | model 26 | |> Ecto.assoc(:authors) 27 | |> EmberWeekendApi.Repo.all 28 | other -> other 29 | end 30 | end 31 | 32 | def show_notes(model, _conn) do 33 | case model.show_notes do 34 | %Ecto.Association.NotLoaded{} -> 35 | model 36 | |> Ecto.assoc(:show_notes) 37 | |> EmberWeekendApi.Repo.all 38 | other -> other 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/views/show_note_view.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.ShowNoteView do 2 | use EmberWeekendApi.Web, :view 3 | use JaSerializer.PhoenixView 4 | alias EmberWeekendApi.Web.ResourceView 5 | alias EmberWeekendApi.Web.EpisodeView 6 | 7 | location "/api/show-notes/:id" 8 | attributes [:time_stamp, :note] 9 | 10 | has_one :resource, 11 | type: "resources", 12 | serializer: ResourceView, 13 | include: false 14 | 15 | has_one :episode, 16 | type: "episodes", 17 | serializer: EpisodeView, 18 | include: false 19 | 20 | def type, do: "show-notes" 21 | 22 | def resource(model, _conn) do 23 | case model.resource do 24 | %Ecto.Association.NotLoaded{} -> 25 | model 26 | |> Ecto.assoc(:resource) 27 | |> EmberWeekendApi.Repo.one 28 | other -> other 29 | end 30 | end 31 | 32 | def episode(model, _conn) do 33 | case model.episode do 34 | %Ecto.Association.NotLoaded{} -> 35 | model 36 | |> Ecto.assoc(:episode) 37 | |> EmberWeekendApi.Repo.one 38 | other -> other 39 | end 40 | end 41 | 42 | end 43 | -------------------------------------------------------------------------------- /lib/ember_weekend_api.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec, warn: false 8 | 9 | children = [ 10 | # Start the endpoint when the application starts 11 | supervisor(EmberWeekendApi.Web.Endpoint, []), 12 | # Start the Ecto repository 13 | supervisor(EmberWeekendApi.Repo, []), 14 | # Here you could define other workers and supervisors as children 15 | # worker(EmberWeekendApi.Worker, [arg1, arg2, arg3]), 16 | ] 17 | 18 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 19 | # for other strategies and supported options 20 | opts = [strategy: :one_for_one, name: EmberWeekendApi.Supervisor] 21 | Supervisor.start_link(children, opts) 22 | end 23 | 24 | # Tell Phoenix to update the endpoint configuration 25 | # whenever the application is updated. 26 | def config_change(changed, _new, removed) do 27 | EmberWeekendApi.Web.Endpoint.config_change(changed, removed) 28 | :ok 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/plugs/auth.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Plugs.Auth do 2 | import Plug.Conn 3 | import Phoenix.Controller 4 | alias EmberWeekendApi.Repo 5 | alias EmberWeekendApi.Web.User 6 | alias EmberWeekendApi.Web.Session 7 | 8 | def init(default), do: default 9 | 10 | def call(conn, _) do 11 | header = get_req_header(conn, "authorization") 12 | |> List.first || "" 13 | case Regex.named_captures(~r/token\W+(?.*)/i, header) do 14 | %{"token" => token} -> 15 | case Repo.get_by(Session, token: token) do 16 | nil -> 17 | conn 18 | |> put_view(EmberWeekendApi.Web.ErrorView) 19 | |> put_status(:unauthorized) 20 | |> render(:errors, errors: [%{ 21 | status: "401", 22 | source: %{pointer: "/token"}, 23 | title: "Unauthorized", 24 | detail: "The authentication token is invalid"}]) 25 | |> halt() 26 | session -> 27 | user = Repo.get(User, session.user_id) 28 | assign(conn, :current_user, user) 29 | end 30 | _ -> conn 31 | end 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :ember_weekend_api, EmberWeekendApi.Web.Endpoint, 4 | http: [port: {:system, "PORT"}], 5 | url: [scheme: "https", host: "ember-weekend-api.herokuapp.com", port: 443], 6 | force_ssl: [rewrite_on: [:x_forwarded_proto]], 7 | secret_key_base: Map.fetch!(System.get_env(), "SECRET_KEY_BASE") 8 | 9 | config :ember_weekend_api, EmberWeekendApi.Repo, 10 | adapter: Ecto.Adapters.Postgres, 11 | url: Map.fetch!(System.get_env(), "DATABASE_URL"), 12 | pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), 13 | ssl: true 14 | 15 | config :logger, level: :info 16 | 17 | config :ember_weekend_api, :github_api, EmberWeekendApi.Github.HTTPClient 18 | 19 | admins = Map.fetch!(System.get_env(), "ADMINS") 20 | |> String.replace(~r/\s/,"") 21 | |> String.split(",") 22 | 23 | config :ember_weekend_api, :admins, admins 24 | 25 | config :ember_weekend_api, 26 | EmberWeekendApi.Github, 27 | client_id: Map.fetch!(System.get_env(), "GITHUB_CLIENT_ID"), 28 | client_secret: Map.fetch!(System.get_env(), "GITHUB_CLIENT_SECRET"), 29 | redirect_uri: Map.fetch!(System.get_env(), "GITHUB_REDIRECT_URI") 30 | 31 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/controller_errors.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.ControllerErrors do 2 | alias Phoenix.Controller 3 | 4 | def model_name(conn, model_name) do 5 | Plug.Conn.assign(conn, :model_name, model_name) 6 | end 7 | 8 | def model_name(conn) do 9 | to_string conn.assigns[:model_name] 10 | end 11 | 12 | def unauthorized(conn) do 13 | action = Atom.to_string Controller.action_name(conn) 14 | model_name = model_name(conn) 15 | conn 16 | |> Plug.Conn.put_status(:unauthorized) 17 | |> Controller.render(:errors, data: %{ 18 | status: 401, 19 | source: %{ pointer: "/token" }, 20 | title: "Unauthorized", 21 | detail: "Must provide auth token to #{action} #{model_name}" 22 | }) 23 | |> Plug.Conn.halt() 24 | end 25 | 26 | def not_found(conn) do 27 | model_name = model_name(conn) 28 | conn 29 | |> Plug.Conn.put_status(:not_found) 30 | |> Controller.render(:errors, data: %{ 31 | source: %{ pointer: "/id" }, 32 | status: 404, 33 | title: "Not found", 34 | detail: "No #{model_name} found for the given id" 35 | }) 36 | |> Plug.Conn.halt() 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | imports other functionality to make it easier 8 | to build and query models. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | alias EmberWeekendApi.Repo 24 | import Ecto 25 | import Ecto.Changeset 26 | import Ecto.Query, only: [from: 1, from: 2] 27 | 28 | 29 | # The default endpoint for testing 30 | @endpoint EmberWeekendApi.Web.Endpoint 31 | end 32 | end 33 | 34 | setup tags do 35 | unless tags[:async] do 36 | Ecto.Adapters.SQL.Sandbox.checkout(EmberWeekendApi.Repo) 37 | end 38 | 39 | :ok 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | if error = form.errors[field] do 13 | content_tag :span, translate_error(error), class: "help-block" 14 | end 15 | end 16 | 17 | @doc """ 18 | Translates an error message using gettext. 19 | """ 20 | def translate_error({msg, opts}) do 21 | # Because error messages were defined within Ecto, we must 22 | # call the Gettext module passing our Gettext backend. We 23 | # also use the "errors" domain as translations are placed 24 | # in the errors.po file. On your own code and templates, 25 | # this could be written simply as: 26 | # 27 | # dngettext "errors", "1 file", "%{count} files", count 28 | # 29 | Gettext.dngettext(EmberWeekendApi.Web.Gettext, "errors", msg, msg, opts[:count], opts) 30 | end 31 | 32 | def translate_error(msg) do 33 | Gettext.dgettext(EmberWeekendApi.Web.Gettext, "errors", msg) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/views/person_view.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.PersonView do 2 | use EmberWeekendApi.Web, :view 3 | use JaSerializer.PhoenixView 4 | alias EmberWeekendApi.Web.Person 5 | alias EmberWeekendApi.Web.EpisodeView 6 | alias EmberWeekendApi.Web.ResourceView 7 | 8 | location "/api/people/:id" 9 | attributes [:name, :handle, :url, :avatar_url, :tagline, :bio] 10 | 11 | def type, do: "people" 12 | 13 | def id(%Person{id: id}, _conn), do: id 14 | 15 | has_many :episodes, 16 | type: "episodes", 17 | serializer: EpisodeView, 18 | include: false 19 | 20 | has_many :resources, 21 | type: "resources", 22 | serializer: ResourceView, 23 | include: false 24 | 25 | def episodes(model, _conn) do 26 | case model.episodes do 27 | %Ecto.Association.NotLoaded{} -> 28 | model 29 | |> Ecto.assoc(:episodes) 30 | |> EmberWeekendApi.Repo.all 31 | other -> other 32 | end 33 | end 34 | 35 | def resources(model, _conn) do 36 | case model.resources do 37 | %Ecto.Association.NotLoaded{} -> 38 | model 39 | |> Ecto.assoc(:resources) 40 | |> EmberWeekendApi.Repo.all 41 | other -> other 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/plugs/url_format.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Plugs.URLFormat do 2 | 3 | def init(default), do: default 4 | 5 | def call(conn, _default) do 6 | conn.path_info |> List.last |> String.split(".") |> Enum.reverse |> case do 7 | [ _ ] -> conn 8 | [ format | fragments ] -> 9 | new_path = fragments |> Enum.reverse |> Enum.join(".") 10 | path_fragments = List.replace_at conn.path_info, -1, new_path 11 | new_headers = conn.req_headers 12 | |> List.keydelete("accept", 0) 13 | |> List.keydelete("content-type", 0) 14 | case format_to_mime(format) do 15 | nil -> conn 16 | mime -> 17 | new_headers = new_headers ++ [ 18 | {"accept", mime}, 19 | {"content-type", mime}] 20 | %{conn | path_info: path_fragments, req_headers: new_headers} 21 | end 22 | end 23 | end 24 | 25 | defp format_to_mime(format) do 26 | mimes = Application.get_env(:mime, :types) 27 | Map.values(mimes) |> Enum.find_index(fn(m) -> m == [format] end) |> case do 28 | nil -> nil 29 | index -> 30 | {:ok, mime} = Enum.fetch(Map.keys(mimes), index) 31 | mime 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/models/person.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.Person do 2 | use EmberWeekendApi.Web, :model 3 | alias EmberWeekendApi.Web.EpisodeGuest 4 | alias EmberWeekendApi.Web.ResourceAuthor 5 | 6 | schema "people" do 7 | field :name, :string 8 | field :handle, :string 9 | field :url, :string 10 | field :avatar_url, :string 11 | field :tagline, :string 12 | field :bio, :string 13 | has_many :episode_guests, EpisodeGuest, foreign_key: :guest_id 14 | has_many :episodes, through: [:episode_guests, :episode] 15 | has_many :resource_authors, ResourceAuthor, foreign_key: :author_id 16 | has_many :resources, through: [:resource_authors, :resource] 17 | 18 | timestamps([type: :utc_datetime_usec]) 19 | end 20 | 21 | @required_fields ~w(name url)a 22 | @optional_fields ~w(tagline bio handle avatar_url)a 23 | 24 | @doc """ 25 | Creates a changeset based on the `struct` and `params`. 26 | 27 | If no params are provided, an invalid changeset is returned 28 | with no validation performed. 29 | """ 30 | def changeset(struct, params \\ %{}) do 31 | struct 32 | |> cast(params, @optional_fields ++ @required_fields) 33 | |> validate_required(@required_fields) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /.projections.json: -------------------------------------------------------------------------------- 1 | { 2 | "lib/ember_weekend_api/web/controllers/*_controller.ex": { 3 | "type": "controller", 4 | "alternate": "test/controllers/{}_controller_test.exs" 5 | }, 6 | "lib/ember_weekend_api/web/models/*.ex": { 7 | "type": "schema", 8 | "alternate": "test/models/{}_test.exs", 9 | }, 10 | "lib/ember_weekend_api/web/channels/*_channel.ex": { 11 | "type": "channel", 12 | "alternate": "test/channels/{}_channel_test.exs", 13 | }, 14 | "lib/ember_weekend_api/web/views/*_view.ex": { 15 | "type": "view", 16 | "alternate": "test/views/{}_view_test.exs", 17 | }, 18 | "test/*_test.exs": { 19 | "type": "test", 20 | "alternate": "lib/ember_weekend_api/web/{}.ex", 21 | }, 22 | "lib/ember_weekend_api/web/templates/*.html.eex": { 23 | "type": "template", 24 | "alternate": "lib/ember_weekend_api/web/views/{dirname|basename}_view.ex" 25 | }, 26 | "assets/css/*.css": { "type": "stylesheet" }, 27 | "assets/js/*.js": { "type": "javascript" }, 28 | "lib/ember_weekend_api/web/router.ex": { "type": "router" }, 29 | "config/*.exs": { "type": "config" }, 30 | "mix.exs": { "type": "mix" }, 31 | "*": { "start": "mix phoenix.server", 32 | "path": "lib/ember_weekend_api/web/**" 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | # Configures the endpoint 9 | config :ember_weekend_api, EmberWeekendApi.Web.Endpoint, 10 | url: [host: "localhost"], 11 | root: Path.dirname(__DIR__), 12 | render_errors: [view: EmberWeekendApi.Web.ErrorView, accepts: ~w(html json-api)], 13 | pubsub: [name: EmberWeekendApi.PubSub, 14 | adapter: Phoenix.PubSub.PG2] 15 | 16 | # Configures Elixir's Logger 17 | config :logger, :console, 18 | format: "$time $metadata[$level] $message\n", 19 | metadata: [:request_id] 20 | 21 | config :phoenix, :format_encoders, "json-api": Poison 22 | 23 | config :ember_weekend_api, ecto_repos: [EmberWeekendApi.Repo] 24 | 25 | # Import environment specific config. This must remain at the bottom 26 | # of this file so it overrides the configuration defined above. 27 | import_config "#{Mix.env}.exs" 28 | 29 | # Configure phoenix generators 30 | config :phoenix, :generators, 31 | migration: true, 32 | binary_id: false 33 | 34 | config :mime, :types, %{ 35 | "application/vnd.api+json" => ["json-api"] 36 | } 37 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/views/episode_show_view.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.EpisodeShowView do 2 | use EmberWeekendApi.Web, :view 3 | use JaSerializer.PhoenixView 4 | alias EmberWeekendApi.Web.ShowNoteView 5 | alias EmberWeekendApi.Web.PersonView 6 | 7 | attributes [ 8 | :number, :title, :description, :slug, 9 | :release_date, :filename, :duration, :published, :length 10 | ] 11 | 12 | has_many :show_notes, 13 | type: "show-notes", 14 | serializer: ShowNoteView, 15 | include: false 16 | 17 | has_many :guests, 18 | type: "people", 19 | serializer: PersonView, 20 | include: false 21 | 22 | def type, do: "episodes" 23 | 24 | def release_date(episode, _conn) do 25 | date = episode.release_date 26 | Enum.join [date.year, date.month, date.day], "-" 27 | end 28 | 29 | def show_notes(model, _conn) do 30 | case model.show_notes do 31 | %Ecto.Association.NotLoaded{} -> 32 | model 33 | |> Ecto.assoc(:show_notes) 34 | |> EmberWeekendApi.Repo.all 35 | other -> other 36 | end 37 | end 38 | 39 | def guests(model, _conn) do 40 | case model.guests do 41 | %Ecto.Association.NotLoaded{} -> 42 | model 43 | |> Ecto.assoc(:guests) 44 | |> EmberWeekendApi.Repo.all 45 | other -> other 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "rooms:*", EmberWeekendApi.RoomChannel 6 | 7 | ## Transports 8 | transport :websocket, Phoenix.Transports.WebSocket 9 | # transport :longpoll, Phoenix.Transports.LongPoll 10 | 11 | # Socket params are passed from the client and can 12 | # be used to verify and authenticate a user. After 13 | # verification, you can put default assigns into 14 | # the socket that will be set for all channels, ie 15 | # 16 | # {:ok, assign(socket, :user_id, verified_user_id)} 17 | # 18 | # To deny connection, return `:error`. 19 | # 20 | # See `Phoenix.Token` documentation for examples in 21 | # performing token verification on connect. 22 | def connect(_params, socket) do 23 | {:ok, socket} 24 | end 25 | 26 | # Socket id's are topics that allow you to identify all sockets for a given user: 27 | # 28 | # def id(socket), do: "users_socket:#{socket.assigns.user_id}" 29 | # 30 | # Would allow you to broadcast a "disconnect" event and terminate 31 | # all active sockets and channels for a given user: 32 | # 33 | # EmberWeekendApi.Web.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{}) 34 | # 35 | # Returning `nil` makes this socket anonymous. 36 | def id(_socket), do: nil 37 | end 38 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/models/episode.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.Episode do 2 | use EmberWeekendApi.Web, :model 3 | alias EmberWeekendApi.Web.ShowNote 4 | alias EmberWeekendApi.Web.EpisodeGuest 5 | import Ecto.Query, only: [from: 2] 6 | 7 | schema "episodes" do 8 | field :number, :integer 9 | field :length, :integer 10 | field :title, :string 11 | field :description, :string 12 | field :slug, :string 13 | field :release_date, :date 14 | field :filename, :string 15 | field :duration, :string 16 | field :published, :boolean 17 | has_many :show_notes, ShowNote 18 | has_many :episode_guests, EpisodeGuest 19 | has_many :guests, through: [:episode_guests, :guest] 20 | 21 | timestamps([type: :utc_datetime_usec]) 22 | end 23 | 24 | @required_fields ~w(number title description slug release_date 25 | filename duration published length)a 26 | @optional_fields ~w()a 27 | 28 | @doc """ 29 | Creates a changeset based on the `struct` and `params`. 30 | 31 | If no params are provided, an invalid changeset is returned 32 | with no validation performed. 33 | """ 34 | def changeset(struct, params \\ %{}) do 35 | struct 36 | |> cast(params, @optional_fields ++ @required_fields) 37 | |> validate_required(@required_fields) 38 | end 39 | 40 | def published(query) do 41 | from(e in query, 42 | where: e.published == true) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/controllers/feed_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.FeedControllerTest do 2 | use EmberWeekendApi.Web.ConnCase 3 | 4 | import EmberWeekendApi.Factory 5 | 6 | setup %{conn: conn} do 7 | {:ok, conn: conn} 8 | end 9 | 10 | test "renders an XML RSS feed", %{conn: conn} do 11 | conn = get conn, feed_path(conn, :index) 12 | {:ok, feed, _} = FeederEx.parse(conn.resp_body) 13 | assert feed.title == "Ember Weekend" 14 | assert Enum.count(feed.entries) == 0 15 | end 16 | 17 | @tag :skip 18 | test "feed contains episodes", %{conn: conn} do 19 | db_episodes = insert_list(3, :episode, published: true) 20 | assert length(db_episodes) == 3 21 | conn = get conn, feed_path(conn, :index) 22 | {:ok, feed, _} = FeederEx.parse(conn.resp_body) 23 | assert Enum.count(feed.entries) == 3 24 | end 25 | 26 | @tag :skip 27 | test "feed items have episode attributes", %{conn: conn} do 28 | insert(:episode, %{ 29 | number: 1, 30 | title: "first episode", 31 | description: "description", 32 | slug: "first-episode", 33 | published: true, 34 | }) 35 | conn = get conn, feed_path(conn, :index) 36 | {:ok, feed, _} = FeederEx.parse(conn.resp_body) 37 | assert item = List.first(feed.entries) 38 | assert item.title == "Episode 1: first episode" 39 | assert item.subtitle == "description" 40 | assert item.link == "https://emberweekend.com/first-episode" 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with brunch.io to recompile .js and .css sources. 9 | config :ember_weekend_api, EmberWeekendApi.Web.Endpoint, 10 | http: [port: 4000], 11 | debug_errors: true, 12 | code_reloader: true, 13 | check_origin: false, 14 | watchers: [] 15 | 16 | # Watch static and templates for browser reloading. 17 | config :ember_weekend_api, EmberWeekendApi.Web.Endpoint, 18 | secret_key_base: "iSFpZGaE90m7xArjQY/hjEBAxC9Wy8NXMYAQQ+OaSKEd4epRi4VJxQXtCxBODwOy", 19 | live_reload: [ 20 | patterns: [ 21 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, 22 | ~r{priv/gettext/.*(po)$}, 23 | ~r{lib/ember_weekend_api/web/views/.*(ex)$}, 24 | ~r{lib/ember_weekend_api/web/templates/.*(eex)$} 25 | ] 26 | ] 27 | 28 | # Do not include metadata nor timestamps in development logs 29 | config :logger, :console, format: "[$level] $message\n" 30 | 31 | # Set a higher stacktrace during development. 32 | # Do not configure such in production as keeping 33 | # and calculating stacktraces is usually expensive. 34 | config :phoenix, :stacktrace_depth, 20 35 | 36 | # Configure your database 37 | config :ember_weekend_api, EmberWeekendApi.Repo, 38 | adapter: Ecto.Adapters.Postgres, 39 | database: "ember_weekend_api_dev", 40 | hostname: "localhost", 41 | username: "postgres", 42 | password: "postgres", 43 | pool_size: 10 44 | 45 | config :ember_weekend_api, :github_api, EmberWeekendApi.Github.HTTPClient 46 | 47 | admins = Map.get(System.get_env(), "ADMINS", "code0100fun, rondale-sc") 48 | |> String.replace(~r/\s/,"") 49 | |> String.split(",") 50 | 51 | config :ember_weekend_api, :admins, admins 52 | 53 | if File.exists?("config/dev.secret.exs") do 54 | import_config "dev.secret.exs" 55 | end 56 | -------------------------------------------------------------------------------- /test/support/model_case.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.ModelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | model tests. 5 | 6 | You may define functions here to be used as helpers in 7 | your model tests. See `errors_on/2`'s definition as reference. 8 | 9 | Finally, if the test case interacts with the database, 10 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | using do 18 | quote do 19 | alias EmberWeekendApi.Repo 20 | 21 | import Ecto 22 | import Ecto.Changeset 23 | import Ecto.Query, only: [from: 1, from: 2] 24 | import EmberWeekendApi.Web.ModelCase 25 | end 26 | end 27 | 28 | setup tags do 29 | 30 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(EmberWeekendApi.Repo) 31 | 32 | unless tags[:async] do 33 | Ecto.Adapters.SQL.Sandbox.mode(EmberWeekendApi.Repo, {:shared, self()}) 34 | end 35 | 36 | :ok 37 | end 38 | 39 | @doc """ 40 | Helper for returning list of errors in model when passed certain data. 41 | 42 | ## Examples 43 | 44 | Given a User model that lists `:name` as a required field and validates 45 | `:password` to be safe, it would return: 46 | 47 | iex> errors_on(%User{}, %{password: "password"}) 48 | [password: "is unsafe", name: "is blank"] 49 | 50 | You could then write your assertion like: 51 | 52 | assert {:password, "is unsafe"} in errors_on(%User{}, %{password: "password"}) 53 | 54 | You can also create the changeset manually and retrieve the errors 55 | field directly: 56 | 57 | iex> changeset = User.changeset(%User{}, password: "password") 58 | iex> {:password, "is unsafe"} in changeset.errors 59 | true 60 | """ 61 | def errors_on(model, data) do 62 | model.__struct__.changeset(model, data).errors 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :ember_weekend_api, 6 | version: "0.0.1", 7 | elixir: "~> 1.0", 8 | elixirc_paths: elixirc_paths(Mix.env), 9 | compilers: [:phoenix, :gettext] ++ Mix.compilers, 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | aliases: aliases(), 13 | deps: deps()] 14 | end 15 | 16 | # Configuration for the OTP application. 17 | # 18 | # Type `mix help compile.app` for more information. 19 | def application do 20 | [mod: {EmberWeekendApi, []}] 21 | end 22 | 23 | # Specifies which paths to compile per environment. 24 | defp elixirc_paths(:test), do: ["lib", "test/support"] 25 | defp elixirc_paths(_), do: ["lib"] 26 | 27 | # Specifies your project dependencies. 28 | # 29 | # Type `mix help deps` for examples and options. 30 | defp deps do 31 | [{:phoenix, "~> 1.3.0-rc.1"}, 32 | {:postgrex, ">= 0.15.8"}, 33 | {:phoenix_ecto, "~> 4.0"}, 34 | {:ecto_sql, "~> 3.0"}, 35 | {:plug_cowboy, "~> 1.0"}, 36 | {:phoenix_html, "~> 2.9"}, 37 | {:phoenix_live_reload, "~> 1.0", only: :dev}, 38 | {:gettext, "~> 0.9"}, 39 | {:cowboy, "~> 1.0"}, 40 | {:httpoison, "~> 0.8.1"}, 41 | {:secure_random, "~>0.2"}, 42 | {:json, "~> 0.3.0"}, 43 | {:timex, "~> 3.1"}, 44 | {:corsica, "~> 0.4"}, 45 | {:feeder_ex, "~> 1.0.1"}, 46 | {:ex_machina, "~> 2.3.0", only: :test}, 47 | {:faker, "~> 0.7", only: :test}, 48 | {:ja_serializer, "~> 0.12.0"}] 49 | end 50 | 51 | # Aliases are shortcut or tasks specific to the current project. 52 | # For example, to create, migrate and run the seeds file at once: 53 | # 54 | # $ mix ecto.setup 55 | # 56 | # See the documentation for `Mix` for more info on aliases. 57 | defp aliases do 58 | ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 59 | "ecto.reset": ["ecto.drop", "ecto.setup"], 60 | "test": ["ecto.create --quiet", "ecto.migrate", "test"]] 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/controllers/person_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.PersonController do 2 | use EmberWeekendApi.Web, :controller 3 | import EmberWeekendApi.Auth 4 | import EmberWeekendApi.Web.ControllerErrors 5 | import Ecto.Query, only: [from: 2] 6 | alias EmberWeekendApi.Web.Person 7 | 8 | plug :model_name, :person 9 | plug :authenticate, :admin when action in [:create, :update, :delete] 10 | 11 | def index(conn, _params) do 12 | people = Repo.all(from(p in Person, order_by: [p.name, p.inserted_at])) 13 | render(conn, data: people) 14 | end 15 | 16 | def show(conn, %{"id" => id}) do 17 | case Repo.get(Person, id) do 18 | nil -> not_found(conn) 19 | person -> 20 | conn 21 | |> render(:show, data: person, opts: [ 22 | include: "episodes,resources" 23 | ]) 24 | end 25 | end 26 | 27 | def create(conn, %{"data" => data}) do 28 | changeset = Person.changeset(%Person{}, data["attributes"]) 29 | case Repo.insert(changeset) do 30 | {:ok, person} -> 31 | conn 32 | |> put_status(:created) 33 | |> render(:show, data: person) 34 | {:error, changeset} -> 35 | conn 36 | |> put_status(:unprocessable_entity) 37 | |> render(:errors, data: changeset) 38 | end 39 | end 40 | 41 | def update(conn, %{"data" => data, "id" => id}) do 42 | case Repo.get(Person, id) do 43 | nil -> not_found(conn) 44 | person -> 45 | changeset = Person.changeset(person, data["attributes"]) 46 | case Repo.update(changeset) do 47 | {:ok, person} -> render(conn, :show, data: person) 48 | {:error, changeset} -> 49 | conn 50 | |> put_status(:unprocessable_entity) 51 | |> render(:errors, data: changeset) 52 | end 53 | end 54 | end 55 | 56 | def delete(conn, %{"id" => id}) do 57 | case Repo.get(Person, id) do 58 | nil -> not_found(conn) 59 | person -> 60 | Repo.delete!(person) 61 | send_resp(conn, :no_content, "") 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/support/factory.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Factory do 2 | use ExMachina.Ecto, repo: EmberWeekendApi.Repo 3 | alias EmberWeekendApi.Web.{Episode, Person, Resource, ShowNote, ResourceAuthor, LinkedAccount, User} 4 | 5 | def episode_factory do 6 | title = Faker.Lorem.words(%Range{first: 1, last: 8}) 7 | 8 | %Episode{ 9 | number: sequence("number", &(&1)), 10 | length: :rand.uniform(10000), 11 | title: Enum.join(title, " "), 12 | description: Faker.Lorem.sentence(20), 13 | slug: Enum.join(title, "-"), 14 | release_date: Date.utc_today, 15 | filename: Enum.join(Enum.concat(title, [".mp3"]), "-"), 16 | duration: sequence("duration", &"00:#{&1}") 17 | } 18 | end 19 | 20 | def person_factory() do 21 | %Person{ 22 | name: "Jerry Smith", 23 | handle: "dr_pluto", 24 | tagline: "Well look where being smart got you.", 25 | bio: "Jerry can sometimes become misguided by his insecurities.", 26 | url: "http://rickandmorty.wikia.com/wiki/Jerry_Smith", 27 | avatar_url: "http://vignette3.wikia.nocookie.net/rickandmorty/images/5/5d/Jerry_S01E11_Sad.JPG/revision/latest?cb=20140501090439" 28 | } 29 | end 30 | 31 | def resource_author_factory() do 32 | %ResourceAuthor{ 33 | author: build(:person), 34 | resource: build(:resource) 35 | } 36 | end 37 | 38 | def resource_factory() do 39 | %Resource{ 40 | title: "Plumbuses", 41 | url: "http://rickandmorty.wikia.com/wiki/Plumbus", 42 | resource_authors: [], 43 | } 44 | end 45 | 46 | def show_note_factory() do 47 | %ShowNote{ 48 | episode: build(:episode), 49 | resource: build(:resource), 50 | time_stamp: "01:14", 51 | note: "Wubalubadub", 52 | } 53 | end 54 | 55 | def user_factory() do 56 | %User{ 57 | name: "Rick Sanchez", 58 | username: "tinyrick", 59 | } 60 | end 61 | 62 | def linked_account_factory() do 63 | %LinkedAccount{ 64 | username: "tinyrick", 65 | provider: "github", 66 | access_token: "valid_token", 67 | provider_id: "1", 68 | user: build(:user) 69 | } 70 | end 71 | end 72 | 73 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/views/feed_view.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.FeedView do 2 | use EmberWeekendApi.Web, :view 3 | 4 | def episodes(conn) do 5 | conn.assigns.episodes 6 | end 7 | 8 | defp rfc_2822_date(date) do 9 | Timex.Format.DateTime.Formatter.format(date, "%a, %d %b %Y %H:%M:%S EST", :strftime) 10 | end 11 | 12 | def release_date(episode) do 13 | {:ok, date} = episode.release_date |> rfc_2822_date 14 | date 15 | end 16 | 17 | def last_espisode_published(episodes) do 18 | case episodes do 19 | [episode | _] -> {:ok, episode} 20 | [] -> :empty 21 | end 22 | end 23 | 24 | def pub_date(episodes) do 25 | case last_espisode_published(episodes) do 26 | {:ok, episode} -> release_date(episode) 27 | _ -> nil 28 | end 29 | end 30 | 31 | def last_espisode_updated(episodes) do 32 | case Enum.sort_by(episodes, &(&1.updated_at)) do 33 | [episode | _] -> {:ok, episode} 34 | [] -> :empty 35 | end 36 | end 37 | 38 | def last_build_date(episodes) do 39 | case last_espisode_updated(episodes) do 40 | {:ok, episode} -> 41 | {:ok, date} = rfc_2822_date(episode.updated_at) 42 | date 43 | _ -> nil 44 | end 45 | end 46 | 47 | def show_notes(episode) do 48 | case episode.show_notes do 49 | nil -> [] 50 | show_notes -> Enum.sort(show_notes, &(&1.time_stamp < &2.time_stamp)) 51 | end 52 | end 53 | 54 | def guests(episode) do 55 | episode.guests 56 | end 57 | 58 | def resource(show_note) do 59 | show_note.resource 60 | end 61 | 62 | def show_note_title(show_note) do 63 | case show_note.resource do 64 | nil -> show_note.note 65 | resource -> resource.title 66 | end 67 | end 68 | 69 | def episode_url(episode) do 70 | "https://emberweekend.com/episodes/#{episode.slug}" 71 | end 72 | 73 | def escape(str) do 74 | {:safe, safe} = Phoenix.HTML.html_escape(str) 75 | safe 76 | end 77 | 78 | def resource_url(show_note) do 79 | case resource(show_note) do 80 | nil -> nil 81 | resource -> case resource.url do 82 | nil -> nil 83 | "" -> nil 84 | not_blank -> not_blank 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/oauth/github_http_client.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Github.HTTPClient do 2 | @access_token_url "https://github.com/login/oauth/access_token" 3 | 4 | def get_access_token(code, state) do 5 | body = {:form, [ 6 | client_id: client_id(), redirect_uri: redirect_uri(), 7 | code: code, client_secret: client_secret(), state: state]} 8 | headers = %{ 9 | "Content-type" => "application/x-www-form-urlencoded", 10 | "Accept" => "application/json" 11 | } 12 | case HTTPoison.post(@access_token_url, body, headers) do 13 | {:ok, %HTTPoison.Response{body: body}} -> 14 | case JSON.decode(body) do 15 | {:ok, %{"access_token" => access_token}} -> 16 | {:ok, %{access_token: access_token}} 17 | {:ok, %{"error_description" => message}} -> 18 | {:error, %{message: message}} 19 | {:error, message } -> {:error, %{message: message}} 20 | end 21 | {:error, %HTTPoison.Error{reason: reason}} -> 22 | {:error, %{message: reason}} 23 | end 24 | end 25 | 26 | def get_user_data(access_token) do 27 | headers = %{ 28 | "Authorization" => "token #{access_token}" 29 | } 30 | case HTTPoison.get("https://api.github.com/user", headers) do 31 | {:ok, %HTTPoison.Response{body: body, status_code: 200}} -> 32 | case JSON.decode(body) do 33 | {:ok, %{"id" => id, "name" => name, "login" => login }} -> 34 | {:ok, %{ 35 | provider_id: "#{id}", 36 | name: name, 37 | username: login 38 | }} 39 | end 40 | {:ok, %HTTPoison.Response{body: body, status_code: 401}} -> 41 | case JSON.decode(body) do 42 | {:ok, %{"message" => message}} -> 43 | {:error, %{message: message}} 44 | end 45 | {:error, %HTTPoison.Error{reason: reason}} -> 46 | {:error, %{message: reason}} 47 | end 48 | end 49 | 50 | defp client_id do 51 | Application.fetch_env!(:ember_weekend_api, EmberWeekendApi.Github) 52 | |> Keyword.fetch!(:client_id) 53 | end 54 | 55 | defp client_secret do 56 | Application.fetch_env!(:ember_weekend_api, EmberWeekendApi.Github) 57 | |> Keyword.fetch!(:client_secret) 58 | end 59 | 60 | defp redirect_uri do 61 | Application.fetch_env!(:ember_weekend_api, EmberWeekendApi.Github) 62 | |> Keyword.fetch!(:redirect_uri) 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/web.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web do 2 | @moduledoc """ 3 | A module that keeps using definitions for controllers, 4 | views and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use EmberWeekendApi.Web, :controller 9 | use EmberWeekendApi.Web, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. 17 | """ 18 | 19 | def model do 20 | quote do 21 | use Ecto.Schema 22 | 23 | import Ecto 24 | import Ecto.Changeset 25 | import Ecto.Query, only: [from: 1, from: 2] 26 | 27 | def count() do 28 | EmberWeekendApi.Repo.aggregate(__MODULE__, :count, :id) 29 | end 30 | 31 | def first() do 32 | EmberWeekendApi.Repo.one(from(__MODULE__, order_by: [asc: :id], limit: 1)) 33 | end 34 | end 35 | end 36 | 37 | def controller do 38 | quote do 39 | use Phoenix.Controller, 40 | namespace: EmberWeekendApi.Web 41 | 42 | alias EmberWeekendApi.Repo 43 | import Ecto 44 | import Ecto.Query, only: [from: 1, from: 2] 45 | 46 | import EmberWeekendApi.Web.Router.Helpers 47 | import EmberWeekendApi.Web.Gettext 48 | end 49 | end 50 | 51 | def view do 52 | quote do 53 | use Phoenix.View, 54 | root: "lib/ember_weekend_api/web/templates", 55 | namespace: EmberWeekendApi.Web 56 | 57 | # Import convenience functions from controllers 58 | import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] 59 | 60 | # Use all HTML functionality (forms, tags, etc) 61 | use Phoenix.HTML 62 | 63 | import EmberWeekendApi.Web.Router.Helpers 64 | import EmberWeekendApi.Web.ErrorHelpers 65 | import EmberWeekendApi.Web.Gettext 66 | end 67 | end 68 | 69 | def router do 70 | quote do 71 | use Phoenix.Router 72 | end 73 | end 74 | 75 | def channel do 76 | quote do 77 | use Phoenix.Channel 78 | 79 | alias EmberWeekendApi.Repo 80 | import Ecto 81 | import Ecto.Query, only: [from: 1, from: 2] 82 | import EmberWeekendApi.Web.Gettext 83 | end 84 | end 85 | 86 | @doc """ 87 | When used, dispatch to the appropriate controller/view/etc. 88 | """ 89 | defmacro __using__(which) when is_atom(which) do 90 | apply(__MODULE__, which, []) 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This file is a PO Template file. `msgid`s here are often extracted from 2 | ## source code; add new translations manually only if they're dynamic 3 | ## translations that can't be statically extracted. Run `mix 4 | ## gettext.extract` to bring this file up to date. Leave `msgstr`s empty as 5 | ## changing them here as no effect; edit them in PO (`.po`) files instead. 6 | 7 | ## From Ecto.Changeset.cast/4 8 | msgid "can't be blank" 9 | msgstr "" 10 | 11 | ## From Ecto.Changeset.unique_constraint/3 12 | msgid "has already been taken" 13 | msgstr "" 14 | 15 | ## From Ecto.Changeset.put_change/3 16 | msgid "is invalid" 17 | msgstr "" 18 | 19 | ## From Ecto.Changeset.validate_format/3 20 | msgid "has invalid format" 21 | msgstr "" 22 | 23 | ## From Ecto.Changeset.validate_subset/3 24 | msgid "has an invalid entry" 25 | msgstr "" 26 | 27 | ## From Ecto.Changeset.validate_exclusion/3 28 | msgid "is reserved" 29 | msgstr "" 30 | 31 | ## From Ecto.Changeset.validate_confirmation/3 32 | msgid "does not match confirmation" 33 | msgstr "" 34 | 35 | ## From Ecto.Changeset.no_assoc_constraint/3 36 | msgid "is still associated to this entry" 37 | msgstr "" 38 | 39 | msgid "are still associated to this entry" 40 | msgstr "" 41 | 42 | ## From Ecto.Changeset.validate_length/3 43 | msgid "should be %{count} character(s)" 44 | msgid_plural "should be %{count} character(s)" 45 | msgstr[0] "" 46 | msgstr[1] "" 47 | 48 | msgid "should have %{count} item(s)" 49 | msgid_plural "should have %{count} item(s)" 50 | msgstr[0] "" 51 | msgstr[1] "" 52 | 53 | msgid "should be at least %{count} character(s)" 54 | msgid_plural "should be at least %{count} character(s)" 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | msgid "should have at least %{count} item(s)" 59 | msgid_plural "should have at least %{count} item(s)" 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | 63 | msgid "should be at most %{count} character(s)" 64 | msgid_plural "should be at most %{count} character(s)" 65 | msgstr[0] "" 66 | msgstr[1] "" 67 | 68 | msgid "should have at most %{count} item(s)" 69 | msgid_plural "should have at most %{count} item(s)" 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | ## From Ecto.Changeset.validate_number/3 74 | msgid "must be less than %{count}" 75 | msgid_plural "must be less than %{count}" 76 | msgstr[0] "" 77 | msgstr[1] "" 78 | 79 | msgid "must be greater than %{count}" 80 | msgid_plural "must be greater than %{count}" 81 | msgstr[0] "" 82 | msgstr[1] "" 83 | 84 | msgid "must be less than or equal to %{count}" 85 | msgid_plural "must be less than or equal to %{count}" 86 | msgstr[0] "" 87 | msgstr[1] "" 88 | 89 | msgid "must be greater than or equal to %{count}" 90 | msgid_plural "must be greater than or equal to %{count}" 91 | msgstr[0] "" 92 | msgstr[1] "" 93 | 94 | msgid "must be equal to %{count}" 95 | msgid_plural "must be equal to %{count}" 96 | msgstr[0] "" 97 | msgstr[1] "" 98 | -------------------------------------------------------------------------------- /priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. Do not add, change, or 2 | ## remove `msgid`s manually here as they're tied to the ones in the 3 | ## corresponding POT file (with the same domain). Use `mix gettext.extract 4 | ## --merge` or `mix gettext.merge` to merge POT files into PO files. 5 | msgid "" 6 | msgstr "" 7 | "Language: en\n" 8 | 9 | ## From Ecto.Changeset.cast/4 10 | msgid "can't be blank" 11 | msgstr "" 12 | 13 | ## From Ecto.Changeset.unique_constraint/3 14 | msgid "has already been taken" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.put_change/3 18 | msgid "is invalid" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.validate_format/3 22 | msgid "has invalid format" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_subset/3 26 | msgid "has an invalid entry" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_exclusion/3 30 | msgid "is reserved" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_confirmation/3 34 | msgid "does not match confirmation" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.no_assoc_constraint/3 38 | msgid "is still associated to this entry" 39 | msgstr "" 40 | 41 | msgid "are still associated to this entry" 42 | msgstr "" 43 | 44 | ## From Ecto.Changeset.validate_length/3 45 | msgid "should be %{count} character(s)" 46 | msgid_plural "should be %{count} character(s)" 47 | msgstr[0] "" 48 | msgstr[1] "" 49 | 50 | msgid "should have %{count} item(s)" 51 | msgid_plural "should have %{count} item(s)" 52 | msgstr[0] "" 53 | msgstr[1] "" 54 | 55 | msgid "should be at least %{count} character(s)" 56 | msgid_plural "should be at least %{count} character(s)" 57 | msgstr[0] "" 58 | msgstr[1] "" 59 | 60 | msgid "should have at least %{count} item(s)" 61 | msgid_plural "should have at least %{count} item(s)" 62 | msgstr[0] "" 63 | msgstr[1] "" 64 | 65 | msgid "should be at most %{count} character(s)" 66 | msgid_plural "should be at most %{count} character(s)" 67 | msgstr[0] "" 68 | msgstr[1] "" 69 | 70 | msgid "should have at most %{count} item(s)" 71 | msgid_plural "should have at most %{count} item(s)" 72 | msgstr[0] "" 73 | msgstr[1] "" 74 | 75 | ## From Ecto.Changeset.validate_number/3 76 | msgid "must be less than %{count}" 77 | msgid_plural "must be less than %{count}" 78 | msgstr[0] "" 79 | msgstr[1] "" 80 | 81 | msgid "must be greater than %{count}" 82 | msgid_plural "must be greater than %{count}" 83 | msgstr[0] "" 84 | msgstr[1] "" 85 | 86 | msgid "must be less than or equal to %{count}" 87 | msgid_plural "must be less than or equal to %{count}" 88 | msgstr[0] "" 89 | msgstr[1] "" 90 | 91 | msgid "must be greater than or equal to %{count}" 92 | msgid_plural "must be greater than or equal to %{count}" 93 | msgstr[0] "" 94 | msgstr[1] "" 95 | 96 | msgid "must be equal to %{count}" 97 | msgid_plural "must be equal to %{count}" 98 | msgstr[0] "" 99 | msgstr[1] "" 100 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/controllers/show_note_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.ShowNoteController do 2 | use EmberWeekendApi.Web, :controller 3 | import EmberWeekendApi.Auth 4 | import EmberWeekendApi.Web.ControllerErrors 5 | 6 | alias EmberWeekendApi.Web.ShowNote 7 | alias EmberWeekendApi.Repo 8 | 9 | plug :model_name, :show_note 10 | plug :authenticate, :admin when action in [:create, :update, :delete] 11 | 12 | def index(conn, _params) do 13 | show_notes = Repo.all(ShowNote) 14 | render(conn, data: show_notes, opts: [include: "resource,resource.authors"]) 15 | end 16 | 17 | def show(conn, %{"id" => id}) do 18 | case Repo.get(ShowNote, id) do 19 | nil -> not_found(conn) 20 | show_note -> render(conn, data: show_note) 21 | end 22 | end 23 | 24 | def create(conn, %{"data" => %{ "relationships" => relationships, "attributes" => attributes}}) do 25 | attributes = Map.merge(attributes, extract_relationships(relationships)) 26 | changeset = ShowNote.changeset(%ShowNote{}, attributes) 27 | case Repo.insert(changeset) do 28 | {:ok, show_note} -> 29 | conn 30 | |> put_status(:created) 31 | |> render(:show, data: show_note) 32 | {:error, changeset} -> 33 | conn 34 | |> put_status(:unprocessable_entity) 35 | |> render(:errors, data: changeset) 36 | end 37 | end 38 | 39 | def update(conn, %{"data" => %{"relationships" => relationships, "attributes" => attributes}, "id" => id}) do 40 | attributes = Map.merge(attributes, extract_relationships(relationships)) 41 | case Repo.get(ShowNote, id) do 42 | nil -> not_found(conn) 43 | show_note -> 44 | changeset = ShowNote.changeset(show_note, attributes) 45 | case Repo.update(changeset) do 46 | {:ok, show_note} -> render(conn, :show, data: show_note) 47 | {:error, changeset} -> 48 | conn 49 | |> put_status(:unprocessable_entity) 50 | |> render(:errors, data: changeset) 51 | end 52 | end 53 | end 54 | 55 | def delete(conn, %{"id" => id}) do 56 | case Repo.get(ShowNote, id) do 57 | nil -> not_found(conn) 58 | show_note -> 59 | Repo.delete!(show_note) 60 | send_resp(conn, :no_content, "") 61 | end 62 | end 63 | 64 | defp extract_relationships(relationships) do 65 | attributes = %{} 66 | 67 | episode_id = relationships["episode"]["data"]["id"] 68 | attributes = case episode_id do 69 | nil -> attributes 70 | episode_id -> 71 | {episode_id,_} = Integer.parse(episode_id) 72 | Map.merge(attributes, %{"episode_id" => episode_id}) 73 | end 74 | 75 | resource_id = relationships["resource"]["data"]["id"] 76 | attributes = case resource_id do 77 | nil -> attributes 78 | resource_id -> 79 | {resource_id,_} = Integer.parse(resource_id) 80 | Map.merge(attributes, %{"resource_id" => resource_id}) 81 | end 82 | 83 | attributes 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/seeder.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Seeder do 2 | alias EmberWeekendApi.Repo 3 | alias EmberWeekendApi.Web.{Episode, Person, Resource, ResourceAuthor, ShowNote, EpisodeGuest} 4 | 5 | @seed_data %{ 6 | Episode => [%{ 7 | number: 1, 8 | title: "Pilot", 9 | description: "Rick moves in with his daughter's family and becomes a bad influence on his grandson, Morty.", 10 | slug: "pilot", 11 | release_date: ~D[2013-12-02], 12 | filename: "s01e01", 13 | duration: "1:00:00", 14 | published: true 15 | }, 16 | %{ 17 | number: 2, 18 | title: "Lawnmower Dog", 19 | description: "Rick helps Jerry with the dog and incept Goldenfold.", 20 | slug: "lawnmower-dog", 21 | release_date: ~D[2013-12-09], 22 | filename: "s01e02", 23 | duration: "1:00:00", 24 | published: true 25 | }, 26 | %{ 27 | number: 3, 28 | title: "Anatomy Park", 29 | description: "Rick and Morty try to save the life of a homeless man; Jerry's parents visit.", 30 | slug: "anatomy-park", 31 | release_date: ~D[2013-12-15], 32 | filename: "s01e03", 33 | duration: "1:00:00" 34 | }], 35 | 36 | Person => [%{ 37 | name: "Jerry Smith", 38 | handle: "dr_pluto", 39 | tagline: "Well look where being smart got you.", 40 | bio: "Jerry can sometimes become misguided by his insecurities.", 41 | url: "http://rickandmorty.wikia.com/wiki/Jerry_Smith", 42 | avatar_url: "http://vignette3.wikia.nocookie.net/rickandmorty/images/5/5d/Jerry_S01E11_Sad.JPG/revision/latest?cb=20140501090439" 43 | }, 44 | %{ 45 | name: "Rick Sanchez", 46 | handle: "tinyrick", 47 | tagline: "wubbalubbadubdub", 48 | bio: "Rick's most prominent personality trait barring his drug and alcohol dependency is his sociopathy.", 49 | url: "http://rickandmorty.wikia.com/wiki/Rick_Sanchez", 50 | avatar_url: "http://vignette4.wikia.nocookie.net/rickandmorty/images/d/dd/Rick.png/revision/latest?cb=20131230003659" 51 | }], 52 | 53 | Resource => [%{ 54 | title: "Plumbuses", 55 | url: "http://rickandmorty.wikia.com/wiki/Plumbus" 56 | }], 57 | 58 | ResourceAuthor => [], 59 | ShowNote => [], 60 | EpisodeGuest => [], 61 | } 62 | 63 | def reset do 64 | @seed_data 65 | |> Map.keys 66 | |> Enum.each(&reset_module/1) 67 | end 68 | 69 | 70 | def seed do 71 | Enum.each(@seed_data, &seed_module/1) 72 | 73 | %ResourceAuthor{ 74 | resource: Resource.first, 75 | author: Person.first 76 | } |> Repo.insert! 77 | 78 | %ShowNote{ 79 | resource: Resource.first, 80 | episode: Episode.first, 81 | time_stamp: "01:12" 82 | } |> Repo.insert! 83 | 84 | %EpisodeGuest{ 85 | episode: Episode.first, 86 | guest: Person.first 87 | } |> Repo.insert! 88 | end 89 | 90 | defp reset_module(module) do 91 | Repo.delete_all(module) 92 | 93 | new_row = struct(module) 94 | {_, table} = new_row.__meta__.source 95 | Ecto.Adapters.SQL.query(EmberWeekendApi.Repo, "ALTER SEQUENCE #{table}_id_seq RESTART WITH 1;", []) 96 | end 97 | 98 | defp seed_module({module, rows}) do 99 | ts = %{ 100 | inserted_at: DateTime.utc_now, 101 | updated_at: DateTime.utc_now 102 | } 103 | Repo.insert_all(module, Enum.map(rows, &Map.merge(&1, ts))) 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/templates/feed/index.xml.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ember Weekend 6 | Elixir Phoenix 7 | Ember.js is a frontend JavaScript framework that has exciting applications. In this podcasts we share news, events, and some of our experiences. 8 | <%= escape("© 2017 Chase McCarthy & Jonathan Jackson") %> 9 | en-us 10 | <%= pub_date(episodes(@conn)) %> 11 | <%= last_build_date(episodes(@conn)) %> 12 | https://emberweekend.com 13 | 14 | https://i.imgur.com/YyAd2Ee.png 15 | Ember Weekend 16 | https://emberweekend.com 17 | 18 | <%= escape("Chase McCarthy & Jonathan Jackson") %> 19 | 20 | Ember.js is a frontend JavaScript framework that has exciting applications. In this podcasts we share news, events, and some of our experiences. 21 | Latest Ember.js news, events, and interesting tidbits. 22 | no 23 | technology, ember, programming, software development, development, tech 24 | 25 | Chase McCarthy 26 | chase@code0100fun.com 27 | 28 | 29 | 30 | 31 | 32 | <%= for episode <- episodes(@conn) do %> 33 | 34 | Episode <%= episode.number %>: <%= escape(episode.title) %> 35 | <%= episode_url(episode) %> 36 | <%= episode_url(episode) %> 37 | <%= episode.description %> 38 | 39 | 41 | <%= for show_note <- show_notes(episode) do %> 42 |
  • 43 | <%= show_note.time_stamp %> - <%= if resource_url(show_note) != nil do %> <%= show_note_title(show_note) %> 44 | <% else %><%= show_note_title(show_note) %> 45 | <% end %>
  • 46 | <% end %> 47 | 48 | ]]> 49 |
    50 | <%= release_date(episode) %> 51 | chase@code0100fun.com (Chase McCarthy, Jonathan Jackson) 52 | 53 | Chase McCarthy, Jonathan Jackson 54 | 55 | <%= episode.duration %> 56 | <%= episode.description %> 57 | <%= episode.description %> 58 | 59 | no 60 |
    61 | <% end %> 62 | 63 |
    64 |
    65 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/controllers/resource_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.ResourceController do 2 | use EmberWeekendApi.Web, :controller 3 | import EmberWeekendApi.Auth 4 | import EmberWeekendApi.Web.ControllerErrors 5 | import Ecto.Query, only: [from: 2] 6 | 7 | alias EmberWeekendApi.Web.Resource 8 | alias EmberWeekendApi.Web.ResourceAuthor 9 | alias Ecto.Multi 10 | 11 | plug :model_name, :resource 12 | plug :authenticate, :admin when action in [:create, :update, :delete] 13 | 14 | def index(conn, _params) do 15 | resources = Repo.all(from(r in Resource, order_by: [r.title, r.inserted_at])) 16 | render(conn, data: resources) 17 | end 18 | 19 | def show(conn, %{"id" => id}) do 20 | case Repo.get(Resource, id) do 21 | nil -> not_found(conn) 22 | resource -> render(conn, data: resource, opts: [ 23 | include: "authors,show_notes" 24 | ]) 25 | end 26 | end 27 | 28 | defp extract_relationship_ids(relationships, name) do 29 | case relationships[name]["data"] do 30 | nil -> [] 31 | data -> Enum.map(data, fn(a) -> 32 | Integer.parse(a["id"]) 33 | |> elem(0) 34 | end) 35 | end 36 | end 37 | 38 | def create(conn, %{"data" => %{ "relationships" => relationships, "attributes" => attributes}}) do 39 | changeset = Resource.changeset(%Resource{}, attributes) 40 | case Repo.insert(changeset) do 41 | {:ok, resource} -> 42 | author_ids = extract_relationship_ids(relationships, "authors") 43 | Enum.each author_ids, fn(id) -> 44 | attributes = %{author_id: id, resource_id: resource.id} 45 | changeset = ResourceAuthor.changeset(%ResourceAuthor{}, attributes) 46 | Repo.insert(changeset) 47 | end 48 | conn 49 | |> put_status(:created) 50 | |> render(:show, data: resource, opts: [ 51 | include: "authors" 52 | ]) 53 | {:error, changeset} -> 54 | conn 55 | |> put_status(:unprocessable_entity) 56 | |> render(:errors, data: changeset) 57 | end 58 | end 59 | def create(conn, %{"data" => %{"attributes" => attributes}}) do 60 | create conn, %{"data" => %{ "relationships" => %{}, "attributes" => attributes}} 61 | end 62 | 63 | def update(conn, %{"data" => %{ "relationships" => relationships, "attributes" => attributes}, "id" => id}) do 64 | case Repo.get(Resource, id) do 65 | nil -> not_found(conn) 66 | resource -> 67 | changeset = Resource.changeset(resource, attributes) 68 | author_ids = extract_relationship_ids(relationships, "authors") 69 | 70 | multi = Multi.new 71 | |> Multi.update(:resource, changeset) 72 | |> Multi.run(:set_resource_authors, fn(_, %{resource: resource}) -> 73 | set_resource_authors(%{resource: resource, author_ids: author_ids }) 74 | end) 75 | 76 | case Repo.transaction(multi) do 77 | {:ok, result} -> render(conn, :show, data: result.resource) 78 | {:error, _, changeset, %{}} -> 79 | conn 80 | |> put_status(:unprocessable_entity) 81 | |> render(:errors, data: changeset) 82 | end 83 | end 84 | end 85 | def update(conn, %{"data" => %{"attributes" => attributes}, "id" => id}) do 86 | update conn, %{"data" => %{ "relationships" => %{}, "attributes" => attributes}, "id" => id} 87 | end 88 | 89 | def delete(conn, %{"id" => id}) do 90 | case Repo.get(Resource, id) do 91 | nil -> not_found(conn) 92 | resource -> 93 | Repo.delete!(resource) 94 | send_resp(conn, :no_content, "") 95 | end 96 | end 97 | 98 | defp set_resource_authors(%{resource: resource, author_ids: author_ids}) do 99 | query = from ra in ResourceAuthor, 100 | where: ra.resource_id == ^resource.id, 101 | select: ra.author_id 102 | existing_author_ids = Repo.all(query) 103 | 104 | new_resource_authors = author_ids -- existing_author_ids 105 | remove_resource_authors = existing_author_ids -- author_ids 106 | 107 | multi = Enum.reduce(new_resource_authors, Multi.new, fn(id, multi) -> 108 | attributes = %{author_id: id, resource_id: resource.id} 109 | changeset = ResourceAuthor.changeset(%ResourceAuthor{}, attributes) 110 | Multi.insert(multi, id, changeset) 111 | end) 112 | 113 | query = from ra in ResourceAuthor, 114 | where: ra.resource_id == ^resource.id and ra.author_id in ^remove_resource_authors 115 | 116 | multi 117 | |> Multi.delete_all(:delete_resource_authors, query) 118 | |> Repo.transaction() 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/controllers/session_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.SessionController do 2 | use EmberWeekendApi.Web, :controller 3 | alias EmberWeekendApi.Web.User 4 | alias EmberWeekendApi.Web.Session 5 | alias EmberWeekendApi.Web.LinkedAccount 6 | 7 | @github_api Application.get_env(:ember_weekend_api, :github_api) 8 | 9 | def create(conn, %{"data" => %{"attributes" => %{"provider" => "github", "code" => code, "state" => state}}}) do 10 | case @github_api.get_access_token(code, state) do 11 | {:ok, %{access_token: access_token}} -> 12 | case create_session(access_token, "github") do 13 | {:ok, %{user: _, session: session}} -> 14 | conn 15 | |> put_status(:created) 16 | |> render(:show, data: session, opts: [ 17 | include: "user" 18 | ]) 19 | {:error, errors} -> 20 | conn 21 | |> put_status(:unprocessable_entity) 22 | |> render(:errors, data: errors) 23 | end 24 | {:error, %{message: message}} -> 25 | conn 26 | |> put_status(:unprocessable_entity) 27 | |> render(:errors, data: %{ 28 | source: %{ pointer: "/code" }, 29 | status: 422, 30 | title: "Failed to create session", 31 | detail: message 32 | }) 33 | end 34 | end 35 | 36 | def delete(conn, %{"token" => token}) do 37 | case Repo.get_by(Session, token: token) do 38 | nil -> 39 | conn 40 | |> put_status(:not_found) 41 | |> render(:errors, data: %{ 42 | source: %{ pointer: "/token" }, 43 | status: 404, 44 | title: "Failed to delete session", 45 | detail: "Invalid token" 46 | }) 47 | session -> 48 | Repo.delete!(session) 49 | send_resp(conn, :no_content, "") 50 | end 51 | end 52 | 53 | defp create_session(access_token, provider) do 54 | case @github_api.get_user_data(access_token) do 55 | {:ok, attrs} -> 56 | case find_or_create_user(attrs, access_token, provider) do 57 | {:ok, user} -> 58 | token = SecureRandom.base64(24) 59 | changeset = Session.changeset(%Session{}, %{user_id: user.id, token: token}) 60 | case Repo.insert(changeset) do 61 | {:ok, session} -> 62 | {:ok, %{session: session, user: user}} 63 | {:error, changeset} -> 64 | errors = EmberWeekendApi.Web.ChangesetView.translate_errors(changeset) 65 | {:error, %{errors: errors}} 66 | end 67 | {:error, error} -> 68 | {:error, error} 69 | end 70 | {:error, %{message: message}} -> 71 | {:error, %{message: message}} 72 | end 73 | end 74 | 75 | defp find_or_create_user(attrs, access_token, provider) do 76 | case Repo.get_by(User, username: attrs[:username]) do 77 | nil -> 78 | case create_user(attrs) do 79 | {:ok, user} -> 80 | case find_or_create_linked_account(user, attrs, access_token, provider) do 81 | {:error, error} -> {:error, error} 82 | _ -> {:ok, user} 83 | end 84 | error -> error 85 | end 86 | user -> 87 | case find_or_create_linked_account(user, attrs, access_token, provider) do 88 | {:error, error} -> {:error, error} 89 | _ -> {:ok, user} 90 | end 91 | end 92 | end 93 | 94 | defp create_user(attrs) do 95 | changeset = User.changeset(%User{}, %{name: attrs[:name], username: attrs[:username]}) 96 | case Repo.insert(changeset) do 97 | {:ok, user} -> 98 | {:ok, user} 99 | {:error, changeset} -> 100 | errors = EmberWeekendApi.Web.ChangesetView.translate_errors(changeset) 101 | {:error, %{errors: errors}} 102 | end 103 | end 104 | 105 | defp find_or_create_linked_account(user, attrs, access_token, provider) do 106 | case Repo.get_by(LinkedAccount, user_id: user.id, provider: provider) do 107 | nil -> 108 | changeset = LinkedAccount.changeset(%LinkedAccount{}, %{ 109 | username: attrs[:username], 110 | user_id: user.id, 111 | access_token: access_token, 112 | provider: provider, 113 | provider_id: attrs[:provider_id] 114 | }) 115 | 116 | case Repo.insert(changeset) do 117 | {:error, changeset} -> 118 | errors = EmberWeekendApi.Web.ChangesetView.translate_errors(changeset) 119 | {:error, %{errors: errors}} 120 | linked_account -> linked_account 121 | end 122 | linked_account -> linked_account 123 | end 124 | end 125 | 126 | end 127 | -------------------------------------------------------------------------------- /test/controllers/session_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.SessionControllerTest do 2 | use EmberWeekendApi.Web.ConnCase 3 | 4 | @valid_params %{ 5 | data: %{ 6 | attributes: %{ 7 | provider: "github", code: "valid_code", state: "123456" 8 | } 9 | }} 10 | @invalid_params %{ 11 | data: %{ 12 | attributes: %{ 13 | provider: "github", code: "invalid_code", state: "123456" 14 | } 15 | }} 16 | 17 | defp setup_conn(conn) do 18 | conn = conn 19 | |> put_req_header("accept", "application/vnd.api+json") 20 | |> put_req_header("content-type", "application/vnd.api+json") 21 | {:ok, conn: conn} 22 | end 23 | 24 | setup %{conn: conn} do 25 | setup_conn conn 26 | end 27 | 28 | test "if not present, creates a user for the github account", %{conn: conn} do 29 | conn = post conn, session_path(conn, :create), @valid_params 30 | 31 | assert conn.status == 201 32 | assert json_api_response(conn)["data"]["attributes"]["token"] 33 | 34 | assert [included] = json_api_response(conn)["included"] 35 | 36 | assert included["type"] == "users" 37 | assert included["attributes"] == %{ 38 | "name" => params_for(:user).name, 39 | "username" => params_for(:user).username 40 | } 41 | 42 | assert db_user = Repo.get_by(User, username: EmberWeekendApi.Github.Stub.username) 43 | assert Repo.one(assoc(db_user, :sessions)) 44 | 45 | assert linked = Repo.one(assoc(db_user, :linked_accounts)) 46 | assert linked.provider == "github" 47 | assert linked.provider_id == "1" 48 | end 49 | 50 | test "signs in existing user with linked github account", %{conn: conn} do 51 | user = insert(:linked_account).user 52 | 53 | conn = post conn, session_path(conn, :create), @valid_params 54 | 55 | assert conn.status == 201 56 | assert json_api_response(conn)["data"]["attributes"]["token"] 57 | 58 | [included] = json_api_response(conn)["included"] 59 | 60 | assert included["type"] == "users" 61 | assert included["attributes"] == %{ 62 | "name" => user.name, 63 | "username" => user.username 64 | } 65 | 66 | assert db_user = Repo.get(User, user.id) 67 | assert Repo.one(assoc(db_user, :sessions)) 68 | 69 | assert linked = Repo.one(assoc(db_user, :linked_accounts)) 70 | assert linked.provider == "github" 71 | assert linked.provider_id == "1" 72 | 73 | conn = build_conn() 74 | {:ok, conn: conn} = setup_conn conn 75 | conn = post conn, session_path(conn, :create), @valid_params 76 | assert conn.status == 201 77 | end 78 | 79 | test "signs in existing user and links github account", %{conn: conn} do 80 | user = insert(:user, username: "tinyrick") 81 | 82 | conn = post conn, session_path(conn, :create), @valid_params 83 | 84 | assert conn.status == 201 85 | assert json_api_response(conn)["data"]["attributes"]["token"] 86 | 87 | assert [included] = json_api_response(conn)["included"] 88 | 89 | assert included["type"] == "users" 90 | assert included["attributes"] == %{ 91 | "name" => user.name, 92 | "username" => user.username 93 | } 94 | 95 | assert db_user = Repo.get(User, user.id) 96 | assert Repo.one(assoc(db_user, :sessions)) 97 | 98 | assert linked = Repo.one(assoc(db_user, :linked_accounts)) 99 | assert linked.provider == "github" 100 | assert linked.provider_id == "1" 101 | end 102 | 103 | test "returns an error if the code is invalid", %{conn: conn} do 104 | conn = post conn, session_path(conn, :create), @invalid_params 105 | 106 | assert conn.status == 422 107 | json = json_api_response(conn) 108 | assert [error] = json["errors"] 109 | 110 | assert error["status"] == 422 111 | assert error["title"] == "Failed to create session" 112 | assert error["detail"] == "Invalid code" 113 | 114 | assert LinkedAccount.count == 0 115 | assert Session.count == 0 116 | assert User.count == 0 117 | end 118 | 119 | test "signs out", %{conn: conn} do 120 | token = "VALID" 121 | session = Repo.insert! %Session{token: token} 122 | 123 | conn = delete conn, "/api/sessions/#{token}" 124 | 125 | assert conn.status == 204 126 | refute Repo.get(Session, session.id) 127 | end 128 | 129 | test "signs out unknown token", %{conn: conn} do 130 | token = "INVALID" 131 | 132 | conn = delete conn, "/api/sessions/#{token}" 133 | 134 | assert conn.status == 404 135 | json = json_api_response(conn) 136 | assert [error] = json["errors"] 137 | 138 | assert error["status"] == 404 139 | assert error["title"] == "Failed to delete session" 140 | assert error["detail"] == "Invalid token" 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | imports other functionality to make it easier 8 | to build and query models. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with connections 21 | use Phoenix.ConnTest 22 | alias Plug.Conn 23 | require IEx 24 | 25 | alias EmberWeekendApi.Repo 26 | alias EmberWeekendApi.Web.{ 27 | Episode, Person, Resource, ResourceAuthor, ShowNote, EpisodeGuest, 28 | Session, LinkedAccount, User 29 | } 30 | import Ecto 31 | import Ecto.Changeset 32 | import Ecto.Query, only: [from: 1, from: 2] 33 | 34 | import EmberWeekendApi.Web.Router.Helpers 35 | import EmberWeekendApi.Factory 36 | 37 | # The default endpoint for testing 38 | @endpoint EmberWeekendApi.Web.Endpoint 39 | 40 | require Logger 41 | 42 | def log(thing) do 43 | IO.puts "\n" 44 | Logger.warn "\n\n#{inspect thing}\n" 45 | end 46 | 47 | def json_api_response(conn) do 48 | case JSON.decode(conn.resp_body) do 49 | {:ok, json} -> 50 | case Conn.get_resp_header(conn, "content-type") do 51 | [] -> {:error, "Content type was not 'application/vnd.api+json'"} 52 | ["application/vnd.api+json"] -> json 53 | ["application/vnd.api+json; charset=utf-8"] -> json 54 | end 55 | {:error, error} -> {:error, error} 56 | end 57 | end 58 | 59 | def string_keys(map) do 60 | for {key, val} <- map, into: %{}, do: {"#{key}",val} 61 | end 62 | 63 | def dasherize_keys(map) do 64 | for {key, val} <- map, into: %{} do 65 | {String.replace("#{key}", "_", "-"),val} 66 | end 67 | end 68 | 69 | def convert_dates(map) do 70 | Enum.map(map, fn 71 | {k, v = %Date{}} -> 72 | {k, Date.to_iso8601(v)} 73 | {k, v = %DateTime{}} -> 74 | {k, DateTime.to_iso8601(v)} 75 | {k, v} -> 76 | {k,v} 77 | end) 78 | |> Enum.into(%{}) 79 | end 80 | 81 | def not_found(model_name) do 82 | [%{ 83 | "title" => "Not found", 84 | "status" => 404, 85 | "source" => %{ 86 | "pointer" => "/id" 87 | }, 88 | "detail" => "No #{model_name} found for the given id" 89 | }] 90 | end 91 | 92 | def unauthorized(model_name, action) do 93 | [%{ 94 | "source" => %{ 95 | "pointer" => "/token" 96 | }, 97 | "title" => "Unauthorized", 98 | "status" => 401, 99 | "detail" => "Must provide auth token to #{action} #{model_name}" 100 | }] 101 | end 102 | 103 | def sort_by(list, attr) do 104 | list 105 | |> (Enum.sort &(Enum.sort([&1[attr], &2[attr]]) 106 | |> List.first) == &1[attr]) 107 | end 108 | 109 | def cant_be_blank(attr) do 110 | humanized = String.replace(attr, "_", " ") 111 | dasherized = String.replace(attr, "_", "-") 112 | %{ 113 | "detail" => "#{String.capitalize humanized} can't be blank", 114 | "source" => %{ 115 | "pointer" => "/data/attributes/#{dasherized}" 116 | }, 117 | "title" => "can't be blank" 118 | } 119 | end 120 | 121 | @valid_admin_user_attrs %{ 122 | name: "Rick Sanchez", 123 | username: "tinyrick" 124 | } 125 | 126 | @valid_user_attrs %{ 127 | name: "Jerry Smith", 128 | username: "dr_pluto" 129 | } 130 | 131 | def admin(conn) do 132 | authenticated(conn, @valid_admin_user_attrs) 133 | end 134 | 135 | def authenticated(conn) do 136 | authenticated(conn, @valid_user_attrs) 137 | end 138 | 139 | def authenticated(conn, attributes) do 140 | token = "VALID" 141 | user = insert(:user, attributes) 142 | insert(:linked_account, user: user, username: user.username) 143 | Repo.insert! %Session{token: token, user_id: user.id} 144 | put_req_header(conn, "authorization", "token #{token}") 145 | end 146 | 147 | end 148 | end 149 | 150 | setup tags do 151 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(EmberWeekendApi.Repo) 152 | unless tags[:async] do 153 | Ecto.Adapters.SQL.Sandbox.mode(EmberWeekendApi.Repo, {:shared, self()}) 154 | end 155 | 156 | {:ok, conn: Phoenix.ConnTest.build_conn()} 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /lib/ember_weekend_api/web/controllers/episode_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.Web.EpisodeController do 2 | use EmberWeekendApi.Web, :controller 3 | import EmberWeekendApi.Auth 4 | import EmberWeekendApi.Web.ControllerErrors 5 | alias EmberWeekendApi.Web.Episode 6 | alias EmberWeekendApi.Web.EpisodeGuest 7 | alias Ecto.Multi 8 | import Ecto.Query, only: [from: 2] 9 | 10 | plug :model_name, :episode 11 | plug :authenticate, :admin when action in [:create, :update, :delete] 12 | 13 | def index(conn, _params) do 14 | query = case admin? conn do 15 | true -> Episode 16 | _ -> Episode.published(Episode) 17 | end 18 | episodes = Repo.all(query) 19 | render(conn, data: episodes) 20 | end 21 | 22 | def show(conn, %{"id" => id}) do 23 | case find_by_slug_or_id(conn, id) do 24 | nil -> not_found(conn) 25 | episode -> 26 | conn 27 | |> put_view(EmberWeekendApi.Web.EpisodeShowView) 28 | |> render(:show, data: episode, opts: [ 29 | include: "show_notes,show_notes.resource,show_notes.resource.authors,guests" 30 | ]) 31 | end 32 | end 33 | 34 | defp find_by_slug_or_id(conn, id) do 35 | query = case admin? conn do 36 | true -> Episode 37 | _ -> Episode.published(Episode) 38 | end 39 | case Integer.parse(id) do 40 | :error -> 41 | from(e in query, where: e.slug == ^id) 42 | |> Repo.one 43 | {id,_} -> 44 | from(e in query, where: e.id == ^id) 45 | |> Repo.one 46 | end 47 | end 48 | 49 | defp extract_relationship_ids(relationships, name) do 50 | case relationships[name]["data"] do 51 | nil -> [] 52 | data -> Enum.map(data, fn(a) -> 53 | Integer.parse(a["id"]) 54 | |> elem(0) 55 | end) 56 | end 57 | end 58 | 59 | def create(conn, %{"data" => %{ "relationships" => relationships, "attributes" => attributes}}) do 60 | changeset = Episode.changeset(%Episode{}, attributes) 61 | case Repo.insert(changeset) do 62 | {:ok, episode} -> 63 | guest_ids = extract_relationship_ids(relationships, "guests") 64 | Enum.each guest_ids, fn(id) -> 65 | attributes = %{episode_id: episode.id, guest_id: id} 66 | changeset = EpisodeGuest.changeset(%EpisodeGuest{}, attributes) 67 | Repo.insert(changeset) 68 | end 69 | conn 70 | |> put_view(EmberWeekendApi.Web.EpisodeShowView) 71 | |> put_status(:created) 72 | |> render(:show, data: episode, opts: [ 73 | include: "show_notes,show_notes.resource,show_notes.resource.authors,guests" 74 | ]) 75 | {:error, changeset} -> 76 | conn 77 | |> put_status(:unprocessable_entity) 78 | |> render(:errors, data: changeset) 79 | end 80 | end 81 | def create(conn, %{"data" => %{"attributes" => attributes}}) do 82 | create conn, %{"data" => %{ "relationships" => %{}, "attributes" => attributes}} 83 | end 84 | 85 | def update(conn, %{"data" => %{ "relationships" => relationships, "attributes" => attributes}, "id" => id}) do 86 | case Repo.get(Episode, id) do 87 | nil -> not_found(conn) 88 | episode -> 89 | changeset = Episode.changeset(episode, attributes) 90 | guest_ids = extract_relationship_ids(relationships, "guests") 91 | 92 | multi = Multi.new 93 | |> Multi.update(:episode, changeset) 94 | |> Multi.run(:set_episode_guests, fn(_, %{episode: episode}) -> 95 | set_episode_guests(%{episode: episode, guest_ids: guest_ids }) 96 | end) 97 | 98 | case Repo.transaction(multi) do 99 | {:ok, result} -> 100 | conn 101 | |> put_view(EmberWeekendApi.Web.EpisodeShowView) 102 | |> render(:show, data: result.episode) 103 | {:error, _, changeset, %{}} -> 104 | conn 105 | |> put_status(:unprocessable_entity) 106 | |> render(:errors, data: changeset) 107 | end 108 | end 109 | end 110 | def update(conn, %{"data" => %{"attributes" => attributes}, "id" => id}) do 111 | update conn, %{"data" => %{ "relationships" => %{}, "attributes" => attributes}, "id" => id} 112 | end 113 | 114 | def delete(conn, %{"id" => id}) do 115 | case Repo.get(Episode, id) do 116 | nil -> not_found(conn) 117 | episode -> 118 | Repo.delete!(episode) 119 | send_resp(conn, :no_content, "") 120 | end 121 | end 122 | 123 | defp set_episode_guests(%{episode: episode, guest_ids: guest_ids}) do 124 | query = from eg in EpisodeGuest, 125 | where: eg.episode_id == ^episode.id, 126 | select: eg.guest_id 127 | existing_guest_ids = Repo.all(query) 128 | 129 | new_episode_guests = guest_ids -- existing_guest_ids 130 | remove_episode_guests = existing_guest_ids -- guest_ids 131 | 132 | multi = Enum.reduce(new_episode_guests, Multi.new, fn(id, multi) -> 133 | attributes = %{guest_id: id, episode_id: episode.id} 134 | changeset = EpisodeGuest.changeset(%EpisodeGuest{}, attributes) 135 | Multi.insert(multi, id, changeset) 136 | end) 137 | 138 | query = from eg in EpisodeGuest, 139 | where: eg.episode_id == ^episode.id and eg.guest_id in ^remove_episode_guests 140 | 141 | multi 142 | |> Multi.delete_all(:delete_episode_guests, query) 143 | |> Repo.transaction() 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /test/controllers/person_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.PersonControllerTest do 2 | use EmberWeekendApi.Web.ConnCase 3 | alias EmberWeekendApi.Web.Person 4 | 5 | setup %{conn: conn} do 6 | conn = conn 7 | |> put_req_header("accept", "application/vnd.api+json") 8 | |> put_req_header("content-type", "application/vnd.api+json") 9 | {:ok, conn: conn} 10 | end 11 | 12 | test "lists all people on index", %{conn: conn} do 13 | attrs = params_for(:person) 14 | person = insert(:person, attrs) 15 | conn = get conn, person_path(conn, :index) 16 | 17 | assert conn.status == 200 18 | assert json_api_response(conn)["data"] == [%{ 19 | "id" => "#{person.id}", 20 | "type" => "people", 21 | "relationships" => %{ 22 | "episodes" => %{}, 23 | "resources" => %{}, 24 | }, 25 | "links" => %{"self" => "/api/people/#{person.id}"}, 26 | "attributes" => attrs 27 | |> string_keys 28 | |> dasherize_keys 29 | }] 30 | end 31 | 32 | test "shows person", %{conn: conn} do 33 | attrs = params_for(:person) 34 | person = insert(:person, attrs) 35 | 36 | conn = get conn, person_path(conn, :show, person) 37 | 38 | assert conn.status == 200 39 | assert json_api_response(conn)["data"] == %{ 40 | "id" => "#{person.id}", 41 | "type" => "people", 42 | "relationships" => %{ 43 | "episodes" => %{ 44 | "data" => [] 45 | }, 46 | "resources" => %{ 47 | "data" => [] 48 | }, 49 | }, 50 | "links" => %{"self" => "/api/people/#{person.id}"}, 51 | "attributes" => attrs 52 | |> string_keys 53 | |> dasherize_keys 54 | } 55 | end 56 | 57 | test "throws error for invalid person id", %{conn: conn} do 58 | conn = get conn, person_path(conn, :show, -1) 59 | 60 | assert conn.status == 404 61 | assert json_api_response(conn)["errors"] == not_found("person") 62 | end 63 | 64 | test "unauthenticated user can't update person", %{conn: conn} do 65 | person = insert(:person) 66 | data = %{data: %{attributes: %{name: "Not secure"}}} 67 | 68 | conn = put conn, person_path(conn, :update, person), data 69 | 70 | assert conn.status == 401 71 | assert json_api_response(conn)["errors"] == unauthorized("person", "update") 72 | db_person = Repo.get!(Person, person.id) 73 | assert db_person.name == person.name 74 | end 75 | 76 | test "unauthenticated user can't delete person", %{conn: conn} do 77 | person = insert(:person) 78 | 79 | conn = delete conn, person_path(conn, :update, person) 80 | 81 | assert conn.status == 401 82 | assert json_api_response(conn)["errors"] == unauthorized("person", "delete") 83 | person = Repo.get!(Person, person.id) 84 | assert person 85 | end 86 | 87 | test "non-admin user can't update person", %{conn: conn} do 88 | conn = authenticated(conn) 89 | person = insert(:person) 90 | data = %{data: %{attributes: %{name: "Not secure"}}} 91 | 92 | conn = put conn, person_path(conn, :update, person), data 93 | 94 | assert conn.status == 401 95 | assert json_api_response(conn)["errors"] == unauthorized("person", "update") 96 | db_person = Repo.get!(Person, person.id) 97 | assert db_person.name == person.name 98 | end 99 | 100 | test "non-admin user can't delete person", %{conn: conn} do 101 | conn = authenticated(conn) 102 | person = insert(:person) 103 | 104 | conn = delete conn, person_path(conn, :update, person) 105 | 106 | assert conn.status == 401 107 | assert json_api_response(conn)["errors"] == unauthorized("person", "delete") 108 | person = Repo.get!(Person, person.id) 109 | assert person 110 | end 111 | 112 | test "admin user can create person", %{conn: conn} do 113 | conn = admin(conn) 114 | attributes = params_for(:person) 115 | |> dasherize_keys 116 | data = %{data: %{type: "people", attributes: attributes}} 117 | 118 | conn = post conn, person_path(conn, :create), data 119 | 120 | assert conn.status == 201 121 | person_id = String.to_integer json_api_response(conn)["data"]["id"] 122 | assert json_api_response(conn)["data"] == %{ 123 | "id" => "#{person_id}", 124 | "type" => "people", 125 | "relationships" => %{ 126 | "episodes" => %{}, 127 | "resources" => %{}, 128 | }, 129 | "links" => %{"self" => "/api/people/#{person_id}"}, 130 | "attributes" => string_keys(attributes) 131 | } 132 | assert Person.count == 1 133 | assert Repo.get!(Person, person_id) 134 | end 135 | 136 | test "admin user can update person", %{conn: conn} do 137 | conn = admin(conn) 138 | person = insert(:person) 139 | attributes = %{name: "Better Name"} 140 | data = %{data: %{id: "#{person.id}", type: "people", attributes: attributes}} 141 | 142 | conn = put conn, person_path(conn, :update, person), data 143 | 144 | assert conn.status == 200 145 | assert json_api_response(conn)["data"]["attributes"]["name"] == "Better Name" 146 | assert Person.count == 1 147 | person = Repo.get!(Person, person.id) 148 | assert person.name == "Better Name" 149 | end 150 | 151 | test "admin user sees validation messages when creating person", %{conn: conn} do 152 | conn = admin(conn) 153 | data = %{data: %{type: "people", attributes: %{}}} 154 | 155 | conn = post conn, person_path(conn, :create), data 156 | 157 | assert conn.status == 422 158 | json_errors = 159 | json_api_response(conn) 160 | |> Map.fetch!("errors") 161 | |> Enum.sort_by(&Map.get(&1, "detail")) 162 | assert json_errors == [ 163 | cant_be_blank("name"), 164 | cant_be_blank("url") 165 | ] 166 | end 167 | 168 | test "admin user sees validation messages when updating person", %{conn: conn} do 169 | conn = admin(conn) 170 | person = insert(:person) 171 | data = %{data: %{id: "#{person.id}", type: "people", attributes: %{"name" => nil}}} 172 | 173 | conn = put conn, person_path(conn, :update, person), data 174 | 175 | assert conn.status == 422 176 | assert json_api_response(conn)["errors"] == [ 177 | cant_be_blank("name") 178 | ] 179 | end 180 | 181 | test "admin user can delete person", %{conn: conn} do 182 | conn = admin(conn) 183 | person = insert(:person) 184 | 185 | conn = delete conn, person_path(conn, :update, person) 186 | 187 | assert conn.status == 204 188 | assert Person.count() == 0 189 | end 190 | 191 | end 192 | -------------------------------------------------------------------------------- /test/controllers/resource_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.ResourceControllerTest do 2 | use EmberWeekendApi.Web.ConnCase 3 | 4 | @valid_attrs %{ 5 | title: "Plumbuses", 6 | url: "http://rickandmorty.wikia.com/wiki/Plumbus" 7 | } 8 | 9 | @invalid_attrs %{} 10 | 11 | setup %{conn: conn} do 12 | conn = conn 13 | |> put_req_header("accept", "application/vnd.api+json") 14 | |> put_req_header("content-type", "application/vnd.api+json") 15 | {:ok, conn: conn} 16 | end 17 | 18 | test "lists all resources on index", %{conn: conn} do 19 | ra = insert(:resource_author) 20 | conn = get conn, resource_path(conn, :index) 21 | 22 | assert conn.status == 200 23 | assert json_api_response(conn)["data"] == [%{ 24 | "relationships" => %{ 25 | "authors" => %{}, 26 | "show-notes" => %{}, 27 | }, 28 | "links" => %{"self" => "/api/resources/#{ra.resource.id}"}, 29 | "id" => "#{ra.resource.id}", 30 | "type" => "resources", 31 | "attributes" => @valid_attrs 32 | |> string_keys 33 | |> dasherize_keys 34 | }] 35 | end 36 | 37 | test "shows resource", %{conn: conn} do 38 | resource = insert(:resource) 39 | 40 | conn = get conn, resource_path(conn, :show, resource) 41 | 42 | assert conn.status == 200 43 | assert json_api_response(conn)["data"] == %{ 44 | "relationships" => %{ 45 | "authors" => %{ 46 | "data" => [] 47 | }, 48 | "show-notes" => %{ 49 | "data" => [] 50 | }, 51 | }, 52 | "id" => "#{resource.id}", 53 | "type" => "resources", 54 | "links" => %{"self" => "/api/resources/#{resource.id}"}, 55 | "attributes" => @valid_attrs 56 | |> string_keys 57 | |> dasherize_keys 58 | } 59 | end 60 | 61 | test "throws error for invalid resource id", %{conn: conn} do 62 | conn = get conn, resource_path(conn, :show, -1) 63 | 64 | assert conn.status == 404 65 | assert json_api_response(conn)["errors"] == not_found("resource") 66 | end 67 | 68 | test "unauthenticated user can't update resource", %{conn: conn} do 69 | resource = insert(:resource) 70 | data = %{data: %{attributes: %{title: "Not secure"}}} 71 | 72 | conn = put conn, resource_path(conn, :update, resource), data 73 | 74 | assert conn.status == 401 75 | assert json_api_response(conn)["errors"] == unauthorized("resource", "update") 76 | resource = Repo.get!(Resource, resource.id) 77 | assert resource.title == @valid_attrs[:title] 78 | end 79 | 80 | test "unauthenticated user can't delete resource", %{conn: conn} do 81 | resource = insert(:resource) 82 | 83 | conn = delete conn, resource_path(conn, :update, resource) 84 | 85 | assert conn.status == 401 86 | assert json_api_response(conn)["errors"] == unauthorized("resource", "delete") 87 | resource = Repo.get!(Resource, resource.id) 88 | assert resource 89 | end 90 | 91 | test "non-admin user can't update resource", %{conn: conn} do 92 | conn = authenticated(conn) 93 | resource = insert(:resource) 94 | data = %{data: %{attributes: %{title: "Not secure"}}} 95 | 96 | conn = put conn, resource_path(conn, :update, resource), data 97 | 98 | assert conn.status == 401 99 | assert json_api_response(conn)["errors"] == unauthorized("resource", "update") 100 | resource = Repo.get!(Resource, resource.id) 101 | assert resource.title == @valid_attrs[:title] 102 | end 103 | 104 | test "non-admin user can't delete resource", %{conn: conn} do 105 | conn = authenticated(conn) 106 | resource = insert(:resource) 107 | 108 | conn = delete conn, resource_path(conn, :update, resource) 109 | 110 | assert conn.status == 401 111 | assert json_api_response(conn)["errors"] == unauthorized("resource", "delete") 112 | resource = Repo.get!(Resource, resource.id) 113 | assert resource 114 | end 115 | 116 | test "admin user can create resource", %{conn: conn} do 117 | conn = admin(conn) 118 | person = insert(:person) 119 | attributes = @valid_attrs 120 | |> dasherize_keys 121 | data = %{ 122 | data: %{ 123 | type: "resources", 124 | attributes: attributes, 125 | relationships: %{ 126 | authors: %{ data: [%{ type: "people", id: "#{person.id}" }] } 127 | } 128 | } 129 | } 130 | 131 | conn = post conn, resource_path(conn, :create), data 132 | 133 | assert conn.status == 201 134 | resource_id = String.to_integer json_api_response(conn)["data"]["id"] 135 | assert json_api_response(conn)["data"] == %{ 136 | "relationships" => %{ 137 | "authors" => %{ 138 | "data" => [%{ "type" => "people", "id" => "#{person.id}" }] 139 | }, 140 | "show-notes" => %{}, 141 | }, 142 | "id" => "#{resource_id}", 143 | "type" => "resources", 144 | "links" => %{"self" => "/api/resources/#{resource_id}"}, 145 | "attributes" => string_keys(attributes) 146 | } 147 | assert Resource.count == 1 148 | assert Repo.get!(Resource, resource_id) 149 | end 150 | 151 | test "admin user can update resource", %{conn: conn} do 152 | conn = admin(conn) 153 | resource = insert(:resource) 154 | attributes = %{title: "Better Title"} 155 | data = %{data: %{id: "#{resource.id}", type: "resources", attributes: attributes}} 156 | 157 | conn = put conn, resource_path(conn, :update, resource), data 158 | 159 | assert conn.status == 200 160 | assert json_api_response(conn)["data"]["attributes"]["title"] == "Better Title" 161 | assert Resource.count == 1 162 | resource = Repo.get!(Resource, resource.id) 163 | assert resource.title == "Better Title" 164 | end 165 | 166 | test "admin user sees validation messages when creating resource", %{conn: conn} do 167 | conn = admin(conn) 168 | data = %{data: %{type: "resources", attributes: @invalid_attrs}} 169 | 170 | conn = post conn, resource_path(conn, :create), data 171 | 172 | assert conn.status == 422 173 | assert (json_api_response(conn)["errors"] |> sort_by("detail")) == [ 174 | cant_be_blank("title"), 175 | cant_be_blank("url") 176 | ] |> sort_by("detail") 177 | end 178 | 179 | test "admin user sees validation messages when updating resource", %{conn: conn} do 180 | conn = admin(conn) 181 | resource = insert(:resource) 182 | data = %{data: %{id: "#{resource.id}", type: "resources", attributes: %{"title" => nil}}} 183 | 184 | conn = put conn, resource_path(conn, :update, resource), data 185 | 186 | assert conn.status == 422 187 | assert json_api_response(conn)["errors"] == [ 188 | cant_be_blank("title") 189 | ] 190 | end 191 | 192 | test "admin user can delete resource", %{conn: conn} do 193 | conn = admin(conn) 194 | resource = insert(:resource) 195 | 196 | conn = delete conn, resource_path(conn, :update, resource) 197 | 198 | assert conn.status == 204 199 | assert Resource.count() == 0 200 | end 201 | 202 | end 203 | -------------------------------------------------------------------------------- /test/controllers/show_note_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.ShowNoteControllerTest do 2 | use EmberWeekendApi.Web.ConnCase 3 | 4 | setup %{conn: conn} do 5 | conn = conn 6 | |> put_req_header("accept", "application/vnd.api+json") 7 | |> put_req_header("content-type", "application/vnd.api+json") 8 | {:ok, conn: conn} 9 | end 10 | 11 | test "lists all entries on index", %{conn: conn} do 12 | ra = insert(:resource_author) 13 | resource = ra.resource 14 | author = ra.author 15 | show_note = insert(:show_note, resource: resource) 16 | 17 | conn = get conn, show_note_path(conn, :index) 18 | resp = json_api_response(conn) 19 | 20 | assert conn.status == 200 21 | assert [author_incl, resource_incl] = Enum.sort_by(resp["included"], & Map.get(&1, "type")) 22 | 23 | assert resource_incl == %{ 24 | "id" => "#{resource.id}", 25 | "type" => "resources", 26 | "links" => %{"self" => "/api/resources/#{resource.id}"}, 27 | "relationships" => %{ 28 | "authors" => %{ 29 | "data" => [%{ "type" => "people", "id" => "#{author.id}" }] 30 | }, 31 | "show-notes" => %{}, 32 | }, 33 | "attributes" => %{ 34 | "title" => resource.title, 35 | "url" => resource.url, 36 | }} 37 | 38 | assert author_incl == %{ 39 | "id" => "#{author.id}", 40 | "type" => "people", 41 | "relationships" => %{ 42 | "episodes" => %{}, 43 | "resources" => %{}, 44 | }, 45 | "links" => %{"self" => "/api/people/#{author.id}"}, 46 | "attributes" => %{ 47 | "name" => author.name, 48 | "avatar-url" => author.avatar_url, 49 | "handle" => author.handle, 50 | "tagline" => author.tagline, 51 | "bio" => author.bio, 52 | "url" => author.url, 53 | }} 54 | 55 | assert json_api_response(conn)["data"] == [%{ 56 | "relationships" => %{ 57 | "resource" => %{ 58 | "data" => %{ "type" => "resources", "id" => "#{resource.id}" } 59 | }, 60 | "episode" => %{ 61 | "data" => %{ "type" => "episodes", "id" => "#{show_note.episode.id}" } 62 | } 63 | }, 64 | "links" => %{"self" => "/api/show-notes/#{show_note.id}"}, 65 | "id" => "#{show_note.id}", 66 | "type" => "show-notes", 67 | "attributes" => %{"time-stamp" => "01:14", "note" => "Wubalubadub"} 68 | }] 69 | end 70 | 71 | test "admin user can create show note", %{conn: conn} do 72 | conn = admin(conn) 73 | ra = insert(:resource_author) 74 | resource = ra.resource 75 | episode = insert(:episode) 76 | 77 | data = %{ 78 | data: %{ 79 | type: "show-notes", 80 | attributes: %{"time-stamp" => "01:14", "note" => "My Note"}, 81 | relationships: %{ 82 | "resource" => %{ "data" => %{ "type" => "resources", "id" => "#{resource.id}" } }, 83 | "episode" => %{ "data" => %{ "type" => "episodes", "id" => "#{episode.id}" } } 84 | } 85 | } 86 | } 87 | 88 | conn = post conn, show_note_path(conn, :create), data 89 | 90 | assert conn.status == 201 91 | show_note_id = String.to_integer json_api_response(conn)["data"]["id"] 92 | assert json_api_response(conn)["data"] == %{ 93 | "id" => "#{show_note_id}", 94 | "type" => "show-notes", 95 | "links" => %{"self" => "/api/show-notes/#{show_note_id}"}, 96 | "attributes" => %{"time-stamp" => "01:14", "note" => "My Note"}, 97 | "relationships" => %{ 98 | "resource" => %{ "data" => %{ "type" => "resources", "id" => "#{resource.id}" } }, 99 | "episode" => %{ "data" => %{ "type" => "episodes", "id" => "#{episode.id}" } } 100 | } 101 | } 102 | assert ShowNote.count == 1 103 | assert Repo.get!(ShowNote, show_note_id) 104 | end 105 | 106 | test "admin user can create show note without resource", %{conn: conn} do 107 | conn = admin(conn) 108 | episode = insert(:episode) 109 | 110 | data = %{ 111 | data: %{ 112 | type: "show-notes", 113 | attributes: %{"time-stamp" => "01:14", "note" => "My Note"}, 114 | relationships: %{ 115 | "episode" => %{ "data" => %{ "type" => "episodes", "id" => "#{episode.id}" } } 116 | } 117 | } 118 | } 119 | 120 | conn = post conn, show_note_path(conn, :create), data 121 | 122 | assert conn.status == 201 123 | show_note_id = String.to_integer json_api_response(conn)["data"]["id"] 124 | assert json_api_response(conn)["data"] == %{ 125 | "id" => "#{show_note_id}", 126 | "type" => "show-notes", 127 | "links" => %{"self" => "/api/show-notes/#{show_note_id}"}, 128 | "attributes" => %{"time-stamp" => "01:14", "note" => "My Note"}, 129 | "relationships" => %{ 130 | "resource" => %{ "data" => nil }, 131 | "episode" => %{ "data" => %{ "type" => "episodes", "id" => "#{episode.id}" } } 132 | } 133 | } 134 | assert ShowNote.count == 1 135 | assert Repo.get!(ShowNote, show_note_id) 136 | end 137 | 138 | 139 | test "admin user can update show note attributes", %{conn: conn} do 140 | conn = admin(conn) 141 | ra = insert(:resource_author) 142 | resource = ra.resource 143 | show_note = insert(:show_note, resource: resource) 144 | episode = show_note.episode 145 | 146 | updated_attrs = %{time_stamp: "10:00", note: "A Note"} 147 | data = %{ 148 | data: %{ 149 | type: "show-notes", 150 | attributes: updated_attrs, 151 | relationships: %{ 152 | "resource" => %{ "data" => %{ "type" => "resources", "id" => "#{resource.id}" } }, 153 | "episode" => %{ "data" => %{ "type" => "episodes", "id" => "#{episode.id}" } } 154 | } 155 | } 156 | } 157 | 158 | conn = put conn, show_note_path(conn, :update, show_note), data 159 | 160 | assert conn.status == 200 161 | show_note_id = String.to_integer json_api_response(conn)["data"]["id"] 162 | assert json_api_response(conn)["data"] == %{ 163 | "id" => "#{show_note_id}", 164 | "type" => "show-notes", 165 | "links" => %{"self" => "/api/show-notes/#{show_note_id}"}, 166 | "attributes" => updated_attrs 167 | |> string_keys 168 | |> dasherize_keys, 169 | "relationships" => %{ 170 | "resource" => %{ "data" => %{ "type" => "resources", "id" => "#{resource.id}" } }, 171 | "episode" => %{ "data" => %{ "type" => "episodes", "id" => "#{episode.id}" } } 172 | } 173 | } 174 | assert ShowNote.count == 1 175 | assert Repo.get!(ShowNote, show_note_id) 176 | end 177 | 178 | test "admin user can update show note relationships", %{conn: conn} do 179 | conn = admin(conn) 180 | ra1 = insert(:resource_author) 181 | show_note1 = insert(:show_note, resource: ra1.resource) 182 | 183 | ra2 = insert(:resource_author) 184 | episode2 = insert(:episode) 185 | 186 | updated_attrs = %{time_stamp: "10:00", note: "A Note"} 187 | data = %{ 188 | data: %{ 189 | type: "show-notes", 190 | attributes: updated_attrs, 191 | relationships: %{ 192 | "resource" => %{ "data" => %{ "type" => "resources", "id" => "#{ra2.resource.id}" } }, 193 | "episode" => %{ "data" => %{ "type" => "episodes", "id" => "#{episode2.id}" } } 194 | } 195 | } 196 | } 197 | 198 | conn = put conn, show_note_path(conn, :update, show_note1), data 199 | 200 | assert conn.status == 200 201 | show_note_id = String.to_integer json_api_response(conn)["data"]["id"] 202 | assert json_api_response(conn)["data"] == %{ 203 | "id" => "#{show_note_id}", 204 | "type" => "show-notes", 205 | "links" => %{"self" => "/api/show-notes/#{show_note_id}"}, 206 | "attributes" => updated_attrs 207 | |> string_keys 208 | |> dasherize_keys, 209 | "relationships" => %{ 210 | "resource" => %{ "data" => %{ "type" => "resources", "id" => "#{ra2.resource.id}" } }, 211 | "episode" => %{ "data" => %{ "type" => "episodes", "id" => "#{episode2.id}" } } 212 | } 213 | } 214 | assert ShowNote.count == 1 215 | assert Repo.get!(ShowNote, show_note_id) 216 | end 217 | 218 | test "admin user can delete show note", %{conn: conn} do 219 | conn = admin(conn) 220 | show_note = insert(:show_note) 221 | 222 | conn = delete conn, show_note_path(conn, :update, show_note) 223 | 224 | assert conn.status == 204 225 | assert ShowNote.count() == 0 226 | end 227 | end 228 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "certifi": {:hex, :certifi, "1.0.0", "1c787a85b1855ba354f0b8920392c19aa1d06b0ee1362f9141279620a5be2039", [:rebar3], [], "hexpm", "44a5aa4261490a7d7fa6909ab4bcf14bff928a4fef49e80fc1e7a8fdb7b45f79"}, 3 | "combine": {:hex, :combine, "0.9.6", "8d1034a127d4cbf6924c8a5010d3534d958085575fa4d9b878f200d79ac78335", [:mix], [], "hexpm", "0b450698443dc9ab84cee85976752b4af1009cdf0f01da9ee8ef2550dc67c47f"}, 4 | "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, 5 | "corsica": {:hex, :corsica, "0.5.0", "eb5b2fccc5bc4f31b8e2b77dd15f5f302aca5d63286c953e8e916f806056d50c", [:mix], [{:cowboy, ">= 1.0.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, ">= 0.9.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "caf752cfe4ecaf18fdf6cb9e6245f66683fd74aed1e76961d908f55e026db652"}, 6 | "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", "f4763bbe08233eceed6f24bc4fcc8d71c17cfeafa6439157c57349aa1bb4f17c"}, 7 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm", "db622da03aa039e6366ab953e31186cc8190d32905e33788a1acb22744e6abd2"}, 8 | "db_connection": {:hex, :db_connection, "2.4.0", "d04b1b73795dae60cead94189f1b8a51cc9e1f911c234cc23074017c43c031e5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad416c21ad9f61b3103d254a71b63696ecadb6a917b36f563921e0de00d7d7c8"}, 9 | "decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"}, 10 | "ecto": {:hex, :ecto, "3.2.2", "bb6d1dbcd7ef975b60637e63182e56f3d7d0b5dd9c46d4b9d6183a5c455d65d1", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "2c40fbcc7c6ff8eeaed021e3dd02b45b8e3824ad790059e6ac10de228b281349"}, 11 | "ecto_sql": {:hex, :ecto_sql, "3.2.0", "751cea597e8deb616084894dd75cbabfdbe7255ff01e8c058ca13f0353a3921b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a2e23cf761668126252418cae07eff7967ad0152fbc5e2d0dc3de487a5ec774c"}, 12 | "ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "b84f6af156264530b312a8ab98ac6088f6b77ae5fe2058305c81434aa01fbaf9"}, 13 | "faker": {:hex, :faker, "0.7.0", "2c42deeac7be717173c78c77fb3edc749fb5d5e460e33d01fe592ae99acc2f0d", [:mix], [], "hexpm", "84eae1a1b31ee989acd043ce6e876c837cae6dd9fd5f81b977359a7cddf129bb"}, 14 | "feeder": {:hex, :feeder, "2.2.1", "9b1236d32cf971a049968b4c3955fa4808bd132b347af6e30a06fc2093751796", [:make], [], "hexpm", "24ecbd9b25bdd724cf12988bef73415c6c8aa62d7fddda75607ad9ea008b27f2"}, 15 | "feeder_ex": {:hex, :feeder_ex, "1.0.1", "5516b570757af89c2148c9ef9985edec4757776a2dad9f90628fe2f67aec71f8", [:mix], [{:feeder, "~> 2.1", [hex: :feeder, repo: "hexpm", optional: false]}], "hexpm", "9087c7c86a2d7167e1b752d5834e67691567f84f006a5a6080755c5ad0b9fcd8"}, 16 | "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], [], "hexpm", "9a00246e8af58cdf465ae7c48fd6fd7ba2e43300413dfcc25447ecd3bf76f0c1"}, 17 | "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], [], "hexpm", "b233b4ab0d349a359b52592d2d591fc6e4b20fdbe0b15a624cc15a3ca509a1cc"}, 18 | "hackney": {:hex, :hackney, "1.6.6", "5564b4695d48fd87859e9df77a7fa4b4d284d24519f0cd7cc898f09e8fbdc8a3", [:rebar3], [{:certifi, "1.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "4.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "6c8293db002ecd6f6126f1048fe6c11f89404f8b2fcdd43343e2a7267db87d7b"}, 19 | "httpoison": {:hex, :httpoison, "0.8.3", "b675a3fdc839a0b8d7a285c6b3747d6d596ae70b6ccb762233a990d7289ccae4", [:mix], [{:hackney, "~> 1.6.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "74f2103e6eff47dcc2b288e37f42629874df3e4a4dce5fbc9dea508de4785e06"}, 20 | "idna": {:hex, :idna, "4.0.0", "10aaa9f79d0b12cf0def53038547855b91144f1bfcc0ec73494f38bb7b9c4961", [:rebar3], [], "hexpm", "f1b699f7275728538da7b5e35679f9e0f41ad8e0a49896e6a27b61867ed344eb"}, 21 | "inflex": {:hex, :inflex, "1.8.0", "7cfc752ae244b30b42ac9cf4c092771b485bc3424139aba4a933e54be8c63931", [:mix], [], "hexpm", "e68fe387f6a399a8f1d634120ec33dc8bb555b38e5056ff665269b8696ee20fc"}, 22 | "ja_serializer": {:hex, :ja_serializer, "0.12.0", "ba4ec5fc7afa6daba815b5cb2b9bd0de410554ac4f0ed54e954d39decb353ca4", [:mix], [{:inflex, "~> 1.4", [hex: :inflex, repo: "hexpm", optional: false]}, {:plug, "> 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, ">= 1.4.0", [hex: :poison, repo: "hexpm", optional: false]}, {:scrivener, "~> 1.2 or ~> 2.0", [hex: :scrivener, repo: "hexpm", optional: true]}], "hexpm", "6b88c2e0d4dd9ed43836f19e3a25a3affaa14520ce084a54d0a72c9fabc9ddd8"}, 23 | "json": {:hex, :json, "0.3.3", "373eb4f7321f898ad6772999f30daf65f9f38e1d3dd4d798d7a82d3b123fe1d3", [:mix], [], "hexpm", "d1986548847189b51f1efb65d196e6ab9f2e88a6878a363aec0e3c77e2550616"}, 24 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 25 | "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"}, 26 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm", "7a4c8e1115a2732a67d7624e28cf6c9f30c66711a9e92928e745c255887ba465"}, 27 | "phoenix": {:hex, :phoenix, "1.3.0-rc.1", "0d04948a4bd24823f101024c07b6a4d35e58f1fd92a465c1bc75dd37acd1041a", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "85a0761719102cdbdcf5fbdef219544f86c6bf3c7e07c75253e037fd3d85ca62"}, 28 | "phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "fe15d9fee5b82f5e64800502011ffe530650d42e1710ae9b14bc4c9be38bf303"}, 29 | "phoenix_html": {:hex, :phoenix_html, "2.13.3", "850e292ff6e204257f5f9c4c54a8cb1f6fbc16ed53d360c2b780a3d0ba333867", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "8b01b3d6d39731ab18aa548d928b5796166d2500755f553725cfe967bafba7d9"}, 30 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.8", "4333f9c74190f485a74866beff2f9304f069d53f047f5fbb0fb8d1ee4c495f73", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2-rc", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "af4d9faeb655ef933cf66fd4d83f8bb0146ab0da30c81cfbdc2152825970fa76"}, 31 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.1", "c10ddf6237007c804bf2b8f3c4d5b99009b42eca3a0dfac04ea2d8001186056a", [:mix], [], "hexpm", "4acd2f6100e6f23f1662afadc6da7342a6718876c41886bbb79acb97a50cd398"}, 32 | "plug": {:hex, :plug, "1.8.3", "12d5f9796dc72e8ac9614e94bda5e51c4c028d0d428e9297650d09e15a684478", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "164baaeb382d19beee0ec484492aa82a9c8685770aee33b24ec727a0971b34d0"}, 33 | "plug_cowboy": {:hex, :plug_cowboy, "1.0.0", "2e2a7d3409746d335f451218b8bb0858301c3de6d668c3052716c909936eb57a", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "01d201427a8a1f4483be2465a98b45f5e82263327507fe93404a61c51eb9e9a8"}, 34 | "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm", "73c1682f0e414cfb5d9b95c8e8cd6ffcfdae699e3b05e1db744e58b7be857759"}, 35 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, 36 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []}, 37 | "postgrex": {:hex, :postgrex, "0.15.8", "f5e782bbe5e8fa178d5e3cd1999c857dc48eda95f0a4d7f7bd92a50e84a0d491", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "698fbfacea34c4cf22c8281abeb5cf68d99628d541874f085520ab3b53d356fe"}, 38 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm", "6e56493a862433fccc3aca3025c946d6720d8eedf6e3e6fb911952a7071c357f"}, 39 | "secure_random": {:hex, :secure_random, "0.5.1", "c5532b37c89d175c328f5196a0c2a5680b15ebce3e654da37129a9fe40ebf51b", [:mix], [], "hexpm", "1b9754f15e3940a143baafd19da12293f100044df69ea12db5d72878312ae6ab"}, 40 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm", "4f8805eb5c8a939cf2359367cb651a3180b27dfb48444846be2613d79355d65e"}, 41 | "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm", "e9e3cacfd37c1531c0ca70ca7c0c30ce2dbb02998a4f7719de180fe63f8d41e4"}, 42 | "timex": {:hex, :timex, "3.1.13", "48b33162e3ec33e9a08fb5f98e3f3c19c3e328dded3156096c1969b77d33eef0", [:mix], [{:combine, "~> 0.7", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "760120055428edad7fd9b67aeed280b89ac7fab4b1e330a274cd09703f31f314"}, 43 | "timex_ecto": {:hex, :timex_ecto, "3.1.1", "37d54f6879d96a6789bb497296531cfb853631de78e152969d95cff03c1368dd", [:mix], [{:ecto, "~> 2.1.0", [hex: :ecto, optional: false]}, {:timex, "~> 3.0", [hex: :timex, optional: false]}]}, 44 | "tzdata": {:hex, :tzdata, "0.5.11", "3d5469a9f46bdf4a8760333dbdabdcc4751325035c454b10521f71e7c611ae50", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "4cc579be66dc95303d8ef240aa3ff9c05df6ba1037ae49baf613a45235a9977a"}, 45 | } 46 | -------------------------------------------------------------------------------- /test/controllers/episode_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EmberWeekendApi.EpisodeControllerTest do 2 | use EmberWeekendApi.Web.ConnCase 3 | 4 | @release_date ~D[2013-12-15] 5 | @valid_attrs %{ 6 | number: 1, 7 | title: "Anatomy Park", 8 | description: "Rick and Morty try to save the life of a homeless man; Jerry's parents visit.", 9 | slug: "anatomy-park", 10 | release_date: @release_date, 11 | filename: "s01e03", 12 | duration: "1:00:00", 13 | published: true, 14 | length: 1 15 | } 16 | 17 | @invalid_attrs %{} 18 | 19 | @valid_show_note_attrs %{time_stamp: "01:14", note: "Wubalubadub"} 20 | 21 | @valid_resource_attrs %{ 22 | title: "Plumbuses", 23 | url: "http://rickandmorty.wikia.com/wiki/Plumbus" 24 | } 25 | 26 | @valid_person_attrs %{ 27 | name: "Jerry Smith", 28 | handle: "dr_pluto", 29 | bio: "Jerry can sometimes become misguided by his insecurities.", 30 | tagline: "Well look where being smart got you.", 31 | url: "http://rickandmorty.wikia.com/wiki/Jerry_Smith", 32 | avatar_url: "http://vignette3.wikia.nocookie.net/rickandmorty/images/5/5d/Jerry_S01E11_Sad.JPG/revision/latest?cb=20140501090439" 33 | } 34 | 35 | setup %{conn: conn} do 36 | conn = conn 37 | |> put_req_header("accept", "application/vnd.api+json") 38 | |> put_req_header("content-type", "application/vnd.api+json") 39 | {:ok, conn: conn} 40 | end 41 | 42 | test "lists all published episodes on index", %{conn: conn} do 43 | attrs = params_for(:episode, published: true, release_date: @release_date) 44 | episode = insert(:episode, attrs) 45 | _unpublished = insert(:episode, published: false) 46 | conn = get conn, episode_path(conn, :index) 47 | 48 | assert conn.status == 200 49 | 50 | assert json_api_response(conn)["data"] == [%{ 51 | "id" => "#{episode.id}", 52 | "type" => "episodes", 53 | "attributes" => attrs 54 | |> string_keys 55 | |> dasherize_keys 56 | |> convert_dates 57 | }] 58 | end 59 | 60 | test "admin user lists all episodes on index", %{conn: conn} do 61 | conn = admin(conn) 62 | episode = insert(:episode) 63 | unpublished = insert(:episode, published: false) 64 | 65 | conn = get conn, episode_path(conn, :index) 66 | 67 | assert conn.status == 200 68 | assert Enum.map(json_api_response(conn)["data"], fn(e) -> e["id"] end) == ["#{episode.id}", "#{unpublished.id}"] 69 | end 70 | 71 | test "non-admin user gets 404 for unpublished episode", %{conn: conn} do 72 | episode = insert(:episode, published: false) 73 | 74 | conn = get conn, episode_path(conn, :show, episode) 75 | 76 | assert conn.status == 404 77 | end 78 | 79 | test "admin user views unpublished episode", %{conn: conn} do 80 | conn = admin(conn) 81 | episode = insert(:episode, published: false) 82 | 83 | conn = get conn, episode_path(conn, :show, episode) 84 | 85 | assert conn.status == 200 86 | end 87 | 88 | test "shows episode", %{conn: conn} do 89 | attrs = params_for(:episode, published: true, release_date: @release_date) 90 | ra = insert(:resource_author) 91 | person = ra.author 92 | resource = ra.resource 93 | episode = insert(:episode, attrs) 94 | show_note = insert(:show_note, resource: resource, episode: episode) 95 | Repo.insert! %EpisodeGuest{episode_id: episode.id, guest_id: person.id} 96 | 97 | conn = get conn, episode_path(conn, :show, episode) 98 | 99 | assert conn.status == 200 100 | assert json_api_response(conn)["data"] == %{ 101 | "id" => "#{episode.id}", 102 | "type" => "episodes", 103 | "attributes" => attrs 104 | |> string_keys 105 | |> dasherize_keys 106 | |> convert_dates, 107 | "relationships" => %{ 108 | "guests" => %{ 109 | "data" => [%{ "type" => "people", "id" => "#{person.id}" }] 110 | }, 111 | "show-notes" => %{ 112 | "data" => [%{ "type" => "show-notes", "id" => "#{show_note.id}" }] 113 | } 114 | } 115 | } 116 | 117 | resp = json_api_response(conn) 118 | assert [person_incl, resource_incl, show_note_incl] = Enum.sort_by(resp["included"], & Map.get(&1, "type")) 119 | 120 | assert show_note_incl == %{ 121 | "attributes" => @valid_show_note_attrs 122 | |> string_keys 123 | |> dasherize_keys, 124 | "id" => "#{show_note.id}", 125 | "links" => %{"self" => "/api/show-notes/#{show_note.id}"}, 126 | "relationships" => %{ 127 | "resource" => %{ "data" => %{ "type" => "resources", "id" => "#{resource.id}" } }, 128 | "episode" => %{ "data" => %{ "type" => "episodes", "id" => "#{episode.id}" } } 129 | }, 130 | "type" => "show-notes" 131 | } 132 | assert resource_incl == %{ 133 | "attributes" => @valid_resource_attrs 134 | |> string_keys 135 | |> dasherize_keys, 136 | "id" => "#{resource.id}", 137 | "links" => %{"self" => "/api/resources/#{resource.id}"}, 138 | "type" => "resources", 139 | "relationships" => %{ 140 | "authors" => %{ "data" => [%{ "type" => "people", "id" => "#{person.id}" }] }, 141 | "show-notes" => %{}, 142 | } 143 | } 144 | assert person_incl == %{ 145 | "attributes" => @valid_person_attrs 146 | |> string_keys 147 | |> dasherize_keys, 148 | "id" => "#{person.id}", 149 | "links" => %{"self" => "/api/people/#{person.id}"}, 150 | "type" => "people", 151 | "relationships" => %{ 152 | "episodes" => %{}, 153 | "resources" => %{}, 154 | } 155 | } 156 | end 157 | 158 | test "shows episode by slug", %{conn: conn} do 159 | attrs = params_for(:episode, published: true, release_date: @release_date) 160 | ra = insert(:resource_author) 161 | person = ra.author 162 | resource = ra.resource 163 | episode = insert(:episode, attrs) 164 | show_note = insert(:show_note, resource: resource, episode: episode) 165 | Repo.insert! %EpisodeGuest{episode_id: episode.id, guest_id: person.id} 166 | 167 | conn = get conn, episode_path(conn, :show, episode.slug) 168 | 169 | assert conn.status == 200 170 | assert json_api_response(conn)["data"] == %{ 171 | "id" => "#{episode.id}", 172 | "type" => "episodes", 173 | "attributes" => attrs 174 | |> string_keys 175 | |> dasherize_keys 176 | |> convert_dates, 177 | "relationships" => %{ 178 | "show-notes" => %{ 179 | "data" => [%{ "type" => "show-notes", "id" => "#{show_note.id}" }] 180 | }, 181 | "guests" => %{ 182 | "data" => [%{ "type" => "people", "id" => "#{person.id}" }] 183 | } 184 | } 185 | } 186 | 187 | resp = json_api_response(conn) 188 | assert [person_incl, resource_incl, show_note_incl] = Enum.sort_by(resp["included"], & Map.get(&1, "type")) 189 | 190 | assert show_note_incl == %{ 191 | "attributes" => @valid_show_note_attrs 192 | |> string_keys 193 | |> dasherize_keys, 194 | "id" => "#{show_note.id}", 195 | "links" => %{"self" => "/api/show-notes/#{show_note.id}"}, 196 | "relationships" => %{ 197 | "resource" => %{ "data" => %{ "type" => "resources", "id" => "#{resource.id}" } }, 198 | "episode" => %{ "data" => %{ "type" => "episodes", "id" => "#{episode.id}" } } 199 | }, 200 | "type" => "show-notes" 201 | } 202 | assert resource_incl == %{ 203 | "attributes" => @valid_resource_attrs 204 | |> string_keys 205 | |> dasherize_keys, 206 | "id" => "#{resource.id}", 207 | "links" => %{"self" => "/api/resources/#{resource.id}"}, 208 | "type" => "resources", 209 | "relationships" => %{ 210 | "authors" => %{ "data" => [%{ "type" => "people", "id" => "#{person.id}" }] }, 211 | "show-notes" => %{}, 212 | } 213 | } 214 | assert person_incl == %{ 215 | "attributes" => @valid_person_attrs 216 | |> string_keys 217 | |> dasherize_keys, 218 | "id" => "#{person.id}", 219 | "links" => %{"self" => "/api/people/#{person.id}"}, 220 | "type" => "people", 221 | "relationships" => %{ 222 | "episodes" => %{}, 223 | "resources" => %{}, 224 | } 225 | } 226 | end 227 | 228 | test "throws error for invalid episode id", %{conn: conn} do 229 | conn = get conn, episode_path(conn, :show, -1) 230 | 231 | assert conn.status == 404 232 | assert json_api_response(conn)["errors"] == not_found("episode") 233 | end 234 | 235 | test "unauthenticated user can't update episode", %{conn: conn} do 236 | episode = insert(:episode, published: true) 237 | data = %{data: %{attributes: %{title: "Not secure"}}} 238 | 239 | conn = put conn, episode_path(conn, :update, episode), data 240 | 241 | assert conn.status == 401 242 | assert json_api_response(conn)["errors"] == unauthorized("episode", "update") 243 | assert Repo.get!(Episode, episode.id) 244 | end 245 | 246 | test "unauthenticated user can't delete episode", %{conn: conn} do 247 | episode = insert(:episode, published: true) 248 | 249 | conn = delete conn, episode_path(conn, :update, episode) 250 | 251 | assert conn.status == 401 252 | assert json_api_response(conn)["errors"] == unauthorized("episode", "delete") 253 | assert Repo.get!(Episode, episode.id) 254 | end 255 | 256 | test "unauthenticated user can't create episode", %{conn: conn} do 257 | params = params_for(:episode, release_date: @release_date) 258 | 259 | conn = post conn, episode_path(conn, :create), params 260 | 261 | assert conn.status == 401 262 | assert json_api_response(conn)["errors"] == unauthorized("episode", "create") 263 | assert Episode.count() == 0 264 | end 265 | 266 | test "non-admin user can't update episode", %{conn: conn} do 267 | conn = authenticated(conn) 268 | episode = insert(:episode) 269 | data = %{data: %{attributes: %{title: "Not secure"}}} 270 | 271 | conn = put conn, episode_path(conn, :update, episode), data 272 | 273 | assert conn.status == 401 274 | assert json_api_response(conn)["errors"] == unauthorized("episode", "update") 275 | assert Repo.get!(Episode, episode.id) 276 | end 277 | 278 | test "non-admin user can't delete episode", %{conn: conn} do 279 | conn = authenticated(conn) 280 | episode = insert(:episode) 281 | 282 | conn = delete conn, episode_path(conn, :update, episode) 283 | 284 | assert conn.status == 401 285 | assert json_api_response(conn)["errors"] == unauthorized("episode", "delete") 286 | episode = Repo.get!(Episode, episode.id) 287 | assert episode 288 | end 289 | 290 | test "non-admin user can't create episode", %{conn: conn} do 291 | conn = authenticated(conn) 292 | params = params_for(:episode, release_date: @release_date) 293 | 294 | conn = post conn, episode_path(conn, :create), params 295 | 296 | assert conn.status == 401 297 | assert json_api_response(conn)["errors"] == unauthorized("episode", "create") 298 | assert Episode.count() == 0 299 | end 300 | 301 | test "admin user can delete episode", %{conn: conn} do 302 | conn = admin(conn) 303 | episode = insert(:episode) 304 | 305 | conn = delete conn, episode_path(conn, :update, episode) 306 | 307 | assert conn.status == 204 308 | assert Episode.count() == 0 309 | end 310 | 311 | test "admin user can create episode", %{conn: conn} do 312 | conn = admin(conn) 313 | person = insert(:person) 314 | attributes = @valid_attrs 315 | |> Map.delete(:release_date) 316 | |> Map.merge(%{"release-date": "2013-12-15"}) 317 | data = %{ 318 | data: %{ 319 | type: "episodes", 320 | attributes: attributes, 321 | relationships: %{ 322 | guests: %{ data: [%{ type: "people", id: "#{person.id}" }] } 323 | } 324 | } 325 | } 326 | 327 | conn = post conn, episode_path(conn, :create), data 328 | 329 | assert conn.status == 201 330 | episode_id = String.to_integer json_api_response(conn)["data"]["id"] 331 | assert json_api_response(conn)["data"] == %{ 332 | "id" => "#{episode_id}", 333 | "type" => "episodes", 334 | "attributes" => attributes 335 | |> string_keys 336 | |> dasherize_keys 337 | |> convert_dates, 338 | "relationships" => %{ 339 | "show-notes" => %{ 340 | "data" => [] 341 | }, 342 | "guests" => %{ 343 | "data" => [ 344 | %{"type" => "people", "id" => "#{person.id}"} 345 | ] 346 | } 347 | } 348 | } 349 | assert Episode.count == 1 350 | assert Repo.get!(Episode, episode_id) 351 | end 352 | 353 | test "admin user can update episode", %{conn: conn} do 354 | conn = admin(conn) 355 | episode = insert(:episode) 356 | attributes = %{title: "Better title"} 357 | data = %{data: %{id: "#{episode.id}", type: "episodes", attributes: attributes}} 358 | 359 | conn = put conn, episode_path(conn, :update, episode), data 360 | 361 | assert conn.status == 200 362 | assert json_api_response(conn)["data"]["attributes"]["title"] == "Better title" 363 | assert Episode.count == 1 364 | episode = Repo.get!(Episode, episode.id) 365 | assert episode.title == "Better title" 366 | end 367 | 368 | test "admin user sees validation messages when creating episode", %{conn: conn} do 369 | conn = admin(conn) 370 | data = %{data: %{type: "episodes", attributes: @invalid_attrs}} 371 | 372 | conn = post conn, episode_path(conn, :create), data 373 | 374 | assert conn.status == 422 375 | assert (json_api_response(conn)["errors"] |> sort_by("detail")) == [ 376 | cant_be_blank("number"), 377 | cant_be_blank("title"), 378 | cant_be_blank("description"), 379 | cant_be_blank("release_date"), 380 | cant_be_blank("slug"), 381 | cant_be_blank("duration"), 382 | cant_be_blank("filename"), 383 | cant_be_blank("published"), 384 | cant_be_blank("length") 385 | ] |> sort_by("detail") 386 | end 387 | 388 | test "admin user sees validation messages when updating episode", %{conn: conn} do 389 | conn = admin(conn) 390 | episode = insert(:episode) 391 | data = %{data: %{id: "#{episode.id}", type: "episodes", attributes: %{ title: nil }}} 392 | |> string_keys 393 | |> dasherize_keys 394 | |> convert_dates 395 | 396 | conn = put conn, episode_path(conn, :update, episode), data 397 | 398 | assert conn.status == 422 399 | assert json_api_response(conn)["errors"] == [cant_be_blank("title")] 400 | end 401 | 402 | end 403 | --------------------------------------------------------------------------------