more progress on /class

pull/5932/head
Thibault Duplessis 2020-01-17 14:05:42 -06:00
parent 1d8a5b1351
commit 49db12c8ef
21 changed files with 314 additions and 100 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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],

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
.flash {
margin: 2em 0;
margin: 1em 0 2em 0;
&__content {
@extend %box-radius;

View File

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

View File

@ -2,4 +2,5 @@
@import '../../../common/css/form/form3';
@import '../../../common/css/component/slist';
@import '../../../common/css/component/tablesort';
@import '../user/activity';
@import '../clas';