Skip to content

Commit

Permalink
Associate games with users
Browse files Browse the repository at this point in the history
Users should only be able to delete their own games
Show how many users own a copy of each game

Issue #71
  • Loading branch information
Chris Rees committed Mar 11, 2018
1 parent 4b98f3d commit 8914084
Show file tree
Hide file tree
Showing 11 changed files with 216 additions and 80 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Nermesterts.Repo.Migrations.RemoveUniqueGameName do
use Ecto.Migration

def change do
drop unique_index(:games, [:name])
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Nermesterts.Repo.Migrations.MakePlayerGamesUnique do
use Ecto.Migration

def change do
create unique_index(:games, [:bgg_id])
end
end
12 changes: 12 additions & 0 deletions priv/repo/migrations/20180310201717_add_games_to_users.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule Nermesterts.Repo.Migrations.AddGamesToUsers do
use Ecto.Migration

def change do
create table(:user_games) do
add :user_id, references(:users, on_delete: :delete_all)
add :game_id, references(:games, column: :bgg_id, on_delete: :delete_all)
end

create unique_index(:user_games, [:user_id, :game_id], name: :unique_user_games_index)
end
end
79 changes: 46 additions & 33 deletions priv/repo/seeds.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,57 @@
# Inside the script, you can read and write to any of your
# repositories directly:
#
# Nermesterts.Repo.insert!(%Nermesterts.SomeModel{})
# Repo.insert!(%Nermesterts.SomeModel{})
#
# We recommend using the bang functions (`insert!`, `update!`
# and so on) as they will fail if something goes wrong.
alias Nermesterts.Game
alias Nermesterts.Phrase
alias Nermesterts.Repo
alias Nermesterts.User

Nermesterts.Repo.delete_all(Game)
Nermesterts.Repo.delete_all(Phrase)
Repo.delete_all(Game)
Repo.delete_all(Phrase)
Repo.delete_all(User)

Nermesterts.Repo.insert!(%Game{name: "BANG!", min_players: 4, max_players: 7})
Nermesterts.Repo.insert!(%Game{name: "Betrayal at House on the Hill", min_players: 3, max_players: 6})
Nermesterts.Repo.insert!(%Game{name: "Boss Monster", min_players: 2, max_players: 4})
Nermesterts.Repo.insert!(%Game{name: "Codenames", min_players: 2, max_players: 8})
Nermesterts.Repo.insert!(%Game{name: "Exploding Kittens", min_players: 2, max_players: 8})
Nermesterts.Repo.insert!(%Game{name: "Hunters of Arcfall", min_players: 2, max_players: 6})
Nermesterts.Repo.insert!(%Game{name: "IncrediBrawl", min_players: 2, max_players: 4})
Nermesterts.Repo.insert!(%Game{name: "Love Letter", min_players: 2, max_players: 4})
Nermesterts.Repo.insert!(%Game{name: "Mascarade", min_players: 2, max_players: 13})
Nermesterts.Repo.insert!(%Game{name: "One Night Ultimate Werewolf", min_players: 3, max_players: 10})
Nermesterts.Repo.insert!(%Game{name: "Saboteur", min_players: 3, max_players: 10})
Nermesterts.Repo.insert!(%Game{name: "Salem", min_players: 4, max_players: 12})
Nermesterts.Repo.insert!(%Game{name: "Samurai Sword", min_players: 3, max_players: 7})
Nermesterts.Repo.insert!(%Game{name: "Secret Hitler", min_players: 5, max_players: 10})
Nermesterts.Repo.insert!(%Game{name: "Shadow Hunters", min_players: 4, max_players: 8})
Nermesterts.Repo.insert!(%Game{name: "Small World", min_players: 2, max_players: 5})
Nermesterts.Repo.insert!(%Game{name: "Spyfall", min_players: 3, max_players: 8})
Nermesterts.Repo.insert!(%Game{name: "Sushi Go!", min_players: 2, max_players: 5})
Nermesterts.Repo.insert!(%Game{name: "The Resistance", min_players: 5, max_players: 10})
Nermesterts.Repo.insert!(%Game{name: "The Resistance: Avalon", min_players: 5, max_players: 10})
Nermesterts.Repo.insert!(%Game{name: "Till Dawn", min_players: 4, max_players: 8})
Nermesterts.Repo.insert!(%Game{name: "Tsuro", min_players: 2, max_players: 8})
Nermesterts.Repo.insert!(%Game{name: "Zombie Dice", min_players: 2, max_players: 99})
user = Repo.insert!(%User{username: "admin", crypted_password: Comeonin.Bcrypt.hashpwsalt("changeme!"), active: true, admin: true}) |> Repo.preload(:games)

