coach review modding/update workflow - closes #3494
parent
7e52b7dda0
commit
a1c90b7f76
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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, _)
|
||||
|
|
|
@ -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]) {
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue