From 1257ef10fc8edfc9db3e66b033e34e72f9987bd3 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Tue, 7 Sep 2021 14:16:07 +0200 Subject: [PATCH] more ublog tier/rank system --- app/controllers/Ublog.scala | 29 ++++++++++-- app/views/ublog/blog.scala | 56 ++++++++++++++++++++++ app/views/ublog/form.scala | 4 +- app/views/ublog/index.scala | 47 +------------------ app/views/ublog/post.scala | 19 ++++++-- bin/mongodb/ublog-blog.js | 2 + conf/routes | 1 + modules/mod/src/main/ModActivity.scala | 2 + modules/mod/src/main/Modlog.scala | 2 + modules/mod/src/main/ModlogApi.scala | 4 ++ modules/round/src/main/Titivate.scala | 1 - modules/ublog/src/main/Env.scala | 4 +- modules/ublog/src/main/UblogApi.scala | 2 +- modules/ublog/src/main/UblogBlog.scala | 14 +++++- modules/ublog/src/main/UblogForm.scala | 4 +- modules/ublog/src/main/UblogLike.scala | 64 +++++++++++++++++--------- ui/site/src/ublog.ts | 3 ++ 17 files changed, 174 insertions(+), 84 deletions(-) create mode 100644 app/views/ublog/blog.scala diff --git a/app/controllers/Ublog.scala b/app/controllers/Ublog.scala index 5aaf712626..0baac87cd6 100644 --- a/app/controllers/Ublog.scala +++ b/app/controllers/Ublog.scala @@ -11,7 +11,8 @@ import lila.user.{ User => UserModel } final class Ublog(env: Env) extends LilaController(env) { - import views.html.ublog.post.{ editUrlOf, urlOf } + import views.html.ublog.post.{ editUrlOfPost, urlOfPost } + import views.html.ublog.blog.{ urlOfBlog } import lila.common.paginator.Paginator.zero def index(username: String, page: Int) = Open { implicit ctx => @@ -19,7 +20,7 @@ final class Ublog(env: Env) extends LilaController(env) { OptionFuResult(env.user.repo named username) { user => env.ublog.api.getUserBlog(user) flatMap { blog => (canViewBlogOf(user, blog) ?? env.ublog.paginator.byUser(user, true, page)) map { posts => - Ok(html.ublog.index(user, posts)) + Ok(html.ublog.blog(user, blog, posts)) } } } @@ -42,7 +43,7 @@ final class Ublog(env: Env) extends LilaController(env) { env.ublog.api.getUserBlog(user) flatMap { blog => env.ublog.api.findByIdAndBlog(UblogPost.Id(id), blog.id) flatMap { _.filter(canViewPost(user, blog)) ?? { post => - if (slug != post.slug) Redirect(urlOf(post)).fuccess + if (slug != post.slug) Redirect(urlOfPost(post)).fuccess else { env.ublog.api.otherPosts(UblogBlog.Id.User(user.id), post) zip ctx.me.??(env.ublog.like.liked(post)) map { case (others, liked) => @@ -94,7 +95,7 @@ final class Ublog(env: Env) extends LilaController(env) { CreateLimitPerUser(me.id, cost = if (me.isVerified) 1 else 3) { env.ublog.api.create(data, me) map { post => lila.mon.ublog.create(me.id).increment() - Redirect(editUrlOf(post)).flashSuccess + Redirect(editUrlOfPost(post)).flashSuccess } }(rateLimitedFu) ) @@ -120,7 +121,7 @@ final class Ublog(env: Env) extends LilaController(env) { err => BadRequest(html.ublog.form.edit(me, prev, err)).fuccess, data => env.ublog.api.update(data, prev, me) map { post => - Redirect(urlOf(post)).flashSuccess + Redirect(urlOfPost(post)).flashSuccess } ) } @@ -144,6 +145,24 @@ final class Ublog(env: Env) extends LilaController(env) { } } + def setTier(blogId: String) = SecureBody(_.ModerateBlog) { implicit ctx => me => + UblogBlog.Id(blogId).??(env.ublog.api.getBlog) flatMap { + _ ?? { blog => + implicit val body = ctx.body + lila.ublog.UblogForm.tier + .bindFromRequest() + .fold( + err => Redirect(urlOfBlog(blog)).flashFailure.fuccess, + tier => + env.ublog.api.setTier(blog.id, tier) >> + env.ublog.like.recomputeRankOfAllPosts(blog.id) >> env.mod.logApi + .blogTier(lila.report.Mod(me.user), blog.id.full, tier) + inject Redirect(urlOfBlog(blog)).flashSuccess + ) + } + } + } + private val ImageRateLimitPerIp = lila.memo.RateLimit.composite[lila.common.IpAddress]( key = "ublog.image.ip" )( diff --git a/app/views/ublog/blog.scala b/app/views/ublog/blog.scala new file mode 100644 index 0000000000..1f5eee0c8a --- /dev/null +++ b/app/views/ublog/blog.scala @@ -0,0 +1,56 @@ +package views.html.ublog + +import controllers.routes + +import lila.api.Context +import lila.app.templating.Environment._ +import lila.app.ui.ScalatagsTemplate._ +import lila.common.paginator.Paginator +import lila.ublog.{ UblogBlog, UblogPost } +import lila.user.User + +object blog { + + import views.html.ublog.{ post => postView } + + def apply(user: User, blog: UblogBlog, posts: Paginator[UblogPost.PreviewPost])(implicit ctx: Context) = + views.html.base.layout( + moreCss = cssTag("ublog"), + moreJs = frag( + posts.hasNextPage option infiniteScrollTag, + ctx.isAuth option jsModule("ublog") + ), + title = trans.ublog.xBlog.txt(user.username) + ) { + main(cls := "box box-pad page page-small ublog-index")( + div(cls := "box__top")( + h1(trans.ublog.xBlog(userLink(user))), + if (ctx is user) + div(cls := "box__top__actions")( + a(href := routes.Ublog.drafts(user.username))(trans.ublog.drafts()), + postView.newPostLink + ) + else isGranted(_.ModerateBlog) option tierForm(blog) + ), + standardFlash(), + if (posts.nbResults > 0) + div(cls := "ublog-index__posts ublog-post-cards infinite-scroll")( + posts.currentPageResults map { postView.card(_) }, + pagerNext(posts, np => s"${routes.Ublog.index(user.username, np).url}") + ) + else + div(cls := "ublog-index__posts--empty")( + trans.ublog.noPostsInThisBlogYet() + ) + ) + } + + def urlOfBlog(blog: UblogBlog) = blog.id match { + case UblogBlog.Id.User(userId) => routes.Ublog.index(usernameOrId(userId)) + } + + private def tierForm(blog: UblogBlog) = postForm(action := routes.Ublog.setTier(blog.id.full)) { + val form = lila.ublog.UblogForm.tier.fill(blog.tier) + form3.select(form("tier"), lila.ublog.UblogBlog.Tier.options) + } +} diff --git a/app/views/ublog/form.scala b/app/views/ublog/form.scala index cbc065a7a6..7a9d0de452 100644 --- a/app/views/ublog/form.scala +++ b/app/views/ublog/form.scala @@ -112,7 +112,9 @@ object form { views.html.base.captcha(form, c) }, form3.actions( - a(href := post.fold(routes.Ublog.index(user.username))(views.html.ublog.post.urlOf))(trans.cancel()), + a(href := post.fold(routes.Ublog.index(user.username))(views.html.ublog.post.urlOfPost))( + trans.cancel() + ), form3.submit(trans.apply()) ) ) diff --git a/app/views/ublog/index.scala b/app/views/ublog/index.scala index 799b9e774c..0dee5a5732 100644 --- a/app/views/ublog/index.scala +++ b/app/views/ublog/index.scala @@ -13,40 +13,6 @@ object index { import views.html.ublog.{ post => postView } - def apply(user: User, posts: Paginator[UblogPost.PreviewPost])(implicit ctx: Context) = - views.html.base.layout( - moreCss = cssTag("ublog"), - moreJs = frag( - posts.hasNextPage option infiniteScrollTag, - ctx.isAuth option jsModule("ublog") - ), - title = trans.ublog.xBlog.txt(user.username) - ) { - main(cls := "box box-pad page page-small ublog-index")( - div(cls := "box__top")( - h1(trans.ublog.xBlog(userLink(user))), - if (ctx is user) - div(cls := "box__top__actions")( - a(href := routes.Ublog.drafts(user.username))(trans.ublog.drafts()), - newPostLink - ) - else if (isGranted(_.ModerateBlog) && user.marks.troll) - badTag("Not visible to the public") - else emptyFrag - ), - standardFlash(), - if (posts.nbResults > 0) - div(cls := "ublog-index__posts ublog-post-cards infinite-scroll")( - posts.currentPageResults map { postView.card(_) }, - pagerNext(posts, np => s"${routes.Ublog.index(user.username, np).url}") - ) - else - div(cls := "ublog-index__posts--empty")( - trans.ublog.noPostsInThisBlogYet() - ) - ) - } - def drafts(user: User, posts: Paginator[UblogPost.PreviewPost])(implicit ctx: Context) = views.html.base.layout( moreCss = frag(cssTag("ublog")), @@ -58,12 +24,12 @@ object index { h1(trans.ublog.drafts()), div(cls := "box__top__actions")( a(href := routes.Ublog.index(user.username))(trans.ublog.published()), - newPostLink + postView.newPostLink ) ), if (posts.nbResults > 0) div(cls := "ublog-index__posts ublog-index__posts--drafts ublog-post-cards infinite-scroll")( - posts.currentPageResults map { postView.card(_, postView.editUrlOf) }, + posts.currentPageResults map { postView.card(_, postView.editUrlOfPost) }, pagerNext(posts, np => routes.Ublog.drafts(user.username, np).url) ) else @@ -117,13 +83,4 @@ object index { ) ) } - - private def newPostLink(implicit ctx: Context) = ctx.me map { u => - a( - href := routes.Ublog.form(u.username), - cls := "button button-green", - dataIcon := "", - title := trans.ublog.newPost.txt() - ) - } } diff --git a/app/views/ublog/post.scala b/app/views/ublog/post.scala index 7c205c0058..2f2b03bfc1 100644 --- a/app/views/ublog/post.scala +++ b/app/views/ublog/post.scala @@ -60,7 +60,7 @@ object post { if (post.live) trans.ublog.thisPostIsPublished() else trans.ublog.thisIsADraft() ), a( - href := editUrlOf(post), + href := editUrlOfPost(post), cls := "button button-empty text", dataIcon := "" )(trans.edit()) @@ -71,7 +71,7 @@ object post { a( titleOrText(trans.reportXToModerators.txt(user.username)), cls := "button button-empty ublog-post__meta__report", - href := s"${routes.Report.form}?username=${user.username}&postUrl=${urlencode(s"${netBaseUrl}${urlOf(post).url}")}&reason=comm", + href := s"${routes.Report.form}?username=${user.username}&postUrl=${urlencode(s"${netBaseUrl}${urlOfPost(post).url}")}&reason=comm", dataIcon := "" ) ), @@ -86,7 +86,7 @@ object post { def card( post: UblogPost.BasePost, - makeUrl: UblogPost.BasePost => Call = urlOf, + makeUrl: UblogPost.BasePost => Call = urlOfPost, showAuthor: Boolean = false )(implicit ctx: Context) = a(cls := "ublog-post-card", href := makeUrl(post))( @@ -99,12 +99,21 @@ object post { ) ) - def urlOf(post: UblogPost.BasePost) = post.blog match { + def urlOfPost(post: UblogPost.BasePost) = post.blog match { case UblogBlog.Id.User(userId) => routes.Ublog.post(usernameOrId(userId), post.slug, post.id.value) } - def editUrlOf(post: UblogPost.BasePost) = routes.Ublog.edit(post.id.value) + def editUrlOfPost(post: UblogPost.BasePost) = routes.Ublog.edit(post.id.value) + + private[ublog] def newPostLink(implicit ctx: Context) = ctx.me map { u => + a( + href := routes.Ublog.form(u.username), + cls := "button button-green", + dataIcon := "", + title := trans.ublog.newPost.txt() + ) + } object thumbnail { def apply(post: UblogPost.BasePost, size: UblogPost.thumbnail.SizeSelector) = diff --git a/bin/mongodb/ublog-blog.js b/bin/mongodb/ublog-blog.js index bad26d72e0..0c2f2f98d7 100644 --- a/bin/mongodb/ublog-blog.js +++ b/bin/mongodb/ublog-blog.js @@ -61,6 +61,8 @@ db.ublog_post.find({ blog: { $exists: false } }).forEach(p => { at: p.liveAt, } : undefined, + likers: [], + likes: NumberInt(0), }, $unset: { user: 1, diff --git a/conf/routes b/conf/routes index 77ca681048..36db114688 100644 --- a/conf/routes +++ b/conf/routes @@ -67,6 +67,7 @@ GET /ublog/$id<\w{8}>/edit controllers.Ublog.edit(id: String) POST /ublog/$id<\w{8}>/edit controllers.Ublog.update(id: String) POST /ublog/$id<\w{8}>/del controllers.Ublog.delete(id: String) POST /ublog/$id<\w{8}>/like controllers.Ublog.like(id: String, v: Boolean) +POST /ublog/:blogId/tier controllers.Ublog.setTier(blogId: String) POST /upload/image/ublog/$id<\w{8}> controllers.Ublog.image(id: String) # User diff --git a/modules/mod/src/main/ModActivity.scala b/modules/mod/src/main/ModActivity.scala index 73d32dcc11..a5339152ef 100644 --- a/modules/mod/src/main/ModActivity.scala +++ b/modules/mod/src/main/ModActivity.scala @@ -170,6 +170,7 @@ object ModActivity { case object Appeal extends Action case object SetEmail extends Action case object Streamer extends Action + case object Blog extends Action case object ForumAdmin extends Action val dbMap = Map( "modMessage" -> Message, @@ -191,6 +192,7 @@ object ModActivity { "streamerDecline" -> Streamer, "streamerunlist" -> Streamer, "streamerTier" -> Streamer, + "blogTier" -> Blog, "deletePost" -> ForumAdmin, "closeTopic" -> ForumAdmin ) diff --git a/modules/mod/src/main/Modlog.scala b/modules/mod/src/main/Modlog.scala index 889c0f3d37..45ef47a2ff 100644 --- a/modules/mod/src/main/Modlog.scala +++ b/modules/mod/src/main/Modlog.scala @@ -68,6 +68,7 @@ case class Modlog( case Modlog.streamerFeature => "feature streamer" // BC case Modlog.streamerUnfeature => "unfeature streamer" // BC case Modlog.streamerTier => "set streamer tier" + case Modlog.blogTier => "set blog tier" case Modlog.teamKick => "kick from team" case Modlog.teamEdit => "edited team" case Modlog.appealPost => "posted in appeal" @@ -135,6 +136,7 @@ object Modlog { val streamerFeature = "streamerFeature" // BC val streamerUnfeature = "streamerUnfeature" // BC val streamerTier = "streamerTier" + val blogTier = "blogTier" val teamKick = "teamKick" val teamEdit = "teamEdit" val appealPost = "appealPost" diff --git a/modules/mod/src/main/ModlogApi.scala b/modules/mod/src/main/ModlogApi.scala index fad3f14160..7f5dae7f14 100644 --- a/modules/mod/src/main/ModlogApi.scala +++ b/modules/mod/src/main/ModlogApi.scala @@ -29,6 +29,10 @@ final class ModlogApi(repo: ModlogRepo, userRepo: UserRepo, ircApi: IrcApi)(impl add { Modlog(mod.user.id, streamerId.some, Modlog.streamerTier, v.toString.some) } + def blogTier(mod: Mod, blogId: String, v: Int) = + add { + Modlog(mod.user.id, blogId.some, Modlog.blogTier, v.toString.some) + } def practiceConfig(mod: User.ID) = add { diff --git a/modules/round/src/main/Titivate.scala b/modules/round/src/main/Titivate.scala index 2967c31302..f09f6b5cf7 100644 --- a/modules/round/src/main/Titivate.scala +++ b/modules/round/src/main/Titivate.scala @@ -3,7 +3,6 @@ package lila.round import akka.actor._ import akka.stream.scaladsl._ import org.joda.time.DateTime - import scala.concurrent.duration._ import lila.common.LilaStream diff --git a/modules/ublog/src/main/Env.scala b/modules/ublog/src/main/Env.scala index 171f32ecef..a6b86cc41e 100644 --- a/modules/ublog/src/main/Env.scala +++ b/modules/ublog/src/main/Env.scala @@ -34,7 +34,9 @@ final class Env( val form = wire[UblogForm] lila.common.Bus.subscribeFun("shadowban") { case lila.hub.actorApi.mod.Shadowban(userId, v) => - api.setShadowban(userId, v).unit + api.setShadowban(userId, v) >> + like.recomputeRankOfAllPosts(UblogBlog.Id.User(userId)) + () } } diff --git a/modules/ublog/src/main/UblogApi.scala b/modules/ublog/src/main/UblogApi.scala index 0d92deb45b..a4a53e78cf 100644 --- a/modules/ublog/src/main/UblogApi.scala +++ b/modules/ublog/src/main/UblogApi.scala @@ -107,7 +107,7 @@ final class UblogApi( def setTier(blog: UblogBlog.Id, tier: Int): Funit = colls.blog.update - .one($id(blog), $set("modTier" -> tier, "tier" -> tier)) + .one($id(blog), $set("modTier" -> tier, "tier" -> tier), upsert = true) .void private[ublog] def setShadowban(userId: User.ID, v: Boolean) = { diff --git a/modules/ublog/src/main/UblogBlog.scala b/modules/ublog/src/main/UblogBlog.scala index a30b8b3c83..7466689970 100644 --- a/modules/ublog/src/main/UblogBlog.scala +++ b/modules/ublog/src/main/UblogBlog.scala @@ -6,8 +6,8 @@ case class UblogBlog( _id: UblogBlog.Id, title: Option[String], intro: Option[String], - tier: Int, // actual tier, auto or set by a mod - modTier: Option[Int] // tier set by a mod + tier: UblogBlog.Tier, // actual tier, auto or set by a mod + modTier: Option[UblogBlog.Tier] // tier set by a mod ) { def id = _id def visible = tier >= UblogBlog.Tier.VISIBLE @@ -26,6 +26,7 @@ object UblogBlog { } } + type Tier = Int object Tier { val HIDDEN = 0 // not visible val VISIBLE = 1 // not listed in community page @@ -38,6 +39,15 @@ object UblogBlog { if (user.marks.troll) Tier.HIDDEN else if (user.hasTitle || user.perfs.standard.glicko.establishedIntRating.exists(_ > 2200)) Tier.NORMAL else Tier.LOW + + val options = List( + HIDDEN -> "Hidden", + VISIBLE -> "Unlisted", + LOW -> "Low tier", + NORMAL -> "Normal tier", + HIGH -> "High tier", + BEST -> "Best tier" + ) } def make(user: User) = UblogBlog( diff --git a/modules/ublog/src/main/UblogForm.scala b/modules/ublog/src/main/UblogForm.scala index a5b654b022..a17112d49e 100644 --- a/modules/ublog/src/main/UblogForm.scala +++ b/modules/ublog/src/main/UblogForm.scala @@ -71,7 +71,7 @@ object UblogForm { created = UblogPost.Recorded(user.id, DateTime.now), updated = none, lived = none, - likes = UblogPost.Likes(0) + likes = UblogPost.Likes(1) ) def update(user: User, prev: UblogPost) = @@ -85,4 +85,6 @@ object UblogForm { lived = prev.lived orElse live.option(UblogPost.Recorded(user.id, DateTime.now)) ) } + + val tier = Form(single("tier" -> number(min = UblogBlog.Tier.HIDDEN, max = UblogBlog.Tier.BEST))) } diff --git a/modules/ublog/src/main/UblogLike.scala b/modules/ublog/src/main/UblogLike.scala index 1548d23ea4..c5796d890a 100644 --- a/modules/ublog/src/main/UblogLike.scala +++ b/modules/ublog/src/main/UblogLike.scala @@ -1,5 +1,6 @@ package lila.ublog +import cats.implicits._ import reactivemongo.api._ import scala.concurrent.ExecutionContext import lila.db.dsl._ @@ -16,30 +17,50 @@ final class UblogLike(colls: UblogColls)(implicit ec: ExecutionContext) { colls.post.exists($id(post.id) ++ selectLiker(user.id)) def apply(postId: UblogPost.Id, user: User, v: Boolean): Fu[UblogPost.Likes] = - countLikes(postId).flatMap { + fetchRankData(postId).flatMap { case None => fuccess(UblogPost.Likes(v ?? 1)) - case Some((blog, prevLikes, liveAt)) => + case Some((prevLikes, liveAt, tier)) => val likes = UblogPost.Likes(prevLikes.value + (if (v) 1 else -1)) colls.post.update.one( $id(postId), $set( "likes" -> likes, - "rank" -> computeRank(blog, likes, liveAt) + "rank" -> computeRank(likes, liveAt, tier) ) ++ { if (v) $addToSet("likers" -> user.id) else $pull("likers" -> user.id) } ) inject likes } - private def computeRank(blog: UblogBlog, likes: UblogPost.Likes, liveAt: DateTime) = + def recomputeRankOfAllPosts(blogId: UblogBlog.Id): Funit = + colls.blog.byId[UblogBlog](blogId.full) flatMap { + _ ?? { blog => + colls.post + .find($doc("blog" -> blog.id), $doc("likes" -> true, "lived" -> true).some) + .cursor[Bdoc]() + .list() flatMap { docs => + lila.common.Future.applySequentially(docs) { doc => + ( + doc.string("_id"), + doc.getAsOpt[UblogPost.Likes]("likes"), + doc.getAsOpt[UblogPost.Recorded]("lived") + ).tupled ?? { case (id, likes, lived) => + colls.post.updateField($id(id), "rank", computeRank(likes, lived.at, blog.tier)).void + } + } + } + } + } + + private def computeRank(likes: UblogPost.Likes, liveAt: DateTime, tier: UblogBlog.Tier) = UblogPost.Rank { liveAt plusHours { val baseHours = likesToHours(likes) - blog.tier match { - case UblogBlog.Tier.LOW => (baseHours * 0.2).toInt + tier match { + case UblogBlog.Tier.LOW => (baseHours * 0.3).toInt case UblogBlog.Tier.NORMAL => baseHours - case UblogBlog.Tier.HIGH => baseHours * 5 - case UblogBlog.Tier.BEST => baseHours * 12 + case UblogBlog.Tier.HIGH => baseHours * 3 + case UblogBlog.Tier.BEST => baseHours * 10 case _ => -99999 } } @@ -47,34 +68,33 @@ final class UblogLike(colls: UblogColls)(implicit ec: ExecutionContext) { private def likesToHours(likes: UblogPost.Likes): Int = if (likes.value < 1) 0 - else (5 * math.log(likes.value) + 1).toInt.min(likes.value) * 24 + else (5 * math.log(likes.value) + 1).toInt.atMost(likes.value) * 12 - private def countLikes(postId: UblogPost.Id): Fu[Option[(UblogBlog, UblogPost.Likes, DateTime)]] = + private def fetchRankData(postId: UblogPost.Id): Fu[Option[(UblogPost.Likes, DateTime, UblogBlog.Tier)]] = colls.post - .aggregateWith[Bdoc]() { framework => + .aggregateOne() { framework => import framework._ - List( - Match($id(postId)), + Match($id(postId)) -> List( PipelineOperator($lookup.simple(colls.blog, "blog", "blog", "_id")), + UnwindField("blog"), Project( $doc( - "_id" -> false, - "blog" -> true, - "likes" -> $doc("$size" -> "$likers"), - "lived.at" -> true + "_id" -> false, + "blog.tier" -> true, + "likes" -> $doc("$size" -> "$likers"), + "lived.at" -> true ) ) ) } - .headOption .map { docOption => for { doc <- docOption likes <- doc.getAsOpt[UblogPost.Likes]("likes") lived <- doc.getAsOpt[Bdoc]("lived") - liveAt <- doc.getAsOpt[DateTime]("at") - blogs <- doc.getAsOpt[List[UblogBlog]]("blog") - blog <- blogs.headOption - } yield (blog, likes, liveAt) + liveAt <- lived.getAsOpt[DateTime]("at") + blog <- doc.getAsOpt[Bdoc]("blog") + tier <- blog int "tier" + } yield (likes, liveAt, tier) } } diff --git a/ui/site/src/ublog.ts b/ui/site/src/ublog.ts index 325b5b05a9..5cc3986312 100644 --- a/ui/site/src/ublog.ts +++ b/ui/site/src/ublog.ts @@ -16,4 +16,7 @@ lichess.load.then(() => { .then(likes => button.text(likes).toggleClass(likeClass, liked)); }) ); + $('#form3-tier').on('change', function (this: HTMLSelectElement) { + (this.parentNode as HTMLFormElement).submit(); + }); });