Work on forum denormalization
parent
582452d3bb
commit
29a734c83b
|
@ -11,4 +11,5 @@ object ForumCateg extends LilaController {
|
|||
IOk(api.list map { html.forum.categ.index(_) })
|
||||
}
|
||||
|
||||
def show(slug: String) = TODO
|
||||
}
|
||||
|
|
|
@ -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 = "") {
|
||||
}
|
||||
|
|
|
@ -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 ()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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 = "") {
|
||||
}
|
|
@ -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 ()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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 ()
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue