Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request #78 from oestrich/featured-homepage
Home page featured sorting
  • Loading branch information
oestrich committed Apr 1, 2019
2 parents eae3566 + fc3fe2e commit a8333f2
Show file tree
Hide file tree
Showing 9 changed files with 322 additions and 4 deletions.
3 changes: 2 additions & 1 deletion lib/grapevine/application.ex
Expand Up @@ -19,7 +19,8 @@ defmodule Grapevine.Application do
{Metrics.Server, []},
{Telemetry.Poller, telemetry_opts()},
{Grapevine.Telnet.Worker, [name: Grapevine.Telnet.Worker]},
{Grapevine.CNAMEs, [name: Grapevine.CNAMEs]}
{Grapevine.CNAMEs, [name: Grapevine.CNAMEs]},
{Grapevine.Featured, [name: Grapevine.Featured]}
]

Metrics.Setup.setup()
Expand Down
33 changes: 33 additions & 0 deletions lib/grapevine/featured.ex
@@ -0,0 +1,33 @@
defmodule Grapevine.Featured do
@moduledoc """
Updates the home page with featured games nightly
Selection includes:
- Games sorted by average player count, top 12 taken
- Games that use the web client or are connected to the chat network, random 4
- Any game not included above, a random 4
"""

use GenServer

alias Grapevine.Featured.Implementation

def start_link(opts) do
GenServer.start_link(__MODULE__, [], opts)
end

def init(_) do
{:ok, %{}, {:continue, :schedule_next_run}}
end

def handle_continue(:schedule_next_run, state) do
next_run_delay = Implementation.calculate_next_cycle_delay(Timex.now())
Process.send_after(self(), :select_featured, next_run_delay)
{:noreply, state}
end

def handle_info(:select_featured, state) do
Implementation.select_featured()
{:noreply, state, {:continue, :schedule_next_run}}
end
end
121 changes: 121 additions & 0 deletions lib/grapevine/featured/implementation.ex
@@ -0,0 +1,121 @@
defmodule Grapevine.Featured.Implementation do
@moduledoc """
Implementation details for the Featured GenServer
"""

import Ecto.Query

alias Grapevine.Games
alias Grapevine.Repo

@doc """
Calculate the delay to the next cycle check which runs at 6 AM UTC
"""
def calculate_next_cycle_delay(now) do
now
|> Timex.set([hour: 6, minute: 0, second: 0])
|> maybe_shift_a_day(now)
|> Timex.diff(now, :milliseconds)
end

defp maybe_shift_a_day(next_run, now) do
case Timex.before?(now, next_run) do
true ->
next_run

false ->
Timex.shift(next_run, days: 1)
end
end

@doc """
Select the featured games for that day
"""
def select_featured() do
Ecto.Multi.new()
|> reset_all()
|> update_selected()
|> Repo.transaction()
end

defp reset_all(multi) do
Ecto.Multi.update_all(multi, :update_all, Grapevine.Games.Game, set: [featured_order: nil])
end

defp update_selected(multi) do
featured_games()
|> Enum.with_index()
|> Enum.reduce(multi, fn {game, order}, multi ->
changeset =
game
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_change(:featured_order, order)

Ecto.Multi.update(multi, {:game, game.id}, changeset)
end)
end

def featured_games() do
top_games = top_games_player_count([])
selected_ids = Enum.map(top_games, & &1.id)

random_games_using = random_games_using_grapevine(already_picked: selected_ids)
selected_ids = selected_ids ++ Enum.map(random_games_using, & &1.id)

random_games = random_games(already_picked: selected_ids)

Enum.shuffle(top_games ++ random_games_using ++ random_games)
end

def top_games_player_count(opts) do
last_few_days =
Timex.now()
|> Timex.shift(days: -2)
|> Timex.set(minute: 0, second: 0)
|> DateTime.truncate(:second)

limit = Keyword.get(opts, :select, 12)

Grapevine.Statistics.PlayerStatistic
|> select([ps], ps.game_id)
|> join(:left, [ps], g in assoc(ps, :game))
|> where([ps], ps.recorded_at >= ^last_few_days)
|> where([ps, g], g.display == true and not is_nil(g.cover_key))
|> group_by([ps], [ps.game_id])
|> order_by([ps], desc: avg(ps.player_count))
|> limit(^limit)
|> Repo.all()
|> Enum.map(fn game_id ->
{:ok, game} = Games.get(game_id)
game
end)
end

def random_games_using_grapevine(opts) do
active_cutoff = Timex.now() |> Timex.shift(minutes: -1)
mssp_cutoff = Timex.now() |> Timex.shift(minutes: -90)

limit = Keyword.get(opts, :select, 4)
already_picked_games = Keyword.get(opts, :already_picked, [])

Grapevine.Games.Game
|> where([g], g.display == true and not is_nil(g.cover_key))
|> where([g], g.last_seen_at > ^active_cutoff or g.mssp_last_seen_at > ^mssp_cutoff)
|> where([g], g.id not in ^already_picked_games)
|> Repo.all()
|> Enum.shuffle()
|> Enum.take(limit)
end

def random_games(opts) do
limit = Keyword.get(opts, :select, 4)
already_picked_games = Keyword.get(opts, :already_picked, [])

Grapevine.Games.Game
|> where([g], g.display == true and not is_nil(g.cover_key))
|> where([g], g.id not in ^already_picked_games)
|> Repo.all()
|> Enum.shuffle()
|> Enum.take(limit)
end
end
8 changes: 8 additions & 0 deletions lib/grapevine/games.ex
Expand Up @@ -56,6 +56,14 @@ defmodule Grapevine.Games do
|> Repo.all()
end

def featured() do
Game
|> where([g], g.display == true)
|> where([g], not is_nil(g.featured_order))
|> order_by([g], asc: g.featured_order)
|> Repo.all()
end

def filter_on_attribute({"name", value}, query) do
where(query, [g], ilike(g.name, ^"%#{value}%"))
end
Expand Down
1 change: 1 addition & 0 deletions lib/grapevine/games/game.ex
Expand Up @@ -27,6 +27,7 @@ defmodule Grapevine.Games.Game do
field(:allow_character_registration, :boolean, default: true)
field(:enable_web_client, :boolean, default: false)
field(:allow_anonymous_client, :boolean, default: false)
field(:featured_order, :integer)

field(:last_seen_at, :utc_datetime)
field(:mssp_last_seen_at, :utc_datetime)
Expand Down
4 changes: 1 addition & 3 deletions lib/web/controllers/page_controller.ex
Expand Up @@ -6,10 +6,8 @@ defmodule Web.PageController do
action_fallback(Web.FallbackController)

def index(conn, _params) do
games = Games.public(filter: %{"online" => "yes", "cover" => "yes"})

conn
|> assign(:games, games)
|> assign(:games, Games.featured())
|> render("index.html")
end

Expand Down
@@ -0,0 +1,9 @@
defmodule Grapevine.Repo.Migrations.AddFeaturedOrderToGames do
use Ecto.Migration

def change do
alter table(:games) do
add(:featured_order, :integer)
end
end
end
139 changes: 139 additions & 0 deletions test/grapevine/featured/implementation_test.exs
@@ -0,0 +1,139 @@
defmodule Grapevine.Featured.ImplementationTest do
use Grapevine.DataCase

alias Grapevine.Featured.Implementation
alias Grapevine.Games
alias Grapevine.Statistics

describe "determining the amount of milliseconds to delay" do
test "for the next cycle" do
now =
Timex.now()
|> Timex.set([hour: 20, minute: 0, second: 0])
|> DateTime.truncate(:second)

delay = Implementation.calculate_next_cycle_delay(now)

assert delay == 36000000
end

test "process is rebooted same day but before cycle runs" do
now =
Timex.now()
|> Timex.set([hour: 4, minute: 0, second: 0])
|> DateTime.truncate(:second)

delay = Implementation.calculate_next_cycle_delay(now)

assert delay == 3600 * 2 * 1000
end
end

