implement study cloning

ios-push-2
Thibault Duplessis 2016-08-31 18:00:45 +02:00
parent b0b8713bc8
commit 99e9ff9ec2
19 changed files with 132 additions and 12 deletions

View File

@ -168,6 +168,42 @@ object Study extends LilaController {
} inject Redirect(routes.Study.allDefault(1))
}
def cloneStudy(id: String) = Auth { implicit ctx =>
me =>
OptionFuResult(env.api.byId(id)) { study =>
CanViewResult(study) {
Ok(html.study.clone(study)).fuccess
}
}
}
private val CloneLimitPerUser = new lila.memo.RateLimit(
credits = 10,
duration = 24 hour,
name = "clone study per user")
private val CloneLimitPerIP = new lila.memo.RateLimit(
credits = 20,
duration = 24 hour,
name = "clone study per IP")
def cloneApply(id: String) = Auth { implicit ctx =>
me =>
val ip = HTTPRequest lastRemoteAddress ctx.req
implicit val default = ornicar.scalalib.Zero.instance[Fu[Result]](notFound)
CloneLimitPerUser(me.id, cost = 1, msg = me.id) {
CloneLimitPerIP(ip, cost = 1, msg = ip) {
OptionFuResult(env.api.byId(id)) { prev =>
CanViewResult(prev) {
env.api.clone(me, prev) map { study =>
Redirect(routes.Study.show(study.id))
}
}
}
}
}
}
private val PgnRateLimitGlobal = new lila.memo.RateLimit(
credits = 30,
duration = 1 minute,

View File

@ -0,0 +1,28 @@
@(s: lila.study.Study)(implicit ctx: Context)
@site.message(
title = s"Clone ${s.name}",
icon = Some(Html("")),
back = false) {
<form action="@routes.Study.cloneApply(s.id)" method="POST">
<p>
This will create a new private study with the same chapters.
</p>
<p>
You will be the owner of that new study.
</p>
<p>
The two studies can be updated separately.
</p>
<p>
Deleting one study will <strong>not</strong> delete the other study.
</p>
<p>
<button type="submit" class="submit button large" data-icon="@openingBrace"
style="margin: 30px auto; display: block; font-size: 2em;">Clone the study</button>
</p>
<p>
<a href="@routes.Study.show(s.id)" class="text" data-icon="I">@trans.cancel()</a>
</p>
</form>
}

View File

@ -12,7 +12,7 @@ control = control) {
<div class="content_box_top">
@searchForm(control.query)
<h1 data-icon="@openingBrace" class="is4 text lichess_title">
<h1 data-icon="" class="is4 text lichess_title">
@if(control.filter.tags.nonEmpty) {
@pluralize("video", videos.nbResults) found
} else {

View File

@ -6,7 +6,7 @@ control = control) {
<div class="content_box_top">
@searchForm(control.query)
<h1 data-icon="@openingBrace" class="is4 text lichess_title">
<h1 data-icon="" class="is4 text lichess_title">
@pluralize("video", videos.nbResults) found
</h1>
</div>

View File

@ -138,6 +138,8 @@ POST /study controllers.Study.create
GET /study/$id<\w{8}>/socket/v:apiVersion controllers.Study.websocket(id: String, apiVersion: Int)
GET /study/$id<\w{8}>.pgn controllers.Study.pgn(id: String)
POST /study/$id<\w{8}>/delete controllers.Study.delete(id: String)
GET /study/$id<\w{8}>/clone controllers.Study.cloneStudy(id: String)
POST /study/$id<\w{8}>/cloneAplly controllers.Study.cloneApply(id: String)
GET /study/$id<\w{8}>/$chapterId<\w{8}> controllers.Study.chapter(id: String, chapterId: String)
# Learn

View File

@ -237,13 +237,15 @@ private object BSONHandlers {
import Study.From
private[study] implicit val FromHandler: BSONHandler[BSONString, From] = new BSONHandler[BSONString, From] {
def read(bs: BSONString) = bs.value.split(' ') match {
case Array("scratch") => From.Scratch
case Array("game", id) => From.Game(id)
case _ => sys error s"Invalid from ${bs.value}"
case Array("scratch") => From.Scratch
case Array("game", id) => From.Game(id)
case Array("study", id) => From.Study(id)
case _ => sys error s"Invalid from ${bs.value}"
}
def write(x: From) = BSONString(x match {
case From.Scratch => "scratch"
case From.Game(id) => s"game $id"
case From.Scratch => "scratch"
case From.Game(id) => s"game $id"
case From.Study(id) => s"study $id"
})
}
import Settings.UserSelection

View File

@ -48,6 +48,12 @@ case class Chapter(
else FullOpeningDB searchInFens root.mainline.map(_.fen)
def isEmptyInitial = order == 1 && root.children.nodes.isEmpty
def cloneFor(study: Study) = copy(
_id = Chapter.makeId,
studyId = study.id,
ownerId = study.ownerId,
createdAt = DateTime.now)
}
object Chapter {
@ -93,7 +99,7 @@ object Chapter {
def makeId = scala.util.Random.alphanumeric take idSize mkString
def make(studyId: Study.ID, name: String, setup: Setup, root: Node.Root, order: Int, ownerId: User.ID, conceal: Option[Ply]) = Chapter(
_id = scala.util.Random.alphanumeric take idSize mkString,
_id = makeId,
studyId = studyId,
name = toName(name),
setup = setup,

View File

@ -34,7 +34,7 @@ final class ChapterRepo(coll: Coll) {
noRootProjection
).sort($sort asc "order").list[Chapter.Metadata](maxChapters)
// loads all study chapters in memory! only used for search indexing
// loads all study chapters in memory! only used for search indexing and cloning
def orderedByStudy(studyId: Study.ID): Fu[List[Chapter]] =
coll.find($studyId(studyId))
.sort($sort asc "order")

View File

@ -40,6 +40,21 @@ case class Study(
def isNew = (nowSeconds - createdAt.getSeconds) < 4
def isOld = (nowSeconds - updatedAt.getSeconds) > 10 * 60
def cloneFor(user: User): Study = {
val owner = StudyMember(
id = user.id,
role = StudyMember.Role.Write,
addedAt = DateTime.now)
copy(
_id = Study.makeId,
members = StudyMembers(Map(user.id -> owner)),
ownerId = owner.id,
visibility = Study.Visibility.Private,
likes = Likes(0),
createdAt = DateTime.now,
updatedAt = DateTime.now)
}
}
object Study {
@ -75,6 +90,7 @@ object Study {
object From {
case object Scratch extends From
case class Game(id: String) extends From
case class Study(id: String) extends From
}
case class Data(
@ -102,13 +118,15 @@ object Study {
val idSize = 8
def makeId = scala.util.Random.alphanumeric take idSize mkString
def make(user: User, from: From) = {
val owner = StudyMember(
id = user.id,
role = StudyMember.Role.Write,
addedAt = DateTime.now)
Study(
_id = scala.util.Random.alphanumeric take idSize mkString,
_id = makeId,
name = s"${user.username}'s Study",
members = StudyMembers(Map(user.id -> owner)),
position = Position.Ref("", Path.root),

View File

@ -62,6 +62,16 @@ final class StudyApi(
scheduleTimeline(res.study.id) inject res
}
def clone(me: User, prev: Study): Fu[Study] = {
val study = prev.cloneFor(me)
studyRepo.insert(study) >>
chapterRepo.orderedByStudy(prev.id).flatMap { chapters =>
chapters.map { chapter =>
chapterRepo insert chapter.cloneFor(study)
}.sequenceFu
} inject study
}
def resetIfOld(study: Study, chapters: List[Chapter.Metadata]): Fu[Study] =
chapters.headOption match {
case Some(c) if study.isOld && study.position != c.initialPosition =>

Binary file not shown.

View File

@ -122,4 +122,5 @@
<glyph glyph-name="ribbon" unicode="&#57368;" d="M425 370c0 78-64 142-142 142-79 0-142-64-142-142 0-46 22-87 56-113l-110-199 81 21 45-71 50 195 3-203 60 59 74-39-60 220c50 22 85 72 85 130z m-268 0c0 69 56 126 126 126 69 0 126-57 126-126 0-70-57-126-126-126-70 0-126 56-126 126z m50-322l-32 50-56-15 90 164 5-1 0 0c12-7 25-12 39-15z m168 3l-52 27-42-40-2 187 15 3 0 0c11 1 21 3 31 6z m-55 349l-6 1c-6 1-14 7-17 12l-2 5c-7 14-22 14-29 0l-3-5c-2-5-10-11-17-12l-5-1c-8-1-13-5-15-10-1-6 1-12 6-17l4-4c5-4 8-14 7-20l-1-6c-2-7 1-12 3-14 4-6 12-7 20-3l5 3c5 3 16 3 21 0l5-3c3-1 6-2 9-2 5 0 8 1 11 5 2 2 4 7 3 14l-1 6c-1 6 2 16 7 20l4 4c5 5 7 11 5 17-2 5-7 9-14 10z m-7-20c-8-8-13-22-11-34l1-5-5 2c-10 5-26 5-36 0l-5-2 1 5c2 12-2 26-11 34l-4 4 6 1c11 2 24 11 29 21l2 5 3-5c5-10 17-19 29-21l5-1z m83-9c0 63-51 114-114 114-62 0-113-51-113-114 0-62 51-113 113-113 63 0 114 51 114 113z m-114-97c-53 0-97 43-97 97 0 54 44 97 97 97 54 0 98-43 98-97 0-54-44-97-98-97z m-122-149l46 93 15-7-46-93z"/>
<glyph glyph-name="cogs" unicode="&#110;" d="M238 256c0 20-7 37-22 52-14 14-31 21-51 21-21 0-38-7-52-21-14-15-22-32-22-52 0-20 8-37 22-52 14-14 31-21 52-21 20 0 37 7 51 21 15 15 22 32 22 52z m219-146c0 10-3 18-11 25-7 8-16 11-25 11-10 0-19-3-26-11-7-7-11-15-11-25 0-10 4-19 11-26 7-7 15-11 26-11 10 0 18 4 25 11 8 7 11 16 11 26z m0 292c0 10-3 19-11 26-7 7-16 11-25 11-10 0-19-4-26-11-7-7-11-16-11-26 0-10 4-18 11-26 7-7 15-10 26-10 10 0 18 3 25 10 8 8 11 16 11 26z m-110-120l0-53c0-2 0-4-2-5-1-2-2-3-4-3l-44-7c-3-7-6-14-10-22 7-9 15-20 26-33 1-2 2-4 2-6 0-2-1-4-2-5-4-6-12-14-23-26-12-11-19-17-23-17-2 0-4 1-6 2l-33 26c-7-3-14-6-22-9-2-20-4-35-6-44-2-5-4-7-9-7l-53 0c-2 0-4 1-6 2-1 2-2 3-3 5l-6 44c-7 2-14 5-22 9l-33-26c-2-1-4-2-6-2-2 0-4 1-6 3-27 25-41 40-41 45 0 2 1 4 2 6 2 3 6 8 12 15 5 7 10 13 13 17-4 9-8 17-10 24l-43 7c-2 0-4 1-5 2-2 2-2 4-2 6l0 53c0 2 0 4 2 5 1 2 3 3 4 3l45 7c2 7 5 14 9 22-7 9-15 20-26 33-1 2-2 4-2 6 0 2 1 4 2 5 4 6 12 14 23 26 12 11 19 17 23 17 2 0 4-1 6-2l33-26c6 3 14 6 22 9 2 21 4 35 6 44 2 5 5 7 9 7l53 0c2 0 4-1 6-2 2-2 3-3 3-5l6-44c7-2 14-5 22-9l33 26c2 1 4 2 6 2 2 0 4-1 6-3 28-25 41-40 41-45 0-2 0-4-2-6-2-3-6-8-12-15-5-8-10-13-13-17 5-10 8-17 10-24l44-6c1-1 3-2 4-3 2-2 2-4 2-6z m183-152l0-40c0-3-14-6-42-9-3-5-5-10-9-15 10-22 15-35 15-39 0-1-1-2-1-2-24-14-35-21-36-21-1 0-6 5-13 14-7 9-12 15-15 19-4 0-7 0-8 0-2 0-5 0-9 0-3-4-8-10-15-19-7-9-11-14-13-14 0 0-12 7-35 21-1 0-2 1-2 2 0 4 5 17 15 39-3 5-6 10-9 15-28 3-42 6-42 9l0 40c0 3 14 6 42 9 3 5 6 10 9 14-10 22-15 35-15 40 0 1 1 1 2 2 0 0 4 2 10 6 5 3 11 6 16 9 6 3 9 5 9 5 2 0 6-5 13-13 7-9 12-16 15-20 4 1 7 1 9 1 1 0 4 0 8-1 10 14 19 24 26 32l2 1c1 0 13-7 36-20 0-1 1-1 1-2 0-5-5-18-15-40 3-4 6-9 9-14 28-3 42-6 42-9z m0 292l0-40c0-3-14-6-42-9-3-5-5-10-9-14 10-22 15-35 15-40 0-1-1-1-1-2-24-13-35-20-36-20-1 0-6 4-13 13-7 9-12 16-15 20-4-1-7-1-8-1-2 0-5 0-9 1-3-4-8-11-15-20-7-9-11-13-13-13 0 0-12 7-35 20-1 1-2 1-2 2 0 5 5 18 15 40-3 4-6 9-9 14-28 3-42 6-42 9l0 40c0 3 14 6 42 9 3 6 6 11 9 15-10 22-15 35-15 39 0 1 1 2 2 2 0 1 4 3 10 6 5 4 11 7 16 10 6 3 9 4 9 4 2 0 6-4 13-13 7-9 12-15 15-19 4 0 7 0 9 0 1 0 4 0 8 0 10 13 19 24 26 32l2 0c1 0 13-6 36-20 0 0 1-1 1-2 0-4-5-17-15-39 3-4 6-9 9-15 28-3 42-6 42-9z"/>
<glyph glyph-name="graduate-cap" unicode="&#58;" d="M256 422l-234-87 234-117 111 56-104 32c-2-1-5-2-7-2-9 0-16 7-16 16 0 9 7 16 16 16l-3-9 19-5c15 0 28-13 28-28 0-16-13-28-28-28l27-9 156 8 0-12c-4-3-7-8-7-13 0-5 3-10 7-13-7-28-7-90-7-115 16-10 16-11 32 0 0 25 0 87-7 115 4 3 7 8 7 13 0 5-3 10-7 13l0 26-59 18 76 38z m-136-159l-13-79c26-3 57-18 86-36 16-11 31-22 44-33 8-6 14-13 19-19 5 6 11 13 19 19 13 11 28 22 44 33 29 18 60 33 87 36l-14 79-6 0-130-65-130 65z"/>
<glyph glyph-name="code-fork" unicode="&#123;" d="M192 91c0 8-3 15-8 20-5 5-12 8-19 8-8 0-15-3-20-8-5-5-8-12-8-20 0-7 3-14 8-19 5-5 12-8 20-8 7 0 14 3 19 8 5 5 8 12 8 19z m0 330c0 7-3 14-8 19-5 5-12 8-19 8-8 0-15-3-20-8-5-5-8-12-8-19 0-8 3-15 8-20 5-5 12-8 20-8 7 0 14 3 19 8 5 5 8 12 8 20z m183-37c0 8-3 14-8 19-5 6-12 8-20 8-7 0-14-2-19-8-5-5-8-11-8-19 0-8 3-14 8-19 5-6 12-8 19-8 8 0 15 2 20 8 5 5 8 11 8 19z m27 0c0-10-2-19-7-28-5-8-12-15-20-19-1-55-22-95-65-119-13-7-32-15-58-23-24-7-40-14-48-20-8-6-12-16-12-29l0-7c8-5 15-12 20-20 5-8 7-18 7-28 0-15-5-28-16-38-10-11-23-16-38-16-16 0-29 5-39 16-11 10-16 23-16 38 0 10 2 20 7 28 5 8 12 15 20 20l0 234c-8 5-15 12-20 20-5 8-7 18-7 28 0 15 5 28 16 38 10 11 23 16 39 16 15 0 28-5 38-16 11-10 16-23 16-38 0-10-2-20-7-28-5-8-12-15-20-20l0-142c10 5 25 11 44 16 10 4 19 6 25 9 6 2 13 5 20 9 7 3 13 7 17 11 4 4 8 9 12 15 3 5 6 12 8 19 1 8 2 17 2 27-8 4-15 11-20 19-5 9-7 18-7 28 0 15 5 28 16 39 10 11 23 16 38 16 16 0 29-5 39-16 11-11 16-24 16-39z"/>
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

View File

@ -475,6 +475,10 @@
<div class="icon icon-graduate-cap"></div>
<input type="text" readonly="readonly" value="graduate-cap">
</li>
<li>
<div class="icon icon-code-fork"></div>
<input type="text" readonly="readonly" value="code-fork">
</li>
</ul>
<h2>Character mapping</h2>
<ul class="glyphs character-mapping">
@ -938,6 +942,10 @@
<div data-icon=":" class="icon"></div>
<input type="text" readonly="readonly" value=":">
</li>
<li>
<div data-icon="{" class="icon"></div>
<input type="text" readonly="readonly" value="{">
</li>
</ul>
</div>
<script>(function() {

View File

@ -383,3 +383,6 @@
.icon-graduate-cap:before {
content: "\3a";
}
.icon-code-fork:before {
content: "\7b";
}

View File

@ -52,8 +52,8 @@ time {
}
@font-face {
font-family: "lichess";
src: url("../font68/fonts/lichess.eot");
src: url("../font68/fonts/lichess.eot?#iefix") format("embedded-opentype"), url("../font68/fonts/lichess.woff") format("woff"), url("../font68/fonts/lichess.ttf") format("truetype"), url("../font68/fonts/lichess.svg#lichess") format("svg");
src: url("../font69/fonts/lichess.eot");
src: url("../font69/fonts/lichess.eot?#iefix") format("embedded-opentype"), url("../font69/fonts/lichess.woff") format("woff"), url("../font69/fonts/lichess.ttf") format("truetype"), url("../font69/fonts/lichess.svg#lichess") format("svg");
font-weight: normal;
font-style: normal;
}

View File

@ -39,6 +39,12 @@ function buttons(root) {
'data-hint': 'Download as PGN',
href: '/study/' + ctrl.data.id + '.pgn'
}, m('i[data-icon=x]')),
m('a.button.hint--top', {
'data-hint': 'Clone this study',
href: '/study/' + ctrl.data.id + '/clone'
}, m('i', {
'data-icon': '{'
})),
canContribute ? [
(function(enabled) {
return m('a.button.comment.hint--top', {