generic config store and editable practice structure
parent
94c2cbfde0
commit
cfa4c07335
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -516,6 +516,12 @@ coach {
|
|||
memo {
|
||||
collection {
|
||||
cache = cache
|
||||
config = flag
|
||||
}
|
||||
}
|
||||
practice {
|
||||
collection {
|
||||
progress = practice_progress
|
||||
}
|
||||
}
|
||||
setup {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 "
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 |
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue