Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
9 changed files
with
322 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 9 additions & 0 deletions
9
priv/repo/migrations/20190401164850_add_featured_order_to_games.exs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters