Work on forum denormalization

pull/1/merge
Thibault Duplessis 2012-05-26 17:22:08 +02:00
parent 582452d3bb
commit 29a734c83b
21 changed files with 495 additions and 8 deletions

View File

@ -11,4 +11,5 @@ object ForumCateg extends LilaController {
IOk(api.list map { html.forum.categ.index(_) })
}
def show(slug: String) = TODO
}

View File

@ -6,5 +6,8 @@ import com.novus.salat.annotations.Key
case class Categ(
@Key("_id") slug: String,
name: String,
desc: String) {
desc: String,
nbTopics: Int = 0,
nbPosts: Int = 0,
lastPostId: String = "") {
}

View File

@ -11,4 +11,19 @@ final class CategApi(env: ForumEnv) {
io(CategView(categ))
}).sequence
} yield views
def denormalize(categ: Categ): IO[Unit] = for {
topics env.topicRepo byCateg categ
nbPosts env.postRepo countByTopics topics
lastPost env.postRepo lastByTopics topics
} yield env.categRepo.save(categ.copy(
nbTopics = topics.size,
nbPosts = nbPosts,
lastPostId = lastPost.id
))
val denormalize: IO[Unit] = for {
categs env.categRepo.all
_ categs.map(denormalize).sequence
} yield ()
}

View File

@ -2,4 +2,9 @@ package lila
package forum
case class CategView(
categ: Categ)
categ: Categ) {
def slug = categ.slug
def name = categ.name
def desc = categ.desc
}

View File

@ -15,5 +15,13 @@ final class ForumEnv(
lazy val categRepo = new CategRepo(mongodb(MongoCollectionForumCateg))
lazy val topicRepo = new TopicRepo(mongodb(MongoCollectionForumTopic))
lazy val postRepo = new PostRepo(mongodb(MongoCollectionForumPost))
lazy val categApi = new CategApi(this)
lazy val topicApi = new TopicApi(this)
lazy val denormalize = topicApi.denormalize flatMap { _ categApi.denormalize }
}

View File

@ -0,0 +1,17 @@
package lila
package forum
import org.joda.time.DateTime
import com.novus.salat.annotations.Key
import com.mongodb.DBRef
case class Post(
@Key("_id") id: String,
topicId: String,
author: String,
user: Option[DBRef],
text: String,
createdAt: DateTime) {
def userId: Option[String] = user map (_.getId.toString)
}

View File

@ -0,0 +1,30 @@
package lila
package forum
import com.novus.salat._
import com.novus.salat.dao._
import com.mongodb.DBRef
import com.mongodb.casbah.MongoCollection
import com.mongodb.casbah.Imports._
import scalaz.effects._
final class PostRepo(
collection: MongoCollection) extends SalatDAO[Post, String](collection) {
def countByTopics(topics: List[Topic]): IO[Int] = io {
count(topicsQuery(topics)).toInt
}
def lastByTopics(topics: List[Topic]): IO[Post] = io {
find(topicsQuery(topics)).sort(sortQuery).limit(1).next
}
val all: IO[List[Post]] = io {
find(DBObject()).toList
}
private val sortQuery = DBObject("createdAt" -> -1)
private def topicsQuery(topics: List[Topic]) =
"topicId" $in topics.map(_.id)
}

View File

@ -0,0 +1,17 @@
package lila
package forum
import org.joda.time.DateTime
import com.novus.salat.annotations.Key
case class Topic(
@Key("_id") id: String,
slug: String,
categId: String,
name: String,
views: Int,
createdAt: DateTime,
updatedAt: DateTime,
nbPosts: Int = 0,
lastPostId: String = "") {
}

View File

@ -0,0 +1,20 @@
package lila
package forum
import scalaz.effects._
final class TopicApi(env: ForumEnv) {
def denormalize(topic: Topic): IO[Unit] = for {
nbPosts env.postRepo countByTopics List(topic)
lastPost env.postRepo lastByTopics List(topic)
} yield env.topicRepo.save(topic.copy(
nbPosts = nbPosts,
lastPostId = lastPost.id
))
val denormalize: IO[Unit] = for {
topics env.topicRepo.all
_ topics.map(denormalize).sequence
} yield ()
}

View File

@ -0,0 +1,22 @@
package lila
package forum
import com.novus.salat._
import com.novus.salat.dao._
import com.mongodb.DBRef
import com.mongodb.casbah.MongoCollection
import com.mongodb.casbah.Imports._
import scalaz.effects._
final class TopicRepo(
collection: MongoCollection
) extends SalatDAO[Topic, String](collection) {
def byCateg(categ: Categ): IO[List[Topic]] = io {
find(DBObject("categId" -> categ.slug)).toList
}
val all: IO[List[Topic]] = io {
find(DBObject()).toList
}
}

View File

@ -7,7 +7,6 @@ import elo.EloRange
import user.User
import com.novus.salat.annotations.Key
import com.mongodb.DBRef
import ornicar.scalalib.OrnicarRandom
case class Hook(

View File

@ -14,9 +14,10 @@ object Permission {
case object ViewBlurs extends Permission("ROLE_VIEW_BLURS")
case object MutePlayer extends Permission("ROLE_CHAT_BAN")
case object MarkEngine extends Permission("ROLE_ADJUST_CHEATER")
case object StaffForum extends Permission("ROLE_STAFF_FORUM")
case object Admin extends Permission("ROLE_ADMIN") {
override val children = List(ViewBlurs, MutePlayer, MarkEngine)
override val children = List(ViewBlurs, MutePlayer, MarkEngine, StaffForum)
}
case object SuperAdmin extends Permission("ROLE_SUPER_ADMIN") {
override val children = List(Admin)

View File

@ -13,7 +13,7 @@ final class SiteMenu(trans: I18nKeys) {
val play = new Elem(routes.Lobby.home, trans.play)
val game = new Elem(routes.Game.realtime, trans.games)
val user = new Elem(routes.User.list(page = 1), trans.people)
val forum = new Elem(routes.Lobby.home, trans.forum)
val forum = new Elem(routes.ForumCateg.index, trans.forum)
val inbox = new Elem(routes.Lobby.home, trans.inbox)
val all = List(play, game, user, forum)

View File

@ -1,7 +1,35 @@
@(categs: List[lila.forum.CategView])(implicit ctx: Context)
@forum.layout(
title = "Forum") {
title = trans.forum.str()) {
categs
<div class="forum forum_index">
<h1>Lichess Forum</h1>
</div>
<table class="categories">
<thead>
<tr class="thead">
<th>Categories</th>
<th class="right">Topics</th>
<th class="right">Posts</th>
<th>Last Post</th>
</tr>
</thead>
<tbody>
@categs.map { categ =>
@if(categ.slug != "staff" || isGranted(Permission.StaffForum)) {
<tr>
<td class="subject">
<a class="category_name" href="@routes.ForumCateg.show(categ.slug)">@categ.name</a>
<h2 class="description">@categ.desc</h2>
</td>
<td class="right"></td>
<td class="right"></td>
<td class="last_post"><a href="#">date</a><br />by author</td>
</td>
</tr>
}
}
</tbody>
</table>
}

View File

@ -12,4 +12,8 @@
title = title,
moreCss = moreCss,
moreJs = moreJs,
active = siteMenu.forum.some)(body)
active = siteMenu.forum.some) {
<div id="lichess_forum" class="content_box">
@body
</div>
}

View File

@ -0,0 +1,15 @@
package lila.cli
import lila.forum.ForumEnv
import scalaz.effects._
case class Forum(env: ForumEnv) {
def denormalize: IO[Unit] = env.denormalize
def typecheck: IO[Unit] = for {
_ env.categRepo.all
_ env.topicRepo.all
_ env.postRepo.all
} yield ()
}

View File

@ -18,6 +18,7 @@ object Main {
lazy val users = Users(env.user.userRepo, env.securityStore)
lazy val games = Games(env.game.gameRepo)
lazy val forum = Forum(env.forum)
lazy val infos = Infos(env)
def main(args: Array[String]): Unit = sys exit {
@ -33,6 +34,8 @@ object Main {
case "user-enable" :: username :: Nil users enable username
case "user-disable" :: username :: Nil users disable username
case "user-info" :: username :: Nil users info username
case "forum-denormalize" :: Nil forum.denormalize
case "forum-typecheck" :: Nil forum.typecheck
case _ putStrLn("Usage: run command args")
}
op.map(_ 0).unsafePerformIO

View File

@ -90,6 +90,7 @@ GET /lobby/socket controllers.Lobby.socket
# Forum
GET /forum controllers.ForumCateg.index
GET /forum/:slug controllers.ForumCateg.show(slug: String)
# Monitor
GET /monitor controllers.Monitor.index

View File

@ -0,0 +1,14 @@
$(function() {
$('.checkmateCaptcha').each(function() {
var $captcha = $(this);
var $input = $captcha.find('input');
var i1, i2;
$captcha.find('div.lmcs').click(function() {
var key = $(this).data('key');
i1 = $input.val();
i2 = i1.length > 3 ? key : i1 + " " + key;
$input.val(i2);
});
});
});

View File

@ -0,0 +1,283 @@
a.forum_feed_link {
width: 16px;
height: 16px;
display: block;
background: url(/assets/images/feed-balloon.png) top left no-repeat;
float: right;
}
.metas .delete {
margin-left: 1em;
}
.forum_topics_list .delete {
margin-right: 1em;
}
ol.crumbs li {
display: inline;
margin-left: 1em;
}
ol.crumbs li:after {
content: "";
padding-left: 1em;
}
ol.crumbs li:last-child:after {
display: none;
}
ol.crumbs li:first-child {
margin-left: 0;
}
#lichess_forum ol.crumbs h1 {
display: inline;
margin: 0;
vertical-align: middle;
}
#lichess_forum table {
width: 100%;
margin-bottom: 5px;
}
#lichess_forum th {
padding: 1em 0 0.5em 0;
}
#lichess_forum th:last {
padding-left: 1em;
}
#lichess_forum td.subject {
padding: 2em 1em;
}
#lichess_forum td.right, #lichess_forum th.right {
text-align: right;
padding-right: 2em;
}
#lichess_forum td.last_post {
min-width: 150px;
padding-right: 1em;
}
#lichess_forum td.feed {
padding-right: 1em;
}
#lichess_forum td.subject a {
font-size: 1.2em;
font-weight: bold;
color: #0090B0;
text-decoration: none;
background: transparent url(/assets/images/balloon.png) top left no-repeat;
padding-left: 20px;
}
#lichess_forum td .description {
margin-top: 0.5em;
display: block;
}
#lichess_forum th {
font-weight: bold;
color: #A0A0A0;
}
#lichess_forum h1 {
font-size: 1.4em;
color: #666;
font-weight: normal;
margin-bottom: 0.5em;
}
.forum_baseline {
margin-bottom: 1em;
}
#lichess_forum tr:nth-child(odd) {
background: #F7F7F7;
}
#lichess_forum tr.thead {
background: #fff;
}
div.post {
font-size: 1.2em;
padding: 2em 0em 1em 0em;
border-top: 1px solid #D4D4D4;
margin-top: 1em;
position: relative;
}
div.post:last-child {
border-bottom: 1px solid #D4D4D4;
padding-bottom: 2em;
margin-bottom: 2em;
}
div.post .anchor {
position: absolute;
top: 2em;
right: 0;
}
div.post .authorName {
font-size: 1.2em;
font-weight: bold;
float: left;
}
div.post .authorName a {
height: 16px;
display: block;
}
div.post .createdAt {
font-size: 0.8em;
font-style: italic;
color: #aaa;
margin-top: 0.3em;
margin-left: 1em;
float: left;
}
div.post .message {
margin-top: 1.5em;
color: #555;
line-height: 1.5em;
}
div.topicReply {
margin-top: 1em;
}
.postNewTitle {
font-size: 1.5em;
color: #444;
font-style: italic;
}
div.category .description {
margin-top: 1em;
}
#lichess_forum form {
margin-top: 2em;
font-size: 1.3em;
}
#lichess_forum form label {
display: block;
margin-bottom: 2em;
}
#lichess_forum form label span {
float: left;
display: block;
width: 100px;
text-align: right;
margin-right: 10px;
margin-top: 3px;
}
#lichess_forum form label span.required:after {
content: "*";
color: #FF6666;
margin-left: 3px;
}
#lichess_forum form input.subject {
width: 500px;
padding: 0.5em;
border: 1px solid #D4D4D4;
}
#lichess_forum form textarea {
width: 500px;
height: 150px;
padding: 0.5em;
border: 1px solid #D4D4D4;
}
#lichess_forum form input.authorName {
padding: 0.5em;
width: 300px;
border: 1px solid #D4D4D4;
}
#lichess_forum form .submit {
margin-left: 110px;
}
#lichess_forum form ul {
margin-left: 110px;
margin-bottom: 10px;
color: red;
}
form.forum_search {
margin: 2em 0 0 0;
font-size: 14px;
}
form.forum_search input {
padding: 2px 5px;
}
form.forum_search input.query {
width: 145px;
}
form.forum_search input.hinted {
color: #aaa;
}
div.search_results .result {
padding: 1em;
border-top: 1px solid #eaeaea;
}
div.search_results .result.even {
background: #f4f4f4;
}
div.search_results .result .subject {
font-size: 1.2em;
font-weight: bold;
margin-bottom: 0.3em;
display: block;
}
div.pagination a, div.pagination span {
font-size: 1.1em;
padding: 0.1em 0.5em;
font-weight: bold;
border-radius: 5px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
text-decoration: none;
}
div.pagination span {
color: #aaa;
}
div.pagination span.current {
margin: 0 0.5em;
}
div.bar {
margin-top: 1em;
width: 100%;
}
div.bar .button {
float: left;
}
div.bar div.pagination {
margin-top: 0.4em;
float: right;
}
div.checkmateFen {
float: left;
width: 220px;
margin-left: 110px;
cursor: pointer;
}
div.checkmateFen div.lmcs:hover {
background: #ddddff;
}
div.checkmateSection {
float: left;
}
div.checkmateCaptcha input {
padding: 0.5em;
border: 1px solid #D4D4D4;
background: #dfdfff;
font-weight: bold;
width: 5em;
}

1
todo
View File

@ -25,3 +25,4 @@ endgame sound http://en.lichess.org/forum/lichess-feedback/checkmate-sound-featu
next deploy:
mongo lichess mongo_migration_user.js
mongo lichess mongo_migration_forum.js
bin/cli forum-denormalize