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 =>
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"
}
}
}
}
}
)

View File

@ -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)
)
}

View File

@ -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 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")(
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 wont 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())
)

View File

@ -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:''}
});
});

View File

@ -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)

View File

@ -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))
}
}

View File

@ -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 {

View File

@ -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;
}
}
}