Forum topic creation

This commit is contained in:
Thibault Duplessis 2012-05-27 00:27:53 +02:00
parent bb346f64f5
commit 1f2073fc48
17 changed files with 194 additions and 44 deletions

View file

@ -2,7 +2,22 @@ package controllers
import lila._
import views._
import security._
import http.Context
object Forum extends LilaController {
import play.api.mvc._
import play.api.mvc.Results._
trait Forum {
def CategGrant[A <: Result](categSlug: String)(a: A)(implicit ctx: Context): Result =
isGranted(categSlug)(ctx).fold(
a,
Forbidden("You cannot access to this category")
)
def isGranted(categSlug: String)(ctx: Context) =
(categSlug == "staff").fold(
ctx.me exists { u Granter(Permission.StaffForum)(u) },
true)
}

View file

@ -3,7 +3,7 @@ package controllers
import lila._
import views._
object ForumCateg extends LilaController {
object ForumCateg extends LilaController with Forum {
def categApi = env.forum.categApi
@ -12,8 +12,10 @@ object ForumCateg extends LilaController {
}
def show(slug: String, page: Int) = Open { implicit ctx
IOptionOk(categApi.show(slug, page)) {
case (categ, topics) html.forum.categ.show(categ, topics)
CategGrant(slug) {
IOptionOk(categApi.show(slug, page)) {
case (categ, topics) html.forum.categ.show(categ, topics)
}
}
}
}

View file

@ -3,24 +3,26 @@ package controllers
import lila._
import views._
object ForumPost extends LilaController {
object ForumPost extends LilaController with Forum {
def topicApi = env.forum.topicApi
def postApi = env.forum.postApi
def forms = forum.DataForm
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
)
CategGrant(categSlug) {
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
)
}
}
}

View file

@ -3,19 +3,42 @@ package controllers
import lila._
import views._
object ForumTopic extends LilaController {
object ForumTopic extends LilaController with Forum {
def categApi = env.forum.topicApi
def topicApi = env.forum.topicApi
def categRepo = env.forum.categRepo
def forms = forum.DataForm
def create(categ: String) = Open { implicit ctx =>
BadRequest
def form(categSlug: String) = Open { implicit ctx
CategGrant(categSlug) {
IOptionOk(categRepo bySlug categSlug) { categ
html.forum.topic.form(categ, forms.topic)
}
}
}
def create(categSlug: String) = OpenBody { implicit ctx
CategGrant(categSlug) {
implicit val req = ctx.body
IOptionResult(categRepo bySlug categSlug) { categ
forms.topic.bindFromRequest.fold(
err BadRequest(html.forum.topic.form(categ, err)),
data (for {
topic topicApi.makeTopic(categ, data, ctx.me)
} yield Redirect(routes.ForumCateg.show(categ.slug))
).unsafePerformIO
)
}
}
}
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,
postForm = (!posts.hasNextPage) option forms.post)
CategGrant(categSlug) {
IOptionOk(topicApi.show(categSlug, slug, page)) {
case (categ, topic, posts) html.forum.topic.show(categ, topic, posts,
postForm = (!posts.hasNextPage) option forms.post)
}
}
}

View file

@ -10,8 +10,19 @@ object DataForm {
text: String,
author: Option[String])
val post = Form(mapping(
lazy val postMapping = mapping(
"text" -> text(minLength = 3),
"author" -> optional(text)
)(PostData.apply)(PostData.unapply))
)(PostData.apply)(PostData.unapply)
lazy val post = Form(postMapping)
case class TopicData(
name: String,
post: PostData)
lazy val topic = Form(mapping(
"name" -> text(minLength = 3),
"post" -> postMapping
)(TopicData.apply)(TopicData.unapply))
}

View file

@ -0,0 +1,11 @@
package lila
package forum
import user.UserHelper
trait ForumHelper { self: UserHelper
def showAuthorName(post: Post) =
post.userId.fold(userIdToUsername, post.showAuthor)
}

View file

@ -3,11 +3,12 @@ package forum
import org.joda.time.DateTime
import com.novus.salat.annotations.Key
import ornicar.scalalib.OrnicarRandom
case class Topic(
@Key("_id") id: String,
slug: String,
categId: String,
slug: String,
name: String,
views: Int,
createdAt: DateTime,
@ -15,3 +16,20 @@ case class Topic(
nbPosts: Int = 0,
lastPostId: String = "") {
}
object Topic {
val idSize = 8
def apply(
categId: String,
slug: String,
name: String): Topic = Topic(
id = OrnicarRandom nextAsciiString idSize,
categId = categId,
slug = slug,
name = name,
views = 0,
createdAt = DateTime.now,
updatedAt = DateTime.now)
}

View file

@ -1,18 +1,41 @@
package lila
package forum
import user.User
import scalaz.effects._
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])]] =
get(categSlug, slug) map {
get(categSlug, slug) map {
_ map {
case (categ, topic) (categ, topic, env.postApi.paginator(topic, page))
}
}
def makeTopic(
categ: Categ,
data: DataForm.TopicData,
user: Option[User]): IO[Topic] = for {
slug env.topicRepo.nextSlug(categ, data.name)
topic = Topic(
categId = categ.slug,
slug = slug,
name = data.name)
post = Post(
topicId = topic.id,
author = data.post.author,
user = user map env.userDbRef,
text = data.post.text,
number = 1)
_ env.topicRepo saveIO topic
_ env.postRepo saveIO post
_ env.topicApi denormalize topic
_ env.categApi denormalize categ
} yield topic
def get(categSlug: String, slug: String) = for {
categOption env.categRepo bySlug categSlug
topicOption env.topicRepo.byTree(categSlug, slug)

View file

@ -9,8 +9,7 @@ import com.mongodb.casbah.Imports._
import scalaz.effects._
final class TopicRepo(
collection: MongoCollection
) extends SalatDAO[Topic, String](collection) {
collection: MongoCollection) extends SalatDAO[Topic, String](collection) {
def byId(id: String): IO[Option[Topic]] = io {
findOneByID(id)
@ -27,6 +26,23 @@ final class TopicRepo(
))
}
def nextSlug(categ: Categ, name: String, it: Int = 1): IO[String] = {
val slug = slugify(name) + (it == 1).fold("", "-" + it)
byTree(categ.slug, slug) flatMap {
_.isDefined.fold(
nextSlug(categ, name, it + 1),
io(slug))
}
}
def slugify(input: String) = {
import java.text.Normalizer
val nowhitespace = input.replace(" ", "-")
val normalized = Normalizer.normalize(nowhitespace, Normalizer.Form.NFD)
val slug = """[^\w-]""".r.replaceAllIn(normalized, "")
slug.toLowerCase
}
val all: IO[List[Topic]] = io {
find(DBObject()).toList
}

View file

@ -22,6 +22,7 @@ object Environment
with round.RoundHelper
with game.GameHelper
with user.UserHelper
with forum.ForumHelper
with security.SecurityHelper
with i18n.I18nHelper {

View file

@ -7,7 +7,8 @@ import play.api.templates.Html
trait FormHelper {
private val errNames = Map(
"error.minLength" -> "Text is too short."
"error.minLength" -> "Text is too short.",
"error.required" -> "Required."
)
def errMsg(form: Field): Html = Html {

View file

@ -28,7 +28,7 @@ title = trans.forum.str()) {
<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)
<a href="@routes.ForumTopic.show(categ.slug, topic.slug, categ pageOf post)#@post.number">@showDate(post.createdAt)</a><br />by @showAuthorName(post)
}
}
</td>

View file

@ -13,7 +13,7 @@ title = categ.name) {
<div class="pagination">
@forum.pagination(routes.ForumCateg.show(categ.slug, 1), topics)
</div>
<a href="@routes.ForumTopic.create(categ.slug)" class="action button">Create a new topic</a>
<a href="@routes.ForumTopic.form(categ.slug)" class="action button">Create a new topic</a>
</div>
<table class="forum_topics_list">
@ -33,7 +33,7 @@ title = categ.name) {
<td class="right">@(topic.nbPosts - 1)</td>
<td class="last_post">
@topic.lastPost.map { post =>
<a href="@routes.ForumTopic.show(categ.slug, topic.slug, topic pageOf post)#@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 @showAuthorName(post)
}
</td>
@if(isGranted(Permission.ModerateForum)) {

View file

@ -0,0 +1,12 @@
@(text: Field, author: Field)(implicit ctx: Context)
<label>
<span class="required">Message</span>
<textarea name="@text.name" id="@text.id"></textarea>
@errMsg(text)
</label>
@if(!ctx.isAuth) {
<label>
<span>Author</span>
<input class="author" type="text" name="@author.name" id="@author.id">
</label>
}

View file

@ -0,0 +1,24 @@
@(categ: lila.forum.Categ, form: Form[lila.forum.DataForm.TopicData])(implicit ctx: Context)
@forum.layout(
title = "New forum topic") {
<ol class="crumbs">
<li><a href="@routes.ForumCateg.index">Forum</a></li>
<li><h1>@categ.name</h1></li>
</ol>
<br />
<h1>New topic</h1>
@form.globalError.map { error =>
<p class="error">@error.message</p>
}
<form action="@routes.ForumTopic.create(categ.slug)" method="POST" novalidate>
<label>
<span class="required">Subject</span>
<input class="subject" type="text" name="@form("name").name" id="@form("name").id">
@errMsg(form("name"))
</label>
@forum.post.formFields(form("post")("text"), form("post")("author"))
<button class="submit button" type="submit">Create the topic</button>
<a href="@routes.ForumCateg.show(categ.slug)" style="margin-left:20px">Cancel</a>
</form>
}

View file

@ -21,7 +21,7 @@ title = topic.name) {
@post.userId.map { userId =>
<span class="author authenticated">@userIdLink(userId.some)</span>
}.getOrElse {
<span class="author">@post.showAuthor</span>
<span class="author">@showAuthorName(post)</span>
}
<span class="createdAt">@showDate(post.createdAt)</span>
<a class="anchor" href="routes.ForumTopic.show(categ.slug, topic.slug, posts.page)">#@post.number</a>
@ -41,17 +41,7 @@ title = topic.name) {
<p class="error">@error.message</p>
}
<form action="@routes.ForumPost.create(categ.slug, topic.slug, posts.currentPage)#reply" 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 class="author" type="text" name="@form("author").name" id="@form("author").id">
</label>
}
@forum.post.formFields(form("text"), form("author"))
<input type="submit" class="submit button" value="Reply" />
<a href="@routes.ForumCateg.show(categ.slug)" style="margin-left:20px">Cancel</a>
</form>

View file

@ -91,6 +91,7 @@ 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/form controllers.ForumTopic.form(categSlug: String)
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)