Nermesterts.Repo.insert!(%Phrase{message: "The King has decreed that you play #GAME# or face the penalty of death!", has_token: true})
Nermesterts.Repo.insert!(%Phrase{message: "A shady figure approaches you. \"Psst. Hey, kid. Why don't you come check out #GAME#?\"", has_token: true})
Nermesterts.Repo.insert!(%Phrase{message: "#GAME# is the kind of game you can take home to Mom.", has_token: true})
Nermesterts.Repo.insert!(%Phrase{message: "You think you see a game off in the distance, but as you approach you realize it was just a mirage.", has_token: false})
Nermesterts.Repo.insert!(%Phrase{message: "I walk this empty street / On the Boulevard of Broken Dreams / Where the city sleeps / And I'm the only one and I walk alone", has_token: false})
Nermesterts.Repo.insert!(%Phrase{message: "My shadow's the only one that walks beside me / My shallow heart's the only thing that's beating / Sometimes I wish someone out there will find me / 'Til then I walk alone", has_token: false})
Nermesterts.Repo.insert!(%Phrase{message: "After a day of adventuring, you come home to find that your home has been raided and all of your games are missing.", has_token: false})
Repo.insert!(%Game{name: "BANG!", min_players: 4, max_players: 7, bgg_id: 3955})
Repo.insert!(%Game{name: "Betrayal at House on the Hill", min_players: 3, max_players: 6, bgg_id: 10547})
Repo.insert!(%Game{name: "Boss Monster", min_players: 2, max_players: 4, bgg_id: 131835})
Repo.insert!(%Game{name: "Codenames", min_players: 2, max_players: 8, bgg_id: 178900})
Repo.insert!(%Game{name: "Exploding Kittens", min_players: 2, max_players: 8, bgg_id: 172225})
Repo.insert!(%Game{name: "Hunters of Arcfall", min_players: 2, max_players: 6, bgg_id: 142988})
Repo.insert!(%Game{name: "IncrediBrawl", min_players: 2, max_players: 4, bgg_id: 142653})
Repo.insert!(%Game{name: "Love Letter", min_players: 2, max_players: 4, bgg_id: 129622})
Repo.insert!(%Game{name: "Mascarade", min_players: 2, max_players: 13, bgg_id: 139030})
Repo.insert!(%Game{name: "One Night Ultimate Werewolf", min_players: 3, max_players: 10, bgg_id: 147949})
Repo.insert!(%Game{name: "Saboteur", min_players: 3, max_players: 10, bgg_id: 9220})
Repo.insert!(%Game{name: "Salem 1692", min_players: 4, max_players: 12, bgg_id: 175549})
Repo.insert!(%Game{name: "Samurai Sword", min_players: 3, max_players: 7, bgg_id: 128667})
Repo.insert!(%Game{name: "Secret Hitler", min_players: 5, max_players: 10, bgg_id: 188834})
Repo.insert!(%Game{name: "Shadow Hunters", min_players: 4, max_players: 8, bgg_id: 24068})
Repo.insert!(%Game{name: "Small World", min_players: 2, max_players: 5, bgg_id: 40692})
Repo.insert!(%Game{name: "Spyfall", min_players: 3, max_players: 8, bgg_id: 166384})
Repo.insert!(%Game{name: "Sushi Go!", min_players: 2, max_players: 5, bgg_id: 133473})
Repo.insert!(%Game{name: "The Resistance", min_players: 5, max_players: 10, bgg_id: 41114})
Repo.insert!(%Game{name: "The Resistance: Avalon", min_players: 5, max_players: 10, bgg_id: 128882})
Repo.insert!(%Game{name: "Till Dawn", min_players: 4, max_players: 8, bgg_id: 154498})
Repo.insert!(%Game{name: "Tsuro", min_players: 2, max_players: 8, bgg_id: 16992})
Repo.insert!(%Game{name: "Zombie Dice", min_players: 2, max_players: 99, bgg_id: 62871})

Enum.each(Game |> Repo.all, fn(game) ->
game
|> Repo.preload(:users)
|> Ecto.Changeset.change
|> Ecto.Changeset.put_assoc(:users, [user])
|> Repo.update
end)

