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