From 915a0fd093cd7038db534d7a22f2e0b7d72de901 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 17 Jan 2020 17:11:01 -0600 Subject: [PATCH] improve student onboarding --- app/controllers/Clas.scala | 34 +++++++++++++++------ app/templating/FlashHelper.scala | 22 +++++++++----- app/views/clas/student.scala | 47 +++++++++++++++++------------ bin/mongodb/clas_v2.js | 5 +++ modules/clas/src/main/ClasApi.scala | 15 ++++----- modules/clas/src/main/Student.scala | 6 ++-- ui/common/css/component/_flash.scss | 13 ++++++++ ui/site/css/_clas.scss | 47 ++++++++++++----------------- 8 files changed, 114 insertions(+), 75 deletions(-) diff --git a/app/controllers/Clas.scala b/app/controllers/Clas.scala index e98ff444d8..eac88f16a9 100644 --- a/app/controllers/Clas.scala +++ b/app/controllers/Clas.scala @@ -79,13 +79,22 @@ final class Clas( def studentForm(id: String) = Secure(_.Teacher) { implicit ctx => me => WithClass(me, id) { _ => clas => - Ok( - html.clas.student.form( - clas, - env.clas.forms.student.invite, - env.clas.forms.student.create + ctx.req.flash.get("created").map(_ split ' ').?? { + case Array(userId, password) => + env.clas.api.student + .get(clas, userId) + .map2(lila.clas.Student.WithPassword(_, lila.user.User.ClearPassword(password))) + case _ => fuccess(none) + } map { created => + Ok( + html.clas.student.form( + clas, + env.clas.forms.student.invite, + env.clas.forms.student.create, + created + ) ) - ).fuccess + } } } @@ -107,8 +116,8 @@ final class Clas( data => env.clas.api.student.create(clas, data, t) map { case (user, password) => - Redirect(routes.Clas.studentShow(clas.id.value, user.username)) - .flashing("password" -> password.value) + Redirect(routes.Clas.studentForm(clas.id.value)) + .flashing("created" -> s"${user.id} ${password.value}") } ) } @@ -132,8 +141,13 @@ final class Clas( data => env.user.repo named data.username flatMap { _ ?? { user => - env.clas.api.student.invite(clas, user, data.realName, t) inject - Redirect(routes.Clas.studentForm(clas.id.value)).flashSuccess + env.clas.api.student.invite(clas, user, data.realName, t) map { so => + Redirect(routes.Clas.studentForm(clas.id.value)).flashing { + so.fold("warning" -> s"${user.username} is already in the class") { s => + "success" -> s"${user.username} (${s.realName}) has been invited" + } + } + } } } ) diff --git a/app/templating/FlashHelper.scala b/app/templating/FlashHelper.scala index 8cccfc7308..dfe31d7033 100644 --- a/app/templating/FlashHelper.scala +++ b/app/templating/FlashHelper.scala @@ -7,26 +7,34 @@ import lila.app.ui.ScalatagsTemplate._ trait FlashHelper { self: I18nHelper => def standardFlash(modifiers: Modifier*)(implicit ctx: Context): Option[Frag] = - successFlash(modifiers) orElse failureFlash(modifiers) + successFlash(modifiers) orElse warningFlash(modifiers) orElse failureFlash(modifiers) def successFlash(modifiers: Seq[Modifier])(implicit ctx: Context): Option[Frag] = ctx.flash("success").map { msg => flashMessage(modifiers ++ Seq(cls := "flash-success"))( - if (msg.isEmpty) trans.success() - else msg + if (msg.isEmpty) trans.success() else msg + ) + } + + def warningFlash(modifiers: Seq[Modifier])(implicit ctx: Context): Option[Frag] = + ctx.flash("warning").map { msg => + flashMessage(modifiers ++ Seq(cls := "flash-warning"))( + if (msg.isEmpty) "Warning" else msg ) } def failureFlash(modifiers: Seq[Modifier])(implicit ctx: Context): Option[Frag] = ctx.flash("failure").map { msg => flashMessage(modifiers ++ Seq(cls := "flash-failure"))( - if (msg.isEmpty) "Failure" - else msg + if (msg.isEmpty) "Failure" else msg ) } - def flashMessage(modifiers: Seq[Modifier])(msg: Frag) = + def flashMessage(modifiers: Seq[Modifier])(msg: Frag): Frag = + flashMessage(modifiers: _*)(msg) + + def flashMessage(modifiers: Modifier*)(contentModifiers: Modifier*): Frag = div(modifiers)(cls := "flash")( - div(cls := "flash__content")(msg) + div(cls := "flash__content")(contentModifiers) ) } diff --git a/app/views/clas/student.scala b/app/views/clas/student.scala index 79e2ad9823..29f60e305b 100644 --- a/app/views/clas/student.scala +++ b/app/views/clas/student.scala @@ -39,22 +39,6 @@ object student { )("profile") ) ), - ctx.flash("password") map { pass => - div(cls := "box__pad password")( - iconTag("E")(cls := "is-green"), - div( - p( - "Make sure to copy or write down the password now. You won’t be able to see it again!" - ), - code(s"Password: $pass"), - s.student.isVeryNew option a( - href := routes.Clas.studentForm(clas.id.value), - cls := "button button-green text", - dataIcon := "O" - )("Add another student") - ) - ) - }, div(cls := "box__pad")( standardFlash(), div( @@ -149,7 +133,12 @@ object student { help = frag("Private info, never visible on Lichess. Helps you remember who that student is.").some )(form3.input(_)) - def form(c: lila.clas.Clas, invite: Form[_], create: Form[_])(implicit ctx: Context) = + def form( + c: lila.clas.Clas, + invite: Form[_], + create: Form[_], + created: Option[lila.clas.Student.WithPassword] = none + )(implicit ctx: Context) = bits.layout("Add student", Left(c))( cls := "box-pad student-add", h1("Add student"), @@ -157,6 +146,22 @@ object student { "To ", a(href := routes.Clas.show(c.id.value))(c.name) ), + created map { + case Student.WithPassword(student, password) => + flashMessage(cls := "student-add__created")( + strong( + "Lichess profile ", + userIdLink(student.userId.some, withOnline = false), + " created for ", + student.realName, + "." + ), + p( + "Make sure to copy or write down the password now. You won’t be able to see it again!" + ), + code("Password: ", password.value) + ) + }, standardFlash(), div(cls := "student-add__choice")( div(cls := "info")( @@ -170,8 +175,8 @@ object student { ) ), postForm(cls := "form3", action := routes.Clas.studentInvite(c.id.value))( - form3.group(invite("invite"), frag("Invite username"))( - form3.input(_, klass = "user-autocomplete")(autofocus)(dataTag := "span") + form3.group(invite("username"), frag("Lichess username"))( + form3.input(_, klass = "user-autocomplete")(created.isEmpty option autofocus)(dataTag := "span") ), realName(invite), form3.submit("Invite") @@ -193,7 +198,9 @@ object student { ) ), postForm(cls := "form3", action := routes.Clas.studentCreate(c.id.value))( - form3.group(create("username"), frag("Create username"))(form3.input(_)(autofocus)), + form3.group(create("username"), frag("Lichess username"))( + form3.input(_)(created.isDefined option autofocus) + ), realName(create), form3.submit(trans.signUp()) ) diff --git a/bin/mongodb/clas_v2.js b/bin/mongodb/clas_v2.js index 5af32510ec..04c07c7071 100644 --- a/bin/mongodb/clas_v2.js +++ b/bin/mongodb/clas_v2.js @@ -11,3 +11,8 @@ db.clas_student.find({createdAt:{$exists:1}}).forEach(stu => { $set:{created:{by:teacher,at:stu.createdAt}} }); }); +db.clas_student.find({realName:{$exists:0}}).forEach(stu => { + db.clas_student.update({_id: stu._id},{ + $set:{realName:stu.userId,notes:''} + }); +}); diff --git a/modules/clas/src/main/ClasApi.scala b/modules/clas/src/main/ClasApi.scala index 5f4c7e1b9d..169dce6de7 100644 --- a/modules/clas/src/main/ClasApi.scala +++ b/modules/clas/src/main/ClasApi.scala @@ -98,10 +98,11 @@ final class ClasApi( def isManaged(user: User): Fu[Boolean] = coll.exists($doc("userId" -> user.id, "managed" -> true)) + def get(clas: Clas, userId: User.ID): Fu[Option[Student]] = + coll.ext.one[Student]($id(Student.id(userId, clas.id))) + def get(clas: Clas, user: User): Fu[Option[Student.WithUser]] = - coll.ext.one[Student]($id(Student.id(user.id, clas.id))) map2 { - Student.WithUser(_, user) - } + get(clas, user.id) map2 { Student.WithUser(_, user) } // def isIn(clas: Clas, userId: User.ID): Fu[Boolean] = // coll.exists($id(Student.id(userId, clas.id))) @@ -132,11 +133,11 @@ final class ClasApi( } } - def invite(clas: Clas, user: User, realName: String, teacher: Teacher.WithUser): Funit = { + def invite(clas: Clas, user: User, realName: String, teacher: Teacher.WithUser): Fu[Option[Student]] = { lila.mon.clas.studentInvite(teacher.user.id) - coll.insert.one(Student.make(user, clas, teacher.teacher.id, realName, managed = false)) >> - sendWelcomeMessage(teacher, user, clas) - }.recover(lila.db.recoverDuplicateKey(_ => ())) + val student = Student.make(user, clas, teacher.teacher.id, realName, managed = false) + coll.insert.one(student) >> sendWelcomeMessage(teacher, user, clas) inject student.some + }.recover(lila.db.recoverDuplicateKey(_ => none)) private[ClasApi] def join(clas: Clas, user: User, teacherId: Teacher.Id): Fu[Student] = { val student = Student.make(user, clas, teacherId, "", managed = false) diff --git a/modules/clas/src/main/Student.scala b/modules/clas/src/main/Student.scala index fc111f8792..739fdf00cc 100644 --- a/modules/clas/src/main/Student.scala +++ b/modules/clas/src/main/Student.scala @@ -21,8 +21,6 @@ case class Student( def isArchived = archived.isDefined def isActive = !isArchived - - def isVeryNew = created.at.isAfter(DateTime.now minusSeconds 3) } object Student { @@ -44,6 +42,8 @@ object Student { case class WithUser(student: Student, user: User) + case class WithPassword(student: Student, password: User.ClearPassword) + private[clas] object password { private val random = new java.security.SecureRandom() @@ -51,7 +51,7 @@ object Student { private val nbChars = chars.size private def secureChar = chars(random nextInt nbChars) - def generate = lila.user.User.ClearPassword { + def generate = User.ClearPassword { new String(Array.fill(7)(secureChar)) } } diff --git a/ui/common/css/component/_flash.scss b/ui/common/css/component/_flash.scss index e2b3ba9370..2a5be085f2 100644 --- a/ui/common/css/component/_flash.scss +++ b/ui/common/css/component/_flash.scss @@ -13,6 +13,19 @@ margin-right: 1em; font-size: 1.5em; } + + a { + color: $c-good-over; + } + } + + &-warning .flash__content { + background: $c-warn; + color: $c-warn-over; + &::before { + @extend %data-icon; + content: ''; + } } &-failure .flash__content { diff --git a/ui/site/css/_clas.scss b/ui/site/css/_clas.scss index e8d0f139df..d902563eeb 100644 --- a/ui/site/css/_clas.scss +++ b/ui/site/css/_clas.scss @@ -105,35 +105,8 @@ } &__archive { - @extend %box-radius, %flex-between; - background: fade-out($c-bad, .8); margin-top: 3em; - padding: 1em 2em; - justify-content: flex-end; - } - - .password { - background: $c-bg-zebra; - padding: 3em; - display: flex; - i { - font-size: 7em; - margin-right: 3rem; - display: none; - @include breakpoint($mq-small) { - display: block; - } - } - code { - font-weight: bold; - font-size: 3em; - margin-top: 1rem; - display: block; - } - .button { - display: block; - margin-top: 3em; - } + text-align: right; } } @@ -157,4 +130,22 @@ font-style: italic; font-size: 2em; } + + &__created { + strong a { + text-decoration: underline; + } + p { + margin-top: 1em; + } + code { + font-weight: bold; + font-size: 2em; + display: block; + } + .button { + display: block; + margin-top: 3em; + } + } }