generic config store and editable practice structure

practice
Thibault Duplessis 2017-01-21 13:34:20 +01:00
parent 94c2cbfde0
commit cfa4c07335
19 changed files with 264 additions and 20 deletions

View File

@ -168,4 +168,5 @@ object Env {
def event = lila.event.Env.current
def coach = lila.coach.Env.current
def pool = lila.pool.Env.current
def practice = lila.practice.Env.current
}

View File

@ -0,0 +1,30 @@
package controllers
import play.api.mvc._
import lila.api.Context
import lila.app._
import views._
object Practice extends LilaController {
private def env = Env.practice
def config = Auth { implicit ctx => me => for {
struct <- env.api.structure.get
form <- env.api.structure.form
} yield Ok(html.practice.config(struct, form))
}
def configSave = SecureBody(_.StreamConfig) { implicit ctx => me =>
implicit val req = ctx.body
env.api.structure.form.flatMap { form =>
FormFuResult(form) { err =>
env.api.structure.get map { html.practice.config(_, err) }
} { text =>
env.api.structure.set(text).valueOr(_ => funit) >>
Env.mod.logApi.practiceConfig(me.id) inject Redirect(routes.Practice.config)
}
}
}
}

View File

@ -15,6 +15,9 @@
@if(isGranted(_.StreamConfig)) {
<a class="@active.active("stream")" href="@routes.Tv.streamConfig">Streams</a>
}
@if(isGranted(_.PracticeConfig)) {
<a class="@active.active("practice")" href="@routes.Practice.config">Practice</a>
}
@if(isGranted(_.ManageTournament)) {
<a class="@active.active("tour")" href="@routes.TournamentCrud.index">Tournaments</a>
}

View File

@ -0,0 +1,33 @@
@(structure: lila.practice.PracticeStructure, form: Form[_])(implicit ctx: Context)
@mod.layout(
title = "Practice structure",
active = "practice",
moreCss = cssTag("mod-practice.css")) {
<div class="content_box practice_config">
<h1 data-icon="" class="text lichess_title">Practice config</h1>
<div class="both">
<form action="@routes.Practice.configSave" method="POST">
<textarea class="practice_text" name="text">@form("text").value</textarea>
@errMsg(form("text"))
<button type="submit" class="button text" data-icon="E">Save</button>
</form>
<ol>
@structure.sections.map { section =>
<li>
<h2>@section.name (#@section.id)</h2>
<ol>
@section.studies.map { stud =>
<li>
<i class="practice icon @stud.id"></i>
<h3>@stud.name (#@stud.id)</h3>
<em>@stud.desc</em>
</li>
}
</ol>
</li>
}
</ol>
</div>
</div>
}

View File

@ -516,6 +516,12 @@ coach {
memo {
collection {
cache = cache
config = flag
}
}
practice {
collection {
progress = practice_progress
}
}
setup {

View File

@ -232,6 +232,10 @@ POST /simul/$id<\w{8}>/abort controllers.Simul.abort(id: String
POST /simul/$id<\w{8}>/join/:variant controllers.Simul.join(id: String, variant: String)
POST /simul/$id<\w{8}>/withdraw controllers.Simul.withdraw(id: String)
# Practice
GET /practice/config controllers.Practice.config
POST /practice/config controllers.Practice.configSave
# Team
GET /team controllers.Team.home(page: Int ?= 1)
GET /team/new controllers.Team.form

View File

@ -0,0 +1,76 @@
package lila.memo
import com.typesafe.config.ConfigFactory
import configs.syntax._
import configs.{ Configs, ConfigError }
import play.api.data._
import play.api.data.Forms._
import play.api.data.validation._
import scala.concurrent.duration._
import scala.util.Try
import lila.db.dsl._
final class ConfigStore[A: Configs](
coll: Coll,
id: String,
ttl: FiniteDuration,
logger: lila.log.Logger) {
private val mongoDocKey = "config"
private val cache = AsyncCache.single[Option[A]](
"db.config_store",
f = rawText.map {
_.flatMap { text =>
parse(text).fold(
errs => {
errs foreach { logger.warn(_) }
none
},
res => res.some)
}
},
timeToLive = ttl)
def parse(text: String): Either[List[String], A] = try {
ConfigFactory.parseString(text).extract[A].toEither.left.map(_.messages.toList.map(_.toString))
}
catch {
case e: com.typesafe.config.ConfigException => Left(List(e.getMessage))
}
def get: Fu[Option[A]] = cache(true)
def rawText: Fu[Option[String]] = coll.primitiveOne[String]($id(id), mongoDocKey)
def set(text: String): Either[List[String], Funit] = parse(text).right map { _ =>
coll.update($id(id), $doc(mongoDocKey -> text), upsert = true) >> cache.clear
}
def makeForm: Fu[Form[String]] = {
val form = Form(single(
"text" -> text.verifying(Constraint[String]("constraint.text_parsable") { t =>
parse(t) match {
case Left(errs) => Invalid(ValidationError(errs mkString ","))
case _ => Valid
}
})
))
rawText map {
_.fold(form)(form.fill)
}
}
}
object ConfigStore {
final class Builder(coll: Coll) {
def apply[A: Configs](
id: String,
ttl: FiniteDuration,
logger: lila.log.Logger) = new ConfigStore[A](coll, id, ttl, logger branch "config_store")
}
def apply(coll: Coll) = new Builder(coll)
}

View File

@ -5,8 +5,11 @@ import com.typesafe.config.Config
final class Env(config: Config, db: lila.db.Env) {
private val CollectionCache = config getString "collection.cache"
private val CollectionConfig = config getString "collection.config"
lazy val mongoCache: MongoCache.Builder = MongoCache(db(CollectionCache))
lazy val configStore: ConfigStore.Builder = ConfigStore(db(CollectionConfig))
}
object Env {

View File

@ -38,7 +38,7 @@ final class MongoCache[K, V: MongoCache.Handler] private (
private def makeKey(k: K) = s"$prefix:${keyToString(k)}"
private def select(k: K) = BSONDocument("_id" -> makeKey(k))
private def select(k: K) = $id(makeKey(k))
}
object MongoCache {

View File

@ -31,6 +31,7 @@ case class Modlog(
case Modlog.deleteQaAnswer => "delete Q&A answer"
case Modlog.deleteQaComment => "delete Q&A comment"
case Modlog.streamConfig => "update streams config"
case Modlog.practiceConfig => "update practice config"
case Modlog.deleteTeam => "delete team"
case Modlog.terminateTournament => "terminate tournament"
case Modlog.chatTimeout => "chat timeout"
@ -69,6 +70,7 @@ object Modlog {
val deleteQaAnswer = "deleteQaAnswer"
val deleteQaComment = "deleteQaComment"
val streamConfig = "streamConfig"
val practiceConfig = "practiceConfig"
val deleteTeam = "deleteTeam"
val terminateTournament = "terminateTournament "
val chatTimeout = "chatTimeout "

View File

@ -12,6 +12,10 @@ final class ModlogApi(coll: Coll) {
Modlog(mod, none, Modlog.streamConfig)
}
def practiceConfig(mod: String) = add {
Modlog(mod, none, Modlog.practiceConfig)
}
def engine(mod: String, user: String, v: Boolean) = add {
Modlog(mod, user.some, v.fold(Modlog.engine, Modlog.unengine))
}

View File

@ -1,20 +1,24 @@
package lila.practice
import scala.concurrent.duration._
import com.typesafe.config.Config
final class Env(
config: Config,
configStore: lila.memo.ConfigStore.Builder,
db: lila.db.Env) {
private val CollectionProgress = config getString "collection.progress"
lazy val api = new PracticeApi(
coll = db(CollectionProgress))
coll = db(CollectionProgress),
configStore = configStore[PracticeStructure]("practice", 1.hour, logger))
}
object Env {
lazy val current: Env = "practice" boot new Env(
config = lila.common.PlayApp loadConfig "practice",
configStore = lila.memo.Env.current.configStore,
db = lila.db.Env.current)
}

View File

@ -1,24 +1,35 @@
package lila.practice
import lila.db.dsl._
import lila.user.User
import lila.study.Chapter
import lila.user.User
final class PracticeApi(coll: Coll) {
final class PracticeApi(
coll: Coll,
configStore: lila.memo.ConfigStore[PracticeStructure]) {
import BSONHandlers._
def get(user: User): Fu[PracticeProgress] =
coll.uno[PracticeProgress]($id(user.id)) map { _ | PracticeProgress.empty(PracticeProgress.UserId(user.id)) }
object structure {
def get = configStore.get map (_ | PracticeStructure.empty)
def set = configStore.set _
def form = configStore.makeForm
}
private def save(p: PracticeProgress): Funit =
coll.update($id(p.id), p, upsert = true).void
object progress {
def setNbMoves(user: User, fullId: Chapter.FullId, score: StudyProgress.NbMoves) =
get(user) flatMap { prog =>
save(prog.withNbMoves(fullId, score))
}
def get(user: User): Fu[PracticeProgress] =
coll.uno[PracticeProgress]($id(user.id)) map { _ | PracticeProgress.empty(PracticeProgress.UserId(user.id)) }
def reset(user: User) =
coll.remove($id(user.id)).void
private def save(p: PracticeProgress): Funit =
coll.update($id(p.id), p, upsert = true).void
def setNbMoves(user: User, fullId: Chapter.FullId, score: StudyProgress.NbMoves) =
get(user) flatMap { prog =>
save(prog.withNbMoves(fullId, score))
}
def reset(user: User) =
coll.remove($id(user.id)).void
}
}

View File

@ -0,0 +1,18 @@
package lila.practice
case class PracticeStructure(
sections: List[PracticeSection])
object PracticeStructure {
val empty = PracticeStructure(Nil)
}
case class PracticeSection(
id: String,
name: String,
studies: List[PracticeStudy])
case class PracticeStudy(
id: String, // study ID
name: String,
desc: String)

View File

@ -27,6 +27,7 @@ object Permission {
case object SeeReport extends Permission("ROLE_SEE_REPORT")
case object SeeInsight extends Permission("ROLE_SEE_INSIGHT")
case object StreamConfig extends Permission("ROLE_STREAM_CONFIG")
case object PracticeConfig extends Permission("ROLE_PRACTICE_CONFIG")
case object Beta extends Permission("ROLE_BETA")
case object GuineaPig extends Permission("ROLE_GUINEA_PIG")
case object MessageAnyone extends Permission("ROLE_MESSAGE_ANYONE")
@ -51,14 +52,14 @@ object Permission {
Hunter, ModerateForum, IpBan, CloseAccount, ReopenAccount,
ChatTimeout, MarkTroll, SetTitle, SetEmail, ModerateQa, StreamConfig,
MessageAnyone, CloseTeam, TerminateTournament, ManageTournament, ManageEvent,
PreviewCoach))
PreviewCoach, PracticeConfig))
case object SuperAdmin extends Permission("ROLE_SUPER_ADMIN", List(
Admin, ChangePermission, PublicMod, Developer))
lazy val allButSuperAdmin: List[Permission] = List(
Admin, Hunter, MarkTroll, ChatTimeout, ChangePermission, ViewBlurs, StaffForum, ModerateForum,
UserSpy, MarkEngine, MarkBooster, IpBan, ModerateQa, StreamConfig,
UserSpy, MarkEngine, MarkBooster, IpBan, ModerateQa, StreamConfig, PracticeConfig,
Beta, MessageAnyone, UserSearch, CloseTeam, TerminateTournament, ManageTournament, ManageEvent,
PublicMod, Developer, Coach, PreviewCoach, GuineaPig, ModNote)

View File

@ -36,7 +36,7 @@ object ApplicationBuild extends Build {
jgit, findbugs, reactivemongo.driver, reactivemongo.iteratees, akka.actor, akka.slf4j,
spray.caching, maxmind, prismic,
kamon.core, kamon.statsd, kamon.influxdb,
java8compat, semver, scrimage),
java8compat, semver, scrimage, configs),
TwirlKeys.templateImports ++= Seq(
"lila.game.{ Game, Player, Pov }",
"lila.tournament.Tournament",
@ -133,7 +133,7 @@ object ApplicationBuild extends Build {
)
lazy val memo = project("memo", Seq(common, db)).settings(
libraryDependencies ++= Seq(findbugs, spray.caching) ++ provided(play.api, reactivemongo.driver)
libraryDependencies ++= Seq(findbugs, spray.caching, configs) ++ provided(play.api, reactivemongo.driver)
)
lazy val search = project("search", Seq(common, hub)).settings(
@ -254,8 +254,8 @@ object ApplicationBuild extends Build {
libraryDependencies ++= provided(play.api, reactivemongo.driver)
)
lazy val practice = project("practice", Seq(common, db, user, study)).settings(
libraryDependencies ++= provided(play.api, reactivemongo.driver)
lazy val practice = project("practice", Seq(common, db, memo, user, study)).settings(
libraryDependencies ++= provided(play.api, reactivemongo.driver, configs)
)
lazy val playban = project("playban", Seq(common, db, game)).settings(

View File

@ -39,6 +39,7 @@ object Dependencies {
val java8compat = "org.scala-lang.modules" %% "scala-java8-compat" % "0.8.0"
val semver = "com.gilt" %% "gfc-semver" % "0.0.5"
val scrimage = "com.sksamuel.scrimage" %% "scrimage-core" % "2.1.8"
val configs = "com.github.kxbmap" %% "configs" % "0.4.4"
object reactivemongo {
val version = "0.12.0"

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="height: 512px; width: 512px;"><path d="M0 0h512v512H0z" fill="#000000" opacity="0"></path><path fill="#fff" d="M256 16C123.45 16 16 123.45 16 256s107.45 240 240 240 240-107.45 240-240S388.55 16 256 16zm0 60c99.41 0 180 80.59 180 180s-80.59 180-180 180S76 355.41 76 256 156.59 76 256 76zm0 30c-66.274 0-120 40.294-120 90 0 30 60 30 60 0 0-16.57 26.862-30 60-30 33.138 0 60 13.43 60 30s-30 15-60 30c-1.875.938-3.478 2.126-4.688 3.28C226.53 244.986 226 271.926 226 286v15c0 16.62 13.38 30 30 30 16.62 0 30-13.38 30-30v-15c0-45 90-40.294 90-90s-53.726-90-120-90zm0 240a30 30 0 0 0-30 30 30 30 0 0 0 30 30 30 30 0 0 0 30-30 30 30 0 0 0-30-30z"></path></svg>

After

Width:  |  Height:  |  Size: 721 B

View File

@ -0,0 +1,46 @@
.practice_config .both {
display: flex;
}
.practice_config .both > * {
flex: 1 1 50%;
width: auto;
}
.practice_config .both .list {
height: 500px;
overflow: hidden;
overflow-y: auto;
}
.practice_config .both > ol {
width: 100%;
overflow: hidden;
}
.practice_config .both li {
margin-left: 2em;
list-style: outside disc;
}
.practice_config .both h2 {
display: inline-block;
font-size: 1.4em;
}
.practice_config .both h3 {
display: inline-block;
font-size: 1.2em;
margin: 0;
}
.practice_config .both em {
display: block;
font-style: italic;
font-size: 0.8em;
}
.practice_config textarea.practice_text {
width: 100%;
height: 500px;
font-family: monospace;
margin-bottom: 2em;
padding: 8px;
box-sizing: border-box;
}
.practice_config .error {
color: red;
margin-bottom: 1em;
}