Repo.insert!(%Phrase{message: "The King has decreed that you play #GAME# or face the penalty of death!", has_token: true})
Repo.insert!(%Phrase{message: "A shady figure approaches you. \"Psst. Hey, kid. Why don't you come check out #GAME#?\"", has_token: true})
Repo.insert!(%Phrase{message: "#GAME# is the kind of game you can take home to Mom.", has_token: true})
Repo.insert!(%Phrase{message: "You think you see a game off in the distance, but as you approach you realize it was just a mirage.", has_token: false})
Repo.insert!(%Phrase{message: "I walk this empty street / On the Boulevard of Broken Dreams / Where the city sleeps / And I'm the only one and I walk alone", has_token: false})
Repo.insert!(%Phrase{message: "My shadow's the only one that walks beside me / My shallow heart's the only thing that's beating / Sometimes I wish someone out there will find me / 'Til then I walk alone", has_token: false})
Repo.insert!(%Phrase{message: "After a day of adventuring, you come home to find that your home has been raided and all of your games are missing.", has_token: false})
9 changes: 1 addition & 8 deletions test/controllers/game_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule Nermesterts.GameControllerTest do
alias Nermesterts.Game
import Nermesterts.TestHelper, only: [log_in: 2]

@valid_attrs %{image: "some content", max_players: 42, min_players: 42, name: "some content"}
@valid_attrs %{image: "some content", max_players: 42, min_players: 42, name: "some content", bgg_id: 1}
@invalid_attrs %{}

test "redirects to sign in page when not logged in", %{conn: conn} do
Expand Down Expand Up @@ -52,12 +52,5 @@ defmodule Nermesterts.GameControllerTest do
get conn, game_path(conn, :show, -1)
end
end

test "deletes chosen resource", %{conn: conn} do
game = Repo.insert! %Game{}
conn = delete conn, game_path(conn, :delete, game)
assert redirected_to(conn) == game_path(conn, :index)
refute Repo.get(Game, game.id)
end
end
end
25 changes: 24 additions & 1 deletion test/models/game_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ defmodule Nermesterts.GameTest do
use Nermesterts.ModelCase

alias Nermesterts.Game
alias Nermesterts.Repo
alias Nermesterts.User

@valid_attrs %{image: "some content", max_players: 42, min_players: 42, name: "some content"}
@valid_attrs %{image: "some content", max_players: 42, min_players: 42, name: "some content", bgg_id: 1}
@invalid_attrs %{}

test "changeset with valid attributes" do
Expand All @@ -15,4 +17,25 @@ defmodule Nermesterts.GameTest do
changeset = Game.changeset(%Game{}, @invalid_attrs)
refute changeset.valid?
end

test "user games are unique" do
{:ok, user} = User.registration_changeset(%User{}, %{username: "foo", password: "barbaz", password_confirmation: "barbaz"})
|> Repo.insert

user = user |> Repo.preload(:games)

{:ok, game} = Game.changeset(%Game{}, @valid_attrs)
|> Repo.insert
game
|> Repo.preload(:users)
|> Ecto.Changeset.change
|> Ecto.Changeset.put_assoc(:users, [user])
|> Repo.update

{:error, changeset} = Game.changeset(%Game{}, @valid_attrs)
|> Repo.insert

assert {"has already been taken", []} = changeset.errors[:bgg_id]
refute changeset.valid?
end
end
26 changes: 21 additions & 5 deletions test/support/factory.ex
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
defmodule Nermesterts.Factory do
use ExMachina.Ecto, repo: Nermesterts.Repo
alias Nermesterts.User
alias Nermesterts.Game

def user_factory do
%User{
username: sequence(:username, &"username #{&1}"),
crypted_password: "123456"
crypted_password: "123456",
games: [build(:game)]
}
end

def user_admin_factory do
%User{
username: sequence(:username, &"username #{&1}"),
crypted_password: "123456",
admin: true
admin: true,
games: [build(:game)]
}
end

Expand All @@ -22,23 +25,36 @@ defmodule Nermesterts.Factory do
username: sequence(:username, &"username #{&1}"),
password: "123456",
password_confirmation: "123456",
crypted_password: Comeonin.Bcrypt.hashpwsalt("123456")
crypted_password: Comeonin.Bcrypt.hashpwsalt("123456"),
games: [build(:game)]
}
end

def user_active_factory do
%User{
username: sequence(:username, &"username #{&1}"),
crypted_password: "123456",
active: true
active: true,
games: [build(:game)]
}
end

def user_guest_factory do
%User{
username: sequence(:username, &"username #{&1}"),
crypted_password: "123456",
guest: true
guest: true,
games: [build(:game)]
}
end

