diff --git a/.gitmodules b/.gitmodules index 94b763b57d..3c1f5aa420 100644 --- a/.gitmodules +++ b/.gitmodules @@ -34,3 +34,6 @@ [submodule "public/vendor/Sunsetter"] path = public/vendor/Sunsetter url = https://github.com/niklasf/Sunsetter.git +[submodule "public/vendor/flatpickr"] + path = public/vendor/flatpickr + url = https://github.com/chmln/flatpickr diff --git a/app/Env.scala b/app/Env.scala index 6a63445aef..d41d98cd01 100644 --- a/app/Env.scala +++ b/app/Env.scala @@ -91,7 +91,8 @@ final class Env( Env.fishnet, // required to schedule the cleaner Env.notifyModule, // required to load the actor Env.plan, // required to load the actor - Env.studySearch // required to load the actor + Env.studySearch, // required to load the actor + Env.event // required to load the actor )) { lap => lila.log("boot").info(s"${lap.millis}ms Preloading complete") } @@ -162,4 +163,5 @@ object Env { def studySearch = lila.studySearch.Env.current def learn = lila.learn.Env.current def plan = lila.plan.Env.current + def event = lila.event.Env.current } diff --git a/app/controllers/EventCrud.scala b/app/controllers/EventCrud.scala new file mode 100644 index 0000000000..d772e4acf2 --- /dev/null +++ b/app/controllers/EventCrud.scala @@ -0,0 +1,53 @@ +package controllers + +import play.api.mvc._ + +import lila.app._ +import views._ + +object EventCrud extends LilaController { + + private def env = Env.event + private def api = env.api + + def index = Secure(_.ManageEvent) { implicit ctx => + me => + api.list map { events => + html.event.index(events) + } + } + + def edit(id: String) = Secure(_.ManageEvent) { implicit ctx => + me => + OptionOk(api one id) { event => + html.event.edit(event, api editForm event) + } + } + + def update(id: String) = SecureBody(_.ManageEvent) { implicit ctx => + me => + OptionFuResult(api one id) { event => + implicit val req = ctx.body + api.editForm(event).bindFromRequest.fold( + err => BadRequest(html.event.edit(event, err)).fuccess, + data => api.update(event, data) inject Redirect(routes.EventCrud.edit(id)) + ) + } + } + + def form = Secure(_.ManageEvent) { implicit ctx => + me => + Ok(html.event.create(api.createForm)).fuccess + } + + def create = SecureBody(_.ManageEvent) { implicit ctx => + me => + implicit val req = ctx.body + api.createForm.bindFromRequest.fold( + err => BadRequest(html.event.create(err)).fuccess, + data => api.create(data, me.id) map { event => + Redirect(routes.EventCrud.edit(event.id)) + } + ) + } +} diff --git a/app/controllers/TournamentCrud.scala b/app/controllers/TournamentCrud.scala index e0f828e515..3a899a3539 100644 --- a/app/controllers/TournamentCrud.scala +++ b/app/controllers/TournamentCrud.scala @@ -1,21 +1,14 @@ package controllers -import play.api.data.Form -import play.api.libs.json._ import play.api.mvc._ -import lila.api.Context import lila.app._ -import lila.common.HTTPRequest -import lila.game.{ Pov, GameRepo } -import lila.tournament.{ System, TournamentRepo, PairingRepo, Tournament => Tourney, VisibleTournaments } -import lila.user.UserRepo import views._ object TournamentCrud extends LilaController { private def env = Env.tournament - private def crud = Env.tournament.crudApi + private def crud = env.crudApi def index = Secure(_.ManageTournament) { implicit ctx => me => @@ -36,7 +29,7 @@ object TournamentCrud extends LilaController { OptionFuResult(crud one id) { tour => implicit val req = ctx.body crud.editForm(tour).bindFromRequest.fold( - err => BadRequest(html.tournament.crud.edit(tour, err.pp)).fuccess, + err => BadRequest(html.tournament.crud.edit(tour, err)).fuccess, data => crud.update(tour, data) inject Redirect(routes.TournamentCrud.edit(id)) ) } diff --git a/app/views/event/create.scala.html b/app/views/event/create.scala.html new file mode 100644 index 0000000000..d36327c295 --- /dev/null +++ b/app/views/event/create.scala.html @@ -0,0 +1,10 @@ +@(form: Form[_])(implicit ctx: Context) + +@layout(title = "New event", active = "form") { +
+

New event

+
+ @inForm(form) +
+
+} diff --git a/app/views/event/edit.scala.html b/app/views/event/edit.scala.html new file mode 100644 index 0000000000..75b8696723 --- /dev/null +++ b/app/views/event/edit.scala.html @@ -0,0 +1,13 @@ +@(event: lila.event.Event, form: Form[_])(implicit ctx: Context) + +@layout(title = event.title, active = "index") { +
+

+ @event.title + Created by @usernameOrId(event.createdBy.value) on @showDate(event.createdAt) +

+
+ @inForm(form) +
+
+} diff --git a/app/views/event/inForm.scala.html b/app/views/event/inForm.scala.html new file mode 100644 index 0000000000..f3ed79ef8c --- /dev/null +++ b/app/views/event/inForm.scala.html @@ -0,0 +1,38 @@ +@(form: Form[_])(implicit ctx: Context) + +@group(field: play.api.data.Field, name: Html, half: Boolean = false)(html: Html) = { +
+ @html + + +
+} + +@dateInput(field: play.api.data.Field) = { + +} + +@group(form("startsAt"), Html("Start date UTC"), half = true) { +@dateInput(form("startsAt")) +} +@group(form("finishesAt"), Html("End date UTC"), half = true) { +@dateInput(form("finishesAt")) +} +@group(form("title"), Html("Title")) { +@base.input(form("title")) +} +@group(form("headline"), Html("Headline")) { +@base.input(form("headline")) +} +
+ @group(form("homepageHours"), Html("Hours on homepage (0 to 24)"), half = true) { + @base.input(form("homepageHours")) + } + @group(form("url"), Html("External URL"), half = true) { + @base.input(form("url")) + } +
+ +
+ +
diff --git a/app/views/event/index.scala.html b/app/views/event/index.scala.html new file mode 100644 index 0000000000..9df9797792 --- /dev/null +++ b/app/views/event/index.scala.html @@ -0,0 +1,35 @@ +@(events: List[lila.event.Event])(implicit ctx: Context) + +@title = {Event manager} + +@layout(title = title.body, active = "index") { +
+

@title

+ + + + + + + + + + + @events.map { event => + + + + + + + } + +
UTC startUTC end
@event.title + @showDateTimeUTC(event.startsAt) + @momentFromNow(event.startsAt) + + @showDateTimeUTC(event.finishesAt) + @momentFromNow(event.finishesAt) + URL
+
+} diff --git a/app/views/event/layout.scala.html b/app/views/event/layout.scala.html new file mode 100644 index 0000000000..34d764d6cb --- /dev/null +++ b/app/views/event/layout.scala.html @@ -0,0 +1,25 @@ +@(title: String, active: String = "")(body: Html)(implicit ctx: Context) + +@menu = { +Recent events +Create an event +} + +@moreCss = { +@cssAt("vendor/flatpickr/dist/flatpickr.min.css") +@cssTag("material.form.css") +@cssTag("event.crud.css") +} + +@moreJs = { +@jsAt("vendor/flatpickr/dist/flatpickr.min.js") +@embedJs { +$(".flatpickr").flatpickr(); +} +} + +@base.layout( +title = title, +menu = menu.some, +moreCss = moreCss, +moreJs = moreJs)(body) diff --git a/app/views/mod/menu.scala.html b/app/views/mod/menu.scala.html index 8c423e3e0c..7cf89f2a83 100644 --- a/app/views/mod/menu.scala.html +++ b/app/views/mod/menu.scala.html @@ -13,7 +13,10 @@ Streams } @if(isGranted(_.ManageTournament)) { -Recent tournaments +Manage tournaments +} +@if(isGranted(_.ManageEvent)) { +Manage events } @if(isGranted(_.SeeReport)) { Mod log diff --git a/app/views/tournament/crud/inForm.scala.html b/app/views/tournament/crud/inForm.scala.html index 40473a3d49..c687a4bb38 100644 --- a/app/views/tournament/crud/inForm.scala.html +++ b/app/views/tournament/crud/inForm.scala.html @@ -36,10 +36,10 @@ @group(form("image"), Html("Custom icon"), half = true) { @base.select(form("image"), imageChoices) } - @group(form("headline"), Html("Homepage headline")) { - @base.input(form("headline")) - } +@group(form("headline"), Html("Homepage headline")) { +@base.input(form("headline")) +} @group(form("description"), Html("Full description")) { } diff --git a/conf/base.conf b/conf/base.conf index 417933303e..369a8dceec 100644 --- a/conf/base.conf +++ b/conf/base.conf @@ -135,6 +135,11 @@ coordinate { score = coordinate_score } } +event { + collection { + event = event + } +} opening { collection { opening = opening diff --git a/conf/routes b/conf/routes index b21ba5a8ca..96d4821f60 100644 --- a/conf/routes +++ b/conf/routes @@ -418,6 +418,13 @@ GET /api/tournament controllers.Api.currentTournaments GET /api/tournament/:id controllers.Api.tournament(id: String) GET /api/status controllers.Api.status +# Events +GET /event/manager controllers.EventCrud.index +GET /event/manager/$id<\w{8}> controllers.EventCrud.edit(id: String) +POST /event/manager/$id<\w{8}> controllers.EventCrud.update(id: String) +GET /event/manager/new controllers.EventCrud.form +POST /event/manager controllers.EventCrud.create + # Misc POST /cli controllers.Cli.command GET /captcha/$id<\w{8}> controllers.Main.captchaCheck(id: String) diff --git a/modules/event/src/main/BsonHandlers.scala b/modules/event/src/main/BsonHandlers.scala new file mode 100644 index 0000000000..66a23a8d7f --- /dev/null +++ b/modules/event/src/main/BsonHandlers.scala @@ -0,0 +1,11 @@ +package lila.event + +import lila.db.dsl._ +import reactivemongo.bson._ + +private[event] object BsonHandlers { + + implicit val UserIdBsonHandler = stringAnyValHandler[Event.UserId](_.value, Event.UserId.apply) + + implicit val EventBsonHandler = Macros.handler[Event] +} diff --git a/modules/event/src/main/Env.scala b/modules/event/src/main/Env.scala new file mode 100644 index 0000000000..d875b3a179 --- /dev/null +++ b/modules/event/src/main/Env.scala @@ -0,0 +1,24 @@ +package lila.event + +import akka.actor._ +import com.typesafe.config.Config + +final class Env( + config: Config, + db: lila.db.Env, + system: ActorSystem) { + + private val CollectionEvent = config getString "collection.event" + + private lazy val eventColl = db(CollectionEvent) + + lazy val api = new EventApi(coll = eventColl) +} + +object Env { + + lazy val current = "event" boot new Env( + config = lila.common.PlayApp loadConfig "event", + db = lila.db.Env.current, + system = lila.common.PlayApp.system) +} diff --git a/modules/event/src/main/Event.scala b/modules/event/src/main/Event.scala new file mode 100644 index 0000000000..7626f9fade --- /dev/null +++ b/modules/event/src/main/Event.scala @@ -0,0 +1,26 @@ +package lila.event + +import org.joda.time.DateTime + +import lila.db.dsl._ + +case class Event( + _id: String, + title: String, + headline: String, + homepageHours: Int, + url: String, + createdBy: Event.UserId, + createdAt: DateTime, + startsAt: DateTime, + finishesAt: DateTime) { + + def id = _id +} + +object Event { + + def makeId = ornicar.scalalib.Random nextStringUppercase 8 + + case class UserId(value: String) extends AnyVal +} diff --git a/modules/event/src/main/EventApi.scala b/modules/event/src/main/EventApi.scala new file mode 100644 index 0000000000..e0a092fd65 --- /dev/null +++ b/modules/event/src/main/EventApi.scala @@ -0,0 +1,28 @@ +package lila.event + +import org.joda.time.{ DateTime, DateTimeZone } + +import lila.db.dsl._ + +final class EventApi(coll: Coll) { + + import BsonHandlers._ + + def list = coll.find($empty).sort($doc("startsAt" -> -1)).list[Event](50) + + def one(id: String) = coll.byId[Event](id) + + def editForm(event: Event) = EventForm.form fill { + EventForm.Data make event + } + + def update(old: Event, data: EventForm.Data) = + coll.update($id(old.id), data update old).void + + def createForm = EventForm.form + + def create(data: EventForm.Data, userId: String): Fu[Event] = { + val event = data make userId + coll.insert(event) inject event + } +} diff --git a/modules/event/src/main/EventForm.scala b/modules/event/src/main/EventForm.scala new file mode 100644 index 0000000000..ce97c02c0d --- /dev/null +++ b/modules/event/src/main/EventForm.scala @@ -0,0 +1,65 @@ +package lila.event + +import org.joda.time.format.DateTimeFormat +import org.joda.time.{ DateTime, DateTimeZone } +import play.api.data._ +import play.api.data.Forms._ +import play.api.data.validation.Constraints._ +import scala.util.Try + +import lila.common.Form._ + +object EventForm { + + private val dateTimePattern = "yyyy-MM-dd HH:mm" + + private implicit val dateTimeFormat = format.Formats.jodaDateTimeFormat(dateTimePattern) + + val form = Form(mapping( + "title" -> nonEmptyText(minLength = 3, maxLength = 40), + "headline" -> nonEmptyText(minLength = 5, maxLength = 30), + "homepageHours" -> number(min = 0, max = 24), + "url" -> nonEmptyText, + "startsAt" -> jodaDate(dateTimePattern), + "finishesAt" -> jodaDate(dateTimePattern) + )(Data.apply)(Data.unapply)) + + case class Data( + title: String, + headline: String, + homepageHours: Int, + url: String, + startsAt: DateTime, + finishesAt: DateTime) { + + def update(event: Event) = event.copy( + title = title, + headline = headline, + homepageHours = homepageHours, + url = url, + startsAt = startsAt, + finishesAt = finishesAt) + + def make(userId: String) = Event( + _id = Event.makeId, + title = title, + headline = headline, + homepageHours = homepageHours, + url = url, + startsAt = startsAt, + finishesAt = finishesAt, + createdBy = Event.UserId(userId), + createdAt = DateTime.now) + } + + object Data { + + def make(event: Event) = Data( + title = event.title, + headline = event.headline, + homepageHours = event.homepageHours, + url = event.url, + startsAt = event.startsAt, + finishesAt = event.finishesAt) + } +} diff --git a/modules/event/src/main/package.scala b/modules/event/src/main/package.scala new file mode 100644 index 0000000000..9b29a68ba0 --- /dev/null +++ b/modules/event/src/main/package.scala @@ -0,0 +1,3 @@ +package lila + +package object event extends PackageObject with WithPlay diff --git a/modules/security/src/main/Permission.scala b/modules/security/src/main/Permission.scala index 118f9941aa..1ee8b5989d 100644 --- a/modules/security/src/main/Permission.scala +++ b/modules/security/src/main/Permission.scala @@ -2,8 +2,7 @@ package lila.security sealed abstract class Permission(val name: String, val children: List[Permission] = Nil) { - final def is(p: Permission): Boolean = - this == p || (children exists (_ is p)) + final def is(p: Permission): Boolean = this == p || (children exists (_ is p)) } object Permission { @@ -35,6 +34,7 @@ object Permission { case object CloseTeam extends Permission("ROLE_CLOSE_TEAM") case object TerminateTournament extends Permission("ROLE_TERMINATE_TOURNAMENT") case object ManageTournament extends Permission("ROLE_MANAGE_TOURNAMENT") + case object ManageEvent extends Permission("ROLE_MANAGE_EVENT") case object ChangePermission extends Permission("ROLE_CHANGE_PERMISSION") case object PublicMod extends Permission("ROLE_PUBLIC_MOD", List(GuineaPig)) case object Developer extends Permission("ROLE_DEVELOPER", List(GuineaPig)) @@ -47,7 +47,8 @@ object Permission { case object Admin extends Permission("ROLE_ADMIN", List( Hunter, ModerateForum, IpBan, CloseAccount, ReopenAccount, ChatTimeout, MarkTroll, SetTitle, SetEmail, ModerateQa, StreamConfig, - MessageAnyone, CloseTeam, TerminateTournament, ManageTournament, GuineaPig)) + MessageAnyone, CloseTeam, TerminateTournament, ManageTournament, ManageEvent, + GuineaPig)) case object SuperAdmin extends Permission("ROLE_SUPER_ADMIN", List( Admin, ChangePermission, PublicMod, Developer)) @@ -55,7 +56,7 @@ object Permission { lazy val allButSuperAdmin: List[Permission] = List( Admin, Hunter, MarkTroll, ChatTimeout, ChangePermission, ViewBlurs, StaffForum, ModerateForum, UserSpy, MarkEngine, MarkBooster, IpBan, ModerateQa, StreamConfig, - Beta, MessageAnyone, UserSearch, CloseTeam, TerminateTournament, ManageTournament, + Beta, MessageAnyone, UserSearch, CloseTeam, TerminateTournament, ManageTournament, ManageEvent, PublicMod, Developer, GuineaPig) lazy private val all: List[Permission] = SuperAdmin :: allButSuperAdmin diff --git a/modules/tournament/src/main/TournamentRepo.scala b/modules/tournament/src/main/TournamentRepo.scala index 174f34ff87..dc85bd8381 100644 --- a/modules/tournament/src/main/TournamentRepo.scala +++ b/modules/tournament/src/main/TournamentRepo.scala @@ -174,7 +174,7 @@ object TournamentRepo { coll.find(selectUnique) .sort($doc("startsAt" -> -1)) .hint($doc("startsAt" -> -1)) - .list[Tournament]() + .list[Tournament](max) def scheduledUnfinished: Fu[List[Tournament]] = coll.find(scheduledSelect ++ unfinishedSelect) diff --git a/modules/tournament/src/main/crud/CrudApi.scala b/modules/tournament/src/main/crud/CrudApi.scala index 330c41363a..18fc743674 100644 --- a/modules/tournament/src/main/crud/CrudApi.scala +++ b/modules/tournament/src/main/crud/CrudApi.scala @@ -7,7 +7,7 @@ import lila.user.User final class CrudApi { - def list = TournamentRepo uniques 30 + def list = TournamentRepo uniques 50 def one(id: String) = TournamentRepo uniqueById id diff --git a/project/Build.scala b/project/Build.scala index 793097054f..94ee1bb317 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -60,7 +60,7 @@ object ApplicationBuild extends Build { evaluation, chat, puzzle, tv, coordinate, blog, qa, history, worldMap, opening, video, shutup, push, playban, insight, perfStat, slack, quote, challenge, - study, studySearch, fishnet, explorer, learn, plan) + study, studySearch, fishnet, explorer, learn, plan, event) lazy val moduleRefs = modules map projectToRef lazy val moduleCPDeps = moduleRefs map { new sbt.ClasspathDependency(_, None) } @@ -148,37 +148,35 @@ object ApplicationBuild extends Build { ) lazy val timeline = project("timeline", Seq(common, db, game, user, hub, security, relation)).settings( - libraryDependencies ++= provided( - play.api, play.test, RM) + libraryDependencies ++= provided(play.api, play.test, RM) + ) + + lazy val event = project("event", Seq(common, db)).settings( + libraryDependencies ++= provided(play.api, play.test, RM) ) lazy val mod = project("mod", Seq(common, db, user, hub, security, game, analyse, evaluation, report, notifyModule)).settings( - libraryDependencies ++= provided( - play.api, play.test, RM) + libraryDependencies ++= provided(play.api, play.test, RM) ) lazy val user = project("user", Seq(common, memo, db, hub, chess, rating)).settings( - libraryDependencies ++= provided( - play.api, play.test, RM, hasher) + libraryDependencies ++= provided(play.api, play.test, RM, hasher) ) lazy val game = project("game", Seq(common, memo, db, hub, user, chess, chat)).settings( - libraryDependencies ++= provided( - play.api, RM) + libraryDependencies ++= provided(play.api, RM) ) lazy val gameSearch = project("gameSearch", Seq(common, hub, chess, search, game)).settings( libraryDependencies ++= provided( - play.api, RM) - ) + play.api, RM)) lazy val tv = project("tv", Seq(common, db, hub, socket, game, user, chess)).settings( libraryDependencies ++= provided(play.api, RM, hasher) ) lazy val analyse = project("analyse", Seq(common, hub, chess, game, user, notifyModule)).settings( - libraryDependencies ++= provided( - play.api, RM, spray.caching) + libraryDependencies ++= provided(play.api, RM, spray.caching) ) lazy val round = project("round", Seq( diff --git a/public/stylesheets/event.crud.css b/public/stylesheets/event.crud.css new file mode 100644 index 0000000000..646a9154cf --- /dev/null +++ b/public/stylesheets/event.crud.css @@ -0,0 +1,49 @@ +h1.lichess_title { + text-align: center; + margin-bottom: 0!important; +} +.event_crud.edit h1.lichess_title { + background: #303F9F; + color: #ddd; + letter-spacing: 0.1em; + padding: 30px!important; + font-size: 27px!important; +} +h1.lichess_title span { + display: block; + font-size: 0.35em; + opacity: 0.8; + margin-top: 10px; + text-transform: uppercase; +} +table.slist { + font-size: 1.2em; + width: 100%; +} +table.slist td { + padding: 20px 10px; +} +table.slist time { + display: block; + font-size: 0.8em; + opacity: 0.8; +} +.material.form { + margin: 30px 20px 20px 20px; +} +.material.form button { + font-size: 1.7em; + font-weight: normal; + padding: 10px 20px; +} +.material.form textarea { + height: 10em; +} +.event_crud a { + color: #3893E8!important; +} +.event_crud h2 { + font-size: 1.5em; + margin: 0px 0 30px 0; + text-align: center; +} diff --git a/public/vendor/flatpickr b/public/vendor/flatpickr new file mode 160000 index 0000000000..6b7b340b6d --- /dev/null +++ b/public/vendor/flatpickr @@ -0,0 +1 @@ +Subproject commit 6b7b340b6d26c8bda5176325bde32ea55ae83fda