From 6ee267e01b7d4de14a2a2713fa97ac8063d75bb1 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sat, 5 Jul 2014 16:50:04 +0200 Subject: [PATCH] more work on Q&A --- .gitmodules | 3 + app/controllers/QaAnswer.scala | 70 ++ app/controllers/QaComment.scala | 46 ++ app/controllers/QaControllers.scala | 10 +- app/controllers/QaQuestion.scala | 55 +- app/templating/AssetHelper.scala | 11 +- app/views/forum/layout.scala.html | 13 +- app/views/forum/topic/show.scala.html | 4 +- app/views/qa/answerList.scala.html | 59 ++ app/views/qa/ask.scala.html | 11 +- app/views/qa/commentList.scala.html | 20 + app/views/qa/edit.scala.html | 15 + app/views/qa/index.scala.html | 2 +- app/views/qa/layout.scala.html | 11 +- app/views/qa/nope.scala.html | 6 + app/views/qa/questionForm.scala.html | 7 +- app/views/qa/questionShow.scala.html | 62 ++ app/views/qa/relatedBox.scala.html | 13 + app/views/qa/tags.scala.html | 19 +- app/views/qa/vote.scala.html | 17 + app/views/timeline/entry.scala.html | 9 + conf/routes | 25 +- modules/blog/src/main/Notifier.scala | 3 +- modules/hub/src/main/actorApi.scala | 9 +- modules/message/src/main/Api.scala | 4 +- modules/mod/src/main/Modlog.scala | 36 +- modules/mod/src/main/ModlogApi.scala | 12 + modules/qa/src/main/DataForms.scala | 4 - modules/qa/src/main/Env.scala | 11 +- modules/qa/src/main/Mailer.scala | 68 -- modules/qa/src/main/Notifier.scala | 44 ++ modules/qa/src/main/QaApi.scala | 44 +- modules/qa/src/main/model.scala | 7 - .../security/src/main/SecurityHelper.scala | 3 + modules/team/src/main/Env.scala | 2 + modules/team/src/main/Notifier.scala | 2 + modules/timeline/src/main/Entry.scala | 6 + public/font15/fonts/lichess.eot | Bin 0 -> 14132 bytes public/font15/fonts/lichess.svg | 81 +++ public/font15/fonts/lichess.ttf | Bin 0 -> 13968 bytes public/font15/fonts/lichess.woff | Bin 0 -> 10160 bytes public/font15/icons-reference.html | 637 ++++++++++++++++++ public/font15/styles.css | 253 +++++++ public/javascripts/big.js | 8 + public/javascripts/forum.js | 11 - public/javascripts/qa.js | 50 ++ public/stylesheets/common.css | 4 +- public/stylesheets/forum.css | 9 +- public/stylesheets/qa.css | 145 ++++ public/vendor/tagmanager | 1 + public/vendor/typeahead.bundle.min.js | 7 + 51 files changed, 1745 insertions(+), 204 deletions(-) create mode 100644 app/controllers/QaAnswer.scala create mode 100644 app/controllers/QaComment.scala create mode 100644 app/views/qa/answerList.scala.html create mode 100644 app/views/qa/commentList.scala.html create mode 100644 app/views/qa/edit.scala.html create mode 100644 app/views/qa/nope.scala.html create mode 100644 app/views/qa/relatedBox.scala.html create mode 100644 app/views/qa/vote.scala.html delete mode 100644 modules/qa/src/main/Mailer.scala create mode 100644 modules/qa/src/main/Notifier.scala create mode 100644 public/font15/fonts/lichess.eot create mode 100644 public/font15/fonts/lichess.svg create mode 100644 public/font15/fonts/lichess.ttf create mode 100644 public/font15/fonts/lichess.woff create mode 100644 public/font15/icons-reference.html create mode 100644 public/font15/styles.css delete mode 100644 public/javascripts/forum.js create mode 100644 public/javascripts/qa.js create mode 160000 public/vendor/tagmanager create mode 100644 public/vendor/typeahead.bundle.min.js diff --git a/.gitmodules b/.gitmodules index c89e01ef3a..e26d3b398e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "scalachess"] path = modules/chess url = git://github.com/ornicar/scalachess.git +[submodule "public/vendor/tagmanager"] + path = public/vendor/tagmanager + url = https://github.com/max-favilli/tagmanager diff --git a/app/controllers/QaAnswer.scala b/app/controllers/QaAnswer.scala new file mode 100644 index 0000000000..1684c8b6ab --- /dev/null +++ b/app/controllers/QaAnswer.scala @@ -0,0 +1,70 @@ +package controllers + +import play.api.data.Form +import play.api.mvc._ + +import lila.api.Context +import lila.app._ +import lila.qa.{ QuestionId, Question, AnswerId, Answer, QuestionWithUsers, QaAuth } +import views._ + +object QaAnswer extends QaController { + + def create(id: QuestionId) = AuthBody { implicit ctx => + me => + WithQuestion(id) { q => + implicit val req = ctx.body + forms.answer.bindFromRequest.fold( + err => renderQuestion(q, Some(err)), + data => api.answer.create(data, q, me) map { answer => + Redirect(routes.QaQuestion.show(q.id, q.slug) + "#answer-" + answer.id) + } + ) + } + } + + def accept(questionId: QuestionId, answerId: AnswerId) = AuthBody { implicit ctx => + me => + (api.question findById questionId) zip (api.answer findById answerId) flatMap { + case (Some(q), Some(a)) if (QaAuth canEdit q) => + api.answer.accept(q, a) inject Redirect(routes.QaQuestion.show(q.id, q.slug) + "#answer-" + a.id) + case _ => notFound + } + } + + def vote(questionId: QuestionId, answerId: AnswerId) = AuthBody { implicit ctx => + me => + implicit val req = ctx.body + forms.vote.bindFromRequest.fold( + err => fuccess(BadRequest), + v => api.answer.vote(answerId, me, v == 1) map { + case Some(vote) => Ok(html.qa.vote(routes.QaAnswer.vote(questionId, answerId).url, vote)) + case None => NotFound + } + ) + } + + def doEdit(questionId: QuestionId, answerId: AnswerId) = AuthBody { implicit ctx => + me => + WithOwnAnswer(questionId, answerId) { q => + a => + implicit val req = ctx.body + forms.answer.bindFromRequest.fold( + err => renderQuestion(q), + data => api.answer.edit(data, a.id) map { + case None => NotFound + case Some(a2) => Redirect(routes.QaQuestion.show(q.id, q.slug) + "#answer-" + a2.id) + } + ) + } + } + + def remove(questionId: QuestionId, answerId: AnswerId) = Secure(_.ModerateQa) { implicit ctx => + me => + OptionFuRedirect(api.answer findById answerId) { a => + (api.answer remove a.id) >> + Env.mod.logApi.deleteQaAnswer(me.id, a.userId, a.body) inject + routes.QaQuestion.show(questionId, "redirect") + } + } +} diff --git a/app/controllers/QaComment.scala b/app/controllers/QaComment.scala new file mode 100644 index 0000000000..9eb4b23290 --- /dev/null +++ b/app/controllers/QaComment.scala @@ -0,0 +1,46 @@ +package controllers + +import play.api.data.Form +import play.api.mvc._ + +import lila.api.Context +import lila.app._ +import lila.qa.{ QuestionId, Question, AnswerId, Answer, QuestionWithUsers, QaAuth } +import views._ + +object QaComment extends QaController { + + def question(id: QuestionId) = AuthBody { implicit ctx => + me => + WithQuestion(id) { q => + implicit val req = ctx.body + forms.comment.bindFromRequest.fold( + err => renderQuestion(q, None), + data => api.comment.create(data, Left(q), me) map { comment => + Redirect(routes.QaQuestion.show(q.id, q.slug) + "#comment-" + comment.id) + } + ) + } + } + + def answer(questionId: QuestionId, answerId: AnswerId) = AuthBody { implicit ctx => + me => + (api.question findById questionId) zip (api.answer findById answerId) flatMap { + case (Some(q), Some(a)) => + implicit val req = ctx.body + forms.comment.bindFromRequest.fold( + err => renderQuestion(q, None), + data => api.comment.create(data, Right(a), me) map { comment => + Redirect(routes.QaQuestion.show(q.id, q.slug) + "#comment-" + comment.id) + } + ) + case _ => notFound + } + } + + def remove(questionId: QuestionId, commentId: String) = Secure(_.ModerateQa) { implicit ctx => + me => + api.comment.remove(questionId, commentId) inject + Redirect(routes.QaQuestion.show(questionId, "redirect")) + } +} diff --git a/app/controllers/QaControllers.scala b/app/controllers/QaControllers.scala index 909c42e5e1..62cef15e1c 100644 --- a/app/controllers/QaControllers.scala +++ b/app/controllers/QaControllers.scala @@ -25,14 +25,22 @@ trait QaController extends LilaController { case _ => notFound } + protected def WithQuestion(id: QuestionId)(block: Question => Fu[Result])(implicit ctx: Context): Fu[Result] = + OptionFuResult(api.question findById id)(block) + protected def WithQuestion(id: QuestionId, slug: String)(block: Question => Fu[Result])(implicit ctx: Context): Fu[Result] = - OptionFuResult(api.question findById id) { q => + WithQuestion(id) { q => if (slug != q.slug) fuccess(Redirect { controllers.routes.QaQuestion.show(id, q.slug) }) else block(q) } + protected def WithOwnQuestion(id: QuestionId)(block: Question => Fu[Result])(implicit ctx: Context): Fu[Result] = + WithQuestion(id) { q => + if (QaAuth canEdit q) block(q) + else fuccess(Unauthorized) + } protected def WithOwnQuestion(id: QuestionId, slug: String)(block: Question => Fu[Result])(implicit ctx: Context): Fu[Result] = WithQuestion(id, slug) { q => if (QaAuth canEdit q) block(q) diff --git a/app/controllers/QaQuestion.scala b/app/controllers/QaQuestion.scala index fa5d5df686..666d300845 100644 --- a/app/controllers/QaQuestion.scala +++ b/app/controllers/QaQuestion.scala @@ -13,7 +13,7 @@ object QaQuestion extends QaController { def index(page: Option[Int] = None) = Open { implicit ctx => api.question.paginatorWithUsers(page getOrElse 1, 20) zip (api.question popular 10) map { - case (questions, popular) => Ok(views.html.qa.index(questions, popular)) + case (questions, popular) => Ok(html.qa.index(questions, popular)) } } @@ -23,14 +23,14 @@ object QaQuestion extends QaController { case "" => (api.question recent 20 flatMap api.question.zipWithUsers) case _ => Env.qa search query flatMap api.question.zipWithUsers }) zip (api.question popular 10) map { - case (questions, popular) => Ok(views.html.qa.search(query, questions, popular)) + case (questions, popular) => Ok(html.qa.search(query, questions, popular)) } } def byTag(tag: String) = Open { implicit ctx => (api.question.byTag(tag, 20) flatMap api.question.zipWithUsers) zip (api.question popular 10) map { - case (questions, popular) => Ok(views.html.qa.byTag(tag, questions, popular)) + case (questions, popular) => Ok(html.qa.byTag(tag, questions, popular)) } } @@ -42,7 +42,7 @@ object QaQuestion extends QaController { private def renderAsk(form: Form[_], status: Results.Status)(implicit ctx: Context) = api.question popular 10 zip api.tag.all map { - case (popular, tags) => status(views.html.qa.ask(form, tags, popular)) + case (popular, tags) => status(html.qa.ask(form, tags, popular)) } def ask = Auth { implicit ctx => @@ -60,4 +60,51 @@ object QaQuestion extends QaController { } ) } + + def edit(id: QuestionId, slug: String) = Auth { implicit ctx => + me => + WithOwnQuestion(id, slug) { q => + renderEdit(forms editQuestion q, q, Results.Ok) + } + } + + def doEdit(id: QuestionId) = AuthBody { implicit ctx => + me => + WithOwnQuestion(id) { q => + implicit val req = ctx.body + forms.question.bindFromRequest.fold( + err => renderEdit(err, q, Results.BadRequest), + data => api.question.edit(data, q.id) map { + case None => NotFound + case Some(q2) => Redirect(routes.QaQuestion.show(q2.id, q2.slug)) + } + ) + } + } + + private def renderEdit(form: Form[_], q: Question, status: Results.Status)(implicit ctx: Context) = + api.question popular 10 zip api.tag.all map { + case (popular, tags) => status(html.qa.edit(form, q, tags, popular)) + } + + def vote(id: QuestionId) = AuthBody { implicit ctx => + me => + implicit val req = ctx.body + forms.vote.bindFromRequest.fold( + err => fuccess(BadRequest), + v => api.question.vote(id, me, v == 1) map { + case Some(vote) => Ok(html.qa.vote(routes.QaQuestion.vote(id).url, vote)) + case None => NotFound + } + ) + } + + def remove(questionId: QuestionId) = Secure(_.ModerateQa) { implicit ctx => + me => + WithQuestion(questionId) { q => + (api.question remove q.id) >> + Env.mod.logApi.deleteQaQuestion(me.id, q.userId, q.title) inject + Redirect(routes.QaQuestion.index()) + } + } } diff --git a/app/templating/AssetHelper.scala b/app/templating/AssetHelper.scala index fb84bf2071..908254e46d 100644 --- a/app/templating/AssetHelper.scala +++ b/app/templating/AssetHelper.scala @@ -18,7 +18,6 @@ trait AssetHelper { def cssTag(name: String, staticDomain: Boolean = true) = cssAt("stylesheets/" + name, staticDomain) - def cssVendorTag(name: String, staticDomain: Boolean = true) = cssAt("vendor/" + name, staticDomain) def cssAt(path: String, staticDomain: Boolean = true) = Html { @@ -60,6 +59,16 @@ trait AssetHelper { test = "window._", local = staticUrl("vendor/underscorejs.min.js")) + val tagmanagerTag = cdnOrLocal( + cdn = "http://cdnjs.cloudflare.com/ajax/libs/tagmanager/3.0.0/tagmanager.js", + test = "$.tagsManager", + local = staticUrl("vendor/tagmanager/tagmanager.js")) + + val typeaheadTag = cdnOrLocal( + cdn = "http://cdnjs.cloudflare.com/ajax/libs/typeahead.js/0.10.2/typeahead.bundle.min.js", + test = "$.typeahead", + local = staticUrl("vendor/typeahead.bundle.min.js")) + private def cdnOrLocal(cdn: String, test: String, local: String) = Html { if (isProd) s"""""" diff --git a/app/views/forum/layout.scala.html b/app/views/forum/layout.scala.html index ee1c65b691..6e6d0b5cbe 100644 --- a/app/views/forum/layout.scala.html +++ b/app/views/forum/layout.scala.html @@ -1,19 +1,10 @@ @(title: String, searchText: String = "", goodies: Option[Html] = None, moreJs: Html = Html(""))(body: Html)(implicit ctx: Context) -@moreCss = { -@cssTag("forum.css") -} - -@forumJs = { -@moreJs -@jsTag("forum.js") -} - @base.layout( title = title, goodies = goodies, -moreCss = moreCss, -moreJs = forumJs, +moreCss = cssTag("forum.css"), +moreJs = moreJs, active = siteMenu.forum.some) {
@forum.searchForm(searchText.trim) diff --git a/app/views/forum/topic/show.scala.html b/app/views/forum/topic/show.scala.html index 2d92b2c385..d797f9210e 100644 --- a/app/views/forum/topic/show.scala.html +++ b/app/views/forum/topic/show.scala.html @@ -29,10 +29,10 @@ title = topic.name) { @momentFromNow(post.createdAt) #@post.number @if(isGranted(_.IpBan)) { - @post.ip + @post.ip } @if(isGrantedMod(categ.slug)) { - Delete + Delete }

@autoLink(post.text)

diff --git a/app/views/qa/answerList.scala.html b/app/views/qa/answerList.scala.html new file mode 100644 index 0000000000..cf2adcb920 --- /dev/null +++ b/app/views/qa/answerList.scala.html @@ -0,0 +1,59 @@ +@(q: lila.qa.Question, as: List[lila.qa.AnswerWithUserAndComments])(implicit ctx: Context) + +
+ @as.size match { + case 0 => { + Be the first to answer! + } + case 1 => { + One answer + } + case n => { + @n Answers + } + } +
+
+ @as.map { a => +
+
+ @views.html.qa.vote(routes.QaAnswer.vote(q.id, a.answer.id).url, a.answer.vote) + @if(a.answer.accepted) { + + } else { + @if(lila.qa.QaAuth canEdit q) { +
+ +
+ } + } +
+
+
+
+ @autoLink(a.answer.body) + @if(lila.qa.QaAuth canEdit a.answer) { +
+ + +
+ } + +
+
+ @views.html.qa.commentList(q, a.comments, routes.QaComment.answer(q.id, a.answer.id).url) +
+
+
+
+ } +
diff --git a/app/views/qa/ask.scala.html b/app/views/qa/ask.scala.html index e41a9541f6..e4631ed298 100644 --- a/app/views/qa/ask.scala.html +++ b/app/views/qa/ask.scala.html @@ -3,13 +3,16 @@ @title = @{ "Q&A - Ask a new question" } @layout( -title = title, +title = "Ask a new question" , goodies = popularBox(popular).some) { -

@title

+

+ + Ask a new question +

-
+ @questionForm(form, tags) - +
} diff --git a/app/views/qa/commentList.scala.html b/app/views/qa/commentList.scala.html new file mode 100644 index 0000000000..b7ec5f46ef --- /dev/null +++ b/app/views/qa/commentList.scala.html @@ -0,0 +1,20 @@ +@(q: lila.qa.Question, comments: List[lila.qa.CommentWithUser], action: String)(implicit ctx: Context) + +@comments.map { c => +
+ + @autoLink(c.comment.body) +
+} +@ctx.me.map { user => +
+ Add comment +
+ + +
+
+} diff --git a/app/views/qa/edit.scala.html b/app/views/qa/edit.scala.html new file mode 100644 index 0000000000..c607c22ee6 --- /dev/null +++ b/app/views/qa/edit.scala.html @@ -0,0 +1,15 @@ +@(form: Form[_], q: lila.qa.Question, tags: List[String], popular: List[lila.qa.Question])(implicit ctx: Context) + +@title = @{ s"""Edit question "${q.title}"""" } + +@layout( +title = title, +goodies = popularBox(popular).some) { + +

@title

+ +
+ @questionForm(form, tags) + +
+} diff --git a/app/views/qa/index.scala.html b/app/views/qa/index.scala.html index acf82c2fd5..6b2ba12339 100644 --- a/app/views/qa/index.scala.html +++ b/app/views/qa/index.scala.html @@ -5,7 +5,7 @@ } @layout( -title = "Questions & answers", +title = "Questions & Answers", goodies = goodies.some) {

Questions & Answers

diff --git a/app/views/qa/layout.scala.html b/app/views/qa/layout.scala.html index 065fe26f78..b80e01205e 100644 --- a/app/views/qa/layout.scala.html +++ b/app/views/qa/layout.scala.html @@ -6,12 +6,19 @@ @goodies } +@moreJs = { +@tagmanagerTag +@typeaheadTag +@jsTag("qa.js") +} + @base.layout( -title = title, +title = s"Q&A - $title", moreCss = cssTag("qa.css"), +moreJs = moreJs, goodies = side.some, openGraph = openGraph) { -
+
@body
} diff --git a/app/views/qa/nope.scala.html b/app/views/qa/nope.scala.html new file mode 100644 index 0000000000..0bc90ed399 --- /dev/null +++ b/app/views/qa/nope.scala.html @@ -0,0 +1,6 @@ +@(title: String, url: Call)(implicit ctx: Context) +@if(isGranted(_.ModerateQa)) { +
+ +
+} diff --git a/app/views/qa/questionForm.scala.html b/app/views/qa/questionForm.scala.html index ba7a2a7aae..3f581f71a0 100644 --- a/app/views/qa/questionForm.scala.html +++ b/app/views/qa/questionForm.scala.html @@ -1,15 +1,16 @@ @(form: Form[_], tags: List[String])(implicit ctx: Context) - + @errMsg(form("title")) @errMsg(form("body")) -
+
+
diff --git a/app/views/qa/questionShow.scala.html b/app/views/qa/questionShow.scala.html index fb60ea827b..125c39de54 100644 --- a/app/views/qa/questionShow.scala.html +++ b/app/views/qa/questionShow.scala.html @@ -1 +1,63 @@ @(q: lila.qa.Question, user: User, answers: List[lila.qa.AnswerWithUserAndComments], comments: List[lila.qa.CommentWithUser], popular: List[lila.qa.Question], related: List[lila.qa.Question], answerForm: Option[Form[_]])(implicit ctx: Context) + +@goodies = { +@relatedBox(related) +@popularBox(popular) +} + +@layout( +title = q.title, +goodies = goodies.some) { + +
+

+ @q.title +

+ + + + + + + @if(q.tags.nonEmpty) { + + + + + } + + + + + + + + + +
Asked@momentFromNow(q.createdAt) by @userLink(user)
Tags@tags(q.tags)
ActivityViewed @q.views times, last updated @momentFromNow(q.updatedAt)
Actions + @if(lila.qa.QaAuth canEdit q) { + Edit question + } + @nope("Remove question", routes.QaQuestion.remove(q.id)) +
+
+ @autoLink(q.body) +
+
+ @views.html.qa.commentList(q, comments, routes.QaComment.question(q.id).url) +
+ @answerList(q, answers) + @answerForm.map { form => +
+ @if(answers.nonEmpty) { +

Your answer

+ } +
+ + @errMsg(form("body")) + +
+
+
+} +} diff --git a/app/views/qa/relatedBox.scala.html b/app/views/qa/relatedBox.scala.html new file mode 100644 index 0000000000..9494d19ce8 --- /dev/null +++ b/app/views/qa/relatedBox.scala.html @@ -0,0 +1,13 @@ +@(qs: List[lila.qa.Question])(implicit ctx: Context) + + diff --git a/app/views/qa/tags.scala.html b/app/views/qa/tags.scala.html index 4386525871..7e3ff20250 100644 --- a/app/views/qa/tags.scala.html +++ b/app/views/qa/tags.scala.html @@ -1,13 +1,8 @@ @(tags: Seq[String]) -
- @defining(tags.size - 1) { last => - @tags.zipWithIndex.map { - case (tag, i) => { - @tag@if(i != last) {,} - } - } - } - @if(tags.isEmpty) { -   - } -
+@defining(tags.size - 1) { last => +@tags.zipWithIndex.map { +case (tag, i) => { +@tag@if(i != last) {,} +} +} +} diff --git a/app/views/qa/vote.scala.html b/app/views/qa/vote.scala.html new file mode 100644 index 0000000000..b75e0b2eb2 --- /dev/null +++ b/app/views/qa/vote.scala.html @@ -0,0 +1,17 @@ +@(url: String, vote: lila.qa.Vote)(implicit ctx: Context) + +@defining(ctx.userId.flatMap(vote.of)) { myVote => +
+ + @vote.score + +
+} diff --git a/app/views/timeline/entry.scala.html b/app/views/timeline/entry.scala.html index 27dab049a8..730783c16f 100644 --- a/app/views/timeline/entry.scala.html +++ b/app/views/timeline/entry.scala.html @@ -22,6 +22,15 @@ case NoteCreate(fromId, toId) => { case TourJoin(userId, tourId, tourName) => { @userIdLink(userId.some, withOnline = false) competes in @trans.xTournament(tourName) } +case QaQuestion(userId, id, title) => { +@userIdLink(userId.some, withOnline = false) asked @title +} +case QaAnswer(userId, id, title, answerId) => { +@userIdLink(userId.some, withOnline = false) answered @title +} +case QaComment(userId, id, title, commentId) => { +@userIdLink(userId.some, withOnline = false) commented @title +} } @momentFromNow(e.date) } diff --git a/conf/routes b/conf/routes index 116bbf684c..a346670de5 100644 --- a/conf/routes +++ b/conf/routes @@ -260,19 +260,18 @@ GET /qa/ask controllers.QaQuestion.ask POST /qa/ask controllers.QaQuestion.doAsk GET /qa/tag/:slug controllers.QaQuestion.byTag(slug: String) GET /qa/:id/:slug controllers.QaQuestion.show(id: Int, slug: String) -# GET /qa/:id/:slug/edit controllers.QaQuestion.edit(id: Int, slug: String) -# POST /qa/:id/:slug/edit controllers.QaQuestion.doEdit(id: Int, slug: String) -# POST /qa/:id/vote controllers.QaQuestion.vote(id: Int) -# POST /qa/:id/favorite controllers.QaQuestion.favorite(id: Int) -# POST /qa/:id/rm controllers.QaQuestion.remove(id: Int) -# POST /qa/:id/answer controllers.QaAnswer.create(id: Int) -# POST /qa/:id/:a/vote controllers.QaAnswer.vote(id: Int, a: Int) -# POST /qa/:id/:a/accept controllers.QaAnswer.accept(id: Int, a: Int) -# POST /qa/:id/:a/edit-answer controllers.QaAnswer.doEdit(id: Int, a: Int) -# POST /qa/:id/:a/rm-answer controllers.QaAnswer.remove(id: Int, a: Int) -# POST /qa/:id/comment controllers.QaComment.question(id: Int) -# POST /qa/:id/:a/comment controllers.QaComment.answer(id: Int, a: Int) -# POST /qa/:id/:c/rm-comment controllers.QaComment.remove(id: Int, c: String) +GET /qa/:id/:slug/edit controllers.QaQuestion.edit(id: Int, slug: String) +POST /qa/:id/edit controllers.QaQuestion.doEdit(id: Int) +POST /qa/:id/vote controllers.QaQuestion.vote(id: Int) +POST /qa/:id/rm controllers.QaQuestion.remove(id: Int) +POST /qa/:id/answer controllers.QaAnswer.create(id: Int) +POST /qa/:id/:a/vote controllers.QaAnswer.vote(id: Int, a: Int) +POST /qa/:id/:a/accept controllers.QaAnswer.accept(id: Int, a: Int) +POST /qa/:id/:a/edit-answer controllers.QaAnswer.doEdit(id: Int, a: Int) +POST /qa/:id/:a/rm-answer controllers.QaAnswer.remove(id: Int, a: Int) +POST /qa/:id/comment controllers.QaComment.question(id: Int) +POST /qa/:id/:a/comment controllers.QaComment.answer(id: Int, a: Int) +POST /qa/:id/:c/rm-comment controllers.QaComment.remove(id: Int, c: String) # API GET /api/user controllers.Api.users diff --git a/modules/blog/src/main/Notifier.scala b/modules/blog/src/main/Notifier.scala index 7efadb85fa..96b70e3011 100644 --- a/modules/blog/src/main/Notifier.scala +++ b/modules/blog/src/main/Notifier.scala @@ -20,7 +20,7 @@ private[blog] final class Notifier( (ThreadRepo reallyDeleteByCreatorId lichessUserId) >> { val thread = makeThread(post) val futures = userIds.toStream map { userId => - messageApi.lichessThread(thread.copy(to = userId), lichessUserId) + messageApi.lichessThread(thread.copy(to = userId)) } lila.common.Future.lazyFold(futures)(())((_, _) => ()) >>- lastPostCache.clear } @@ -33,6 +33,7 @@ private[blog] final class Notifier( private def makeThread(doc: io.prismic.Document) = lila.hub.actorApi.message.LichessThread( + from = lichessUserId, to = "", subject = s"New blog post: ${~doc.getText("blog.title")}", message = s"""${~doc.getText("blog.shortlede")} diff --git a/modules/hub/src/main/actorApi.scala b/modules/hub/src/main/actorApi.scala index a650a49d69..f1ddd442db 100644 --- a/modules/hub/src/main/actorApi.scala +++ b/modules/hub/src/main/actorApi.scala @@ -76,6 +76,9 @@ case class TeamCreate(userId: String, teamId: String) extends Atom case class ForumPost(userId: String, topicName: String, postId: String) extends Atom case class NoteCreate(from: String, to: String) extends Atom case class TourJoin(userId: String, tourId: String, tourName: String) extends Atom +case class QaQuestion(userId: String, id: Int, title: String) extends Atom +case class QaAnswer(userId: String, id: Int, title: String, answerId: Int) extends Atom +case class QaComment(userId: String, id: Int, title: String, commentId: String) extends Atom object atomFormat { implicit val followFormat = Json.format[Follow] @@ -84,6 +87,9 @@ object atomFormat { implicit val forumPostFormat = Json.format[ForumPost] implicit val noteCreateFormat = Json.format[NoteCreate] implicit val tourJoinFormat = Json.format[TourJoin] + implicit val qaQuestionFormat = Json.format[QaQuestion] + implicit val qaAnswerFormat = Json.format[QaAnswer] + implicit val qaCommentFormat = Json.format[QaComment] } object propagation { @@ -99,6 +105,7 @@ import propagation._ case class Propagate(data: Atom, propagations: List[Propagation] = Nil) { def toUsers(ids: List[String]) = add(Users(ids)) + def toUser(id: String) = add(Users(List(id))) def toFollowersOf(id: String) = add(Followers(id)) def toFriendsOf(id: String) = add(Friends(id)) def toStaffFriendsOf(id: String) = add(StaffFriends(id)) @@ -113,7 +120,7 @@ case object Count } package message { -case class LichessThread(to: String, subject: String, message: String) +case class LichessThread(from: String, to: String, subject: String, message: String) } package router { diff --git a/modules/message/src/main/Api.scala b/modules/message/src/main/Api.scala index 3ced9607be..e7510f013a 100644 --- a/modules/message/src/main/Api.scala +++ b/modules/message/src/main/Api.scala @@ -55,11 +55,11 @@ final class Api( } } - def lichessThread(lt: LichessThread, creatorId: String = "lichess"): Funit = + def lichessThread(lt: LichessThread): Funit = $insert(Thread.make( name = lt.subject, text = lt.message, - creatorId = creatorId, + creatorId = lt.from, invitedId = lt.to)) >> unreadCache.clear(lt.to) def makePost(thread: Thread, text: String, me: User) = { diff --git a/modules/mod/src/main/Modlog.scala b/modules/mod/src/main/Modlog.scala index 4001ea97cf..f7282733f6 100644 --- a/modules/mod/src/main/Modlog.scala +++ b/modules/mod/src/main/Modlog.scala @@ -11,21 +11,24 @@ case class Modlog( date: DateTime = DateTime.now) { def showAction = action match { - case Modlog.engine => "mark as engine" - case Modlog.unengine => "un-mark as engine" - case Modlog.deletePost => "delete forum post" - case Modlog.ban => "ban user" - case Modlog.ipban => "ban IPs" - case Modlog.ipunban => "unban IPs" - case Modlog.closeAccount => "close account" - case Modlog.reopenAccount => "reopen account" - case Modlog.openTopic => "reopen topic" - case Modlog.closeTopic => "close topic" - case Modlog.showTopic => "show topic" - case Modlog.hideTopic => "hide topic" - case Modlog.setTitle => "set FIDE title" - case Modlog.removeTitle => "remove FIDE title" - case a => a + case Modlog.engine => "mark as engine" + case Modlog.unengine => "un-mark as engine" + case Modlog.deletePost => "delete forum post" + case Modlog.ban => "ban user" + case Modlog.ipban => "ban IPs" + case Modlog.ipunban => "unban IPs" + case Modlog.closeAccount => "close account" + case Modlog.reopenAccount => "reopen account" + case Modlog.openTopic => "reopen topic" + case Modlog.closeTopic => "close topic" + case Modlog.showTopic => "show topic" + case Modlog.hideTopic => "hide topic" + case Modlog.setTitle => "set FIDE title" + case Modlog.removeTitle => "remove FIDE title" + case Modlog.deleteQaQuestion => "delete Q&A question" + case Modlog.deleteQaAnswer => "delete Q&A answer" + case Modlog.deleteQaComment => "delete Q&A comment" + case a => a } override def toString = s"$mod $showAction ${~user}" @@ -49,6 +52,9 @@ object Modlog { val hideTopic = "hideTopic" val setTitle = "setTitle" val removeTitle = "removeTitle" + val deleteQaQuestion = "deleteQaQuestion" + val deleteQaAnswer = "deleteQaAnswer" + val deleteQaComment = "deleteQaComment" import lila.db.JsTube import JsTube.Helpers._ diff --git a/modules/mod/src/main/ModlogApi.scala b/modules/mod/src/main/ModlogApi.scala index cce86c948c..c9b958bbe2 100644 --- a/modules/mod/src/main/ModlogApi.scala +++ b/modules/mod/src/main/ModlogApi.scala @@ -53,6 +53,18 @@ final class ModlogApi { )) } + def deleteQaQuestion(mod: String, user: String, title: String) = add { + Modlog(mod, user.some, Modlog.deleteQaQuestion, details = Some(title take 140)) + } + + def deleteQaAnswer(mod: String, user: String, text: String) = add { + Modlog(mod, user.some, Modlog.deleteQaAnswer, details = Some(text take 140)) + } + + def deleteQaComment(mod: String, user: String, text: String) = add { + Modlog(mod, user.some, Modlog.deleteQaComment, details = Some(text take 140)) + } + def recent = $find($query($select.all) sort $sort.naturalDesc, 100) private def add(m: Modlog): Funit = { diff --git a/modules/qa/src/main/DataForms.scala b/modules/qa/src/main/DataForms.scala index 95ba2f2750..3c5970a0bd 100644 --- a/modules/qa/src/main/DataForms.scala +++ b/modules/qa/src/main/DataForms.scala @@ -42,8 +42,4 @@ object DataForms { val vote = Form(single( "vote" -> number )) - - val favorite = Form(single( - "favorite" -> number - )) } diff --git a/modules/qa/src/main/Env.scala b/modules/qa/src/main/Env.scala index 5616b91791..5594146a23 100644 --- a/modules/qa/src/main/Env.scala +++ b/modules/qa/src/main/Env.scala @@ -5,18 +5,24 @@ import lila.common.PimpedConfig._ final class Env( config: Config, + hub: lila.hub.Env, db: lila.db.Env) { private val CollectionQuestion = config getString "collection.question" private val CollectionAnswer = config getString "collection.answer" - private val NotifyUserId = config getString "notify.user_id" + private val NotifierSender = config getString "notifier.sender" private lazy val questionColl = db(CollectionQuestion) lazy val api = new QaApi( questionColl = questionColl, answerColl = db(CollectionAnswer), - mailer = new Mailer(NotifyUserId)) + notifier = notifier) + + private lazy val notifier = new Notifier( + sender = NotifierSender, + messenger = hub.actor.messenger, + timeline = hub.actor.timeline) lazy val search = new Search(questionColl) @@ -27,5 +33,6 @@ object Env { lazy val current = "[boot] qa" describes new Env( config = lila.common.PlayApp loadConfig "qa", + hub = lila.hub.Env.current, db = lila.db.Env.current) } diff --git a/modules/qa/src/main/Mailer.scala b/modules/qa/src/main/Mailer.scala deleted file mode 100644 index 108130b21c..0000000000 --- a/modules/qa/src/main/Mailer.scala +++ /dev/null @@ -1,68 +0,0 @@ -package lila.qa - -import lila.common.String._ -import lila.user.User - -private[qa] final class Mailer(sender: String) { - - private[qa] def createAnswer(q: Question, a: Answer, u: User, favoriters: List[User]): Funit = ??? - // send( - // to = (rudyEmail :: user.email :: favoriters.map(_.email)) filterNot (u.email.==), - // subject = s"""${u.displaynameOrFullname} answered your question""", - // content = s"""New answer on prismic.io Q&A: ${questionUrl(q)}#answer-${a.id} - - -// By ${u.displaynameOrFullname} -// On question ${q.title} - -// ${a.body} - -// URL: ${questionUrl(q)}#answer-${a.id}""") - - private[qa] def createQuestionComment(q: Question, c: Comment, u: User): Funit = ??? - // send( - // to = List(rudyEmail, questionAuthor.email) filterNot (u.email.==), - // subject = s"""${u.displaynameOrFullname} commented your question""", - // content = s"""New comment on prismic.io Q&A: ${questionUrl(question)}#comment-${c.id} - - -// By ${u.displaynameOrFullname} -// On question ${question.title} - -// ${c.body} - -// URL: ${questionUrl(question)}#comment-${c.id}""") - // case _ => Future successful () - - private[qa] def createAnswerComment(q: Question, a: Answer, c: Comment, u: User): Funit = - ??? - // QaApi.answer.withUser(a) flatMap { - // case Some(AnswerWithUser(answer, answerAuthor)) => send( - // to = List(rudyEmail, answerAuthor.email) filterNot (u.email.==), - // subject = s"""${u.displaynameOrFullname} commented your answer""", - // content = s"""New comment on prismic.io Q&A: ${questionUrl(q)}#comment-${c.id} - - -// By ${u.displaynameOrFullname} -// On question ${q.title} - -// ${c.body} - -// URL: ${questionUrl(q)}#comment-${c.id}""") - - private def questionUrl(q: Question) = - s"http://lichess.org/qa/${q.id}/${q.slug}" - - private def send(to: List[String], subject: String, content: String) = { - to foreach { recipient => - // common.utils.Mailer.send( - // to = recipient, - // from = Some(sender), - // fromname = Some(common.Wroom.domain), - // subject = s"[Q&A] $subject", - // content = Html(nl2br(content))) - } - fuccess(()) - } -} - diff --git a/modules/qa/src/main/Notifier.scala b/modules/qa/src/main/Notifier.scala new file mode 100644 index 0000000000..e9919d1ef0 --- /dev/null +++ b/modules/qa/src/main/Notifier.scala @@ -0,0 +1,44 @@ +package lila.qa + +import lila.common.String._ +import lila.hub.actorApi.message.LichessThread +import lila.hub.actorApi.timeline.{ Propagate, QaQuestion, QaAnswer, QaComment } +import lila.user.User + +import akka.actor.ActorSelection + +private[qa] final class Notifier( + sender: String, + messenger: ActorSelection, + timeline: ActorSelection) { + + private[qa] def createQuestion(q: Question, u: User) { + val msg = Propagate(QaQuestion(u.id, q.id, q.title)) + timeline ! (msg toFollowersOf u.id) + } + + private[qa] def createAnswer(q: Question, a: Answer, u: User) { + val msg = Propagate(QaAnswer(u.id, q.id, q.title, a.id)) + timeline ! (msg toFollowersOf u.id toUser q.userId exceptUser u.id) + messenger ! LichessThread( + from = sender, + to = q.userId, + subject = s"""${u.username} replied to your question""", + message = s"""Your question "${q.title}" got a new answer from ${u.username}! + +Check it out on ${questionUrl(q)}#answer-${a.id}""") + } + + private[qa] def createQuestionComment(q: Question, c: Comment, u: User) { + val msg = Propagate(QaComment(u.id, q.id, q.title, c.id)) + timeline ! (msg toFollowersOf u.id toUser q.userId exceptUser u.id) + } + + private[qa] def createAnswerComment(q: Question, a: Answer, c: Comment, u: User) { + val msg = Propagate(QaComment(u.id, q.id, q.title, c.id)) + timeline ! (msg toFollowersOf u.id toUser a.userId exceptUser u.id) + } + + private def questionUrl(q: Question) = + s"http://lichess.org/qa/${q.id}/${q.slug}" +} diff --git a/modules/qa/src/main/QaApi.scala b/modules/qa/src/main/QaApi.scala index 25eaefa7a1..a59b23b1f9 100644 --- a/modules/qa/src/main/QaApi.scala +++ b/modules/qa/src/main/QaApi.scala @@ -14,7 +14,7 @@ import lila.db.paginator._ import lila.db.Types.Coll import lila.user.{ User, UserRepo } -final class QaApi(questionColl: Coll, answerColl: Coll, mailer: Mailer) { +final class QaApi(questionColl: Coll, answerColl: Coll, notifier: Notifier) { object question { @@ -31,7 +31,6 @@ final class QaApi(questionColl: Coll, answerColl: Coll, mailer: Mailer) { body = data.body, tags = data.tags, vote = Vote(Set.empty, Set.empty, 0), - favoriters = Set.empty, comments = Nil, views = 0, answers = 0, @@ -42,7 +41,8 @@ final class QaApi(questionColl: Coll, answerColl: Coll, mailer: Mailer) { (questionColl insert q) >> tag.clearCache >> - relation.clearCache inject q + relation.clearCache >>- + notifier.createQuestion(q, user) inject q } def edit(data: DataForms.QuestionData, id: QuestionId): Fu[Option[Question]] = findById(id) flatMap { @@ -89,13 +89,6 @@ final class QaApi(questionColl: Coll, answerColl: Coll, mailer: Mailer) { .sort(BSONDocument("createdAt" -> -1)) .cursor[Question].collect[List](max) - def favoriteByUser(u: User, max: Int): Fu[List[Question]] = - questionColl.find(BSONDocument("favoriters" -> u.id)) - .sort(BSONDocument("createdAt" -> -1)) - .cursor[Question].collect[List](max) - - def favoriters(q: Question): Fu[List[User]] = UserRepo byIds q.favoriters.toList - def popular(max: Int): Fu[List[Question]] = questionColl.find(BSONDocument()) .sort(BSONDocument("vote.score" -> -1)) @@ -147,17 +140,6 @@ final class QaApi(questionColl: Coll, answerColl: Coll, mailer: Mailer) { case None => fuccess(none) } - def favorite(id: QuestionId, user: User, v: Boolean): Fu[Option[Question]] = - question findById id flatMap { - case Some(q) => - val newFavs = q.setFavorite(user.id, v) - questionColl.update( - BSONDocument("_id" -> q.id), - BSONDocument("$set" -> BSONDocument("favoriters" -> newFavs.favoriters, "updatedAt" -> DateTime.now)) - ) >> profile.clearCache inject Some(newFavs) - case None => fuccess(none) - } - def incViews(q: Question) = questionColl.update( BSONDocument("_id" -> q.id), BSONDocument("$inc" -> BSONDocument("views" -> BSONInteger(1)))) @@ -208,11 +190,8 @@ final class QaApi(questionColl: Coll, answerColl: Coll, mailer: Mailer) { editedAt = None) (answerColl insert a) >> - (question recountAnswers q.id) >> { - question favoriters q flatMap { - mailer.createAnswer(q, a, user, _) - } - } inject a + (question recountAnswers q.id) >>- + notifier.createAnswer(q, a, user) inject a } def edit(data: DataForms.AnswerData, id: AnswerId): Fu[Option[Answer]] = findById(id) flatMap { @@ -315,12 +294,13 @@ final class QaApi(questionColl: Coll, answerColl: Coll, mailer: Mailer) { userId = user.id, body = data.body, createdAt = DateTime.now) - subject.fold(question addComment c, answer addComment c) >> { + subject.fold(question addComment c, answer addComment c) >>- { subject match { - case Left(q) => funit - case Right(a) => question findById a.questionId flatMap { - case None => funit - case Some(q) => mailer.createAnswerComment(q, a, c, user) + case Left(q) => notifier.createQuestionComment(q, c, user) + case Right(a) => question findById a.questionId foreach { + _ foreach { q => + notifier.createAnswerComment(q, a, c, user) + } } } } inject c @@ -365,7 +345,7 @@ final class QaApi(questionColl: Coll, answerColl: Coll, mailer: Mailer) { question.recentByUser(u, 300) zip answer.recentByUser(u, 500) map { case (qs, as) => Profile( reputation = math.max(0, qs.map { q => - q.vote.score + q.favoriters.size + q.vote.score }.sum + as.map { a => a.vote.score + (if (a.accepted && !qs.exists(_.userId == a.userId)) 5 else 0) }.sum), diff --git a/modules/qa/src/main/model.scala b/modules/qa/src/main/model.scala index 0af3029f8e..3248c4ad57 100644 --- a/modules/qa/src/main/model.scala +++ b/modules/qa/src/main/model.scala @@ -11,7 +11,6 @@ case class Question( body: String, // markdown tags: List[String], vote: Vote, - favoriters: Set[String], comments: List[Comment], views: Int, answers: Int, @@ -39,12 +38,6 @@ case class Question( def editNow = copy(editedAt = Some(DateTime.now)).updateNow def accepted = acceptedAt.isDefined - - def favorite(user: User): Boolean = favoriters(user.id) - - def setFavorite(userId: String, v: Boolean) = copy( - favoriters = if (v) favoriters + userId else favoriters - userId - ) } case class QuestionWithUser(question: Question, user: User) diff --git a/modules/security/src/main/SecurityHelper.scala b/modules/security/src/main/SecurityHelper.scala index 9cdc7209af..37a2b7a819 100644 --- a/modules/security/src/main/SecurityHelper.scala +++ b/modules/security/src/main/SecurityHelper.scala @@ -7,6 +7,9 @@ trait SecurityHelper { def isGranted(permission: Permission)(implicit ctx: UserContext): Boolean = ctx.me ?? Granter(permission) + def isGranted(permission: Permission.type => Permission)(implicit ctx: UserContext): Boolean = + isGranted(permission(Permission)) + def isGranted(permission: Permission, user: User): Boolean = Granter(permission)(user) } diff --git a/modules/team/src/main/Env.scala b/modules/team/src/main/Env.scala index aecc22d0aa..678fbb4d7f 100644 --- a/modules/team/src/main/Env.scala +++ b/modules/team/src/main/Env.scala @@ -12,6 +12,7 @@ final class Env(config: Config, hub: lila.hub.Env, db: lila.db.Env) { val CollectionRequest = config getString "collection.request" val PaginatorMaxPerPage = config getInt "paginator.max_per_page" val PaginatorMaxUserPerPage = config getInt "paginator.max_user_per_page" + val NotifierSender = config getString "notifier.sender" } import settings._ @@ -37,6 +38,7 @@ final class Env(config: Config, hub: lila.hub.Env, db: lila.db.Env) { private[team] lazy val memberColl = db(CollectionMember) private lazy val notifier = new Notifier( + sender = NotifierSender, messenger = hub.actor.messenger, router = hub.actor.router) } diff --git a/modules/team/src/main/Notifier.scala b/modules/team/src/main/Notifier.scala index 6018bdc1c4..22c1690c8e 100644 --- a/modules/team/src/main/Notifier.scala +++ b/modules/team/src/main/Notifier.scala @@ -7,6 +7,7 @@ import lila.hub.actorApi.message.LichessThread import lila.hub.actorApi.router._ private[team] final class Notifier( + sender: String, messenger: ActorSelection, router: ActorSelection) { @@ -15,6 +16,7 @@ private[team] final class Notifier( def acceptRequest(team: Team, request: Request) { teamUrl(team.id) foreach { url => messenger ! LichessThread( + from = sender, to = request.user, subject = """You have joined the team %s""".format(team.name), message = """Congratulation, your request to join the team was accepted! diff --git a/modules/timeline/src/main/Entry.scala b/modules/timeline/src/main/Entry.scala index 515d67961d..2b30e98d75 100644 --- a/modules/timeline/src/main/Entry.scala +++ b/modules/timeline/src/main/Entry.scala @@ -24,6 +24,9 @@ case class Entry( case "forum-post" => Json.fromJson[ForumPost](data) case "note-create" => Json.fromJson[NoteCreate](data) case "tour-join" => Json.fromJson[TourJoin](data) + case "qa-question" => Json.fromJson[QaQuestion](data) + case "qa-answer" => Json.fromJson[QaAnswer](data) + case "qa-comment" => Json.fromJson[QaComment](data) }).asOpt } @@ -36,6 +39,9 @@ object Entry { case d: ForumPost => "forum-post" -> Json.toJson(d) case d: NoteCreate => "note-create" -> Json.toJson(d) case d: TourJoin => "tour-join" -> Json.toJson(d) + case d: QaQuestion => "qa-question" -> Json.toJson(d) + case d: QaAnswer => "qa-answer" -> Json.toJson(d) + case d: QaComment => "qa-comment" -> Json.toJson(d) }) match { case (typ, json) => json.asOpt[JsObject] map { new Entry(users, typ, _, DateTime.now) } } diff --git a/public/font15/fonts/lichess.eot b/public/font15/fonts/lichess.eot new file mode 100644 index 0000000000000000000000000000000000000000..9a5f5bb10e48e8d476df33924908c8ad65d5538b GIT binary patch literal 14132 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 F`oFv956u7o literal 0 HcmV?d00001 diff --git a/public/font15/fonts/lichess.svg b/public/font15/fonts/lichess.svg new file mode 100644 index 0000000000..d39aafddee --- /dev/null +++ b/public/font15/fonts/lichess.svg @@ -0,0 +1,81 @@ + + + +Generated by Fontastic.me + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/font15/fonts/lichess.ttf b/public/font15/fonts/lichess.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8211d137252500e16304b1dbf151675c8ca3fd40 GIT binary patch literal 13968 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 literal 0 HcmV?d00001 diff --git a/public/font15/fonts/lichess.woff b/public/font15/fonts/lichess.woff new file mode 100644 index 0000000000000000000000000000000000000000..c04546e50bddd023551cdba7c14728b0507ed39f GIT binary patch literal 10160 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 + + + + + + Font Reference - lichess + + + + + +
+