def game_factory do
%Game{
bgg_id: sequence(:bgg_id, &"#{&1}") |> String.to_integer,
name: "Zombicide: Invader",
min_players: 1,
max_players: 6,
image: "/tmp"
}
end
end
71 changes: 56 additions & 15 deletions web/controllers/game_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ defmodule Nermesterts.GameController do
require BoardGameGeekClient

alias Nermesterts.Game
alias Nermesterts.User

def index(conn, _params) do
games = Game |> Game.ordered |> Repo.all
games = Game |> Game.inventory |> Game.ordered |> Repo.all
render(conn, "index.html", games: games)
end

Expand All @@ -15,16 +16,9 @@ defmodule Nermesterts.GameController do
end

def create(conn, %{"game" => game_params}) do
changeset = Game.changeset(%Game{}, game_params)

case Repo.insert(changeset) do
{:ok, _game} ->
conn
|> put_flash(:info, "Game created successfully.")
|> redirect(to: game_path(conn, :index))
{:error, changeset} ->
render(conn, "new.html", changeset: changeset)
end
bgg_id = Map.get(game_params, "bgg_id") || -1
existing_game = Repo.get_by(Game, bgg_id: bgg_id)
create(conn, game_params, existing_game)
end

def show(conn, %{"id" => id}) do
Expand All @@ -33,11 +27,16 @@ defmodule Nermesterts.GameController do
end

def delete(conn, %{"id" => id}) do
game = Repo.get!(Game, id)
user = Nermesterts.Session.current_user(conn)
game = Repo.get!(Game, id) |> Repo.preload(:users)
new_users = game.users -- [user]
|> Enum.map(&Ecto.Changeset.change/1)

# Here we use delete! (with a bang) because we expect
# it to always work (and if it does not, it will raise).
Repo.delete!(game)
game
|> Ecto.Changeset.change
|> Ecto.Changeset.put_assoc(:users, new_users)
|> Repo.update!
IO.puts "Updated"

conn
|> put_flash(:info, "Game deleted successfully.")
Expand All @@ -52,4 +51,46 @@ defmodule Nermesterts.GameController do
result = BoardGameGeekClient.get_games_info([id])
render conn, "games_info.json", result: result
end

defp create(conn, game_params, nil) do
changeset = Game.changeset(%Game{}, game_params)

case Repo.insert(changeset) do
{:ok, game} ->
add_game_to_user(conn, game)

conn
|> put_flash(:info, "Game created successfully.")
|> redirect(to: game_path(conn, :index))
{:error, changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
defp create(conn, _game_params, existing_game) do
add_game_to_user(conn, existing_game)

conn
|> put_flash(:info, "Game created successfully.")
|> redirect(to: game_path(conn, :index))
end

defp add_game_to_user(conn, %Game{} = game) do
user = Nermesterts.Session.current_user(conn)
|> Repo.preload(:games)
game = game
|> Repo.preload(:users)
add_game_to_user(game, user, Enum.member?(game.users, user))
end

defp add_game_to_user(game, %User{} = user, false) do
new_users = game.users ++ [user]
|> Enum.map(&Ecto.Changeset.change/1)
game
|> Ecto.Changeset.change
|> Ecto.Changeset.put_assoc(:users, new_users)
|> Repo.update!
end
defp add_game_to_user(game, _, true) do
game
end
end
19 changes: 16 additions & 3 deletions web/models/game.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ defmodule Nermesterts.Game do
field :max_players, :integer
field :image, :string

many_to_many :users, Nermesterts.User, join_through: "user_games",
join_keys: [game_id: :bgg_id, user_id: :id], unique: true,
on_replace: :delete

timestamps()
end

@required_fields ~w(name min_players max_players)a
@optional_fields ~w(bgg_id image)a
@required_fields ~w(name bgg_id min_players max_players)a
@optional_fields ~w(image)a
@all_fields @required_fields ++ @optional_fields

@doc """
Expand All @@ -29,7 +33,8 @@ defmodule Nermesterts.Game do
|> validate_number(:min_players,
less_than_or_equal_to: Map.get(changeset.changes, :max_players),
message: "Min players must be less than or equal to the max players.")
|> unique_constraint(:name)
|> unique_constraint(:bgg_id)
|> unique_constraint(:unique_user_games_index)
end

@doc """
Expand All @@ -39,4 +44,12 @@ defmodule Nermesterts.Game do
from g in query,
order_by: g.name
end

def inventory(query) do
from g in query,
right_join: ug in "user_games", on: [game_id: g.bgg_id],
select: {g, count(ug.game_id)},
group_by: [g.id, ug.game_id],
preload: [:users]
end
end
Loading

0 comments on commit 8914084

Please sign in to comment.