describe "selecting games to feature" do
test "updates the sort order for all games" do
user = create_user()
game1 = create_game(user, %{name: "Game 1", short_name: "Game1"})
game2 = create_game(user, %{name: "Game 2", short_name: "Game2"})
game3 = create_game(user, %{name: "Game 3", short_name: "Game3"})

Implementation.select_featured()

Enum.each([game1, game2, game3], fn game ->
{:ok, game} = Games.get(game.id)
assert game.featured_order
end)
end

test "selects from all three" do
user = create_user()
game1 = create_game(user, %{name: "Game 1", short_name: "Game1"})
game2 = create_game(user, %{name: "Game 2", short_name: "Game2"})
game3 = create_game(user, %{name: "Game 3", short_name: "Game3"})

{:ok, _stats} = Statistics.record_mssp_players(game1, 2, Timex.now())
Games.seen_on_mssp(game2)

games = Implementation.featured_games()

game_ids =
games
|> Enum.map(& &1.id)
|> Enum.sort()

assert game_ids == [game1.id, game2.id, game3.id]
end

test "top games based on player count" do
user = create_user()
game1 = create_game(user, %{name: "Game 1", short_name: "Game1"})
game2 = create_game(user, %{name: "Game 2", short_name: "Game2"})
_game3 = create_game(user, %{name: "Game 3", short_name: "Game3"})

{:ok, _stats} = Statistics.record_mssp_players(game1, 2, Timex.now())
{:ok, _stats} = Statistics.record_mssp_players(game2, 3, Timex.now())

games = Implementation.top_games_player_count(select: 2)

game_ids =
games
|> Enum.map(& &1.id)
|> Enum.sort()

assert game_ids == [game1.id, game2.id]
end

test "random games using the web client or chat network" do
user = create_user()
game1 = create_game(user, %{name: "Game 1", short_name: "Game1"})
game2 = create_game(user, %{name: "Game 2", short_name: "Game2"})
_game3 = create_game(user, %{name: "Game 3", short_name: "Game3"})

Games.seen_on_mssp(game1)
Games.seen_on_socket(game2)

games = Implementation.random_games_using_grapevine(select: 2)

game_ids =
games
|> Enum.map(& &1.id)
|> Enum.sort()

assert game_ids == [game1.id, game2.id]
end

test "random games not already picked using client or chat" do
user = create_user()
game1 = create_game(user, %{name: "Game 1", short_name: "Game1"})
game2 = create_game(user, %{name: "Game 2", short_name: "Game2"})
_game3 = create_game(user, %{name: "Game 3", short_name: "Game3"})

Games.seen_on_mssp(game1)
Games.seen_on_socket(game2)

games = Implementation.random_games_using_grapevine(select: 2, already_picked: [game1.id])

game_ids =
games
|> Enum.map(& &1.id)
|> Enum.sort()

assert game_ids == [game2.id]
end

test "random selection of games that have not been picked" do
user = create_user()
game1 = create_game(user, %{name: "Game 1", short_name: "Game1"})
game2 = create_game(user, %{name: "Game 2", short_name: "Game2"})
game3 = create_game(user, %{name: "Game 3", short_name: "Game3"})

games = Implementation.random_games(select: 2, already_picked: [game1.id])

game_ids =
games
|> Enum.map(& &1.id)
|> Enum.sort()

assert game_ids == [game2.id, game3.id]
end
end
end
8 changes: 8 additions & 0 deletions test/support/test_helpers.ex
Expand Up @@ -6,6 +6,7 @@ defmodule Grapevine.TestHelpers do
alias Grapevine.Channels
alias Grapevine.Games
alias Grapevine.Gauges
alias Grapevine.Repo

def create_channel(attributes \\ %{}) do
attributes =
Expand Down Expand Up @@ -46,6 +47,13 @@ defmodule Grapevine.TestHelpers do
def create_game(user, attributes \\ %{}) do
{:ok, game} = Games.register(user, game_attributes(attributes))

{:ok, game} =
game
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_change(:cover_key, UUID.uuid4())
|> Ecto.Changeset.put_change(:cover_extension, ".png")
|> Repo.update()

game
end

Expand Down

0 comments on commit a8333f2

Please sign in to comment.