post in the forum

pull/1/merge
Thibault Duplessis 2012-05-26 22:37:01 +02:00
parent 8d801171d5
commit fbf469dbd2
20 changed files with 268 additions and 106 deletions

View File

@ -5,9 +5,24 @@ import views._
object ForumPost extends LilaController {
def topicApi = env.forum.topicApi
def postApi = env.forum.postApi
def forms = forum.DataForm
def create(categSlug: String, slug: String) = TODO
def create(categSlug: String, slug: String, page: Int) = OpenBody { implicit ctx
implicit val req = ctx.body
IOptionResult(topicApi.show(categSlug, slug, page)) {
case (categ, topic, posts) forms.post.bindFromRequest.fold(
err BadRequest(html.forum.topic.show(categ, topic, posts, err.some)),
data (for {
post postApi.makePost(categ, topic, data, ctx.me)
} yield Redirect("%s#%d".format(
routes.ForumTopic.show(categ.slug, topic.slug, postApi pageOf post),
post.number)
)).unsafePerformIO
)
}
}
def delete(id: String) = TODO
}

View File

@ -6,12 +6,16 @@ import views._
object ForumTopic extends LilaController {
def topicApi = env.forum.topicApi
def forms = forum.DataForm
def create(categ: String) = TODO
def create(categ: String) = Open { implicit ctx =>
BadRequest
}
def show(categSlug: String, slug: String, page: Int) = Open { implicit ctx
IOptionOk(topicApi.show(categSlug, slug, page)) {
case (categ, topic, posts) html.forum.topic.show(categ, topic, posts)
case (categ, topic, posts) html.forum.topic.show(categ, topic, posts,
postForm = (!posts.hasNextPage) option forms.post)
}
}

View File

@ -32,7 +32,8 @@ final class CoreEnv private (application: Application, val settings: Settings) {
lazy val forum = new lila.forum.ForumEnv(
settings = settings,
mongodb = mongodb.apply _,
userRepo = user.userRepo)
userRepo = user.userRepo,
userDbRef = user.userRepo.dbRef)
lazy val lobby = new lila.lobby.LobbyEnv(
app = app,

View File

@ -9,8 +9,8 @@ final class CategApi(env: ForumEnv) {
val list: IO[List[CategView]] = for {
categs env.categRepo.all
views (categs map { categ
env.postRepo byId categ.lastPostId map { post
CategView(categ, post)
env.postApi get categ.lastPostId map { post
CategView(categ, post, env.postApi.pageOf)
}
}).sequence
} yield views

View File

@ -3,7 +3,8 @@ package forum
case class CategView(
categ: Categ,
lastPost: Option[Post]) {
lastPost: Option[(Topic, Post)],
pageOf: Post Int) {
def slug = categ.slug
def name = categ.name

View File

@ -0,0 +1,17 @@
package lila
package forum
import play.api.data._
import play.api.data.Forms._
object DataForm {
case class PostData(
text: String,
author: Option[String])
val post = Form(mapping(
"text" -> text(minLength = 3),
"author" -> optional(text)
)(PostData.apply)(PostData.unapply))
}

View File

@ -1,15 +1,17 @@
package lila
package forum
import user.UserRepo
import user.{ User, UserRepo }
import core.Settings
import com.mongodb.casbah.MongoCollection
import com.mongodb.DBRef
final class ForumEnv(
settings: Settings,
mongodb: String MongoCollection,
userRepo: UserRepo) {
userRepo: UserRepo,
val userDbRef: User DBRef) {
import settings._

View File

@ -4,11 +4,12 @@ package forum
import org.joda.time.DateTime
import com.novus.salat.annotations.Key
import com.mongodb.DBRef
import ornicar.scalalib.OrnicarRandom
case class Post(
@Key("_id") id: String,
topicId: String,
author: String,
author: Option[String],
user: Option[DBRef],
text: String,
number: Int,
@ -16,3 +17,22 @@ case class Post(
def userId: Option[String] = user map (_.getId.toString)
}
object Post {
val idSize = 8
def apply(
topicId: String,
author: Option[String],
user: Option[DBRef],
text: String,
number: Int): Post = Post(
id = OrnicarRandom nextAsciiString idSize,
topicId = topicId,
author = author,
user = user,
text = text,
number = number,
createdAt = DateTime.now)
}

View File

@ -1,11 +1,52 @@
package lila
package forum
import user.User
import scalaz.effects._
import com.github.ornicar.paginator._
import scala.math.ceil
final class PostApi(env: ForumEnv, maxPerPage: Int) {
def create(categSlug: String, slug: String, page: Int): IO[Option[(Categ, Topic, Paginator[Post])]] =
for {
categOption env.categRepo bySlug categSlug
topicOption env.topicRepo.byTree(categSlug, slug)
} yield categOption |@| topicOption apply {
case (categ, topic) (categ, topic, env.postApi.paginator(topic, page))
}
def makePost(
categ: Categ,
topic: Topic,
data: DataForm.PostData,
user: Option[User]): IO[Post] = for {
number lastNumberOf(topic)
post = Post(
topicId = topic.id,
author = data.author,
user = user map env.userDbRef,
text = data.text,
number = number + 1)
_ env.postRepo saveIO post
_ env.topicApi denormalize topic
_ env.categApi denormalize categ
} yield post
def get(postId: String): IO[Option[(Topic, Post)]] = for {
postOption env.postRepo byId postId
topicOption postOption.fold(
post env.topicRepo byId post.topicId,
io(none[Topic])
)
} yield (topicOption |@| postOption).tupled
def lastNumberOf(topic: Topic): IO[Int] =
env.postRepo lastByTopics List(topic) map (_.number)
def pageOf(post: Post) = ceil(post.number / maxPerPage.toFloat).toInt
def paginator(topic: Topic, page: Int): Paginator[Post] =
Paginator(
SalatAdapter(

View File

@ -20,14 +20,23 @@ final class PostRepo(
}
def lastByTopics(topics: List[Topic]): IO[Post] = io {
find(byTopicsQuery(topics)).sort(sortQuery).limit(1).next
find(byTopicsQuery(topics)).sort(sortQuery(-1)).limit(1).next
}
val all: IO[List[Post]] = io {
find(DBObject()).toList
}
val sortQuery = DBObject("createdAt" -> -1)
val sortQuery: DBObject = sortQuery(1)
def sortQuery(order: Int): DBObject = DBObject("createdAt" -> order)
def saveIO(post: Post): IO[Unit] = io {
update(
DBObject("_id" -> post.id),
_grater asDBObject post,
upsert = true)
}
def byTopicQuery(topic: Topic) = DBObject("topicId" -> topic.id)

View File

@ -7,13 +7,17 @@ import com.github.ornicar.paginator._
final class TopicApi(env: ForumEnv, maxPerPage: Int) {
def show(categSlug: String, slug: String, page: Int): IO[Option[(Categ, Topic, Paginator[Post])]] =
for {
categOption env.categRepo bySlug categSlug
topicOption env.topicRepo.byTree(categSlug, slug)
} yield categOption |@| topicOption apply {
case (categ, topic) (categ, topic, env.postApi.paginator(topic, page))
get(categSlug, slug) map {
_ map {
case (categ, topic) (categ, topic, env.postApi.paginator(topic, page))
}
}
def get(categSlug: String, slug: String) = for {
categOption env.categRepo bySlug categSlug
topicOption env.topicRepo.byTree(categSlug, slug)
} yield (categOption |@| topicOption).tupled
def paginator(categ: Categ, page: Int): Paginator[TopicView] =
Paginator(
SalatAdapter(
@ -21,7 +25,7 @@ final class TopicApi(env: ForumEnv, maxPerPage: Int) {
query = env.topicRepo byCategQuery categ,
sort = env.topicRepo.sortQuery) map { topic
env.postRepo byId topic.lastPostId map { post
TopicView(categ, topic, post)
TopicView(categ, topic, post, env.postApi.pageOf)
} unsafePerformIO
},
currentPage = page,

View File

@ -12,6 +12,10 @@ final class TopicRepo(
collection: MongoCollection
) extends SalatDAO[Topic, String](collection) {
def byId(id: String): IO[Option[Topic]] = io {
findOneByID(id)
}
def byCateg(categ: Categ): IO[List[Topic]] = io {
find(DBObject("categId" -> categ.slug)).toList
}

View File

@ -4,7 +4,8 @@ package forum
case class TopicView(
categ: Categ,
topic: Topic,
lastPost: Option[Post]) {
lastPost: Option[Post],
pageOf: Post Int) {
def id = topic.id
def slug = topic.slug

View File

@ -18,6 +18,7 @@ object Environment
with DateHelper
with JsonHelper
with PaginatorHelper
with FormHelper
with round.RoundHelper
with game.GameHelper
with user.UserHelper

View File

@ -0,0 +1,19 @@
package lila
package templating
import play.api.data._
import play.api.templates.Html
trait FormHelper {
private val errNames = Map(
"error.minLength" -> "Text is too short."
)
def errMsg(form: Field): Html = Html {
form.errors map { e
"""<p class="error">%s</p>""".format(
(errNames get e.message) | e.message)
} mkString
}
}

View File

@ -4,36 +4,38 @@
title = trans.forum.str()) {
<div class="forum forum_index">
<h1>Lichess Forum</h1>
<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">@categ.nbTopics</td>
<td class="right">@categ.nbPosts</td>
<td class="last_post">
@categ.lastPost.map { post =>
<a href="#">@showDate(post.createdAt)</a><br />by @userIdToUsername(post.userId)
}
</td>
</td>
</tr>
}
}
<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">@categ.nbTopics</td>
<td class="right">@categ.nbPosts</td>
<td class="last_post">
@categ.lastPost.map {
case (topic, post) => {
<a href="@routes.ForumTopic.show(categ.slug, topic.slug, categ pageOf post)#@post.number">@showDate(post.createdAt)</a><br />by @userIdToUsername(post.userId)
}
}
</td>
</td>
</tr>
}
}
</tbody>
</table>
}

View File

@ -30,10 +30,10 @@ title = categ.name) {
<tr>
<td class="subject"><a href="@routes.ForumTopic.show(categ.slug, topic.slug)">@topic.name</a></td>
<td class="right">@topic.views</td>
<td class="right">@topic.nbPosts</td>
<td class="right">@(topic.nbPosts - 1)</td>
<td class="last_post">
@topic.lastPost.map { post =>
<a href="@routes.ForumTopic.show(categ.slug, topic.slug)#@post.number">@showDate(post.createdAt)</a><br />by @userIdToUsername(post.userId)
<a href="@routes.ForumTopic.show(categ.slug, topic.slug, topic pageOf post)#@post.number">@showDate(post.createdAt)</a><br />by @userIdToUsername(post.userId)
}
</td>
@if(isGranted(Permission.ModerateForum)) {

View File

@ -1,4 +1,4 @@
@(categ: lila.forum.Categ, topic: lila.forum.Topic, posts: Paginator[lila.forum.Post])(implicit ctx: Context)
@(categ: lila.forum.Categ, topic: lila.forum.Topic, posts: Paginator[lila.forum.Post], postForm: Option[Form[lila.forum.DataForm.PostData]])(implicit ctx: Context)
@forum.layout(
title = topic.name) {
@ -34,13 +34,34 @@ title = topic.name) {
}
</div>
<div class="bar bottom clearfix">
@for(form <- postForm) {
<div class="topicReply">
<h2 class="postNewTitle" id="reply">Reply to this topic</h2>
@form.globalError.map { error =>
<p class="error">@error.message</p>
}
<form action="@routes.ForumPost.create(categ.slug, topic.slug, posts.currentPage)#bottom" method="POST" novalidate>
<label>
<span class="required">Message</span>
<textarea name="@form("text").name" id="@form("text").id"></textarea>
@errMsg(form("text"))
</label>
@if(!ctx.isAuth) {
<label>
<span>Author</span>
<input type="text" name="@form("author").name" id="@form("author").id">
</label>
}
<input type="submit" class="submit button" value="Reply" />
<a href="@routes.ForumCateg.show(categ.slug)" style="margin-left:20px">Cancel</a>
</form>
</div>
}
<div id="bottom" class="bar bottom clearfix">
<div class="pagination">
@forum.pagination(routes.ForumTopic.show(categ.slug, topic.slug, 1), posts)
</div>
@if(posts.currentPage == posts.nbPages) {
<a href="@routes.ForumPost.create(categ.slug, topic.slug)" class="action button">Reply to this topic</a>
}
</div>
</div>
}

View File

@ -1,11 +1,11 @@
#Site
GET /socket controllers.Main.websocket
GET /socket controllers.Main.websocket
# Game
GET /games controllers.Game.realtime
GET /games/refresh/:ids controllers.Game.realtimeInner(ids: String)
GET /games/all controllers.Game.all(page: Int ?= 1)
GET /games/checkmate controllers.Game.checkmate(page: Int ?= 1)
GET /games controllers.Game.realtime
GET /games/refresh/:ids controllers.Game.realtimeInner(ids: String)
GET /games/all controllers.Game.all(page: Int ?= 1)
GET /games/checkmate controllers.Game.checkmate(page: Int ?= 1)
# Round
GET /$gameId<[\w\-]{8}> controllers.Round.watcher(gameId: String, color: String = "white")
@ -38,71 +38,71 @@ GET /analyse/$gameId<[\w\-]{8}>/$color<white|black> controllers.Analyse.replay
GET /$gameId<[\w\-]{8}>/stats controllers.Analyse.stats(gameId: String)
# Setting
POST /setting/color controllers.Setting.color
POST /setting/sound controllers.Setting.sound
POST /setting/color controllers.Setting.color
POST /setting/sound controllers.Setting.sound
# Setup
GET /setup/ai controllers.Setup.aiForm
POST /setup/ai controllers.Setup.ai
GET /setup/friend controllers.Setup.friendForm
POST /setup/friend controllers.Setup.friend
GET /$fullId<[\w\-]{12}>/await controllers.Setup.await(fullId: String)
GET /$fullId<[\w\-]{12}>/cancel controllers.Setup.cancel(fullId: String)
GET /setup/hook controllers.Setup.hookForm
POST /setup/hook controllers.Setup.hook
GET /setup/ai controllers.Setup.aiForm
POST /setup/ai controllers.Setup.ai
GET /setup/friend controllers.Setup.friendForm
POST /setup/friend controllers.Setup.friend
GET /$fullId<[\w\-]{12}>/await controllers.Setup.await(fullId: String)
GET /$fullId<[\w\-]{12}>/cancel controllers.Setup.cancel(fullId: String)
GET /setup/hook controllers.Setup.hookForm
POST /setup/hook controllers.Setup.hook
# I18n
GET /translation/contribute controllers.I18n.contribute
GET /translation/contribute controllers.I18n.contribute
# Authentication
GET /login controllers.Auth.login
POST /login controllers.Auth.authenticate
GET /logout controllers.Auth.logout
GET /login controllers.Auth.login
POST /login controllers.Auth.authenticate
GET /logout controllers.Auth.logout
# User
GET /@/:username/export controllers.User.export(username: String)
GET /@/:username/:filterName controllers.User.showFilter(username: String, filterName: String, page: Int ?= 1)
GET /@/:username controllers.User.show(username: String)
POST /@/:username/engine controllers.User.engine(username: String)
POST /@/:username/mute controllers.User.mute(username: String)
GET /signup controllers.User.signUp
GET /people controllers.User.list(page: Int ?= 1)
GET /people/stats controllers.User.stats
GET /people/autocomplete controllers.User.autocomplete
GET /people/online controllers.User.online
GET /@/:username/export controllers.User.export(username: String)
GET /@/:username/:filterName controllers.User.showFilter(username: String, filterName: String, page: Int ?= 1)
GET /@/:username controllers.User.show(username: String)
POST /@/:username/engine controllers.User.engine(username: String)
POST /@/:username/mute controllers.User.mute(username: String)
GET /signup controllers.User.signUp
GET /people controllers.User.list(page: Int ?= 1)
GET /people/stats controllers.User.stats
GET /people/autocomplete controllers.User.autocomplete
GET /people/online controllers.User.online
GET /account/bio controllers.User.getBio
POST /account/bio controllers.User.setBio
GET /account/close controllers.User.close
POST /account/closeConfirm controllers.User.closeConfirm
GET /account/close controllers.User.close
POST /account/closeConfirm controllers.User.closeConfirm
# Wiki
GET /wiki controllers.Wiki.home
GET /wiki controllers.Wiki.home
# AI
GET /ai controllers.Ai.run
GET /ai controllers.Ai.run
# Lobby
GET / controllers.Lobby.home
GET /new/$ownerId<[\w\-]{12}> controllers.Lobby.hook(ownerId: String)
GET /new/$ownerId<[\w\-]{12}>/cancel controllers.Lobby.cancel(ownerId: String)
GET /new/$hookId<[\w\-]{8}>/join controllers.Lobby.join(hookId: String)
GET /lobby/socket controllers.Lobby.socket
# Lobby
GET / controllers.Lobby.home
GET /new/$ownerId<[\w\-]{12}> controllers.Lobby.hook(ownerId: String)
GET /new/$ownerId<[\w\-]{12}>/cancel controllers.Lobby.cancel(ownerId: String)
GET /new/$hookId<[\w\-]{8}>/join controllers.Lobby.join(hookId: String)
GET /lobby/socket controllers.Lobby.socket
# Forum
GET /forum controllers.ForumCateg.index
GET /forum/:slug controllers.ForumCateg.show(slug: String, page: Int ?= 1)
GET /forum/:categSlug/new controllers.ForumTopic.create(categSlug: String)
GET /forum/:categSlug/:slug controllers.ForumTopic.show(categSlug: String, slug: String, page: Int ?= 1)
GET /forum/:categSlug/:slug/new controllers.ForumPost.create(categSlug: String, slug: String)
GET /forum/delete/topic/:id controllers.ForumTopic.delete(id: String)
GET /forum controllers.ForumCateg.index
GET /forum/:slug controllers.ForumCateg.show(slug: String, page: Int ?= 1)
POST /forum/:categSlug/new controllers.ForumTopic.create(categSlug: String)
GET /forum/:categSlug/:slug controllers.ForumTopic.show(categSlug: String, slug: String, page: Int ?= 1)
POST /forum/:categSlug/:slug/new controllers.ForumPost.create(categSlug: String, slug: String, page: Int ?= 1)
GET /forum/delete/topic/:id controllers.ForumTopic.delete(id: String)
GET /forum/delete/post/:id controllers.ForumPost.delete(id: String)
# Monitor
GET /monitor controllers.Monitor.index
GET /monitor/stream controllers.Monitor.stream
GET /nb-players controllers.Monitor.nbPlayers
GET /nb-playing controllers.Monitor.nbPlaying
GET /status controllers.Monitor.status
GET /monitor controllers.Monitor.index
GET /monitor/stream controllers.Monitor.stream
GET /nb-players controllers.Monitor.nbPlayers
GET /nb-playing controllers.Monitor.nbPlaying
GET /status controllers.Monitor.status
# Assets
GET /assets/*file controllers.Assets.at(path="/public", file)
GET /assets/*file controllers.Assets.at(path="/public", file)

View File

@ -200,7 +200,7 @@ div.category .description {
margin-left: 110px;
}
#lichess_forum form ul {
#lichess_forum form p.error {
margin-left: 110px;
margin-bottom: 10px;
color: red;