402 lines
13 KiB
Scala
402 lines
13 KiB
Scala
package lila.clas
|
|
|
|
import org.joda.time.DateTime
|
|
import reactivemongo.api._
|
|
|
|
import lila.common.config.BaseUrl
|
|
import lila.common.EmailAddress
|
|
import lila.db.dsl._
|
|
import lila.msg.MsgApi
|
|
import lila.security.Permission
|
|
import lila.user.{ Authenticator, User, UserRepo }
|
|
import lila.user.Holder
|
|
import lila.hub.actorApi.user.KidId
|
|
|
|
final class ClasApi(
|
|
colls: ClasColls,
|
|
studentCache: ClasStudentCache,
|
|
nameGenerator: NameGenerator,
|
|
userRepo: UserRepo,
|
|
msgApi: MsgApi,
|
|
authenticator: Authenticator,
|
|
baseUrl: BaseUrl
|
|
)(implicit ec: scala.concurrent.ExecutionContext) {
|
|
|
|
import BsonHandlers._
|
|
|
|
object clas {
|
|
|
|
val coll = colls.clas
|
|
|
|
def byId(id: Clas.Id) = coll.byId[Clas](id.value)
|
|
|
|
def of(teacher: User): Fu[List[Clas]] =
|
|
coll
|
|
.find($doc("teachers" -> teacher.id))
|
|
.sort($doc("archived" -> 1, "viewedAt" -> -1))
|
|
.cursor[Clas]()
|
|
.list(100)
|
|
|
|
def byIds(clasIds: List[Clas.Id]): Fu[List[Clas]] =
|
|
coll
|
|
.find($inIds(clasIds))
|
|
.sort($sort desc "createdAt")
|
|
.cursor[Clas]()
|
|
.list()
|
|
|
|
def create(data: ClasForm.ClasData, teacher: User): Fu[Clas] = {
|
|
val clas = Clas.make(teacher, data.name, data.desc)
|
|
coll.insert.one(clas) inject clas
|
|
}
|
|
|
|
def update(from: Clas, data: ClasForm.ClasData): Fu[Clas] = {
|
|
val clas = data update from
|
|
userRepo.filterEnabled(clas.teachers.toList) flatMap { filtered =>
|
|
val checked = clas.copy(
|
|
teachers = clas.teachers.toList.filter(filtered.contains).toNel | from.teachers
|
|
)
|
|
coll.update.one($id(clas.id), checked) inject checked
|
|
}
|
|
}
|
|
|
|
def updateWall(clas: Clas, text: String): Funit =
|
|
coll.updateField($id(clas.id), "wall", text).void
|
|
|
|
def getAndView(id: Clas.Id, teacher: User): Fu[Option[Clas]] =
|
|
coll.ext
|
|
.findAndUpdate[Clas](
|
|
selector = $id(id) ++ $doc("teachers" -> teacher.id),
|
|
update = $set("viewedAt" -> DateTime.now),
|
|
fetchNewObject = true
|
|
)
|
|
|
|
def teachers(clas: Clas): Fu[List[User]] =
|
|
userRepo.byOrderedIds(clas.teachers.toList, ReadPreference.secondaryPreferred)
|
|
|
|
def isTeacherOf(teacher: User, clasId: Clas.Id): Fu[Boolean] =
|
|
coll.exists($id(clasId) ++ $doc("teachers" -> teacher.id))
|
|
|
|
def areKidsInSameClass(kid1: KidId, kid2: KidId): Fu[Boolean] =
|
|
fuccess(studentCache.isStudent(kid1.id) && studentCache.isStudent(kid2.id)) >>&
|
|
colls.student.aggregateExists(readPreference = ReadPreference.secondaryPreferred) {
|
|
implicit framework =>
|
|
import framework._
|
|
Match($doc("userId" $in List(kid1.id, kid2.id))) -> List(
|
|
GroupField("clasId")("nb" -> SumAll),
|
|
Match($doc("nb" -> 2)),
|
|
Limit(1)
|
|
)
|
|
}
|
|
|
|
def isTeacherOf(teacher: User.ID, student: User.ID): Fu[Boolean] =
|
|
studentCache.isStudent(student) ?? colls.student
|
|
.aggregateExists(readPreference = ReadPreference.secondaryPreferred) { implicit framework =>
|
|
import framework._
|
|
Match($doc("userId" -> student)) -> List(
|
|
Project($doc("clasId" -> true)),
|
|
PipelineOperator(
|
|
$lookup.pipeline(
|
|
from = colls.clas,
|
|
as = "clas",
|
|
local = "clasId",
|
|
foreign = "_id",
|
|
pipe = List(
|
|
$doc(
|
|
"$match" -> $doc(
|
|
"$expr" -> $doc("$in" -> $arr(teacher, "$teachers"))
|
|
)
|
|
),
|
|
$doc("$limit" -> 1),
|
|
$doc("$project" -> $id(true))
|
|
)
|
|
)
|
|
),
|
|
Match("clas" $ne $arr()),
|
|
Limit(1),
|
|
Project($id(true))
|
|
)
|
|
}
|
|
|
|
def archive(c: Clas, t: User, v: Boolean): Funit =
|
|
coll.update
|
|
.one(
|
|
$id(c.id),
|
|
if (v) $set("archived" -> Clas.Recorded(t.id, DateTime.now))
|
|
else $unset("archived")
|
|
)
|
|
.void
|
|
}
|
|
|
|
object student {
|
|
|
|
import User.ClearPassword
|
|
|
|
val coll = colls.student
|
|
|
|
def activeOf(clas: Clas): Fu[List[Student]] =
|
|
of($doc("clasId" -> clas.id) ++ selectArchived(false))
|
|
|
|
def allWithUsers(clas: Clas, selector: Bdoc = $empty): Fu[List[Student.WithUser]] =
|
|
colls.student
|
|
.aggregateList(Int.MaxValue, ReadPreference.secondaryPreferred) { framework =>
|
|
import framework._
|
|
Match($doc("clasId" -> clas.id) ++ selector) -> List(
|
|
PipelineOperator(
|
|
$lookup.simple(
|
|
from = userRepo.coll,
|
|
as = "user",
|
|
local = "userId",
|
|
foreign = "_id"
|
|
)
|
|
),
|
|
UnwindField("user")
|
|
)
|
|
}
|
|
.map { docs =>
|
|
for {
|
|
doc <- docs
|
|
student <- doc.asOpt[Student]
|
|
user <- doc.getAsOpt[User]("user")
|
|
} yield Student.WithUser(student, user)
|
|
}
|
|
|
|
def activeWithUsers(clas: Clas): Fu[List[Student.WithUser]] =
|
|
allWithUsers(clas, selectArchived(false))
|
|
|
|
private def of(selector: Bdoc): Fu[List[Student]] =
|
|
coll
|
|
.find(selector)
|
|
.sort($sort asc "userId")
|
|
.cursor[Student]()
|
|
.list(500)
|
|
|
|
def clasIdsOfUser(userId: User.ID): Fu[List[Clas.Id]] =
|
|
coll.distinctEasy[Clas.Id, List]("clasId", $doc("userId" -> userId) ++ selectArchived(false))
|
|
|
|
def count(clasId: Clas.Id): Fu[Int] = coll.countSel($doc("clasId" -> clasId))
|
|
|
|
def isManaged(user: User): Fu[Boolean] =
|
|
coll.exists($doc("userId" -> user.id, "managed" -> true))
|
|
|
|
def release(user: User): Funit =
|
|
coll.updateField($doc("userId" -> user.id, "managed" -> true), "managed", false).void
|
|
|
|
def findManaged(user: User): Fu[Option[Student.ManagedInfo]] =
|
|
coll.find($doc("userId" -> user.id, "managed" -> true)).one[Student] flatMap {
|
|
_ ?? { student =>
|
|
userRepo.byId(student.created.by) zip clas.byId(student.clasId) map {
|
|
case (Some(teacher), Some(clas)) => Student.ManagedInfo(teacher, clas).some
|
|
case _ => none
|
|
}
|
|
}
|
|
}
|
|
|
|
def get(clas: Clas, userId: User.ID): Fu[Option[Student]] =
|
|
coll.one[Student]($id(Student.id(userId, clas.id)))
|
|
|
|
def get(clas: Clas, user: User): Fu[Option[Student.WithUser]] =
|
|
get(clas, user.id) map2 { Student.WithUser(_, user) }
|
|
|
|
def withManagingClas(s: Student.WithUser, clas: Clas): Fu[Student.WithUserAndManagingClas] = {
|
|
if (s.student.managed) fuccess(clas.some)
|
|
else
|
|
colls.student
|
|
.aggregateOne(ReadPreference.secondaryPreferred) { framework =>
|
|
import framework._
|
|
Match($doc("userId" -> s.user.id, "managed" -> true)) -> List(
|
|
PipelineOperator(
|
|
$lookup.simple(
|
|
from = colls.clas,
|
|
as = "clas",
|
|
local = "clasId",
|
|
foreign = "_id"
|
|
)
|
|
),
|
|
UnwindField("clas")
|
|
)
|
|
}
|
|
.map {
|
|
_.flatMap(_.getAsOpt[Clas]("clas"))
|
|
}
|
|
} map { Student.WithUserAndManagingClas(s, _) }
|
|
|
|
def update(from: Student, data: ClasForm.StudentData): Fu[Student] = {
|
|
val student = data update from
|
|
coll.update.one($id(student.id), student) inject student
|
|
}
|
|
|
|
def create(
|
|
clas: Clas,
|
|
data: ClasForm.NewStudent,
|
|
teacher: User
|
|
): Fu[Student.WithPassword] = {
|
|
val email = EmailAddress(s"noreply.class.${clas.id}.${data.username}@lichess.org")
|
|
val password = Student.password.generate
|
|
lila.mon.clas.student.create(teacher.id).increment()
|
|
userRepo
|
|
.create(
|
|
username = data.username,
|
|
passwordHash = authenticator.passEnc(password),
|
|
email = email,
|
|
blind = false,
|
|
mobileApiVersion = none,
|
|
mustConfirmEmail = false,
|
|
lang = teacher.lang
|
|
)
|
|
.orFail(s"No user could be created for ${data.username}")
|
|
.flatMap { user =>
|
|
studentCache addStudent user.id
|
|
val student = Student.make(user, clas, teacher.id, data.realName, managed = true)
|
|
userRepo.setKid(user, v = true) >>
|
|
userRepo.setManagedUserInitialPerfs(user.id) >>
|
|
coll.insert.one(student) >>
|
|
sendWelcomeMessage(teacher.id, user, clas) inject
|
|
Student.WithPassword(student, password)
|
|
}
|
|
}
|
|
|
|
def manyCreate(
|
|
clas: Clas,
|
|
data: ClasForm.ManyNewStudent,
|
|
teacher: User
|
|
): Fu[List[Student.WithPassword]] =
|
|
count(clas.id) flatMap { nbCurrentStudents =>
|
|
lila.common.Future.linear(data.realNames.take(Clas.maxStudents - nbCurrentStudents)) { realName =>
|
|
nameGenerator() flatMap { username =>
|
|
val data = ClasForm.NewStudent(
|
|
username = username | lila.common.ThreadLocalRandom.nextString(10),
|
|
realName = realName
|
|
)
|
|
create(clas, data, teacher)
|
|
}
|
|
}
|
|
}
|
|
|
|
def resetPassword(s: Student): Fu[ClearPassword] = {
|
|
val password = Student.password.generate
|
|
authenticator.setPassword(s.userId, password) inject password
|
|
}
|
|
|
|
def archive(sId: Student.Id, by: Holder, v: Boolean): Fu[Option[Student]] =
|
|
coll.ext
|
|
.findAndUpdate[Student](
|
|
selector = $id(sId),
|
|
update =
|
|
if (v) $set("archived" -> Clas.Recorded(by.id, DateTime.now))
|
|
else $unset("archived"),
|
|
fetchNewObject = true
|
|
)
|
|
|
|
def closeAccount(s: Student.WithUser): Funit =
|
|
coll.delete.one($id(s.student.id)).void
|
|
|
|
private[ClasApi] def sendWelcomeMessage(teacherId: User.ID, student: User, clas: Clas): Funit =
|
|
msgApi
|
|
.post(
|
|
orig = teacherId,
|
|
dest = student.id,
|
|
text = s"""${lila.i18n.I18nKeys.clas.welcomeToClass
|
|
.txt(clas.name)(student.realLang | lila.i18n.defaultLang)}
|
|
|
|
$baseUrl/class/${clas.id}
|
|
|
|
${clas.desc}""",
|
|
multi = true
|
|
)
|
|
.void
|
|
}
|
|
|
|
object invite {
|
|
|
|
import ClasInvite.Feedback._
|
|
|
|
def create(clas: Clas, user: User, realName: String, teacher: Holder): Fu[ClasInvite.Feedback] =
|
|
student
|
|
.archive(Student.id(user.id, clas.id), teacher, v = false)
|
|
.map2[ClasInvite.Feedback](_ => Already) getOrElse {
|
|
lila.mon.clas.student.invite(teacher.id).increment()
|
|
val invite = ClasInvite.make(clas, user, realName, teacher)
|
|
colls.invite.insert
|
|
.one(invite)
|
|
.void
|
|
.flatMap { _ =>
|
|
sendInviteMessage(teacher, user, clas, invite)
|
|
}
|
|
.recover {
|
|
lila.db.recoverDuplicateKey(_ => Found)
|
|
}
|
|
}
|
|
|
|
def get(id: ClasInvite.Id) = colls.invite.one[ClasInvite]($id(id))
|
|
|
|
def view(id: ClasInvite.Id, user: User): Fu[Option[(ClasInvite, Clas)]] =
|
|
colls.invite.one[ClasInvite]($id(id) ++ $doc("userId" -> user.id)) flatMap {
|
|
_ ?? { invite =>
|
|
colls.clas.byId[Clas](invite.clasId.value).map2 { invite -> _ }
|
|
}
|
|
}
|
|
|
|
def accept(id: ClasInvite.Id, user: User): Fu[Option[Student]] =
|
|
colls.invite.one[ClasInvite]($id(id) ++ $doc("userId" -> user.id)) flatMap {
|
|
_ ?? { invite =>
|
|
colls.clas.one[Clas]($id(invite.clasId)) flatMap {
|
|
_ ?? { clas =>
|
|
studentCache addStudent user.id
|
|
val stu = Student.make(user, clas, invite.created.by, invite.realName, managed = false)
|
|
colls.student.insert.one(stu) >>
|
|
colls.invite.updateField($id(id), "accepted", true) >>
|
|
student.sendWelcomeMessage(invite.created.by, user, clas) inject
|
|
stu.some recoverWith lila.db.recoverDuplicateKey { _ =>
|
|
student.get(clas, user.id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
def decline(id: ClasInvite.Id): Fu[Option[ClasInvite]] =
|
|
colls.invite.ext
|
|
.findAndUpdate[ClasInvite](
|
|
selector = $id(id),
|
|
update = $set("accepted" -> false)
|
|
)
|
|
|
|
def listPending(clas: Clas): Fu[List[ClasInvite]] =
|
|
colls.invite
|
|
.find($doc("clasId" -> clas.id, "accepted" $ne true))
|
|
.sort($sort desc "created.at")
|
|
.cursor[ClasInvite]()
|
|
.list(100)
|
|
|
|
def delete(id: ClasInvite.Id): Funit =
|
|
colls.invite.delete.one($id(id)).void
|
|
|
|
private def sendInviteMessage(
|
|
teacher: Holder,
|
|
student: User,
|
|
clas: Clas,
|
|
invite: ClasInvite
|
|
): Fu[ClasInvite.Feedback] = {
|
|
val url = s"$baseUrl/class/invitation/${invite._id}"
|
|
if (student.kid) fuccess(ClasInvite.Feedback.CantMsgKid(url))
|
|
else {
|
|
import lila.i18n.I18nKeys.clas._
|
|
implicit val lang = student.realLang | lila.i18n.defaultLang
|
|
msgApi
|
|
.post(
|
|
orig = teacher.id,
|
|
dest = student.id,
|
|
text = s"""${invitationToClass.txt(clas.name)}
|
|
|
|
${clickToViewInvitation.txt()}
|
|
|
|
$url""",
|
|
multi = true
|
|
) inject ClasInvite.Feedback.Invited
|
|
}
|
|
}
|
|
}
|
|
|
|
private def selectArchived(v: Boolean) = $doc("archived" $exists v)
|
|
}
|