Merge pull request #2256 from Happy0/post_edit
[WIP] Allow users to edit their forum posts
This commit is contained in:
commit
530dafd58c
|
@ -1,7 +1,6 @@
|
|||
package controllers
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import lila.common.HTTPRequest
|
||||
import lila.app._
|
||||
import views._
|
||||
|
@ -53,6 +52,18 @@ object ForumPost extends LilaController with ForumController {
|
|||
}
|
||||
}
|
||||
|
||||
def edit(postId: String) = AuthBody { implicit ctx =>
|
||||
me =>
|
||||
implicit val req = ctx.body
|
||||
|
||||
forms.postEdit.bindFromRequest.fold(err => Redirect(routes.ForumPost.redirect(postId)).fuccess,
|
||||
data =>
|
||||
postApi.editPost(postId, data.changes, me).map { post =>
|
||||
Redirect(routes.ForumPost.redirect(post.id))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
def delete(categSlug: String, id: String) = Auth { implicit ctx =>
|
||||
me =>
|
||||
CategGrantMod(categSlug) {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
@forum.layout(
|
||||
title = s"${categ.name} / ${topic.name} page ${posts.currentPage}",
|
||||
menu = modMenu.some.ifTrue(categ.isStaff),
|
||||
moreJs = jsTag("forum-post.js"),
|
||||
openGraph = lila.app.ui.OpenGraph(
|
||||
title = s"Forum: ${categ.name} / ${topic.name} page ${posts.currentPage}",
|
||||
url = s"$netBaseUrl${routes.ForumTopic.show(categ.slug, topic.slug).url}",
|
||||
|
@ -36,8 +37,16 @@ description = shorten(posts.currentPageResults.headOption.??(_.text).replace("\n
|
|||
<div class="post" id="@post.number">
|
||||
<div class="metas clearfix">
|
||||
@authorLink(post, "author".some)
|
||||
@momentFromNow(post.createdAt)
|
||||
|
||||
@if(post.hasEdits) {
|
||||
@momentFromNow(post.updatedAt)
|
||||
<span class="post-edited"> - Edited</span>
|
||||
} else {
|
||||
@momentFromNow(post.createdAt)
|
||||
}
|
||||
|
||||
<a class="anchor" href="@routes.ForumTopic.show(categ.slug, topic.slug, posts.currentPage)#@post.number">#@post.number</a>
|
||||
|
||||
@if(isGranted(_.IpBan)) {
|
||||
<span class="mod postip">@post.ip</span>
|
||||
}
|
||||
|
@ -46,6 +55,11 @@ description = shorten(posts.currentPageResults.headOption.??(_.text).replace("\n
|
|||
}
|
||||
</div>
|
||||
<p class="message">@autoLink(post.text)</p>
|
||||
@if(ctx.userId.fold(false)(post.canBeEditedBy(_)) && post.canStillBeEdited) {
|
||||
<div class="edit-buttons">
|
||||
<a class="mod" id="edit-button-@post.number" data-icon="r" onclick="editForumPost('@post.id', @post.number)">Edit</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -362,6 +362,7 @@ POST /forum/:categSlug/:slug/close controllers.ForumTopic.close(categSlug: S
|
|||
POST /forum/:categSlug/:slug/hide controllers.ForumTopic.hide(categSlug: String, slug: String)
|
||||
POST /forum/:categSlug/:slug/new controllers.ForumPost.create(categSlug: String, slug: String, page: Int ?= 1)
|
||||
POST /forum/:categSlug/delete/:id controllers.ForumPost.delete(categSlug: String, id: String)
|
||||
POST /forum/post/:id controllers.ForumPost.edit(id: String)
|
||||
GET /forum/redirect/post/:id controllers.ForumPost.redirect(id: String)
|
||||
|
||||
# Message
|
||||
|
|
|
@ -7,7 +7,10 @@ import reactivemongo.bson._
|
|||
private object BSONHandlers {
|
||||
|
||||
implicit val CategBSONHandler = Macros.handler[Categ]
|
||||
|
||||
implicit val PostEditBSONHandler = Macros.handler[OldVersion]
|
||||
implicit val PostBSONHandler = Macros.handler[Post]
|
||||
|
||||
private val topicHandler: BSONDocumentReader[Topic] with BSONDocumentWriter[Topic] with BSONHandler[Bdoc, Topic] = Macros.handler[Topic]
|
||||
implicit val TopicBSONHandler: BSONDocumentReader[Topic] with BSONDocumentWriter[Topic] with BSONHandler[Bdoc, Topic] = LoggingHandler(logger)(topicHandler)
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ private[forum] final class DataForm(val captcher: akka.actor.ActorSelection) ext
|
|||
|
||||
val post = Form(postMapping)
|
||||
|
||||
val postEdit = Form(mapping("changes" -> text(minLength=3))(PostEdit.apply)(PostEdit.unapply))
|
||||
|
||||
def postWithCaptcha = withCaptcha(post)
|
||||
|
||||
val topic = Form(mapping(
|
||||
|
@ -36,4 +38,6 @@ object DataForm {
|
|||
case class TopicData(
|
||||
name: String,
|
||||
post: PostData)
|
||||
|
||||
case class PostEdit(changes: String)
|
||||
}
|
||||
|
|
|
@ -2,8 +2,10 @@ package lila.forum
|
|||
|
||||
import org.joda.time.DateTime
|
||||
import ornicar.scalalib.Random
|
||||
|
||||
import lila.user.User
|
||||
import scala.concurrent.duration._
|
||||
|
||||
case class OldVersion(text: String, createdAt: DateTime)
|
||||
|
||||
case class Post(
|
||||
_id: String,
|
||||
|
@ -17,7 +19,11 @@ case class Post(
|
|||
troll: Boolean,
|
||||
hidden: Boolean,
|
||||
lang: Option[String],
|
||||
createdAt: DateTime) {
|
||||
editHistory: List[OldVersion],
|
||||
createdAt: DateTime,
|
||||
updatedAt: DateTime) {
|
||||
|
||||
private val permitEditsFor = 3 hours
|
||||
|
||||
def id = _id
|
||||
|
||||
|
@ -28,6 +34,21 @@ case class Post(
|
|||
def isTeam = categId startsWith teamSlug("")
|
||||
|
||||
def isStaff = categId == "staff"
|
||||
|
||||
def canStillBeEdited() = {
|
||||
updatedAt.plus(permitEditsFor.toMillis).isAfter(DateTime.now)
|
||||
}
|
||||
|
||||
def canBeEditedBy(editingId: String) : Boolean = userId.fold(false)(editingId == _)
|
||||
|
||||
def editPost(updated: DateTime, newText: String) : Post = {
|
||||
val oldVersion = new OldVersion(text, updatedAt)
|
||||
val history = oldVersion :: editHistory
|
||||
|
||||
this.copy(editHistory = history, text = newText, updatedAt = updated)
|
||||
}
|
||||
|
||||
def hasEdits = editHistory.nonEmpty
|
||||
}
|
||||
|
||||
object Post {
|
||||
|
@ -44,17 +65,24 @@ object Post {
|
|||
number: Int,
|
||||
lang: Option[String],
|
||||
troll: Boolean,
|
||||
hidden: Boolean): Post = Post(
|
||||
_id = Random nextStringUppercase idSize,
|
||||
topicId = topicId,
|
||||
author = author,
|
||||
userId = userId,
|
||||
ip = ip,
|
||||
text = text,
|
||||
number = number,
|
||||
lang = lang,
|
||||
troll = troll,
|
||||
hidden = hidden,
|
||||
createdAt = DateTime.now,
|
||||
categId = categId)
|
||||
hidden: Boolean): Post = {
|
||||
|
||||
val now = DateTime.now
|
||||
|
||||
Post(
|
||||
_id = Random nextStringUppercase idSize,
|
||||
topicId = topicId,
|
||||
author = author,
|
||||
userId = userId,
|
||||
ip = ip,
|
||||
text = text,
|
||||
number = number,
|
||||
lang = lang,
|
||||
editHistory = List.empty,
|
||||
troll = troll,
|
||||
hidden = hidden,
|
||||
createdAt = now,
|
||||
updatedAt = now,
|
||||
categId = categId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,14 +3,13 @@ package lila.forum
|
|||
import actorApi._
|
||||
import akka.actor.ActorSelection
|
||||
import org.joda.time.DateTime
|
||||
|
||||
import lila.common.paginator._
|
||||
import lila.db.dsl._
|
||||
import lila.db.paginator._
|
||||
import lila.hub.actorApi.timeline.{ Propagate, ForumPost }
|
||||
import lila.hub.actorApi.timeline.{ForumPost, Propagate}
|
||||
import lila.mod.ModlogApi
|
||||
import lila.security.{ Granter => MasterGranter }
|
||||
import lila.user.{ User, UserContext }
|
||||
import lila.security.{Granter => MasterGranter}
|
||||
import lila.user.{User, UserContext}
|
||||
|
||||
final class PostApi(
|
||||
env: Env,
|
||||
|
@ -69,6 +68,25 @@ final class PostApi(
|
|||
}
|
||||
}
|
||||
|
||||
def editPost(postId: String, newText: String, user: User) : Fu[Post] = {
|
||||
|
||||
get(postId) flatMap { post =>
|
||||
val now = DateTime.now
|
||||
|
||||
post match {
|
||||
case Some((_, post)) if !post.canBeEditedBy(user.id) =>
|
||||
fufail("You are not authorized to modify this post.")
|
||||
case Some((_, post)) if !post.canStillBeEdited() =>
|
||||
fufail("Post can no longer be edited")
|
||||
case Some((_,post)) =>
|
||||
val spamEscapedTest = lila.security.Spam.replace(newText)
|
||||
val newPost = post.editPost(now, spamEscapedTest)
|
||||
env.postColl.update($id(post.id), newPost) inject newPost
|
||||
case None => fufail("Post no longer exists.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val quickHideCategs = Set("lichess-feedback", "off-topic-discussion")
|
||||
|
||||
private def shouldHideOnPost(topic: Topic) =
|
||||
|
|
46
public/javascripts/forum-post.js
Normal file
46
public/javascripts/forum-post.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
|
||||
// Map of old post contents by post number in case the user clicks 'edit' on multiple posts
|
||||
// on the same page (although rare.)
|
||||
var oldContents = {};
|
||||
|
||||
var editForumPost = function(postId, postNumber) {
|
||||
var postSelector = $("#" + postNumber);
|
||||
|
||||
// We grab the text element of the post and turn it into a textarea to make it editable
|
||||
oldContents[postNumber] = postSelector.find(".message");
|
||||
|
||||
var old = oldContents[postNumber];
|
||||
|
||||
var postContents = old.text();
|
||||
|
||||
var editForm = $('<form>', {
|
||||
id: 'post-edit-form-' + postNumber,
|
||||
method: 'POST',
|
||||
action:"/forum/post/" + postId
|
||||
});
|
||||
|
||||
var formTextArea = $('<textarea>', {
|
||||
id:'post-edit-area-' + postNumber,
|
||||
name:"changes",
|
||||
class:'edit-post-box',
|
||||
'minlength': 3
|
||||
});
|
||||
formTextArea.text(postContents);
|
||||
|
||||
var formSubmitButton = $('<input>', {type:'submit', value: 'Submit'});
|
||||
var formCancelButton = $('<a>', {'data-icon':'s', onclick:'cancelEdit(' + postNumber + ')' }).text("Cancel");
|
||||
|
||||
editForm.append(formTextArea);
|
||||
editForm.append(formSubmitButton);
|
||||
editForm.append(formCancelButton);
|
||||
|
||||
old.replaceWith(editForm);
|
||||
|
||||
$("#edit-button-" + postNumber).hide();
|
||||
};
|
||||
|
||||
var cancelEdit = function(postNumber) {
|
||||
$('#post-edit-form-' + postNumber).replaceWith(oldContents[postNumber]);
|
||||
|
||||
$("#edit-button-" + postNumber).show();
|
||||
}
|
|
@ -183,6 +183,28 @@ div.post .message {
|
|||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
div.post .edit-buttons {
|
||||
float: right;
|
||||
}
|
||||
|
||||
div.post .edit-post-box {
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
div.post a.post-edited {
|
||||
font-size: 0.8em;
|
||||
font-style: bold;
|
||||
color: #aaa;
|
||||
margin-top: 0.3em;
|
||||
margin-left: 1em;
|
||||
float: left;
|
||||
}
|
||||
|
||||
div.post .button-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.topicReply,
|
||||
div.category .description {
|
||||
margin: 1em 24px 0 24px;
|
||||
|
|
Loading…
Reference in a new issue