lichess

+

This font was created withFontastic

+

Character mapping

+
    +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
+

CSS mapping

+
    +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • + + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
+
+ + \ No newline at end of file diff --git a/public/font15/styles.css b/public/font15/styles.css new file mode 100644 index 0000000000..cccb78a33a --- /dev/null +++ b/public/font15/styles.css @@ -0,0 +1,253 @@ +@charset "UTF-8"; + +@font-face { + font-family: "lichess"; + src:url("fonts/lichess.eot"); + src:url("fonts/lichess.eot?#iefix") format("embedded-opentype"), + url("fonts/lichess.woff") format("woff"), + url("fonts/lichess.ttf") format("truetype"), + url("fonts/lichess.svg#lichess") format("svg"); + font-weight: normal; + font-style: normal; + +} + +[data-icon]:before { + font-family: "lichess" !important; + content: attr(data-icon); + font-style: normal !important; + font-weight: normal !important; + font-variant: normal !important; + text-transform: none !important; + speak: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +[class^="icon-"]:before, +[class*=" icon-"]:before { + font-family: "lichess" !important; + font-style: normal !important; + font-weight: normal !important; + font-variant: normal !important; + text-transform: none !important; + speak: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-fontawesome-webfont-1:before { + content: "a"; +} +.icon-fontawesome-webfont-2:before { + content: "b"; +} +.icon-fontawesome-webfont-3:before { + content: "c"; +} +.icon-fontawesome-webfont-4:before { + content: "d"; +} +.icon-fontawesome-webfont-5:before { + content: "e"; +} +.icon-fontawesome-webfont-6:before { + content: "f"; +} +.icon-fontawesome-webfont-7:before { + content: "g"; +} +.icon-fontawesome-webfont-8:before { + content: "h"; +} +.icon-fontawesome-webfont-9:before { + content: "j"; +} +.icon-fontawesome-webfont-10:before { + content: "k"; +} +.icon-fontawesome-webfont-11:before { + content: "l"; +} +.icon-fontawesome-webfont-12:before { + content: "m"; +} +.icon-fontawesome-webfont-15:before { + content: "p"; +} +.icon-fontawesome-webfont-17:before { + content: "r"; +} +.icon-fontawesome-webfont-18:before { + content: "s"; +} +.icon-fontawesome-webfont-19:before { + content: "t"; +} +.icon-fontawesome-webfont-20:before { + content: "u"; +} +.icon-eye-view-1:before { + content: "v"; +} +.icon-fontawesome-webfont-21:before { + content: "w"; +} +.icon-fontawesome-webfont-22:before { + content: "x"; +} +.icon-fontawesome-webfont-23:before { + content: "z"; +} +.icon-microscope:before { + content: "A"; +} +.icon-crown-king-1:before { + content: "C"; +} +.icon-fontawesome-webfont-24:before { + content: "D"; +} +.icon-fontawesome-webfont-25:before { + content: "E"; +} +.icon-fontawesome-webfont-26:before { + content: "F"; +} +.icon-fontawesome-webfont-27:before { + content: "G"; +} +.icon-fontawesome-webfont-28:before { + content: "H"; +} +.icon-fontawesome-webfont-29:before { + content: "I"; +} +.icon-fontawesome-webfont-30:before { + content: "J"; +} +.icon-fontawesome-webfont-31:before { + content: "K"; +} +.icon-loop-alt2:before { + content: "B"; +} +.icon-arrow-full-lowerright:before { + content: "M"; +} +.icon-arrow-full-upperright:before { + content: "N"; +} +.icon-fontawesome-webfont-32:before { + content: "L"; +} +.icon-plus-squared:before { + content: "O"; +} +.icon-plus-circled:before { + content: "O"; +} +.icon-fontawesome-webfont-33:before { + content: "O"; +} +.icon-fontawesome-webfont-34:before { + content: "Q"; +} +.icon-fire-station-24:before { + content: "Q"; +} +.icon-burning-fire:before { + content: "Q"; +} +.icon-arrow-sans-down:before { + content: "R"; +} +.icon-arrow-sans-up:before { + content: "S"; +} +.icon-fontawesome-webfont-34:before { + content: "T"; +} +.icon-crossed-swords-small:before { + content: "U"; +} +.icon-fontawesome-webfont-35:before { + content: "V"; +} +.icon-fontawesome-webfont-36:before { + content: "W"; +} +.icon-fontawesome-webfont-37:before { + content: "X"; +} +.icon-fontawesome-webfont-38:before { + content: "Y"; +} +.icon-fontawesome-webfont-39:before { + content: "Z"; +} +.icon-fontawesome-webfont-40:before { + content: "!"; +} +.icon-fontawesome-webfont-41:before { + content: "P"; +} +.icon-fontawesome-webfont:before { + content: "i"; +} +.icon-hand-stop:before { + content: "2"; +} +.icon-ionicons:before { + content: "3"; +} +.icon-fontawesome-webfont-42:before { + content: "0"; +} +.icon-television-tv:before { + content: "1"; +} +.icon-moon:before { + content: "4"; +} +.icon-sun:before { + content: "5"; +} +.icon-ink-pen:before { + content: "6"; +} +.icon-rocket:before { + content: "8"; +} +.icon-chart-line:before { + content: "9"; +} +.icon-link:before { + content: "\""; +} +.icon-ionicons-1:before { + content: "7"; +} +.icon-unmute:before { + content: "#"; +} +.icon-mute:before { + content: "$"; +} +.icon-gear:before { + content: "%"; +} +.icon-repo:before { + content: "&"; +} +.icon-law:before { + content: "n"; +} +.icon-tag:before { + content: "o"; +} +.icon-trash-bin:before { + content: "q"; +} diff --git a/public/javascripts/big.js b/public/javascripts/big.js index 51e109f4f5..649ec1b8e0 100644 --- a/public/javascripts/big.js +++ b/public/javascripts/big.js @@ -2929,6 +2929,14 @@ var storage = { })); }); + /////////////// forum.js //////////////////// + + $('#lichess_forum').on('click', 'a.delete', function() { + $.post($(this).attr("href")); + $(this).closest(".post").slideUp(100); + return false; + }); + $.fn.sortable = function(sortFns) { return this.each(function() { var $table = $(this); diff --git a/public/javascripts/forum.js b/public/javascripts/forum.js deleted file mode 100644 index 770c0f4c23..0000000000 --- a/public/javascripts/forum.js +++ /dev/null @@ -1,11 +0,0 @@ -$(function() { - $("#lichess_forum a.delete").unbind("click").click(function() { - if (confirm("Delete?")) { - var $this = $(this) - $.post($this.attr("href"), function(d) { - $this.closest(".post").slideUp(500); - }); - } - return false; - }); -}); diff --git a/public/javascripts/qa.js b/public/javascripts/qa.js new file mode 100644 index 0000000000..8dfb58cead --- /dev/null +++ b/public/javascripts/qa.js @@ -0,0 +1,50 @@ +$(function() { + var $qa = $('#qa'); + $qa.find('form.question').each(function() { + var $form = $(this); + $form.find('input.tm-input').each(function() { + var $input = $(this); + var tagApi; + tagApi = $input.tagsManager({ + prefilled: $input.data('prefill'), + backspace: [], + delimiters: [13, 44], + tagsContainer: $form.find('.tags') + }); + var tagSource = new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + local: $.map($input.data('tags').split(','), function(t) { + return { + value: t + }; + }) + }); + tagSource.initialize(); + $input.typeahead({ + minLength: 1, + highlight: true + }, { + source: tagSource.ttAdapter(), + name: 'tags', + displayKey: 'value', + limit: 15 + }).on('typeahead:selected', function(e, d) { + tagApi.tagsManager("pushTag", d.value); + $input.val(''); + }); + }); + }); + $qa.on('click', '.your-comment .toggle', function() { + var $form = $(this).siblings('form'); + $form.toggle(200, function() { + if ($form.is(':visible')) $form.find('textarea').focus(); + }); + }); + $qa.find('.your-comment form').submit(function() { + if ($(this).find('textarea').val().length < 20) { + alert("Comment must be longer than 20 characters"); + return false; + } + }); +}); diff --git a/public/stylesheets/common.css b/public/stylesheets/common.css index 836da5a4de..95ba63f386 100644 --- a/public/stylesheets/common.css +++ b/public/stylesheets/common.css @@ -70,8 +70,8 @@ time { } @font-face { font-family: "lichess"; - src: url("../font14/fonts/lichess.eot"); - src: url("../font14/fonts/lichess.eot?#iefix") format("embedded-opentype"), url("../font14/fonts/lichess.woff") format("woff"), url("../font14/fonts/lichess.ttf") format("truetype"), url("../font14/fonts/lichess.svg#lichess") format("svg"); + src: url("../font15/fonts/lichess.eot"); + src: url("../font15/fonts/lichess.eot?#iefix") format("embedded-opentype"), url("../font15/fonts/lichess.woff") format("woff"), url("../font15/fonts/lichess.ttf") format("truetype"), url("../font15/fonts/lichess.svg#lichess") format("svg"); font-weight: normal; font-style: normal; } diff --git a/public/stylesheets/forum.css b/public/stylesheets/forum.css index 051e1a4e32..accaff1b29 100644 --- a/public/stylesheets/forum.css +++ b/public/stylesheets/forum.css @@ -1,17 +1,16 @@ -.metas .delete, -.metas .postip { +div.post .metas .mod { margin-left: 1em; font-size: 0.9em; + visibility: hidden; } -.forum_topics_list .delete { - margin-right: 1em; +div.post:hover .metas .mod { + visibility: visible; } ol.crumbs { padding: 20px 24px 0 24px; } - ol.crumbs li { display: inline; margin-left: 1em; diff --git a/public/stylesheets/qa.css b/public/stylesheets/qa.css index e69de29bb2..da6905a43a 100644 --- a/public/stylesheets/qa.css +++ b/public/stylesheets/qa.css @@ -0,0 +1,145 @@ +#qa form.wide { + font-size: 1.2em; +} +#qa form.wide input.title { + width: 500px; + padding: 5px 10px; + margin-top: 20px; +} +#qa form.wide textarea { + width: 500px; + height: 150px; + padding: 5px 10px; + margin-top: 20px; +} +#qa form.wide .tags_wrap { + margin-top: 20px; +} +#qa form.wide .tags_wrap input { + display: block; +} +#qa form.wide .submit { + margin-top: 20px; +} +#qa form.wide p.error { + margin-left: 110px; + margin-bottom: 10px; + color: red; +} +#qa form.mod { + display: inline; +} +#qa table.meta { + margin-top: 20px; +} +#qa table.meta th { + width: 80px; + font-weight: bold; + padding: 2px 0; +} +#qa .tag { + white-space: nowrap; + text-decoration: none; + font-weight: bold; +} +#qa .tag:hover { + text-decoration: underline; +} +#qa .question h1 a { + display: inline-block; + width: 80px; + text-decoration: none; +} +#qa .question > .body { + font-size: 1.2em; + margin: 40px 0 40px 80px; +} +#qa .comments { + margin: 0 0 20px 80px; + border-top: 1px dotted #ccc; +} +#qa .comments .comment { + border-bottom: 1px dotted #ccc; + padding: 10px 0; +} +#qa .question .your-comment { + margin-top: 10px; +} +#qa .question .your-comment form { + /* display: none; */ +} +#qa .comment .mod { + visibility: hidden; +} +#qa .question .your-comment textarea { + width: 500px; + height: 50px; + margin-bottom: 10px; + padding: 5px 10px; + display: block; +} +#qa .comment:hover .mod { + visibility: visible; +} +#qa .answers-header { + margin: 40px 0 0 80px; + font-size: 1.4em; +} +#qa .your-answer { + margin: 0 0 0 80px; +} +.tt-hint { + display: none; +} +.tt-dropdown-menu { + width: 422px; + margin-top: 12px; + padding: 8px 0; + background-color: #fff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-border-radius: 8px; + -moz-border-radius: 8px; + border-radius: 8px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); + box-shadow: 0 5px 10px rgba(0, 0, 0, .2); +} +.tt-suggestion { + padding: 3px 20px; + font-size: 18px; + line-height: 24px; +} +.tt-suggestion.tt-cursor { + color: #fff; + background-color: #0097cf; +} +.tt-suggestion p { + margin: 0; +} +.tm-tag { + background-color: #f5f5f5; + border: #bbbbbb 1px solid; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset; + display: inline-block; + border-radius: 3px; + margin: 0 5px 5px 0; + padding: 7px; + text-decoration: none; + transition: border 0.2s linear 0s, box-shadow 0.2s linear 0s; + -moz-transition: border 0.2s linear 0s, box-shadow 0.2s linear 0s; + -webkit-transition: border 0.2s linear 0s, box-shadow 0.2s linear 0s; + vertical-align: middle; +} +.tm-tag .tm-tag-remove { + color: #000000; + font-weight: bold; + margin-left: 4px; + opacity: 0.2; + text-decoration: none; +} +.tm-tag .tm-tag-remove:hover { + color: #000000; + text-decoration: none; + opacity: 0.4; +} diff --git a/public/vendor/tagmanager b/public/vendor/tagmanager new file mode 160000 index 0000000000..be5d4100ec --- /dev/null +++ b/public/vendor/tagmanager @@ -0,0 +1 @@ +Subproject commit be5d4100ec3ff755bb380452a8824d56d7a517c9 diff --git a/public/vendor/typeahead.bundle.min.js b/public/vendor/typeahead.bundle.min.js new file mode 100644 index 0000000000..dff8ef56ec --- /dev/null +++ b/public/vendor/typeahead.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * typeahead.js 0.10.2 + * https://github.com/twitter/typeahead.js + * Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT + */ + +!function(a){var b={isMsie:function(){return/(msie|trident)/i.test(navigator.userAgent)?navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2]:!1},isBlankString:function(a){return!a||/^\s*$/.test(a)},escapeRegExChars:function(a){return a.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},isString:function(a){return"string"==typeof a},isNumber:function(a){return"number"==typeof a},isArray:a.isArray,isFunction:a.isFunction,isObject:a.isPlainObject,isUndefined:function(a){return"undefined"==typeof a},bind:a.proxy,each:function(b,c){function d(a,b){return c(b,a)}a.each(b,d)},map:a.map,filter:a.grep,every:function(b,c){var d=!0;return b?(a.each(b,function(a,e){return(d=c.call(null,e,a,b))?void 0:!1}),!!d):d},some:function(b,c){var d=!1;return b?(a.each(b,function(a,e){return(d=c.call(null,e,a,b))?!1:void 0}),!!d):d},mixin:a.extend,getUniqueId:function(){var a=0;return function(){return a++}}(),templatify:function(b){function c(){return String(b)}return a.isFunction(b)?b:c},defer:function(a){setTimeout(a,0)},debounce:function(a,b,c){var d,e;return function(){var f,g,h=this,i=arguments;return f=function(){d=null,c||(e=a.apply(h,i))},g=c&&!d,clearTimeout(d),d=setTimeout(f,b),g&&(e=a.apply(h,i)),e}},throttle:function(a,b){var c,d,e,f,g,h;return g=0,h=function(){g=new Date,e=null,f=a.apply(c,d)},function(){var i=new Date,j=b-(i-g);return c=this,d=arguments,0>=j?(clearTimeout(e),e=null,g=i,f=a.apply(c,d)):e||(e=setTimeout(h,j)),f}},noop:function(){}},c="0.10.2",d=function(){function a(a){return a.split(/\s+/)}function b(a){return a.split(/\W+/)}function c(a){return function(b){return function(c){return a(c[b])}}}return{nonword:b,whitespace:a,obj:{nonword:c(b),whitespace:c(a)}}}(),e=function(){function a(a){this.maxSize=a||100,this.size=0,this.hash={},this.list=new c}function c(){this.head=this.tail=null}function d(a,b){this.key=a,this.val=b,this.prev=this.next=null}return b.mixin(a.prototype,{set:function(a,b){var c,e=this.list.tail;this.size>=this.maxSize&&(this.list.remove(e),delete this.hash[e.key]),(c=this.hash[a])?(c.val=b,this.list.moveToFront(c)):(c=new d(a,b),this.list.add(c),this.hash[a]=c,this.size++)},get:function(a){var b=this.hash[a];return b?(this.list.moveToFront(b),b.val):void 0}}),b.mixin(c.prototype,{add:function(a){this.head&&(a.next=this.head,this.head.prev=a),this.head=a,this.tail=this.tail||a},remove:function(a){a.prev?a.prev.next=a.next:this.head=a.next,a.next?a.next.prev=a.prev:this.tail=a.prev},moveToFront:function(a){this.remove(a),this.add(a)}}),a}(),f=function(){function a(a){this.prefix=["__",a,"__"].join(""),this.ttlKey="__ttl__",this.keyMatcher=new RegExp("^"+this.prefix)}function c(){return(new Date).getTime()}function d(a){return JSON.stringify(b.isUndefined(a)?null:a)}function e(a){return JSON.parse(a)}var f,g;try{f=window.localStorage,f.setItem("~~~","!"),f.removeItem("~~~")}catch(h){f=null}return g=f&&window.JSON?{_prefix:function(a){return this.prefix+a},_ttlKey:function(a){return this._prefix(a)+this.ttlKey},get:function(a){return this.isExpired(a)&&this.remove(a),e(f.getItem(this._prefix(a)))},set:function(a,e,g){return b.isNumber(g)?f.setItem(this._ttlKey(a),d(c()+g)):f.removeItem(this._ttlKey(a)),f.setItem(this._prefix(a),d(e))},remove:function(a){return f.removeItem(this._ttlKey(a)),f.removeItem(this._prefix(a)),this},clear:function(){var a,b,c=[],d=f.length;for(a=0;d>a;a++)(b=f.key(a)).match(this.keyMatcher)&&c.push(b.replace(this.keyMatcher,""));for(a=c.length;a--;)this.remove(c[a]);return this},isExpired:function(a){var d=e(f.getItem(this._ttlKey(a)));return b.isNumber(d)&&c()>d?!0:!1}}:{get:b.noop,set:b.noop,remove:b.noop,clear:b.noop,isExpired:b.noop},b.mixin(a.prototype,g),a}(),g=function(){function c(b){b=b||{},this._send=b.transport?d(b.transport):a.ajax,this._get=b.rateLimiter?b.rateLimiter(this._get):this._get}function d(c){return function(d,e){function f(a){b.defer(function(){h.resolve(a)})}function g(a){b.defer(function(){h.reject(a)})}var h=a.Deferred();return c(d,e,f,g),h}}var f=0,g={},h=6,i=new e(10);return c.setMaxPendingRequests=function(a){h=a},c.resetCache=function(){i=new e(10)},b.mixin(c.prototype,{_get:function(a,b,c){function d(b){c&&c(null,b),i.set(a,b)}function e(){c&&c(!0)}function j(){f--,delete g[a],l.onDeckRequestArgs&&(l._get.apply(l,l.onDeckRequestArgs),l.onDeckRequestArgs=null)}var k,l=this;(k=g[a])?k.done(d).fail(e):h>f?(f++,g[a]=this._send(a,b).done(d).fail(e).always(j)):this.onDeckRequestArgs=[].slice.call(arguments,0)},get:function(a,c,d){var e;return b.isFunction(c)&&(d=c,c={}),(e=i.get(a))?b.defer(function(){d&&d(null,e)}):this._get(a,c,d),!!e}}),c}(),h=function(){function c(b){b=b||{},b.datumTokenizer&&b.queryTokenizer||a.error("datumTokenizer and queryTokenizer are both required"),this.datumTokenizer=b.datumTokenizer,this.queryTokenizer=b.queryTokenizer,this.reset()}function d(a){return a=b.filter(a,function(a){return!!a}),a=b.map(a,function(a){return a.toLowerCase()})}function e(){return{ids:[],children:{}}}function f(a){for(var b={},c=[],d=0;db[e]?e++:(f.push(a[d]),d++,e++);return f}return b.mixin(c.prototype,{bootstrap:function(a){this.datums=a.datums,this.trie=a.trie},add:function(a){var c=this;a=b.isArray(a)?a:[a],b.each(a,function(a){var f,g;f=c.datums.push(a)-1,g=d(c.datumTokenizer(a)),b.each(g,function(a){var b,d,g;for(b=c.trie,d=a.split("");g=d.shift();)b=b.children[g]||(b.children[g]=e()),b.ids.push(f)})})},get:function(a){var c,e,h=this;return c=d(this.queryTokenizer(a)),b.each(c,function(a){var b,c,d,f;if(e&&0===e.length)return!1;for(b=h.trie,c=a.split("");b&&(d=c.shift());)b=b.children[d];return b&&0===c.length?(f=b.ids.slice(0),void(e=e?g(e,f):f)):(e=[],!1)}),e?b.map(f(e),function(a){return h.datums[a]}):[]},reset:function(){this.datums=[],this.trie=e()},serialize:function(){return{datums:this.datums,trie:this.trie}}}),c}(),i=function(){function d(a){return a.local||null}function e(d){var e,f;return f={url:null,thumbprint:"",ttl:864e5,filter:null,ajax:{}},(e=d.prefetch||null)&&(e=b.isString(e)?{url:e}:e,e=b.mixin(f,e),e.thumbprint=c+e.thumbprint,e.ajax.type=e.ajax.type||"GET",e.ajax.dataType=e.ajax.dataType||"json",!e.url&&a.error("prefetch requires url to be set")),e}function f(c){function d(a){return function(c){return b.debounce(c,a)}}function e(a){return function(c){return b.throttle(c,a)}}var f,g;return g={url:null,wildcard:"%QUERY",replace:null,rateLimitBy:"debounce",rateLimitWait:300,send:null,filter:null,ajax:{}},(f=c.remote||null)&&(f=b.isString(f)?{url:f}:f,f=b.mixin(g,f),f.rateLimiter=/^throttle$/i.test(f.rateLimitBy)?e(f.rateLimitWait):d(f.rateLimitWait),f.ajax.type=f.ajax.type||"GET",f.ajax.dataType=f.ajax.dataType||"json",delete f.rateLimitBy,delete f.rateLimitWait,!f.url&&a.error("remote requires url to be set")),f}return{local:d,prefetch:e,remote:f}}();!function(c){function e(b){b&&(b.local||b.prefetch||b.remote)||a.error("one of local, prefetch, or remote is required"),this.limit=b.limit||5,this.sorter=j(b.sorter),this.dupDetector=b.dupDetector||k,this.local=i.local(b),this.prefetch=i.prefetch(b),this.remote=i.remote(b),this.cacheKey=this.prefetch?this.prefetch.cacheKey||this.prefetch.url:null,this.index=new h({datumTokenizer:b.datumTokenizer,queryTokenizer:b.queryTokenizer}),this.storage=this.cacheKey?new f(this.cacheKey):null}function j(a){function c(b){return b.sort(a)}function d(a){return a}return b.isFunction(a)?c:d}function k(){return!1}var l,m;return l=c.Bloodhound,m={data:"data",protocol:"protocol",thumbprint:"thumbprint"},c.Bloodhound=e,e.noConflict=function(){return c.Bloodhound=l,e},e.tokenizers=d,b.mixin(e.prototype,{_loadPrefetch:function(b){function c(a){f.clear(),f.add(b.filter?b.filter(a):a),f._saveToStorage(f.index.serialize(),b.thumbprint,b.ttl)}var d,e,f=this;return(d=this._readFromStorage(b.thumbprint))?(this.index.bootstrap(d),e=a.Deferred().resolve()):e=a.ajax(b.url,b.ajax).done(c),e},_getFromRemote:function(a,b){function c(a,c){b(a?[]:f.remote.filter?f.remote.filter(c):c)}var d,e,f=this;return a=a||"",e=encodeURIComponent(a),d=this.remote.replace?this.remote.replace(this.remote.url,a):this.remote.url.replace(this.remote.wildcard,e),this.transport.get(d,this.remote.ajax,c)},_saveToStorage:function(a,b,c){this.storage&&(this.storage.set(m.data,a,c),this.storage.set(m.protocol,location.protocol,c),this.storage.set(m.thumbprint,b,c))},_readFromStorage:function(a){var b,c={};return this.storage&&(c.data=this.storage.get(m.data),c.protocol=this.storage.get(m.protocol),c.thumbprint=this.storage.get(m.thumbprint)),b=c.thumbprint!==a||c.protocol!==location.protocol,c.data&&!b?c.data:null},_initialize:function(){function c(){e.add(b.isFunction(f)?f():f)}var d,e=this,f=this.local;return d=this.prefetch?this._loadPrefetch(this.prefetch):a.Deferred().resolve(),f&&d.done(c),this.transport=this.remote?new g(this.remote):null,this.initPromise=d.promise()},initialize:function(a){return!this.initPromise||a?this._initialize():this.initPromise},add:function(a){this.index.add(a)},get:function(a,c){function d(a){var d=f.slice(0);b.each(a,function(a){var c;return c=b.some(d,function(b){return e.dupDetector(a,b)}),!c&&d.push(a),d.length0||!this.transport)&&c&&c(f)},clear:function(){this.index.reset()},clearPrefetchCache:function(){this.storage&&this.storage.clear()},clearRemoteCache:function(){this.transport&&g.resetCache()},ttAdapter:function(){return b.bind(this.get,this)}}),e}(this);var j={wrapper:'',dropdown:'',dataset:'
',suggestions:'',suggestion:'
'},k={wrapper:{position:"relative",display:"inline-block"},hint:{position:"absolute",top:"0",left:"0",borderColor:"transparent",boxShadow:"none"},input:{position:"relative",verticalAlign:"top",backgroundColor:"transparent"},inputWithNoHint:{position:"relative",verticalAlign:"top"},dropdown:{position:"absolute",top:"100%",left:"0",zIndex:"100",display:"none"},suggestions:{display:"block"},suggestion:{whiteSpace:"nowrap",cursor:"pointer"},suggestionChild:{whiteSpace:"normal"},ltr:{left:"0",right:"auto"},rtl:{left:"auto",right:" 0"}};b.isMsie()&&b.mixin(k.input,{backgroundImage:"url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)"}),b.isMsie()&&b.isMsie()<=7&&b.mixin(k.input,{marginTop:"-1px"});var l=function(){function c(b){b&&b.el||a.error("EventBus initialized without el"),this.$el=a(b.el)}var d="typeahead:";return b.mixin(c.prototype,{trigger:function(a){var b=[].slice.call(arguments,1);this.$el.trigger(d+a,b)}}),c}(),m=function(){function a(a,b,c,d){var e;if(!c)return this;for(b=b.split(i),c=d?h(c,d):c,this._callbacks=this._callbacks||{};e=b.shift();)this._callbacks[e]=this._callbacks[e]||{sync:[],async:[]},this._callbacks[e][a].push(c);return this}function b(b,c,d){return a.call(this,"async",b,c,d)}function c(b,c,d){return a.call(this,"sync",b,c,d)}function d(a){var b;if(!this._callbacks)return this;for(a=a.split(i);b=a.shift();)delete this._callbacks[b];return this}function e(a){var b,c,d,e,g;if(!this._callbacks)return this;for(a=a.split(i),d=[].slice.call(arguments,1);(b=a.shift())&&(c=this._callbacks[b]);)e=f(c.sync,this,[b].concat(d)),g=f(c.async,this,[b].concat(d)),e()&&j(g);return this}function f(a,b,c){function d(){for(var d,e=0;!d&&e