coach review modding/update workflow - closes #3494

pull/3613/head
Thibault Duplessis 2017-09-17 17:32:28 -05:00
parent 7e52b7dda0
commit a1c90b7f76
12 changed files with 66 additions and 19 deletions

View File

@ -27,9 +27,9 @@ object Coach extends LilaController {
c.coach.profile.studyIds.map(_.value).map(lila.study.Study.Id.apply)
} flatMap Env.study.pager.withChaptersAndLiking(ctx.me) flatMap { studies =>
api.reviews.approvedByCoach(c.coach) flatMap { reviews =>
ctx.me.?? { api.reviews.isPending(_, c.coach) } map { isPending =>
ctx.me.?? { api.reviews.mine(_, c.coach) } map { myReview =>
lila.mon.coach.pageView.profile(c.coach.id.value)()
Ok(html.coach.show(c, reviews, studies, reviewApproval = isPending))
Ok(html.coach.show(c, reviews, studies, myReview))
}
}
}
@ -68,6 +68,13 @@ object Coach extends LilaController {
}
}
def modReview(id: String) = SecureBody(_.DisapproveCoachReview) { implicit ctx => me =>
OptionFuResult(api.reviews byId id) { review =>
Env.mod.logApi.coachReview(me.id, review.coachId.value, review.userId) >>
api.reviews.mod(review) inject Redirect(routes.Coach.show(review.coachId.value))
}
}
private def WithVisibleCoach(c: CoachModel.WithUser)(f: Fu[Result])(implicit ctx: Context) =
if (c.coach.isListed || ctx.me.??(c.coach.is) || isGranted(_.Admin)) f
else notFound

View File

@ -105,9 +105,21 @@ side = side.some) {
@barRating(selected = r.score.some, enabled = false)
@momentFromNow(r.updatedAt)
</div>
<div class="content">@autoLink(r.text)</div>
<div class="content">
@if(r.moddedAt.isDefined) {
<div class="modded">
Moderators have disapproved this review. Please only accept reviews from
actual students, based on actual lessons. Reviews must be about your coaching services.
<br />
You may delete this review, or ask the author to rephrase it, then approve it.
</div>
}
@autoLink(r.text)
</div>
<div class="actions">
@if(r.moddedAt.fold(true)(_.isBefore(r.updatedAt))) {
<a data-value="1" class="yes" data-icon="E"></a>
}
<a data-value="0" class="no" data-icon="L"></a>
</div>
</div>

View File

@ -1,7 +1,7 @@
@(c: lila.coach.Coach.WithUser, approval: Boolean)(implicit ctx: Context)
@(c: lila.coach.Coach.WithUser, mine: Option[lila.coach.CoachReview])(implicit ctx: Context)
<div class="review-form">
@if(approval) {
@if(mine.exists(_.pendingApproval)) {
<div class="approval">
<p>Thank you for the review!</p>
<p>@c.user.realNameOrUsername will approve it very soon.</p>
@ -15,10 +15,10 @@
}
<form action="@routes.Coach.review(c.user.username)" method="POST">
@barRating(selected = none, enabled = true)
@barRating(selected = mine.map(_.score), enabled = true)
<textarea name="text"
required minlength=3 maxlength=1000
placeholder="What do you enjoy about taking lessons with @c.user.realNameOrUsername?"></textarea>
placeholder="What do you enjoy about taking lessons with @c.user.realNameOrUsername?">@mine.map(_.text)</textarea>
<button type="submit" class="button">@trans.apply()</button>
</form>
</div>

View File

@ -10,6 +10,11 @@
@barRating(selected = r.score.some, enabled = false)
</div>
<div class="content">@autoLink(r.text)</div>
@if(isGranted(_.DisapproveCoachReview)) {
<form method="post" action="@routes.Coach.modReview(r.id)">
<button class="thin button confirm" type="submit" title="Instructs the coach to reject the review, or to ask the author to rephrase it.">Disapprove</a>
<form/>
}
</div>
}
</div>

View File

@ -1,4 +1,4 @@
@(c: lila.coach.Coach.WithUser, coachReviews: lila.coach.CoachReview.Reviews, studies: Seq[lila.study.Study.WithChaptersAndLiked], reviewApproval: Boolean)(implicit ctx: Context)
@(c: lila.coach.Coach.WithUser, coachReviews: lila.coach.CoachReview.Reviews, studies: Seq[lila.study.Study.WithChaptersAndLiked], myReview: Option[lila.coach.CoachReview])(implicit ctx: Context)
@moreCss = {
@cssTag("coach.css")
@ -25,7 +25,9 @@ $('.review-form form').show();
<div class="profile">
<a class="blue" href="@routes.User.show(c.user.username)">View @c.user.username lichess profile</a>
</div>
@reviewForm(c, reviewApproval)
@if(ctx.me.exists(_.id != c.user.id)) {
@reviewForm(c, myReview)
}
@reviews(c, coachReviews)
</div>
}

View File

@ -419,6 +419,7 @@ GET /coach/picture/edit controllers.Coach.picture
POST /coach/picture/upload controllers.Coach.pictureApply
POST /coach/picture/delete controllers.Coach.pictureDelete
POST /coach/approve-review/:id controllers.Coach.approveReview(id: String)
POST /coach/mod-review/:id controllers.Coach.modReview(id: String)
GET /coach/:username controllers.Coach.show(username: String)
POST /coach/:username/review controllers.Coach.review(username: String)

View File

@ -121,16 +121,22 @@ final class CoachApi(
def byId(id: String) = reviewColl.byId[CoachReview](id)
def isPending(user: User, coach: Coach): Fu[Boolean] =
reviewColl.exists(
$id(CoachReview.makeId(user, coach)) ++ $doc("approved" -> false)
)
def mine(user: User, coach: Coach): Fu[Option[CoachReview]] =
reviewColl.byId[CoachReview](CoachReview.makeId(user, coach))
def approve(r: CoachReview, v: Boolean) = {
if (v) reviewColl.update($id(r.id), $set("approved" -> v)).void
if (v) reviewColl.update(
$id(r.id),
$set("approved" -> v) ++ $unset("moddedAt")
).void
else reviewColl.remove($id(r.id)).void
} >> refreshCoachNbReviews(r.coachId)
def mod(r: CoachReview) = reviewColl.update($id(r.id), $set(
"approved" -> false,
"moddedAt" -> DateTime.now
)) >> refreshCoachNbReviews(r.coachId)
private def refreshCoachNbReviews(id: Coach.Id): Funit =
reviewColl.countSel($doc("coachId" -> id.value, "approved" -> true)) flatMap {
setNbReviews(id, _)

View File

@ -12,19 +12,20 @@ case class CoachReview(
text: String,
approved: Boolean,
createdAt: DateTime,
updatedAt: DateTime
updatedAt: DateTime,
moddedAt: Option[DateTime] = None // a mod disapproved it
) {
def id = _id
def pendingApproval = !approved && moddedAt.isEmpty
}
object CoachReview {
def makeId(user: User, coach: Coach) = s"${user.id}:${coach.id.value}"
case class Score(value: Double) extends AnyVal {
}
case class Score(value: Double) extends AnyVal
case class Reviews(list: List[CoachReview]) {

View File

@ -47,6 +47,7 @@ case class Modlog(
case Modlog.reportban => "reportban"
case Modlog.unreportban => "un-reportban"
case Modlog.modMessage => "send message"
case Modlog.coachReview => "disapprove coach review"
case a => a
}
@ -97,4 +98,5 @@ object Modlog {
val reportban = "reportban"
val unreportban = "unreportban"
val modMessage = "modMessage"
val coachReview = "coachReview"
}

View File

@ -118,6 +118,10 @@ final class ModlogApi(coll: Coll) {
Modlog(mod, user.some, Modlog.modMessage, details = subject.some)
}
def coachReview(mod: String, coach: String, author: String) = add {
Modlog(mod, coach.some, Modlog.coachReview, details = s"by $author".some)
}
def recent = coll.find($empty).sort($sort naturalDesc).cursor[Modlog]().gather[List](100)
def wasUnengined(sus: Suspect) = coll.exists($doc(

View File

@ -46,6 +46,7 @@ object Permission {
case object ReportBan extends Permission("ROLE_REPORT_BAN")
case object ModMessage extends Permission("ROLE_MOD_MESSAGE")
case object Impersonate extends Permission("ROLE_IMPERSONATE")
case object DisapproveCoachReview extends Permission("ROLE_DISAPPROVE_COACH_REVIEW")
case object Hunter extends Permission("ROLE_HUNTER", List(
ViewBlurs, MarkEngine, MarkBooster, StaffForum,
@ -57,7 +58,7 @@ object Permission {
Hunter, ModerateForum, IpBan, CloseAccount, ReopenAccount, ViewPrivateComms,
ChatTimeout, MarkTroll, SetTitle, SetEmail, ModerateQa, StreamConfig,
MessageAnyone, CloseTeam, TerminateTournament, ManageTournament, ManageEvent,
PreviewCoach, PracticeConfig, RemoveRanking, ReportBan, Beta
PreviewCoach, PracticeConfig, RemoveRanking, ReportBan, Beta, DisapproveCoachReview
))
case object SuperAdmin extends Permission("ROLE_SUPER_ADMIN", List(

View File

@ -136,6 +136,12 @@ body.dark .coach_picture .cancel {
.coach_edit .reviews .user > * {
display: block;
}
.coach_edit .reviews .modded {
background: #dc322f;
color: #fff;
padding: 8px 10px;
margin-bottom: 1em;
}
.coach_edit .reviews .content {
margin: 0 20px;
box-sizing: border-box;