improve student onboarding

pull/5932/head
Thibault Duplessis 2020-01-17 17:11:01 -06:00
parent 69ede0b239
commit 915a0fd093
8 changed files with 114 additions and 75 deletions

View File

@ -79,13 +79,22 @@ final class Clas(
def studentForm(id: String) = Secure(_.Teacher) { implicit ctx => me => def studentForm(id: String) = Secure(_.Teacher) { implicit ctx => me =>
WithClass(me, id) { _ => clas => WithClass(me, id) { _ => clas =>
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( Ok(
html.clas.student.form( html.clas.student.form(
clas, clas,
env.clas.forms.student.invite, env.clas.forms.student.invite,
env.clas.forms.student.create env.clas.forms.student.create,
created
) )
).fuccess )
}
} }
} }
@ -107,8 +116,8 @@ final class Clas(
data => data =>
env.clas.api.student.create(clas, data, t) map { env.clas.api.student.create(clas, data, t) map {
case (user, password) => case (user, password) =>
Redirect(routes.Clas.studentShow(clas.id.value, user.username)) Redirect(routes.Clas.studentForm(clas.id.value))
.flashing("password" -> password.value) .flashing("created" -> s"${user.id} ${password.value}")
} }
) )
} }
@ -132,8 +141,13 @@ final class Clas(
data => data =>
env.user.repo named data.username flatMap { env.user.repo named data.username flatMap {
_ ?? { user => _ ?? { user =>
env.clas.api.student.invite(clas, user, data.realName, t) inject env.clas.api.student.invite(clas, user, data.realName, t) map { so =>
Redirect(routes.Clas.studentForm(clas.id.value)).flashSuccess 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"
}
}
}
} }
} }
) )

View File

@ -7,26 +7,34 @@ import lila.app.ui.ScalatagsTemplate._
trait FlashHelper { self: I18nHelper => trait FlashHelper { self: I18nHelper =>
def standardFlash(modifiers: Modifier*)(implicit ctx: Context): Option[Frag] = 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] = def successFlash(modifiers: Seq[Modifier])(implicit ctx: Context): Option[Frag] =
ctx.flash("success").map { msg => ctx.flash("success").map { msg =>
flashMessage(modifiers ++ Seq(cls := "flash-success"))( flashMessage(modifiers ++ Seq(cls := "flash-success"))(
if (msg.isEmpty) trans.success() if (msg.isEmpty) trans.success() else msg
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] = def failureFlash(modifiers: Seq[Modifier])(implicit ctx: Context): Option[Frag] =
ctx.flash("failure").map { msg => ctx.flash("failure").map { msg =>
flashMessage(modifiers ++ Seq(cls := "flash-failure"))( flashMessage(modifiers ++ Seq(cls := "flash-failure"))(
if (msg.isEmpty) "Failure" if (msg.isEmpty) "Failure" else msg
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(modifiers)(cls := "flash")(
div(cls := "flash__content")(msg) div(cls := "flash__content")(contentModifiers)
) )
} }

View File

@ -39,22 +39,6 @@ object student {
)("profile") )("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 wont 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")( div(cls := "box__pad")(
standardFlash(), standardFlash(),
div( div(
@ -149,7 +133,12 @@ object student {
help = frag("Private info, never visible on Lichess. Helps you remember who that student is.").some help = frag("Private info, never visible on Lichess. Helps you remember who that student is.").some
)(form3.input(_)) )(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))( bits.layout("Add student", Left(c))(
cls := "box-pad student-add", cls := "box-pad student-add",
h1("Add student"), h1("Add student"),
@ -157,6 +146,22 @@ object student {
"To ", "To ",
a(href := routes.Clas.show(c.id.value))(c.name) 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 wont be able to see it again!"
),
code("Password: ", password.value)
)
},
standardFlash(), standardFlash(),
div(cls := "student-add__choice")( div(cls := "student-add__choice")(
div(cls := "info")( div(cls := "info")(
@ -170,8 +175,8 @@ object student {
) )
), ),
postForm(cls := "form3", action := routes.Clas.studentInvite(c.id.value))( postForm(cls := "form3", action := routes.Clas.studentInvite(c.id.value))(
form3.group(invite("invite"), frag("Invite username"))( form3.group(invite("username"), frag("Lichess username"))(
form3.input(_, klass = "user-autocomplete")(autofocus)(dataTag := "span") form3.input(_, klass = "user-autocomplete")(created.isEmpty option autofocus)(dataTag := "span")
), ),
realName(invite), realName(invite),
form3.submit("Invite") form3.submit("Invite")
@ -193,7 +198,9 @@ object student {
) )
), ),
postForm(cls := "form3", action := routes.Clas.studentCreate(c.id.value))( 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), realName(create),
form3.submit(trans.signUp()) form3.submit(trans.signUp())
) )

View File

@ -11,3 +11,8 @@ db.clas_student.find({createdAt:{$exists:1}}).forEach(stu => {
$set:{created:{by:teacher,at:stu.createdAt}} $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:''}
});
});

View File

@ -98,10 +98,11 @@ final class ClasApi(
def isManaged(user: User): Fu[Boolean] = def isManaged(user: User): Fu[Boolean] =
coll.exists($doc("userId" -> user.id, "managed" -> true)) 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]] = def get(clas: Clas, user: User): Fu[Option[Student.WithUser]] =
coll.ext.one[Student]($id(Student.id(user.id, clas.id))) map2 { get(clas, user.id) map2 { Student.WithUser(_, user) }
Student.WithUser(_, user)
}
// def isIn(clas: Clas, userId: User.ID): Fu[Boolean] = // def isIn(clas: Clas, userId: User.ID): Fu[Boolean] =
// coll.exists($id(Student.id(userId, clas.id))) // 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) lila.mon.clas.studentInvite(teacher.user.id)
coll.insert.one(Student.make(user, clas, teacher.teacher.id, realName, managed = false)) >> val student = Student.make(user, clas, teacher.teacher.id, realName, managed = false)
sendWelcomeMessage(teacher, user, clas) coll.insert.one(student) >> sendWelcomeMessage(teacher, user, clas) inject student.some
}.recover(lila.db.recoverDuplicateKey(_ => ())) }.recover(lila.db.recoverDuplicateKey(_ => none))
private[ClasApi] def join(clas: Clas, user: User, teacherId: Teacher.Id): Fu[Student] = { private[ClasApi] def join(clas: Clas, user: User, teacherId: Teacher.Id): Fu[Student] = {
val student = Student.make(user, clas, teacherId, "", managed = false) val student = Student.make(user, clas, teacherId, "", managed = false)

View File

@ -21,8 +21,6 @@ case class Student(
def isArchived = archived.isDefined def isArchived = archived.isDefined
def isActive = !isArchived def isActive = !isArchived
def isVeryNew = created.at.isAfter(DateTime.now minusSeconds 3)
} }
object Student { object Student {
@ -44,6 +42,8 @@ object Student {
case class WithUser(student: Student, user: User) case class WithUser(student: Student, user: User)
case class WithPassword(student: Student, password: User.ClearPassword)
private[clas] object password { private[clas] object password {
private val random = new java.security.SecureRandom() private val random = new java.security.SecureRandom()
@ -51,7 +51,7 @@ object Student {
private val nbChars = chars.size private val nbChars = chars.size
private def secureChar = chars(random nextInt nbChars) private def secureChar = chars(random nextInt nbChars)
def generate = lila.user.User.ClearPassword { def generate = User.ClearPassword {
new String(Array.fill(7)(secureChar)) new String(Array.fill(7)(secureChar))
} }
} }

View File

@ -13,6 +13,19 @@
margin-right: 1em; margin-right: 1em;
font-size: 1.5em; 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 { &-failure .flash__content {

View File

@ -105,35 +105,8 @@
} }
&__archive { &__archive {
@extend %box-radius, %flex-between;
background: fade-out($c-bad, .8);
margin-top: 3em; margin-top: 3em;
padding: 1em 2em; text-align: right;
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;
}
} }
} }
@ -157,4 +130,22 @@
font-style: italic; font-style: italic;
font-size: 2em; 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;
}
}
} }