add search by winner

pull/83/head
Thibault Duplessis 2012-09-07 17:14:59 +02:00
parent 478c01ff21
commit 2c5708047c
12 changed files with 152 additions and 84 deletions

View File

@ -12,7 +12,11 @@ import chess.{ Mode }
final class DataForm {
val search = Form(mapping(
"usernames" -> optional(nonEmptyText),
"players" -> mapping(
"a" -> optional(nonEmptyText),
"b" -> optional(nonEmptyText),
"winner" -> optional(nonEmptyText)
)(SearchPlayer.apply)(SearchPlayer.unapply),
"variant" -> numberIn(Query.variants),
"mode" -> numberIn(Query.modes),
"opening" -> stringIn(Query.openings),
@ -28,8 +32,10 @@ final class DataForm {
"dateMin" -> stringIn(Query.dates),
"dateMax" -> stringIn(Query.dates),
"status" -> numberIn(Query.statuses),
"sortField" -> nonEmptyText.verifying(hasKey(Sorting.fields, _)),
"sortOrder" -> nonEmptyText.verifying(hasKey(Sorting.orders, _))
"sort" -> mapping(
"field" -> nonEmptyText.verifying(hasKey(Sorting.fields, _)),
"order" -> nonEmptyText.verifying(hasKey(Sorting.orders, _))
)(SearchSort.apply)(SearchSort.unapply)
)(SearchData.apply)(SearchData.unapply))
private def numberIn(choices: Seq[(Int, String)]) =
@ -43,7 +49,7 @@ final class DataForm {
}
case class SearchData(
usernames: Option[String] = None,
players: SearchPlayer = SearchPlayer(),
variant: Option[Int] = None,
mode: Option[Int] = None,
opening: Option[String] = None,
@ -59,11 +65,12 @@ case class SearchData(
dateMin: Option[String] = None,
dateMax: Option[String] = None,
status: Option[Int] = None,
sortField: String = Sorting.default.field,
sortOrder: String = Sorting.default.order) {
sort: SearchSort = SearchSort()) {
lazy val query = Query(
usernames = (~usernames).split(" ").toList map clean filter (_.nonEmpty),
user1 = players.cleanA,
user2 = players.cleanB,
winner = players.cleanWinner,
variant = variant,
rated = mode flatMap Mode.apply map (_.rated),
opening = opening map clean,
@ -74,7 +81,7 @@ case class SearchData(
duration = Range(durationMin, durationMax),
date = Range(dateMin flatMap toDate, dateMax flatMap toDate),
status = status,
sorting = Sorting(sortField, sortOrder)
sorting = Sorting(sort.field, sort.order)
)
private def clean(s: String) = s.trim.toLowerCase
@ -88,3 +95,22 @@ case class SearchData(
case _ None
}
}
case class SearchPlayer(
a: Option[String] = None,
b: Option[String] = None,
winner: Option[String] = None) {
def cleanA = clean(a)
def cleanB = clean(b)
def cleanWinner = clean(winner) |> { w
w filter List(a, b).flatten.contains
}
private def clean(s: Option[String]) =
s map (_.trim.toLowerCase) filter (_.nonEmpty)
}
case class SearchSort(
field: String = Sorting.default.field,
order: String = Sorting.default.order)

View File

@ -14,6 +14,7 @@ object Game {
val rated = "ra"
val variant = "va"
val uids = "ui"
val winner = "wi"
val averageElo = "el"
val ai = "ai"
val opening = "op"
@ -37,6 +38,7 @@ object Game {
field(rated, "boolean"),
field(variant, "short"),
field(uids, "string"),
field(winner, "string"),
field(averageElo, "short"),
field(ai, "short"),
field(opening, "string"),
@ -54,6 +56,7 @@ object Game {
rated -> game.rated.some,
variant -> game.variant.id.some,
uids -> (game.userIds.toNel map (_.list)),
winner -> (game.winner flatMap (_.userId)),
averageElo -> game.averageUsersElo,
ai -> game.aiLevel,
date -> (dateFormatter print createdAt).some,

View File

@ -9,7 +9,9 @@ import org.joda.time.DateTime
import org.scala_tools.time.Imports._
case class Query(
usernames: List[String] = Nil,
user1: Option[String] = None,
user2: Option[String] = None,
winner: Option[String] = None,
variant: Option[Int] = None,
status: Option[Int] = None,
turns: Range[Int] = Range.none,
@ -23,7 +25,9 @@ case class Query(
sorting: Sorting = Sorting.default) {
def nonEmpty =
usernames.nonEmpty ||
user1.nonEmpty ||
user2.nonEmpty ||
winner.nonEmpty ||
variant.nonEmpty ||
status.nonEmpty ||
turns.nonEmpty ||
@ -44,8 +48,11 @@ case class Query(
def countRequest = CountRequest(matchAllQuery, filters)
def usernames = List(user1, user2).flatten
private def filters = List(
usernames map { u termFilter(fields.uids, u.toLowerCase) },
usernames map { termFilter(fields.uids, _) },
toFilters(winner, fields.winner),
turns filters fields.turns,
averageElo filters fields.averageElo,
duration map (60 *) filters fields.duration,
@ -108,25 +115,4 @@ object Query {
}
val statuses = Status.finishedNotCheated map { s s.id -> s.name }
def test = Query(
usernames = List("thibault"),
duration = Range(1.some, 3.some),
sorting = Sorting(fields.averageElo, "desc")
)
def test2 = Query(
opening = "A04".some,
sorting = Sorting(fields.turns, "desc")
)
def test3 = Query(
usernames = List("controlaltdelete"),
variant = 1.some,
turns = Range(20.some, 100.some),
averageElo = Range(1100.some, 2000.some),
opening = "A00".some,
hasAi = true.some,
aiLevel = Range.none,
date = Range(Some(DateTime.now - 1.year), none),
sorting = Sorting(fields.date, "desc")
)
}

View File

@ -21,7 +21,7 @@ object Sorting {
def fieldKeys = fields map (_._1)
val orders = List(SortOrder.ASC, SortOrder.DESC) map { s s.toString -> s.toString }
val orders = List(SortOrder.DESC, SortOrder.ASC) map { s s.toString -> s.toString }
val default = Sorting(Game.fields.date, "desc")
}

View File

@ -7,7 +7,7 @@ import play.api.templates.Html
trait AssetHelper {
val assetVersion = 71
val assetVersion = 73
def cssTag(name: String) = css("stylesheets/" + name)

View File

@ -26,16 +26,22 @@ moreCss = moreCss) {
<table>
<tr>
<th>
<label for="@form("usernames").id">Players</label>
<label>Players</label>
</th>
<td>
<input
type="text"
value="@form("usernames").value"
name="@form("usernames").name"
id="@form("usernames").id" />
<td class="usernames">
<div class="half">@input(form("players")("a"))</div>
<div class="half">vs @input(form("players")("b"))</div>
</td>
</tr>
<tr class="winner none">
<th>
<label for="@form("players")("winner").id">Winner</label>
</th>
<td class="single">
<select id="@form("players")("winner").id" name="@form("players")("winner").name">
<option class="blank" value=""></option>
</select>
</td>
<th class="help">Type up to two usernames</th>
</tr>
<tr>
<th>
@ -55,7 +61,7 @@ moreCss = moreCss) {
@select(form("hasAi"), Query.hasAis, "Human or Computer".some)
</td>
</tr>
<tr class="aiLevel">
<tr class="aiLevel none">
<th>
<label for="@form("aiLevel").id">Stockfish level</label>
</th>
@ -128,8 +134,8 @@ moreCss = moreCss) {
<label>Sort</label>
</th>
<td>
<div class="half">By @select(form("sortField"), Sorting.fields)</div>
<div class="half">Order @select(form("sortOrder"), Sorting.orders)</div>
<div class="half">By @select(form("sort")("field"), Sorting.fields)</div>
<div class="half">Order @select(form("sort")("order"), Sorting.orders)</div>
</td>
</tr>
</table>
@ -137,7 +143,7 @@ moreCss = moreCss) {
<div class="search_result">
@paginator.map { pager =>
@if(pager.nbResults > 0) {
<div class="nb_results">
<div class="search_status">
@paginator.map { pager =>
@pager.nbResults.localize games found
}
@ -151,9 +157,11 @@ moreCss = moreCss) {
@game.widgets(pager.currentPageResults)
</div>
} else {
<div class="no_result">No game found</div>
}
<div class="search_status">No game found</div>
}
}.getOrElse {
<div class="search_status">Search ready</div>
}
</div>
</div>
}

View File

@ -0,0 +1,3 @@
@(field: play.api.data.Field)
<input type="text" value="@field.value" name="@field.name" id="@field.id" />

View File

@ -2,7 +2,7 @@
<select id="@field.id" name="@field.name">
@default.map { d =>
<option class="blank" value=""></option>
<option value=""></option>
}
@options.map { v =>
<option value="@v._1" @(if(field.value == Some(v._1.toString)) "selected" else "")>@v._2</option>

View File

@ -38,7 +38,6 @@ object Main {
case "game-per-day" :: days :: Nil games.perDay(parseIntOption(days) err "days: Int")
case "wiki-fetch" :: Nil wiki.fetch
case "search-reset" :: Nil search.reset
case "search-test" :: Nil search.test
case _
putStrLn("Unknown command: " + args.mkString(" "))
}

View File

@ -8,12 +8,6 @@ case class Search(env: SearchEnv) {
def reset: IO[Unit] = env.indexer.rebuildAll
def test: IO[Unit] = env.indexer toGames {
env.indexer search Query.test.searchRequest(0, 10)
} flatMap { games
putStrLn(games map showGame mkString "\n")
}
private def showGame(game: lila.game.DbGame) =
game.id + " " + game.turns //+ " " + game
}

View File

@ -1,9 +1,13 @@
$(function() {
var $form = $("form.search");
var $usernames = $form.find(".usernames input");
var $winnerRow = $form.find(".winner");
var $winner = $winnerRow.find("select");
var $result = $(".search_result");
function realtimeResults() {
$("div.search_status").text("Searching...");
$result.load(
$form.attr("action") + "?" + $form.serialize() + " .search_result",
function() {
@ -11,27 +15,76 @@ $(function() {
$result.find('.search_infinitescroll:has(.pager a)').each(function() {
var $next = $(this).find(".pager a:last")
$next.attr("href", $next.attr("href") + "&" + $form.serialize());
$(this).infinitescroll({
navSelector: ".pager",
nextSelector: $next,
itemSelector: ".search_infinitescroll .paginated_element",
loading: {
msgText: "",
img: "/assets/images/hloader3.gif",
finishedMsg: "---"
}
}, function() {
$("#infscr-loading").remove();
$('body').trigger('lichess.content_loaded');
});
$(this).infinitescroll({
navSelector: ".pager",
nextSelector: $next,
itemSelector: ".search_infinitescroll .paginated_element",
loading: {
msgText: "",
img: "/assets/images/hloader3.gif",
finishedMsg: "---"
}
}, function() {
$("#infscr-loading").remove();
$('body').trigger('lichess.content_loaded');
});
});
});
}
function winnerChoices() {
var options = ["<option value=''></option>"];
$usernames.each(function() {
var user = $.trim($(this).val());
if (user.length > 1) {
options.push("<option value='"+user+"'>"+user+"</option>");
}
});
$winner.html(options.join(""));
$winnerRow.toggle(options.length > 1);
}
$form.find("select").change(realtimeResults);
$form.find("input").change(realtimeResults);
$usernames.bind("keyup", winnerChoices).trigger("keyup");
$usernames.bindWithDelay("keyup", realtimeResults, 400);
$form.find(".opponent select").change(function() {
$form.find(".aiLevel").toggle($(this).val() == 1);
}).trigger("change");
});
// https://github.com/bgrins/bindWithDelay/blob/master/bindWithDelay.js
$.fn.bindWithDelay = function( type, data, fn, timeout, throttle ) {
if ( $.isFunction( data ) ) {
throttle = timeout;
timeout = fn;
fn = data;
data = undefined;
}
// Allow delayed function to be removed with fn in unbind function
fn.guid = fn.guid || ($.guid && $.guid++);
// Bind each separately so that each element has its own delay
return this.each(function() {
var wait = null;
function cb() {
var e = $.extend(true, { }, arguments[0]);
var ctx = this;
var throttler = function() {
wait = null;
fn.apply(ctx, [e]);
};
if (!throttle) { clearTimeout(wait); wait = null; }
if (!wait) { wait = setTimeout(throttler, timeout); }
}
cb.guid = fn.guid;
$(this).bind(type, data, cb);
});
}

View File

@ -2,14 +2,6 @@ form.search {
padding: 10px 25px;
}
div.nb_results {
margin-top: 10px;
padding: 10px 25px;
background: #f4f4f4;
border: 1px solid #e4e4e4;
font-weight: bold;
}
form.search td {
padding: 5px 0;
}
@ -39,7 +31,8 @@ form.search .single select {
width: 99%;
}
form.search .half select {
form.search .half select,
form.search .half input {
width: 80%;
}
@ -55,13 +48,16 @@ form.search th.help {
padding-left: 10px;
}
div.no_result {
padding: 10px;
font-size: 1.3em;
font-style: italic;
text-align: center
div.search_status {
margin-top: 10px;
padding: 10px 25px;
background: #f4f4f4;
border-top: 1px solid #e4e4e4;
border-bottom: 1px solid #e4e4e4;
font-weight: bold;
}
/* Errors */
form.search .error {
margin-left: 160px;