create users vs invite to a class

class
Thibault Duplessis 2020-01-16 17:41:46 -06:00
parent 4b5bcb8a09
commit 9beea401c3
9 changed files with 135 additions and 22 deletions

View File

@ -66,7 +66,13 @@ final class Clas(
def studentForm(id: String) = Secure(_.Teacher) { implicit ctx => me =>
WithClass(me, lila.clas.Clas.Id(id)) { _ => clas =>
Ok(html.clas.student.form(clas, env.clas.forms.student.create)).fuccess
Ok(
html.clas.student.form(
clas,
env.clas.forms.student.invite,
env.clas.forms.student.create
)
).fuccess
}
}
@ -77,15 +83,19 @@ final class Clas(
env.clas.forms.student.create
.bindFromRequest()(ctx.body)
.fold(
err => BadRequest(html.clas.student.form(clas, err)).fuccess,
err =>
BadRequest(
html.clas.student.form(
clas,
env.clas.forms.student.invite,
err
)
).fuccess,
username =>
env.clas.api.student.create(clas, username)(env.user.authenticator.passEnc) flatMap {
env.clas.api.student.create(clas, username)(env.user.authenticator.passEnc) map {
case (user, password) =>
env.clas.api.student.get(clas, user) map {
_ ?? { student =>
Ok(html.clas.student.show(clas, student, password.some))
}
}
Redirect(routes.Clas.studentShow(clas.id.value, user.username))
.flashing("password" -> password.value)
}
)
}
@ -93,6 +103,32 @@ final class Clas(
}
}
def studentInvite(id: String) = SecureBody(_.Teacher) { implicit ctx => me =>
WithClass(me, lila.clas.Clas.Id(id)) { _ => clas =>
env.clas.forms.student.invite
.bindFromRequest()(ctx.body)
.pp
.fold(
err =>
BadRequest(
html.clas.student.form(
clas,
err,
env.clas.forms.student.create
)
).fuccess,
username =>
env.user.repo named username flatMap {
_ ?? { user =>
env.clas.api.student.invite(clas, user) inject
Redirect(routes.Clas.studentForm(clas.id.value))
.flashing("success" -> s"${user.username} has been invited")
}
}
)
}
}
def studentShow(id: String, username: String) = Secure(_.Teacher) { implicit ctx => me =>
WithClass(me, lila.clas.Clas.Id(id)) { t => clas =>
env.user.repo named username flatMap {

View File

@ -12,8 +12,7 @@ object student {
def show(
clas: Clas,
student: Student.WithUser,
password: Option[lila.user.User.ClearPassword] = none
student: Student.WithUser
)(implicit ctx: Context) =
bits.layout(student.user.username, Left(clas))(
cls := "student-show",
@ -31,16 +30,16 @@ object student {
)("User profile")
)
),
password map { pass =>
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.value}"),
code(s"Password: $pass"),
a(
href := routes.Clas.studentCreate(clas.id.value),
href := routes.Clas.studentForm(clas.id.value),
cls := "button button-green text",
dataIcon := "O"
)("Add another student")
@ -78,19 +77,51 @@ object student {
)
)
def form(c: lila.clas.Clas, form: Form[String])(implicit ctx: Context) =
def form(c: lila.clas.Clas, invite: Form[String], create: Form[String])(implicit ctx: Context) =
bits.layout("Add student", Left(c))(
cls := "box-pad",
cls := "box-pad student-add",
h1("Add student"),
p(
"To ",
a(href := routes.Clas.show(c.id.value))(c.name)
),
postForm(cls := "form3", action := routes.Clas.studentCreate(c.id.value))(
form3.group(form("username"), frag("Username"))(form3.input(_)(autofocus)),
form3.actions(
a(href := routes.Clas.show(c.id.value))(trans.cancel()),
form3.submit(trans.signUp())
ctx.flash("success") map { msg =>
div(cls := "flash-success")(msg)
},
div(cls := "student-add__choice")(
div(cls := "student-add__choice__invite")(
h2("Invite a Lichess account"),
p(
"If the student already has a Lichess account, ",
"you can invite them to the class. ",
"They will receive a message on Lichess with a link to join the class.",
strong("Important: only invite students you know, and who actively want to join the class."),
"Never send unsolicited invites to arbitrary players."
),
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.submit("Invite")
)
),
div(cls := "student-add__choice__create")(
h2("Create a new Lichess account"),
p(
"If the student doesn't have a Lichess account yet, ",
"you can create one for them here. ",
br,
"No email address is required. A password will be generated, ",
"and you will have to transmit it to the student, so they can log in.",
br,
strong("Important: a student must not have multiple accounts."),
" ",
"If they already have one, use the invite form instead."
),
postForm(cls := "form3", action := routes.Clas.studentCreate(c.id.value))(
form3.group(create("username"), frag("Create username"))(form3.input(_)(autofocus)),
form3.submit(trans.signUp())
)
)
)
)

View File

@ -444,8 +444,9 @@ POST /class/new controllers.Clas.create
GET /class/$id<\w{8}> controllers.Clas.show(id: String)
GET /class/$id<\w{8}>/edit controllers.Clas.edit(id: String)
POST /class/$id<\w{8}>/edit controllers.Clas.update(id: String)
GET /class/$id<\w{8}>/student/new controllers.Clas.studentForm(id: String)
GET /class/$id<\w{8}>/student/add controllers.Clas.studentForm(id: String)
POST /class/$id<\w{8}>/student/new controllers.Clas.studentCreate(id: String)
POST /class/$id<\w{8}>/student/invite controllers.Clas.studentInvite(id: String)
GET /class/$id<\w{8}>/student/:username controllers.Clas.studentShow(id: String, username: String)
# DB image

View File

@ -80,6 +80,8 @@ sealed trait Context extends lila.user.UserContextWrapper {
def zoom: Int = {
req.session get "zoom2" flatMap (_.toIntOption) map (_ - 100) filter (0 <=) filter (100 >=)
} | 85
def flash(name: String): Option[String] = req.flash get name
}
sealed abstract class BaseContext(

View File

@ -102,5 +102,7 @@ final class ClasApi(
(user -> password)
}
}
def invite(clas: Clas, user: User): Funit = funit
}
}

View File

@ -2,8 +2,10 @@ package lila.clas
import play.api.data._
import play.api.data.Forms._
import scala.concurrent.duration._
final class ClasForm(
lightUserAsync: lila.common.LightUser.Getter,
securityForms: lila.security.DataForm
) {
@ -26,6 +28,17 @@ final class ClasForm(
object student {
def create = securityForms.signup.managed
def invite = Form(
single(
"invite" -> lila.user.DataForm.historicalUsernameField.verifying("Unknown username", {
blockingFetchUser(_).isDefined
})
)
)
private def blockingFetchUser(username: String) =
lightUserAsync(lila.user.User normalize username).await(1 second, "clasInviteUser")
}
}

View File

@ -8,6 +8,7 @@ import lila.common.config._
final class Env(
db: lila.db.Db,
userRepo: lila.user.UserRepo,
lightUserAsync: lila.common.LightUser.Getter,
securityForms: lila.security.DataForm
)(implicit ec: scala.concurrent.ExecutionContext) {

View File

@ -38,7 +38,7 @@ object Student {
private[clas] object password {
private val random = new java.security.SecureRandom()
private val chars = ('2' to '9') ++ ('a' to 'z' - 'l') mkString
private val chars = ('2' to '9') ++ (('a' to 'z').toSet - 'l') mkString
private val nbChars = chars.size
private def secureChar = chars(random nextInt nbChars)

View File

@ -99,3 +99,30 @@
}
}
}
.student-add {
.flash-success {
@extend %box-radius;
padding: 1em 2em;
margin: 2em 0;
background: $c-good;
color: $c-good-over;
&::before {
@extend %data-icon;
content: 'E';
margin-right: 1em;
font-size: 1.5em;
}
}
&__choice {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(20em, 1fr));
grid-gap: var(--box-padding);
h2 {
margin: 1em 0;
}
}
}