improve student onboarding
parent
69ede0b239
commit
915a0fd093
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
)
|
||||
|
|
|
@ -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:''}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue