streamer approval process WIP
parent
63e0bf972d
commit
62185c316c
|
@ -52,11 +52,15 @@ object Streamer extends LilaController {
|
|||
implicit val req = ctx.body
|
||||
StreamerForm.userForm(s.streamer).bindFromRequest.fold(
|
||||
error => BadRequest(html.streamer.edit(s, error)).fuccess,
|
||||
data => api.update(s.streamer, data) inject Redirect(routes.Streamer.edit())
|
||||
data => api.update(s.streamer, data, isGranted(_.Streamers)) inject Redirect(routes.Streamer.edit())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def approvalRequest = AuthBody { implicit ctx => me =>
|
||||
api.approval.request(me) inject Redirect(routes.Streamer.edit)
|
||||
}
|
||||
|
||||
def picture = Auth { implicit ctx => _ =>
|
||||
AsStreamer { s =>
|
||||
NoCache(Ok(html.streamer.picture(s))).fuccess
|
||||
|
@ -81,7 +85,7 @@ object Streamer extends LilaController {
|
|||
}
|
||||
|
||||
private def AsStreamer(f: StreamerModel.WithUser => Fu[Result])(implicit ctx: Context) =
|
||||
ctx.me ?? api.find flatMap { _.fold(notFound)(f) }
|
||||
get("u").ifTrue(isGranted(_.Streamers)).orElse(ctx.userId) ?? api.find flatMap { _.fold(notFound)(f) }
|
||||
|
||||
private def WithVisibleStreamer(s: StreamerModel.WithUser)(f: Fu[Result])(implicit ctx: Context) =
|
||||
if (s.streamer.isListed || ctx.me.??(s.streamer.is) || isGranted(_.Admin)) f
|
||||
|
|
|
@ -30,12 +30,12 @@ side = side.some) {
|
|||
<div class="top">
|
||||
<div class="picture_wrap">
|
||||
@if(s.streamer.hasPicture) {
|
||||
<a class="upload_picture" href="@routes.Streamer.picture" title="Change/delete your profile picture">
|
||||
<a class="upload_picture" href="@routes.Streamer.picture" title="Change/delete your streamer picture">
|
||||
@pic(s, 250)
|
||||
</a>
|
||||
} else {
|
||||
<div class="upload_picture">
|
||||
<a class="button" href="@routes.Streamer.picture">Upload a profile picture</a>
|
||||
<a class="button" href="@routes.Streamer.picture">Upload a streamer picture</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
@ -44,21 +44,55 @@ side = side.some) {
|
|||
@rules()
|
||||
</div>
|
||||
</div>
|
||||
@defining(s.streamer.approval.granted) { granted =>
|
||||
<div class="status is@if(granted){-green}" data-icon="@if(granted){E}else{}">
|
||||
@if(granted) {
|
||||
Your stream is approved and listed on <a href="@routes.Streamer.index()">lichess streamers list</a>.
|
||||
} else {
|
||||
@if(s.streamer.approval.requested) {
|
||||
Your stream is being reviewed by moderators, and will soon be listed on <a href="@routes.Streamer.index()">lichess streamers list</a>.
|
||||
} else {
|
||||
@if(s.streamer.completeEnough) {
|
||||
When you are ready to be listed on <a href="@routes.Streamer.index()">lichess streamers list</a>,
|
||||
<form method="post" action="@routes.Streamer.approvalRequest">
|
||||
<button type="submmit" class="button">request a moderator review</button>
|
||||
</form>
|
||||
} else {
|
||||
Please fill in your streamer information below.
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<form class="content_box_content material form" action="@routes.Streamer.edit" method="POST">
|
||||
@errMsgMaterial(form.errors)
|
||||
@base.form.group(form("twitch"), Html("Your Twitch username or URL"), help = Html("Optional. Leave empty if none").some) {
|
||||
@base.form.input(form("twitch"))
|
||||
@if(isGranted(_.Streamers)) {
|
||||
@base.form.group(form("mod.approved"), Html("Publish on the streamers list"), help = Html("Moderation").some, half = true) {
|
||||
@base.form.select(form("mod.approved"), booleanChoices)
|
||||
}
|
||||
@base.form.group(form("youTube"), Html("Your YouTube channel ID or URL"), help = Html("Optional. Leave empty if none").some) {
|
||||
@base.form.input(form("youTube"))
|
||||
@if(granted) {
|
||||
@base.form.group(form("mod.featured"), Html("Feature on lichess homepage"), help = Html("Moderation").some, half = true) {
|
||||
@base.form.select(form("mod.featured"), booleanChoices)
|
||||
}
|
||||
} else {
|
||||
@base.form.group(form("mod.ignored"), Html("Ignore further approval requests"), help = Html("Moderation").some, half = true) {
|
||||
@base.form.select(form("mod.ignored"), booleanChoices)
|
||||
}
|
||||
}
|
||||
}
|
||||
@base.form.group(form("twitch"), Html("Your Twitch username or URL"), help = Html("Optional. Leave empty if none").some) {
|
||||
@base.form.input(form("twitch"))
|
||||
}
|
||||
@base.form.group(form("youTube"), Html("Your YouTube channel ID or URL"), help = Html("Optional. Leave empty if none. Example: youtube.com/channel/UCr6RfQga70yMM9-nuzAYTsA").some) {
|
||||
@base.form.input(form("youTube"))
|
||||
}
|
||||
@base.form.group(form("name"), Html("Your streamer name"), help = Html("Keep it short: 20 characters max").some) {
|
||||
@base.form.input(form("name"), maxLength = 20)
|
||||
@base.form.input(form("name"), maxLength = 20)
|
||||
}
|
||||
@base.form.group(form("description"), Html("Short description"), help = Html("Who are you, why do you love streaming?").some) {
|
||||
<textarea name="description" id="description" maxlength="300">@form("description").value</textarea>
|
||||
<textarea name="description" id="description" maxlength="300">@form("description").value</textarea>
|
||||
}
|
||||
@base.form.submit()
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
<div class="streamer-side">
|
||||
<div class="profile">
|
||||
<a class="blue" href="@routes.User.show(s.user.username)">View @s.user.username lichess profile</a>
|
||||
@if(ctx.is(s.user)) {
|
||||
<br /><a class="blue" href="@routes.Streamer.edit()">Edit streamer page</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -196,6 +196,7 @@ GET /streamer/new controllers.Streamer.create
|
|||
POST /streamer/new controllers.Streamer.createApply
|
||||
GET /streamer/edit controllers.Streamer.edit
|
||||
POST /streamer/edit controllers.Streamer.editApply
|
||||
POST /streamer/approval/request controllers.Streamer.approvalRequest
|
||||
GET /streamer/picture/edit controllers.Streamer.picture
|
||||
POST /streamer/picture/upload controllers.Streamer.pictureApply
|
||||
POST /streamer/picture/delete controllers.Streamer.pictureDelete
|
||||
|
|
|
@ -9,7 +9,20 @@ object CoachProfileForm {
|
|||
def edit(coach: Coach) = Form(mapping(
|
||||
"listed" -> boolean,
|
||||
"available" -> boolean,
|
||||
"profile" -> profileMapping
|
||||
"profile" -> mapping(
|
||||
"headline" -> optional(nonEmptyText(minLength = 5, maxLength = 170)),
|
||||
"languages" -> optional(nonEmptyText(minLength = 3, maxLength = 140)),
|
||||
"hourlyRate" -> optional(nonEmptyText(minLength = 3, maxLength = 140)),
|
||||
"description" -> optional(richText),
|
||||
"playingExperience" -> optional(richText),
|
||||
"teachingExperience" -> optional(richText),
|
||||
"otherExperience" -> optional(richText),
|
||||
"skills" -> optional(richText),
|
||||
"methodology" -> optional(richText),
|
||||
"youtubeVideos" -> optional(nonEmptyText),
|
||||
"youtubeChannel" -> optional(nonEmptyText),
|
||||
"publicStudies" -> optional(nonEmptyText)
|
||||
)(CoachProfile.apply)(CoachProfile.unapply)
|
||||
)(Data.apply)(Data.unapply)) fill Data(
|
||||
listed = coach.listed.value,
|
||||
available = coach.available.value,
|
||||
|
@ -30,21 +43,6 @@ object CoachProfileForm {
|
|||
)
|
||||
}
|
||||
|
||||
private def profileMapping = mapping(
|
||||
"headline" -> optional(nonEmptyText(minLength = 5, maxLength = 170)),
|
||||
"languages" -> optional(nonEmptyText(minLength = 3, maxLength = 140)),
|
||||
"hourlyRate" -> optional(nonEmptyText(minLength = 3, maxLength = 140)),
|
||||
"description" -> optional(richText),
|
||||
"playingExperience" -> optional(richText),
|
||||
"teachingExperience" -> optional(richText),
|
||||
"otherExperience" -> optional(richText),
|
||||
"skills" -> optional(richText),
|
||||
"methodology" -> optional(richText),
|
||||
"youtubeVideos" -> optional(nonEmptyText),
|
||||
"youtubeChannel" -> optional(nonEmptyText),
|
||||
"publicStudies" -> optional(nonEmptyText)
|
||||
)(CoachProfile.apply)(CoachProfile.unapply)
|
||||
|
||||
import CoachProfile.RichText
|
||||
|
||||
private implicit val richTextFormat = lila.common.Form.formatter.stringFormatter[RichText](_.value, RichText.apply)
|
||||
|
|
|
@ -51,6 +51,7 @@ object Permission {
|
|||
case object Relay extends Permission("ROLE_RELAY")
|
||||
case object Cli extends Permission("ROLE_ClI")
|
||||
case object Settings extends Permission("ROLE_SETTINGS")
|
||||
case object Streamers extends Permission("ROLE_STREAMERS")
|
||||
|
||||
case object Hunter extends Permission("ROLE_HUNTER", List(
|
||||
ViewBlurs, MarkEngine, MarkBooster, StaffForum,
|
||||
|
@ -63,7 +64,7 @@ object Permission {
|
|||
ChatTimeout, MarkTroll, SetTitle, SetEmail, ModerateQa, StreamConfig,
|
||||
MessageAnyone, CloseTeam, TerminateTournament, ManageTournament, ManageEvent,
|
||||
PreviewCoach, PracticeConfig, RemoveRanking, ReportBan, Beta, DisapproveCoachReview,
|
||||
Relay
|
||||
Relay, Streamers
|
||||
))
|
||||
|
||||
case object SuperAdmin extends Permission("ROLE_SUPER_ADMIN", List(
|
||||
|
@ -75,7 +76,7 @@ object Permission {
|
|||
UserSpy, MarkEngine, MarkBooster, IpBan, ModerateQa, StreamConfig, PracticeConfig,
|
||||
Beta, MessageAnyone, UserSearch, CloseTeam, TerminateTournament, ManageTournament, ManageEvent,
|
||||
PublicMod, Developer, Coach, PreviewCoach, ModNote, RemoveRanking, ReportBan,
|
||||
Relay, Cli, Settings
|
||||
Relay, Cli, Settings, Streamers
|
||||
)
|
||||
|
||||
lazy private val all: List[Permission] = SuperAdmin :: allButSuperAdmin
|
||||
|
|
|
@ -7,17 +7,16 @@ private object BsonHandlers {
|
|||
|
||||
implicit val StreamerIdBSONHandler = stringAnyValHandler[Streamer.Id](_.value, Streamer.Id.apply)
|
||||
implicit val StreamerListedBSONHandler = booleanAnyValHandler[Streamer.Listed](_.value, Streamer.Listed.apply)
|
||||
implicit val StreamerApprovedBSONHandler = booleanAnyValHandler[Streamer.Approved](_.value, Streamer.Approved.apply)
|
||||
implicit val StreamerAutoFeaturedBSONHandler = booleanAnyValHandler[Streamer.AutoFeatured](_.value, Streamer.AutoFeatured.apply)
|
||||
implicit val StreamerChatEnabledBSONHandler = booleanAnyValHandler[Streamer.ChatEnabled](_.value, Streamer.ChatEnabled.apply)
|
||||
implicit val StreamerPicturePathBSONHandler = stringAnyValHandler[Streamer.PicturePath](_.value, Streamer.PicturePath.apply)
|
||||
implicit val StreamerNameBSONHandler = stringAnyValHandler[Streamer.Name](_.value, Streamer.Name.apply)
|
||||
implicit val StreamerDescriptionBSONHandler = stringAnyValHandler[Streamer.Description](_.value, Streamer.Description.apply)
|
||||
|
||||
import Streamer.{ Live, Twitch, YouTube, Sorting }
|
||||
import Streamer.{ Live, Twitch, YouTube, Sorting, Approval }
|
||||
implicit val StreamerLiveBSONHandler = Macros.handler[Live]
|
||||
implicit val StreamerTwitchBSONHandler = Macros.handler[Twitch]
|
||||
implicit val StreamerYouTubeBSONHandler = Macros.handler[YouTube]
|
||||
implicit val StreamerSortingBSONHandler = Macros.handler[Sorting]
|
||||
implicit val StreamerApprovalBSONHandler = Macros.handler[Approval]
|
||||
implicit val StreamerBSONHandler = Macros.handler[Streamer]
|
||||
}
|
||||
|
|
|
@ -20,8 +20,12 @@ private final class Importer(api: StreamerApi, flagColl: Coll) {
|
|||
api.save(Streamer(
|
||||
_id = Id(s.lichessName.toLowerCase),
|
||||
listed = Listed(true),
|
||||
approved = Approved(true),
|
||||
autoFeatured = AutoFeatured(s.featured),
|
||||
approval = Approval(
|
||||
requested = false,
|
||||
granted = true,
|
||||
ignored = false,
|
||||
autoFeatured = s.featured
|
||||
),
|
||||
chatEnabled = ChatEnabled(s.chat),
|
||||
picturePath = none,
|
||||
name = Name {
|
||||
|
|
|
@ -7,8 +7,7 @@ import lila.user.User
|
|||
case class Streamer(
|
||||
_id: Streamer.Id, // user ID
|
||||
listed: Streamer.Listed, // user wants to be in the list
|
||||
approved: Streamer.Approved, // mods agree about being in the list
|
||||
autoFeatured: Streamer.AutoFeatured, // on homepage when title contains "lichess.org"
|
||||
approval: Streamer.Approval,
|
||||
chatEnabled: Streamer.ChatEnabled, // embed chat inside lichess
|
||||
picturePath: Option[Streamer.PicturePath],
|
||||
name: Streamer.Name,
|
||||
|
@ -26,7 +25,7 @@ case class Streamer(
|
|||
|
||||
def hasPicture = picturePath.isDefined
|
||||
|
||||
def isListed = listed.value && approved.value
|
||||
def isListed = listed.value && approval.granted
|
||||
|
||||
def seenAt: Option[DateTime] = sorting.seenAt
|
||||
def liveAt: Option[DateTime] = (twitch.flatMap(_.live.liveAt), youTube.flatMap(_.live.liveAt)) match {
|
||||
|
@ -35,6 +34,10 @@ case class Streamer(
|
|||
}
|
||||
case (twitch, youTube) => twitch orElse youTube
|
||||
}
|
||||
|
||||
def completeEnough = {
|
||||
twitch.isDefined || youTube.isDefined
|
||||
} && description.isDefined && hasPicture
|
||||
}
|
||||
|
||||
object Streamer {
|
||||
|
@ -42,8 +45,12 @@ object Streamer {
|
|||
def make(user: User) = Streamer(
|
||||
_id = Id(user.id),
|
||||
listed = Listed(true),
|
||||
approved = Approved(false),
|
||||
autoFeatured = AutoFeatured(false),
|
||||
approval = Approval(
|
||||
requested = false,
|
||||
granted = false,
|
||||
ignored = false,
|
||||
autoFeatured = false
|
||||
),
|
||||
chatEnabled = ChatEnabled(true),
|
||||
picturePath = none,
|
||||
name = Name(user.realNameOrUsername),
|
||||
|
@ -57,8 +64,12 @@ object Streamer {
|
|||
|
||||
case class Id(value: User.ID) extends AnyVal with StringValue
|
||||
case class Listed(value: Boolean) extends AnyVal
|
||||
case class Approved(value: Boolean) extends AnyVal
|
||||
case class AutoFeatured(value: Boolean) extends AnyVal
|
||||
case class Approval(
|
||||
requested: Boolean, // user requests a mod to approve
|
||||
granted: Boolean, // a mod approved
|
||||
ignored: Boolean, // further requests are ignored
|
||||
autoFeatured: Boolean // on homepage when title contains "lichess.org"
|
||||
)
|
||||
case class ChatEnabled(value: Boolean) extends AnyVal
|
||||
case class PicturePath(value: String) extends AnyVal with StringValue
|
||||
case class Name(value: String) extends AnyVal with StringValue
|
||||
|
|
|
@ -34,13 +34,13 @@ final class StreamerApi(
|
|||
coll.update($id(s.id), s, upsert = true).void
|
||||
|
||||
def setSeenAt(user: User): Funit =
|
||||
listedIdsCache.get.pp(user.username) flatMap { ids =>
|
||||
listedIdsCache.get flatMap { ids =>
|
||||
ids.contains(Streamer.Id(user.id)) ??
|
||||
coll.update($id(user.id), $set("sorting.seenAt" -> DateTime.now)).void
|
||||
}
|
||||
|
||||
def update(s: Streamer, data: StreamerForm.UserData): Funit =
|
||||
coll.update($id(s.id), data(s)).void
|
||||
def update(s: Streamer, data: StreamerForm.UserData, asMod: Boolean): Funit =
|
||||
coll.update($id(s.id), data(s, asMod)).void
|
||||
|
||||
def create(u: User): Funit =
|
||||
isStreamer(u) flatMap { exists =>
|
||||
|
@ -57,11 +57,20 @@ final class StreamerApi(
|
|||
def deletePicture(s: Streamer): Funit =
|
||||
coll.update($id(s.id), $unset("picturePath")).void
|
||||
|
||||
object approval {
|
||||
|
||||
def request(user: User) = find(user) flatMap {
|
||||
_.filter(!_.streamer.approval.granted) ?? { s =>
|
||||
coll.updateField($id(s.streamer.id), "approval.requested", true).void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def withUser(user: User)(streamer: Streamer) = Streamer.WithUser(streamer, user)
|
||||
|
||||
private def selectListedApproved = $doc(
|
||||
"listed" -> true,
|
||||
"approved" -> true
|
||||
"approval.granted" -> true
|
||||
)
|
||||
|
||||
private val listedIdsCache = asyncCache.single[Set[Streamer.Id]](
|
||||
|
|
|
@ -12,36 +12,67 @@ object StreamerForm {
|
|||
"name" -> name,
|
||||
"description" -> optional(description),
|
||||
"twitch" -> optional(nonEmptyText.verifying("Invalid Twitch username", s => Streamer.Twitch.parseUserId(s).isDefined)),
|
||||
"youTube" -> optional(nonEmptyText.verifying("Invalid YouTube channel", s => Streamer.YouTube.parseChannelId(s).isDefined))
|
||||
"youTube" -> optional(nonEmptyText.verifying("Invalid YouTube channel", s => Streamer.YouTube.parseChannelId(s).isDefined)),
|
||||
"mod" -> optional(mapping(
|
||||
"approved" -> boolean,
|
||||
"ignored" -> boolean,
|
||||
"featured" -> boolean
|
||||
)(ModData.apply)(ModData.unapply))
|
||||
)(UserData.apply)(UserData.unapply))
|
||||
|
||||
def userForm(streamer: Streamer) = emptyUserForm fill UserData(
|
||||
name = streamer.name,
|
||||
description = streamer.description,
|
||||
twitch = streamer.twitch.map(_.userId),
|
||||
youTube = streamer.youTube.map(_.channelId)
|
||||
youTube = streamer.youTube.map(_.channelId),
|
||||
mod = ModData(
|
||||
approved = streamer.approval.granted,
|
||||
ignored = streamer.approval.ignored,
|
||||
featured = streamer.approval.autoFeatured
|
||||
).some
|
||||
)
|
||||
|
||||
case class UserData(
|
||||
name: Name,
|
||||
description: Option[Description],
|
||||
twitch: Option[String],
|
||||
youTube: Option[String]
|
||||
youTube: Option[String],
|
||||
mod: Option[ModData]
|
||||
) {
|
||||
|
||||
def apply(streamer: Streamer) = streamer.copy(
|
||||
name = name,
|
||||
description = description,
|
||||
twitch = twitch.flatMap(Twitch.parseUserId).fold(streamer.twitch) { userId =>
|
||||
streamer.twitch.fold(Twitch(userId, Live.empty))(_.copy(userId = userId)).some
|
||||
},
|
||||
youTube = youTube.flatMap(YouTube.parseChannelId).fold(streamer.youTube) { channelId =>
|
||||
streamer.youTube.fold(YouTube(channelId, Live.empty))(_.copy(channelId = channelId)).some
|
||||
},
|
||||
updatedAt = DateTime.now
|
||||
)
|
||||
def apply(streamer: Streamer, asMod: Boolean) = {
|
||||
val newStreamer = streamer.copy(
|
||||
name = name,
|
||||
description = description,
|
||||
twitch = twitch.flatMap(Twitch.parseUserId).fold(streamer.twitch) { userId =>
|
||||
streamer.twitch.fold(Twitch(userId, Live.empty))(_.copy(userId = userId)).some
|
||||
},
|
||||
youTube = youTube.flatMap(YouTube.parseChannelId).fold(streamer.youTube) { channelId =>
|
||||
streamer.youTube.fold(YouTube(channelId, Live.empty))(_.copy(channelId = channelId)).some
|
||||
},
|
||||
updatedAt = DateTime.now
|
||||
)
|
||||
newStreamer.copy(
|
||||
approval = mod match {
|
||||
case Some(m) if asMod => streamer.approval.copy(
|
||||
granted = m.approved,
|
||||
ignored = m.ignored && !m.approved,
|
||||
autoFeatured = m.featured && m.approved
|
||||
)
|
||||
case None if streamer.twitch != newStreamer.twitch || streamer.youTube != newStreamer.youTube =>
|
||||
streamer.approval.copy(granted = false, autoFeatured = false)
|
||||
case None => streamer.approval
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
case class ModData(
|
||||
approved: Boolean,
|
||||
ignored: Boolean,
|
||||
featured: Boolean
|
||||
)
|
||||
|
||||
private implicit val descriptionFormat = lila.common.Form.formatter.stringFormatter[Description](_.value, Description.apply)
|
||||
private def description = of[Description]
|
||||
private implicit val nameFormat = lila.common.Form.formatter.stringFormatter[Name](_.value, Name.apply)
|
||||
|
|
|
@ -51,14 +51,18 @@ body.dark .streamer_edit .top a {
|
|||
}
|
||||
|
||||
.streamer_edit .status {
|
||||
opacity: 0;
|
||||
transition: 0.5s;
|
||||
text-align: center;
|
||||
color: #759900;
|
||||
margin: 30px 20px 20px 20px;
|
||||
padding: 20px;
|
||||
background: rgba(128,128,128,0.2);
|
||||
}
|
||||
.streamer_edit .status.saved {
|
||||
opacity: 1;
|
||||
.streamer_edit .status::before {
|
||||
font-size: 2em;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
.streamer_edit .status form {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#site_header a.preview {
|
||||
display: block;
|
||||
margin-top: 40px;
|
||||
|
|
Loading…
Reference in New Issue