more progress on /class
parent
1d8a5b1351
commit
49db12c8ef
|
@ -267,28 +267,29 @@ final class Account(
|
|||
}
|
||||
|
||||
def kid = Auth { implicit ctx => me =>
|
||||
env.security.forms toggleKid me map { form =>
|
||||
Ok(html.account.kid(me, form))
|
||||
env.clas.api.student.isManaged(me) flatMap { managed =>
|
||||
env.security.forms toggleKid me map { form =>
|
||||
Ok(html.account.kid(me, form, managed))
|
||||
}
|
||||
}
|
||||
}
|
||||
def apiKid = Scoped(_.Preference.Read) { _ => me =>
|
||||
JsonOk(Json.obj("kid" -> me.kid)).fuccess
|
||||
}
|
||||
|
||||
// App BC
|
||||
def kidToggle = Auth { _ => me =>
|
||||
env.user.repo.setKid(me, !me.kid) inject Ok
|
||||
}
|
||||
|
||||
def kidPost = AuthBody { implicit ctx => me =>
|
||||
implicit val req = ctx.body
|
||||
env.security.forms toggleKid me flatMap { form =>
|
||||
FormFuResult(form) { err =>
|
||||
fuccess(html.account.kid(me, err))
|
||||
} { _ =>
|
||||
env.user.repo.setKid(me, getBool("v")) inject
|
||||
Redirect(routes.Account.kid).flashSuccess
|
||||
}
|
||||
env.clas.api.student.isManaged(me) flatMap {
|
||||
case true => notFound
|
||||
case _ =>
|
||||
implicit val req = ctx.body
|
||||
env.security.forms toggleKid me flatMap { form =>
|
||||
FormFuResult(form) { err =>
|
||||
fuccess(html.account.kid(me, err, false))
|
||||
} { _ =>
|
||||
env.user.repo.setKid(me, getBool("v")) inject
|
||||
Redirect(routes.Account.kid).flashSuccess
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
def apiKidPost = Scoped(_.Preference.Write) { req => me =>
|
||||
|
|
|
@ -37,14 +37,14 @@ final class Clas(
|
|||
|
||||
def show(id: String) = Auth { implicit ctx => me =>
|
||||
if (isGranted(_.Teacher))
|
||||
WithClass(me, lila.clas.Clas.Id(id)) { _ => clas =>
|
||||
env.clas.api.student.of(clas) map { students =>
|
||||
WithClass(me, id) { _ => clas =>
|
||||
env.clas.api.student.allOf(clas) map { students =>
|
||||
views.html.clas.clas.showToTeacher(clas, students)
|
||||
}
|
||||
} else
|
||||
env.clas.api.clas.byId(lila.clas.Clas.Id(id)) flatMap {
|
||||
_ ?? { clas =>
|
||||
env.clas.api.student.of(clas) flatMap { students =>
|
||||
env.clas.api.student.activeOf(clas) flatMap { students =>
|
||||
if (students.exists(_.student is me))
|
||||
Ok(views.html.clas.clas.showToStudent(clas, students)).fuccess
|
||||
else notFound
|
||||
|
@ -54,13 +54,13 @@ final class Clas(
|
|||
}
|
||||
|
||||
def edit(id: String) = Secure(_.Teacher) { implicit ctx => me =>
|
||||
WithClass(me, lila.clas.Clas.Id(id)) { _ => clas =>
|
||||
WithClass(me, id) { _ => clas =>
|
||||
Ok(html.clas.clas.edit(clas, env.clas.forms.edit(clas))).fuccess
|
||||
}
|
||||
}
|
||||
|
||||
def update(id: String) = SecureBody(_.Teacher) { implicit ctx => me =>
|
||||
WithClass(me, lila.clas.Clas.Id(id)) { _ => clas =>
|
||||
WithClass(me, id) { _ => clas =>
|
||||
env.clas.forms
|
||||
.edit(clas)
|
||||
.bindFromRequest()(ctx.body)
|
||||
|
@ -75,7 +75,7 @@ final class Clas(
|
|||
}
|
||||
|
||||
def studentForm(id: String) = Secure(_.Teacher) { implicit ctx => me =>
|
||||
WithClass(me, lila.clas.Clas.Id(id)) { _ => clas =>
|
||||
WithClass(me, id) { _ => clas =>
|
||||
Ok(
|
||||
html.clas.student.form(
|
||||
clas,
|
||||
|
@ -89,7 +89,7 @@ final class Clas(
|
|||
def studentCreate(id: String) = SecureBody(_.Teacher) { implicit ctx => me =>
|
||||
NoTor {
|
||||
Firewall {
|
||||
WithClass(me, lila.clas.Clas.Id(id)) { t => clas =>
|
||||
WithClass(me, id) { t => clas =>
|
||||
env.clas.forms.student.create
|
||||
.bindFromRequest()(ctx.body)
|
||||
.fold(
|
||||
|
@ -102,7 +102,7 @@ final class Clas(
|
|||
)
|
||||
).fuccess,
|
||||
username =>
|
||||
env.clas.api.student.create(clas, username, t.teacher)(env.user.authenticator.passEnc) map {
|
||||
env.clas.api.student.create(clas, username, t.teacher) map {
|
||||
case (user, password) =>
|
||||
Redirect(routes.Clas.studentShow(clas.id.value, user.username))
|
||||
.flashing("password" -> password.value)
|
||||
|
@ -114,7 +114,7 @@ final class Clas(
|
|||
}
|
||||
|
||||
def studentInvite(id: String) = SecureBody(_.Teacher) { implicit ctx => me =>
|
||||
WithClass(me, lila.clas.Clas.Id(id)) { t => clas =>
|
||||
WithClass(me, id) { t => clas =>
|
||||
env.clas.forms.student.invite
|
||||
.bindFromRequest()(ctx.body)
|
||||
.fold(
|
||||
|
@ -146,12 +146,14 @@ final class Clas(
|
|||
}
|
||||
|
||||
def studentShow(id: String, username: String) = Secure(_.Teacher) { implicit ctx => me =>
|
||||
WithClass(me, lila.clas.Clas.Id(id)) { _ => clas =>
|
||||
WithClass(me, id) { _ => clas =>
|
||||
env.user.repo named username flatMap {
|
||||
_ ?? { user =>
|
||||
env.clas.api.student.get(clas, user) map {
|
||||
env.clas.api.student.get(clas, user) flatMap {
|
||||
_ ?? { student =>
|
||||
views.html.clas.student.show(clas, student)
|
||||
env.activity.read.recent(student.user, 14) map { activity =>
|
||||
views.html.clas.student.show(clas, student, activity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -159,17 +161,57 @@ final class Clas(
|
|||
}
|
||||
}
|
||||
|
||||
def studentArchive(id: String, username: String, v: Boolean) = Secure(_.Teacher) { _ => me =>
|
||||
WithClass(me, id) { t => clas =>
|
||||
WithStudent(clas, username) { s =>
|
||||
env.clas.api.student.archive(s.student, t.teacher, v) inject
|
||||
Redirect(routes.Clas.studentShow(clas.id.value, username)).flashSuccess
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def studentSetKid(id: String, username: String, v: Boolean) = Secure(_.Teacher) { _ => me =>
|
||||
WithClass(me, id) { _ => clas =>
|
||||
WithStudent(clas, username) { s =>
|
||||
(s.student.managed ?? env.user.repo.setKid(s.user, v)) inject
|
||||
Redirect(routes.Clas.studentShow(clas.id.value, username)).flashSuccess
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def studentResetPassword(id: String, username: String) = Secure(_.Teacher) { _ => me =>
|
||||
WithClass(me, id) { _ => clas =>
|
||||
WithStudent(clas, username) { s =>
|
||||
env.clas.api.student.resetPassword(s.student) map { password =>
|
||||
Redirect(routes.Clas.studentShow(clas.id.value, username))
|
||||
.flashing("password" -> password.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def WithTeacher(me: lila.user.User)(
|
||||
f: lila.clas.Teacher.WithUser => Fu[Result]
|
||||
): Fu[Result] =
|
||||
env.clas.api.teacher withOrCreate me flatMap f
|
||||
|
||||
private def WithClass(me: lila.user.User, clasId: lila.clas.Clas.Id)(
|
||||
private def WithClass(me: lila.user.User, clasId: String)(
|
||||
f: lila.clas.Teacher.WithUser => lila.clas.Clas => Fu[Result]
|
||||
): Fu[Result] =
|
||||
WithTeacher(me) { t =>
|
||||
env.clas.api.clas.getAndView(clasId, t.teacher) flatMap {
|
||||
env.clas.api.clas.getAndView(lila.clas.Clas.Id(clasId), t.teacher) flatMap {
|
||||
_ ?? f(t)
|
||||
}
|
||||
}
|
||||
|
||||
private def WithStudent(clas: lila.clas.Clas, username: String)(
|
||||
f: lila.clas.Student.WithUser => Fu[Result]
|
||||
): Fu[Result] =
|
||||
env.user.repo named username flatMap {
|
||||
_ ?? { user =>
|
||||
env.clas.api.student.get(clas, user) flatMap {
|
||||
_ ?? f
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ final class Event(env: Env) extends LilaController(env) {
|
|||
.bindFromRequest
|
||||
.fold(
|
||||
err => BadRequest(html.event.edit(event, err)).fuccess,
|
||||
data => api.update(event, data) inject Redirect(routes.Event.edit(id))
|
||||
data => api.update(event, data) inject Redirect(routes.Event.edit(id)).flashSuccess
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ final class Event(env: Env) extends LilaController(env) {
|
|||
err => BadRequest(html.event.create(err)).fuccess,
|
||||
data =>
|
||||
api.create(data, me.id) map { event =>
|
||||
Redirect(routes.Event.edit(event.id))
|
||||
Redirect(routes.Event.edit(event.id)).flashSuccess
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -283,7 +283,7 @@ final class Tournament(
|
|||
Redirect {
|
||||
if (tour.isTeamBattle) routes.Tournament.teamBattleEdit(tour.id)
|
||||
else routes.Tournament.show(tour.id)
|
||||
}
|
||||
}.flashSuccess
|
||||
}
|
||||
}(rateLimited)
|
||||
}(rateLimited)
|
||||
|
|
|
@ -27,7 +27,7 @@ final class TournamentCrud(env: Env) extends LilaController(env) {
|
|||
.bindFromRequest
|
||||
.fold(
|
||||
err => BadRequest(html.tournament.crud.edit(tour, err)).fuccess,
|
||||
data => crud.update(tour, data) inject Redirect(routes.TournamentCrud.edit(id))
|
||||
data => crud.update(tour, data) inject Redirect(routes.TournamentCrud.edit(id)).flashSuccess
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ final class TournamentCrud(env: Env) extends LilaController(env) {
|
|||
Redirect {
|
||||
if (tour.isTeamBattle) routes.Tournament.teamBattleEdit(tour.id)
|
||||
else routes.TournamentCrud.edit(tour.id)
|
||||
}
|
||||
}.flashSuccess
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import controllers.routes
|
|||
|
||||
object kid {
|
||||
|
||||
def apply(u: lila.user.User, form: play.api.data.Form[_])(implicit ctx: Context) =
|
||||
def apply(u: lila.user.User, form: play.api.data.Form[_], managed: Boolean)(implicit ctx: Context) =
|
||||
account.layout(
|
||||
title = s"${u.username} - ${trans.kidMode.txt()}",
|
||||
active = "kid"
|
||||
|
@ -21,15 +21,18 @@ object kid {
|
|||
br,
|
||||
br,
|
||||
br,
|
||||
postForm(cls := "form3", action := s"${routes.Account.kidPost}?v=${!u.kid}")(
|
||||
form3.passwordModified(form("passwd"), trans.password())(autofocus, autocomplete := "off"),
|
||||
submitButton(
|
||||
cls := List(
|
||||
"button" -> true,
|
||||
"button-red" -> u.kid
|
||||
)
|
||||
)(if (u.kid) trans.disableKidMode.txt() else trans.enableKidMode.txt())
|
||||
),
|
||||
if (managed)
|
||||
p("Your account is managed. Ask your chess teacher about lifting kid mode.")
|
||||
else
|
||||
postForm(cls := "form3", action := s"${routes.Account.kidPost}?v=${!u.kid}")(
|
||||
form3.passwordModified(form("passwd"), trans.password())(autofocus, autocomplete := "off"),
|
||||
submitButton(
|
||||
cls := List(
|
||||
"button" -> true,
|
||||
"button-red" -> u.kid
|
||||
)
|
||||
)(if (u.kid) trans.disableKidMode.txt() else trans.enableKidMode.txt())
|
||||
),
|
||||
br,
|
||||
br,
|
||||
p(trans.inKidModeTheLichessLogoGetsIconX(span(cls := "kiddo", title := trans.kidMode.txt())(":)")))
|
||||
|
|
|
@ -61,7 +61,15 @@ object clas {
|
|||
),
|
||||
clas.desc.nonEmpty option div(cls := "box__pad clas-desc")(clas.desc),
|
||||
teachers(clas),
|
||||
div(cls := "students")(student.list(clas, students, true))
|
||||
students.partition(_.student.isArchived) match {
|
||||
case (archived, active) =>
|
||||
frag(
|
||||
div(cls := "students")(student.list(clas, active, true)("Students")),
|
||||
archived.nonEmpty option div(cls := "students students-archived")(
|
||||
student.list(clas, archived, true)("Archived students")
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
def showToStudent(
|
||||
|
@ -75,7 +83,7 @@ object clas {
|
|||
),
|
||||
clas.desc.nonEmpty option div(cls := "box__pad clas-desc")(clas.desc),
|
||||
teachers(clas),
|
||||
div(cls := "students")(student.list(clas, students, false))
|
||||
div(cls := "students")(student.list(clas, students, false)("Students"))
|
||||
)
|
||||
|
||||
private def teachers(clas: Clas) =
|
||||
|
|
|
@ -12,22 +12,31 @@ object student {
|
|||
|
||||
def show(
|
||||
clas: Clas,
|
||||
student: Student.WithUser
|
||||
s: Student.WithUser,
|
||||
activities: Vector[lila.activity.ActivityView]
|
||||
)(implicit ctx: Context) =
|
||||
bits.layout(student.user.username, Left(clas))(
|
||||
bits.layout(s.user.username, Left(clas))(
|
||||
cls := "student-show",
|
||||
div(cls := "box__top")(
|
||||
div(cls := "student-show__title", dataIcon := "r")(
|
||||
div(
|
||||
h1(student.user.username),
|
||||
a(href := routes.Clas.show(clas.id.value))(clas.name)
|
||||
h1(s.user.username),
|
||||
p(
|
||||
"Invited to ",
|
||||
a(href := routes.Clas.show(clas.id.value))(clas.name),
|
||||
" by ",
|
||||
userIdLink(s.student.created.by.value.some),
|
||||
" ",
|
||||
momentFromNowOnce(s.student.created.at)
|
||||
)
|
||||
)
|
||||
),
|
||||
div(cls := "box__top__actions")(
|
||||
a(
|
||||
href := routes.User.show(student.user.username),
|
||||
cls := "button button-empty"
|
||||
)("User profile")
|
||||
href := routes.User.show(s.user.username),
|
||||
cls := "button button-empty",
|
||||
title := "View full Lichess profile"
|
||||
)("profile")
|
||||
)
|
||||
),
|
||||
ctx.flash("password") map { pass =>
|
||||
|
@ -38,27 +47,77 @@ object student {
|
|||
"Make sure to copy or write down the password now. You won’t be able to see it again!"
|
||||
),
|
||||
code(s"Password: $pass"),
|
||||
a(
|
||||
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(
|
||||
),
|
||||
s.student.archived map { archived =>
|
||||
div(cls := "student-show__archived")(
|
||||
div(
|
||||
"Archived by ",
|
||||
userIdLink(archived.by.value.some),
|
||||
" ",
|
||||
momentFromNowOnce(archived.at)
|
||||
),
|
||||
postForm(action := routes.Clas.studentArchive(clas.id.value, s.user.username, false))(
|
||||
form3.submit("Restore", icon = none)(
|
||||
cls := "confirm button-empty",
|
||||
title := "Get the student back into the class"
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
s.student.managed option div(cls := "student-show__managed")(
|
||||
p("This student account is managed"),
|
||||
div(cls := "student-show__managed__actions")(
|
||||
postForm(action := routes.Clas.studentSetKid(clas.id.value, s.user.username, !s.user.kid))(
|
||||
form3.submit(if (s.user.kid) "Disable kid mode" else "Enable kid mode", icon = none)(
|
||||
s.student.isArchived option disabled,
|
||||
cls := List("confirm button-empty" -> true, "disabled" -> s.student.isArchived),
|
||||
title := "Kid mode prevents the student from communicating with Lichess players"
|
||||
)
|
||||
),
|
||||
postForm(action := routes.Clas.studentResetPassword(clas.id.value, s.user.username))(
|
||||
form3.submit("Reset password", icon = none)(
|
||||
s.student.isArchived option disabled,
|
||||
cls := List("confirm button-empty" -> true, "disabled" -> s.student.isArchived),
|
||||
title := "Generate a new password for the student"
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
views.html.activity(s.user, activities),
|
||||
s.student.isActive option postForm(
|
||||
action := routes.Clas.studentArchive(clas.id.value, s.user.username, true),
|
||||
cls := "student-show__archive"
|
||||
)(
|
||||
form3.submit("Archive", icon = none)(
|
||||
cls := "confirm button-red button-empty",
|
||||
title := "Remove the student from the class"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
private val sortNumberTh = th(attr("data-sort-method") := "number")
|
||||
private val dataSort = attr("data-sort")
|
||||
|
||||
def list(c: Clas, students: List[Student.WithUser], sortable: Boolean)(implicit ctx: Context) =
|
||||
def list(c: Clas, students: List[Student.WithUser], teacher: Boolean)(title: Frag)(implicit ctx: Context) =
|
||||
if (students.isEmpty)
|
||||
frag(hr, p(cls := "box__pad students__empty")("No students in the class, yet."))
|
||||
else
|
||||
table(cls := s"slist slist-pad ${sortable ?? " sortable"}")(
|
||||
table(cls := s"slist slist-pad ${teacher ?? " sortable"}")(
|
||||
thead(
|
||||
tr(
|
||||
th(attr("data-sort-default") := "1")("Student"),
|
||||
th(attr("data-sort-default") := "1")(title),
|
||||
sortNumberTh("Rating"),
|
||||
sortNumberTh("Games"),
|
||||
sortNumberTh("Active")
|
||||
|
@ -69,9 +128,11 @@ object student {
|
|||
case Student.WithUser(_, user) =>
|
||||
tr(
|
||||
td(
|
||||
a(href := routes.Clas.studentShow(c.id.value, user.username))(
|
||||
userSpan(user)
|
||||
)
|
||||
if (teacher)
|
||||
a(href := routes.Clas.studentShow(c.id.value, user.username))(
|
||||
userSpan(user)
|
||||
)
|
||||
else userLink(user)
|
||||
),
|
||||
td(user.perfs.bestRating),
|
||||
td(user.count.game.localize),
|
||||
|
|
|
@ -33,6 +33,7 @@ object event {
|
|||
form3.submit("Clone", "".some, klass = "button-green")
|
||||
)
|
||||
),
|
||||
standardFlash(),
|
||||
postForm(cls := "content_box_content form3", action := routes.Event.update(event.id))(inForm(form))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ object crud {
|
|||
form3.submit("Clone", "g".some, klass = "button-green")
|
||||
)
|
||||
),
|
||||
standardFlash(),
|
||||
postForm(cls := "form3", action := routes.TournamentCrud.update(tour.id))(inForm(form))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ object teamBattle {
|
|||
main(cls := "page-small")(
|
||||
div(cls := "tour__form box box-pad")(
|
||||
h1(tour.fullName),
|
||||
standardFlash(),
|
||||
if (tour.isFinished) p("This tournament is over, and the teams can no longer be updated.")
|
||||
else p("List the teams that will compete in this battle."),
|
||||
postForm(cls := "form3", action := routes.Tournament.teamBattleUpdate(tour.id))(
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
db.clas_clas.find({createdAt:{$exists:1}}).forEach(clas => {
|
||||
db.clas_clas.update({_id: clas._id},{
|
||||
$unset:{updatedAt:1,createdAt:1},
|
||||
$set:{created:{by:clas.teachers[0],at:clas.createdAt}}
|
||||
});
|
||||
});
|
||||
db.clas_student.find({createdAt:{$exists:1}}).forEach(stu => {
|
||||
let teacher = db.clas_clas.findOne({_id:stu.clasId}).teachers[0];
|
||||
db.clas_student.update({_id: stu._id},{
|
||||
$unset:{updatedAt:1,createdAt:1},
|
||||
$set:{created:{by:teacher,at:stu.createdAt}}
|
||||
});
|
||||
});
|
|
@ -449,6 +449,9 @@ 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/join/:token controllers.Clas.studentJoin(id: String, token: String)
|
||||
GET /class/$id<\w{8}>/student/:username controllers.Clas.studentShow(id: String, username: String)
|
||||
POST /class/$id<\w{8}>/student/:username/archive controllers.Clas.studentArchive(id: String, username: String, v: Boolean)
|
||||
POST /class/$id<\w{8}>/student/:username/reset-password controllers.Clas.studentResetPassword(id: String, username: String)
|
||||
POST /class/$id<\w{8}>/student/:username/set-kid controllers.Clas.studentSetKid(id: String, username: String, v: Boolean)
|
||||
|
||||
# DB image
|
||||
GET /image/:id/:hash/:name controllers.Main.image(id: String, hash: String, name: String)
|
||||
|
@ -544,7 +547,6 @@ POST /account/reopen/send controllers.Account.reopenApply
|
|||
GET /account/reopen/sent/:email controllers.Account.reopenSent(email: String)
|
||||
GET /account/reopen/login/:token controllers.Account.reopenLogin(token: String)
|
||||
# App BC
|
||||
POST /account/kidConfirm controllers.Account.kidToggle
|
||||
GET /account/security controllers.Account.security
|
||||
POST /account/signout/:sessionId controllers.Account.signout(sessionId: String)
|
||||
GET /account/info controllers.Account.info
|
||||
|
|
|
@ -8,6 +8,9 @@ private[clas] object BsonHandlers {
|
|||
implicit val teacherIdBSONHandler = stringAnyValHandler[Teacher.Id](_.value, Teacher.Id.apply)
|
||||
implicit val teacherBSONHandler = Macros.handler[Teacher]
|
||||
|
||||
import Clas.Recorded
|
||||
implicit val recordedBSONHandler = Macros.handler[Recorded]
|
||||
|
||||
implicit val clasIdBSONHandler = stringAnyValHandler[Clas.Id](_.value, Clas.Id.apply)
|
||||
implicit val clasBSONHandler = Macros.handler[Clas]
|
||||
|
||||
|
|
|
@ -9,9 +9,9 @@ case class Clas(
|
|||
desc: String,
|
||||
teachers: NonEmptyList[Teacher.Id], // first is owner
|
||||
nbStudents: Int,
|
||||
createdAt: DateTime,
|
||||
updatedAt: DateTime,
|
||||
viewedAt: DateTime
|
||||
created: Clas.Recorded,
|
||||
viewedAt: DateTime,
|
||||
archived: Option[Clas.Recorded]
|
||||
) {
|
||||
|
||||
def id = _id
|
||||
|
@ -25,15 +25,17 @@ object Clas {
|
|||
desc = desc,
|
||||
teachers = NonEmptyList(teacher.id),
|
||||
nbStudents = 0,
|
||||
createdAt = DateTime.now,
|
||||
updatedAt = DateTime.now,
|
||||
viewedAt = DateTime.now
|
||||
created = Recorded(teacher.id, DateTime.now),
|
||||
viewedAt = DateTime.now,
|
||||
archived = none
|
||||
)
|
||||
|
||||
case class WithOwner(clas: Clas, teacher: Teacher)
|
||||
|
||||
case class Id(value: String) extends AnyVal with StringValue
|
||||
|
||||
case class Recorded(by: Teacher.Id, at: DateTime)
|
||||
|
||||
// case class WithAll(
|
||||
// clas: Clas,
|
||||
// teachers: List[Teacher],
|
||||
|
|
|
@ -5,7 +5,7 @@ import reactivemongo.api._
|
|||
import scala.concurrent.duration._
|
||||
|
||||
import lila.db.dsl._
|
||||
import lila.user.{ User, UserRepo }
|
||||
import lila.user.{ Authenticator, User, UserRepo }
|
||||
import lila.common.EmailAddress
|
||||
import lila.common.config.{ BaseUrl, Secret }
|
||||
import lila.message.MessageApi
|
||||
|
@ -16,6 +16,7 @@ final class ClasApi(
|
|||
inviteSecret: Secret,
|
||||
userRepo: UserRepo,
|
||||
messageApi: MessageApi,
|
||||
authenticator: Authenticator,
|
||||
baseUrl: BaseUrl
|
||||
)(implicit ec: scala.concurrent.ExecutionContext) {
|
||||
|
||||
|
@ -70,14 +71,18 @@ final class ClasApi(
|
|||
|
||||
object student {
|
||||
|
||||
import lila.user.HashedPassword
|
||||
import User.ClearPassword
|
||||
|
||||
val coll = colls.student
|
||||
|
||||
def of(clas: Clas): Fu[List[Student.WithUser]] =
|
||||
def allOf(clas: Clas): Fu[List[Student.WithUser]] =
|
||||
of($doc("clasId" -> clas.id))
|
||||
def activeOf(clas: Clas): Fu[List[Student.WithUser]] =
|
||||
of($doc("clasId" -> clas.id, "archived" $exists false))
|
||||
|
||||
private def of(selector: Bdoc): Fu[List[Student.WithUser]] =
|
||||
coll.ext
|
||||
.find($doc("clasId" -> clas.id))
|
||||
.find(selector)
|
||||
.list[Student]() flatMap { students =>
|
||||
userRepo.coll.idsMap[User, User.ID](
|
||||
students.map(_.userId),
|
||||
|
@ -89,6 +94,9 @@ final class ClasApi(
|
|||
}
|
||||
}
|
||||
|
||||
def isManaged(user: User): Fu[Boolean] =
|
||||
coll.exists($doc("userId" -> user.id, "managed" -> true))
|
||||
|
||||
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)
|
||||
|
@ -97,16 +105,14 @@ final class ClasApi(
|
|||
def isIn(clas: Clas, userId: User.ID): Fu[Boolean] =
|
||||
coll.exists($id(Student.id(userId, clas.id)))
|
||||
|
||||
def create(clas: Clas, username: String, teacher: Teacher)(
|
||||
hashPassword: ClearPassword => HashedPassword
|
||||
): Fu[(User, ClearPassword)] = {
|
||||
def create(clas: Clas, username: String, teacher: Teacher): Fu[(User, ClearPassword)] = {
|
||||
val email = EmailAddress(s"noreply.class.${clas.id}.$username@lichess.org")
|
||||
val password = Student.password.generate
|
||||
lila.mon.clas.studentCreate(teacher.userId)
|
||||
userRepo
|
||||
.create(
|
||||
username = username,
|
||||
passwordHash = hashPassword(password),
|
||||
passwordHash = authenticator.passEnc(password),
|
||||
email = email,
|
||||
blind = false,
|
||||
mobileApiVersion = none,
|
||||
|
@ -114,7 +120,7 @@ final class ClasApi(
|
|||
)
|
||||
.orFail(s"No user could be created for $username")
|
||||
.flatMap { user =>
|
||||
coll.insert.one(Student.make(user, clas, managed = true)) inject
|
||||
coll.insert.one(Student.make(user, clas, teacher.id, managed = true)) inject
|
||||
(user -> password)
|
||||
}
|
||||
}
|
||||
|
@ -126,30 +132,54 @@ final class ClasApi(
|
|||
}
|
||||
}
|
||||
|
||||
private[ClasApi] def join(clas: Clas, user: User): Fu[Student] = {
|
||||
val student = Student.make(user, clas, managed = false)
|
||||
private[ClasApi] def join(clas: Clas, user: User, teacherId: Teacher.Id): Fu[Student] = {
|
||||
val student = Student.make(user, clas, teacherId, managed = false)
|
||||
coll.insert.one(student) inject student
|
||||
}
|
||||
|
||||
def resetPassword(s: Student): Fu[ClearPassword] = {
|
||||
val password = Student.password.generate
|
||||
authenticator.setPassword(s.userId, password) inject password
|
||||
}
|
||||
|
||||
def archive(s: Student, t: Teacher, v: Boolean): Funit =
|
||||
coll.update
|
||||
.one(
|
||||
$id(s.id),
|
||||
if (v) $set("archived" -> Clas.Recorded(t.id, DateTime.now))
|
||||
else $unset("archived")
|
||||
)
|
||||
.void
|
||||
}
|
||||
|
||||
object invite {
|
||||
|
||||
import StringToken.DateStr
|
||||
private case class Invite(studentId: Student.Id, teacherId: Teacher.Id)
|
||||
|
||||
private val lifetime = 7.days
|
||||
private lazy val tokener = new StringToken[String](
|
||||
secret = inviteSecret,
|
||||
getCurrentValue = _ => fuccess(DateStr toStr DateTime.now),
|
||||
currentValueHashSize = none,
|
||||
valueChecker = StringToken.ValueChecker.Custom(v =>
|
||||
fuccess {
|
||||
DateStr.toDate(v) exists DateTime.now.minusSeconds(lifetime.toSeconds.toInt).isBefore
|
||||
private val tokener = {
|
||||
import StringToken._
|
||||
implicit val tokenSerializable = new Serializable[Invite] {
|
||||
def read(str: String) = str.split(' ') match {
|
||||
case Array(s, t) => Invite(Student.Id(s), Teacher.Id(t))
|
||||
case _ => sys error s"Invalid invite token $str"
|
||||
}
|
||||
def write(i: Invite) = s"${i.studentId} ${i.teacherId}"
|
||||
}
|
||||
val lifetime = 7.days
|
||||
new StringToken[Invite](
|
||||
secret = inviteSecret,
|
||||
getCurrentValue = _ => fuccess(DateStr toStr DateTime.now),
|
||||
currentValueHashSize = none,
|
||||
valueChecker = StringToken.ValueChecker.Custom(v =>
|
||||
fuccess {
|
||||
DateStr.toDate(v) exists DateTime.now.minusSeconds(lifetime.toSeconds.toInt).isBefore
|
||||
}
|
||||
)
|
||||
)
|
||||
).pp
|
||||
}
|
||||
|
||||
def create(clas: Clas, user: User, teacher: Teacher.WithUser): Funit =
|
||||
tokener make Student.id(user.id, clas.id).value flatMap { token =>
|
||||
tokener make Invite(Student.id(user.id, clas.id), teacher.teacher.id) flatMap { token =>
|
||||
messageApi.sendOnBehalf(
|
||||
sender = teacher.user,
|
||||
dest = user,
|
||||
|
@ -167,10 +197,12 @@ ${clas.desc}"""
|
|||
clas.coll.one[Clas]($id(clasId)) flatMap {
|
||||
_ ?? { clas =>
|
||||
student.get(clas, user).map2(_.student) orElse {
|
||||
tokener read token map2 Student.Id.apply flatMap {
|
||||
_.exists {
|
||||
_ == Student.id(user.id, clas.id)
|
||||
} ?? student.join(clas, user).dmap(some)
|
||||
tokener read token flatMap {
|
||||
_.filter {
|
||||
_.studentId == Student.id(user.id, clas.id)
|
||||
} ?? { invite =>
|
||||
student.join(clas, user, invite.teacherId).dmap(some)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ final class Env(
|
|||
messageApi: lila.message.MessageApi,
|
||||
lightUserAsync: lila.common.LightUser.Getter,
|
||||
securityForms: lila.security.DataForm,
|
||||
authenticator: lila.user.Authenticator,
|
||||
baseUrl: BaseUrl
|
||||
)(implicit ec: scala.concurrent.ExecutionContext) {
|
||||
|
||||
|
|
|
@ -9,26 +9,31 @@ case class Student(
|
|||
userId: User.ID,
|
||||
clasId: Clas.Id,
|
||||
managed: Boolean, // created for the class by the teacher
|
||||
createdAt: DateTime,
|
||||
updatedAt: DateTime
|
||||
created: Clas.Recorded,
|
||||
archived: Option[Clas.Recorded]
|
||||
) {
|
||||
|
||||
def id = _id
|
||||
|
||||
def is(user: User) = userId == user.id
|
||||
|
||||
def isArchived = archived.isDefined
|
||||
def isActive = !isArchived
|
||||
|
||||
def isVeryNew = created.at.isAfter(DateTime.now minusSeconds 3)
|
||||
}
|
||||
|
||||
object Student {
|
||||
|
||||
def id(userId: User.ID, clasId: Clas.Id) = Id(s"${userId}:${clasId}")
|
||||
|
||||
def make(user: User, clas: Clas, managed: Boolean) = Student(
|
||||
def make(user: User, clas: Clas, teacherId: Teacher.Id, managed: Boolean) = Student(
|
||||
_id = id(user.id, clas.id),
|
||||
userId = user.id,
|
||||
clasId = clas.id,
|
||||
managed = managed,
|
||||
createdAt = DateTime.now,
|
||||
updatedAt = DateTime.now
|
||||
created = Clas.Recorded(teacherId, DateTime.now),
|
||||
archived = none
|
||||
)
|
||||
|
||||
case class Id(value: String) extends AnyVal with StringValue
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.flash {
|
||||
margin: 2em 0;
|
||||
margin: 1em 0 2em 0;
|
||||
|
||||
&__content {
|
||||
@extend %box-radius;
|
||||
|
|
|
@ -59,6 +59,9 @@
|
|||
}
|
||||
|
||||
.students {
|
||||
&-archived {
|
||||
margin-top: 2em;
|
||||
}
|
||||
&__empty {
|
||||
margin-bottom: 8em;
|
||||
}
|
||||
|
@ -67,6 +70,8 @@
|
|||
|
||||
.student-show {
|
||||
|
||||
padding-bottom: 3em;
|
||||
|
||||
&__title {
|
||||
@extend %flex-center;
|
||||
&::before {
|
||||
|
@ -75,6 +80,38 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__managed {
|
||||
@extend %box-radius, %flex-between;
|
||||
background: fade-out($c-primary, .8);
|
||||
padding: 1em 2em;
|
||||
p {
|
||||
margin: 0;
|
||||
&::before {
|
||||
@extend %data-icon;
|
||||
content: '5';
|
||||
font-size: 3em;
|
||||
}
|
||||
}
|
||||
&__actions {
|
||||
@extend %flex-center;
|
||||
}
|
||||
}
|
||||
|
||||
&__archived {
|
||||
@extend %box-radius, %flex-between;
|
||||
background: $c-bg-zebra2;
|
||||
padding: 1em 2em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
&__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;
|
||||
|
|
|
@ -2,4 +2,5 @@
|
|||
@import '../../../common/css/form/form3';
|
||||
@import '../../../common/css/component/slist';
|
||||
@import '../../../common/css/component/tablesort';
|
||||
@import '../user/activity';
|
||||
@import '../clas';
|
||||
|
|
Loading…
Reference in New Issue