From d7447289d255b7c4317e936858d5c81d8ceb1d9c Mon Sep 17 00:00:00 2001 From: YaFred Date: Sat, 14 Dec 2024 08:00:32 +0100 Subject: [PATCH 01/16] move student to another class --- app/controllers/Clas.scala | 14 ++++++++++++++ app/views/clas.scala | 2 +- conf/clas.routes | 2 ++ modules/clas/src/main/ui/StudentFormUi.scala | 20 ++++++++++++++++++++ modules/clas/src/main/ui/StudentUi.scala | 7 ++++++- 5 files changed, 43 insertions(+), 2 deletions(-) diff --git a/app/controllers/Clas.scala b/app/controllers/Clas.scala index b27410747d61..cf3336181ada 100644 --- a/app/controllers/Clas.scala +++ b/app/controllers/Clas.scala @@ -472,6 +472,20 @@ final class Clas(env: Env, authC: Auth) extends LilaController(env): else redirectTo(clas) } + def studentMove(id: ClasId, username: UserStr) = Secure(_.Teacher) { ctx ?=> me ?=> + WithClassAndStudents(id): (clas, students) => + WithStudent(clas, username): s => + if s.student.managed + then Ok.page(views.clas.student.move(clas, students, s)) + else Redirect(routes.Clas.studentShow(clas.id, s.user.username)) + } + + def studentMovePost(id: ClasId, username: UserStr) = SecureBody(_.Teacher) { ctx ?=> me ?=> + WithClassAndStudents(id): (clas, students) => + WithStudent(clas, username): s => + Redirect(routes.Clas.show(clas.id)) + } + def becomeTeacher = AuthBody { ctx ?=> me ?=> couldBeTeacher.elseNotFound: val perm = lila.core.perm.Permission.Teacher.dbKey diff --git a/app/views/clas.scala b/app/views/clas.scala index 8811ff105f2d..3a8518dc90c0 100644 --- a/app/views/clas.scala +++ b/app/views/clas.scala @@ -18,7 +18,7 @@ object student: lazy val formUi = lila.clas.ui.StudentFormUi(helpers, views.clas.ui, ui) export ui.{ invite } - export formUi.{ newStudent as form, many as manyForm, edit, release, close } + export formUi.{ newStudent as form, many as manyForm, edit, release, close, move } def show( clas: Clas, diff --git a/conf/clas.routes b/conf/clas.routes index 7d119b15396b..d0a03ee08cdc 100644 --- a/conf/clas.routes +++ b/conf/clas.routes @@ -32,3 +32,5 @@ POST /class/$id<\w{8}>/student/:username/release controllers.clas.Clas.studentR GET /class/$id<\w{8}>/student/:username/close controllers.clas.Clas.studentClose(id: ClasId, username: UserStr) POST /class/$id<\w{8}>/student/:username/close controllers.clas.Clas.studentClosePost(id: ClasId, username: UserStr) POST /class/$id<\w{8}>/invitation/revoke controllers.clas.Clas.invitationRevoke(id: ClasInviteId) +GET /class/$id<\w{8}>/student/:username/move controllers.clas.Clas.studentMove(id: ClasId, username: UserStr) +POST /class/$id<\w{8}>/student/:username/move controllers.clas.Clas.studentMovePost(id: ClasId, username: UserStr) diff --git a/modules/clas/src/main/ui/StudentFormUi.scala b/modules/clas/src/main/ui/StudentFormUi.scala index 59803a941710..840e28a31d39 100644 --- a/modules/clas/src/main/ui/StudentFormUi.scala +++ b/modules/clas/src/main/ui/StudentFormUi.scala @@ -250,6 +250,26 @@ final class StudentFormUi(helpers: Helpers, clasUi: ClasUi, studentUi: StudentUi ) ) + def move(clas: Clas, students: List[Student], s: Student.WithUser)(using Context) = + ClasPage(s.user.username.value, Left(clas.withStudents(students)), s.student.some)( + cls := "student-show student-edit" + ): + frag( + studentUi.top(clas, s), + div(cls := "box__pad")( + h2("Move to another class"), + p( + "Select a class" + ), + postForm(cls := "form3", action := routes.Clas.studentMovePost(clas.id, s.user.username))( + form3.actions( + a(href := routes.Clas.studentShow(clas.id, s.user.username))(trans.site.cancel()), + form3.submit(trans.site.apply()) + ) + ) + ) + ) + def close(clas: Clas, students: List[Student], s: Student.WithUser)(using Context) = ClasPage(s.user.username.value, Left(clas.withStudents(students)), s.student.some)( cls := "student-show student-edit" diff --git a/modules/clas/src/main/ui/StudentUi.scala b/modules/clas/src/main/ui/StudentUi.scala index e54f41201eb6..85407d8da9df 100644 --- a/modules/clas/src/main/ui/StudentUi.scala +++ b/modules/clas/src/main/ui/StudentUi.scala @@ -64,7 +64,12 @@ final class StudentUi(helpers: Helpers, clasUi: ClasUi)(using NetDomain): href := routes.Clas.studentRelease(clas.id, s.user.username), cls := "button button-empty", title := trans.clas.upgradeFromManaged.txt() - )(trans.clas.release()) + )(trans.clas.release()), + a( + href := routes.Clas.studentMove(clas.id, s.user.username), + cls := "button button-empty", + title := "Move" + )("Move") ) ) ) From 734b9f3cf49378e3ab54f05976d7267caa6de8be Mon Sep 17 00:00:00 2001 From: YaFred Date: Sat, 14 Dec 2024 19:05:54 +0100 Subject: [PATCH 02/16] move student to another class --- app/controllers/Clas.scala | 17 +++++++++--- conf/clas.routes | 2 +- modules/clas/src/main/ui/StudentFormUi.scala | 29 ++++++++++++++------ 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/app/controllers/Clas.scala b/app/controllers/Clas.scala index cf3336181ada..a09b24b7ccbb 100644 --- a/app/controllers/Clas.scala +++ b/app/controllers/Clas.scala @@ -475,12 +475,13 @@ final class Clas(env: Env, authC: Auth) extends LilaController(env): def studentMove(id: ClasId, username: UserStr) = Secure(_.Teacher) { ctx ?=> me ?=> WithClassAndStudents(id): (clas, students) => WithStudent(clas, username): s => - if s.student.managed - then Ok.page(views.clas.student.move(clas, students, s)) - else Redirect(routes.Clas.studentShow(clas.id, s.user.username)) + WithMyOtherClasses(clas): classes => + if s.student.managed + then Ok.page(views.clas.student.move(clas, students, s, classes)) + else Redirect(routes.Clas.studentShow(clas.id, s.user.username)) } - def studentMovePost(id: ClasId, username: UserStr) = SecureBody(_.Teacher) { ctx ?=> me ?=> + def studentMovePost(id: ClasId, username: UserStr, to: ClasId) = SecureBody(_.Teacher) { ctx ?=> me ?=> WithClassAndStudents(id): (clas, students) => WithStudent(clas, username): s => Redirect(routes.Clas.show(clas.id)) @@ -537,6 +538,14 @@ final class Clas(env: Env, authC: Auth) extends LilaController(env): WithClass(clasId): c => env.clas.api.student.activeOf(c).flatMap { f(c, _) } + private def WithMyOtherClasses(clas: lila.clas.Clas)( + f: (List[lila.clas.Clas]) => Fu[Result] + )(using teacher: Me): Fu[Result] = + val res = + for classes <- env.clas.api.clas.of(teacher) + yield classes.filter(_.id != clas.id) + res.flatMap(f) + private def WithStudent(clas: lila.clas.Clas, username: UserStr)( f: lila.clas.Student.WithUser => Fu[Result] )(using Context): Fu[Result] = diff --git a/conf/clas.routes b/conf/clas.routes index d0a03ee08cdc..893c5a1bc7c8 100644 --- a/conf/clas.routes +++ b/conf/clas.routes @@ -33,4 +33,4 @@ GET /class/$id<\w{8}>/student/:username/close controllers.clas.Clas.studentClo POST /class/$id<\w{8}>/student/:username/close controllers.clas.Clas.studentClosePost(id: ClasId, username: UserStr) POST /class/$id<\w{8}>/invitation/revoke controllers.clas.Clas.invitationRevoke(id: ClasInviteId) GET /class/$id<\w{8}>/student/:username/move controllers.clas.Clas.studentMove(id: ClasId, username: UserStr) -POST /class/$id<\w{8}>/student/:username/move controllers.clas.Clas.studentMovePost(id: ClasId, username: UserStr) +POST /class/$id<\w{8}>/student/:username/move/$to<\w{8}> controllers.clas.Clas.studentMovePost(id: ClasId, username: UserStr, to: ClasId) diff --git a/modules/clas/src/main/ui/StudentFormUi.scala b/modules/clas/src/main/ui/StudentFormUi.scala index 840e28a31d39..a764181272e5 100644 --- a/modules/clas/src/main/ui/StudentFormUi.scala +++ b/modules/clas/src/main/ui/StudentFormUi.scala @@ -250,22 +250,33 @@ final class StudentFormUi(helpers: Helpers, clasUi: ClasUi, studentUi: StudentUi ) ) - def move(clas: Clas, students: List[Student], s: Student.WithUser)(using Context) = + def move(clas: Clas, students: List[Student], s: Student.WithUser, otherClasses: List[Clas])(using + Context + ) = ClasPage(s.user.username.value, Left(clas.withStudents(students)), s.student.some)( cls := "student-show student-edit" ): + + val classForms: Frag = + for (aClass <- otherClasses) + yield ( + postForm( + action := routes.Clas.studentMovePost(clas.id, s.student.userId, aClass.id) + )( + form3.submit(aClass.name, icon = Icon.Target.some)( + cls := "yes-no-confirm button-blue button-empty", + title := "Move to " + aClass.name + ) + ) + ) + frag( studentUi.top(clas, s), div(cls := "box__pad")( h2("Move to another class"), - p( - "Select a class" - ), - postForm(cls := "form3", action := routes.Clas.studentMovePost(clas.id, s.user.username))( - form3.actions( - a(href := routes.Clas.studentShow(clas.id, s.user.username))(trans.site.cancel()), - form3.submit(trans.site.apply()) - ) + classForms, + form3.actions( + a(href := routes.Clas.studentShow(clas.id, s.user.username))(trans.site.cancel()) ) ) ) From b35a2c111bb8c914f8285935980eccdaf5658960 Mon Sep 17 00:00:00 2001 From: YaFred Date: Sat, 14 Dec 2024 22:07:56 +0100 Subject: [PATCH 03/16] move student to another class --- app/controllers/Clas.scala | 11 +++++++---- modules/clas/src/main/ClasApi.scala | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/controllers/Clas.scala b/app/controllers/Clas.scala index a09b24b7ccbb..430ac3775299 100644 --- a/app/controllers/Clas.scala +++ b/app/controllers/Clas.scala @@ -11,6 +11,7 @@ import lila.clas.ClasForm.ClasData import lila.clas.ClasInvite import lila.core.id.{ ClasId, ClasInviteId } import lila.core.security.ClearPassword +import lila.clas.ClasForm.CreateStudent final class Clas(env: Env, authC: Auth) extends LilaController(env): @@ -476,15 +477,17 @@ final class Clas(env: Env, authC: Auth) extends LilaController(env): WithClassAndStudents(id): (clas, students) => WithStudent(clas, username): s => WithMyOtherClasses(clas): classes => - if s.student.managed - then Ok.page(views.clas.student.move(clas, students, s, classes)) - else Redirect(routes.Clas.studentShow(clas.id, s.user.username)) + Ok.page(views.clas.student.move(clas, students, s, classes)) } def studentMovePost(id: ClasId, username: UserStr, to: ClasId) = SecureBody(_.Teacher) { ctx ?=> me ?=> WithClassAndStudents(id): (clas, students) => WithStudent(clas, username): s => - Redirect(routes.Clas.show(clas.id)) + WithClass(to): toClas => + (env.clas.api.student + .createCopy(toClas, s, me) >> + env.clas.api.student.closeAccount(s)) + .inject(Redirect(routes.Clas.show(clas.id))) } def becomeTeacher = AuthBody { ctx ?=> me ?=> diff --git a/modules/clas/src/main/ClasApi.scala b/modules/clas/src/main/ClasApi.scala index 94404a085ef2..015fd51e44d3 100644 --- a/modules/clas/src/main/ClasApi.scala +++ b/modules/clas/src/main/ClasApi.scala @@ -10,6 +10,8 @@ import lila.core.id.{ ClasId, ClasInviteId, StudentId } import lila.core.msg.MsgApi import lila.db.dsl.{ *, given } import lila.rating.{ Perf, PerfType, UserPerfs } +import lila.core.user.Me.userId +import lila.clas.Student.WithUser final class ClasApi( colls: ClasColls, @@ -258,6 +260,19 @@ final class ClasApi( sendWelcomeMessage(teacher.id, user, clas)).inject(Student.WithPassword(student, password)) } + def createCopy( + clas: Clas, + s: WithUser, + teacher: Me + ): Fu[Option[Student]] = + val stu = Student.make(s.user, clas, teacher.userId, s.student.realName, s.student.managed) + colls.student.insert + .one(stu) + .inject(stu.some) + .recoverWith(lila.db.recoverDuplicateKey { _ => + student.get(clas, s.user.id) + }) + def manyCreate( clas: Clas, data: ClasForm.ManyNewStudent, From c1ad04ae2abddbbd2ef6aacc3d6ff30357cd7960 Mon Sep 17 00:00:00 2001 From: YaFred Date: Sun, 15 Dec 2024 01:22:10 +0100 Subject: [PATCH 04/16] useless import --- app/controllers/Clas.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/Clas.scala b/app/controllers/Clas.scala index 430ac3775299..6c71f8f438c5 100644 --- a/app/controllers/Clas.scala +++ b/app/controllers/Clas.scala @@ -11,7 +11,6 @@ import lila.clas.ClasForm.ClasData import lila.clas.ClasInvite import lila.core.id.{ ClasId, ClasInviteId } import lila.core.security.ClearPassword -import lila.clas.ClasForm.CreateStudent final class Clas(env: Env, authC: Auth) extends LilaController(env): From 7438d5f816ad55e909c89353f66a2d7d59406342 Mon Sep 17 00:00:00 2001 From: YaFred Date: Tue, 17 Dec 2024 02:59:00 +0100 Subject: [PATCH 05/16] even non managed student can be moved to another class --- modules/clas/src/main/ui/StudentFormUi.scala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/clas/src/main/ui/StudentFormUi.scala b/modules/clas/src/main/ui/StudentFormUi.scala index a764181272e5..350ff656f89c 100644 --- a/modules/clas/src/main/ui/StudentFormUi.scala +++ b/modules/clas/src/main/ui/StudentFormUi.scala @@ -217,6 +217,13 @@ final class StudentFormUi(helpers: Helpers, clasUi: ClasUi, studentUi: StudentUi cls := "button button-empty button-red", title := trans.clas.closeDesc1.txt() )(trans.clas.closeStudent()) + ), + (!s.student.managed).option( + a( + href := routes.Clas.studentMove(clas.id, s.user.username), + cls := "button button-empty", + title := "Move" + )("Move") ) ) ) From 7213ee2583430b12d52e97ecbf75bae4ac8480e5 Mon Sep 17 00:00:00 2001 From: YaFred Date: Wed, 18 Dec 2024 01:50:43 +0100 Subject: [PATCH 06/16] new translations --- modules/clas/src/main/ui/StudentFormUi.scala | 8 ++++---- modules/clas/src/main/ui/StudentUi.scala | 4 ++-- modules/coreI18n/src/main/key.scala | 3 +++ translation/source/class.xml | 3 +++ ui/@types/lichess/i18n.d.ts | 6 ++++++ 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/modules/clas/src/main/ui/StudentFormUi.scala b/modules/clas/src/main/ui/StudentFormUi.scala index 350ff656f89c..ea0016375b73 100644 --- a/modules/clas/src/main/ui/StudentFormUi.scala +++ b/modules/clas/src/main/ui/StudentFormUi.scala @@ -222,8 +222,8 @@ final class StudentFormUi(helpers: Helpers, clasUi: ClasUi, studentUi: StudentUi a( href := routes.Clas.studentMove(clas.id, s.user.username), cls := "button button-empty", - title := "Move" - )("Move") + title := trans.clas.move.txt() + )(trans.clas.move()) ) ) ) @@ -272,7 +272,7 @@ final class StudentFormUi(helpers: Helpers, clasUi: ClasUi, studentUi: StudentUi )( form3.submit(aClass.name, icon = Icon.Target.some)( cls := "yes-no-confirm button-blue button-empty", - title := "Move to " + aClass.name + title := trans.clas.moveToClass.txt(aClass.name) ) ) ) @@ -280,7 +280,7 @@ final class StudentFormUi(helpers: Helpers, clasUi: ClasUi, studentUi: StudentUi frag( studentUi.top(clas, s), div(cls := "box__pad")( - h2("Move to another class"), + h2(trans.clas.moveToAnotherClass()), classForms, form3.actions( a(href := routes.Clas.studentShow(clas.id, s.user.username))(trans.site.cancel()) diff --git a/modules/clas/src/main/ui/StudentUi.scala b/modules/clas/src/main/ui/StudentUi.scala index 85407d8da9df..34637847d8ba 100644 --- a/modules/clas/src/main/ui/StudentUi.scala +++ b/modules/clas/src/main/ui/StudentUi.scala @@ -68,8 +68,8 @@ final class StudentUi(helpers: Helpers, clasUi: ClasUi)(using NetDomain): a( href := routes.Clas.studentMove(clas.id, s.user.username), cls := "button button-empty", - title := "Move" - )("Move") + title := trans.clas.move.txt() + )(trans.clas.move()) ) ) ) diff --git a/modules/coreI18n/src/main/key.scala b/modules/coreI18n/src/main/key.scala index e4179c5b9321..5107ed9cfa43 100644 --- a/modules/coreI18n/src/main/key.scala +++ b/modules/coreI18n/src/main/key.scala @@ -330,6 +330,9 @@ object I18nKey: val `anInvitationHasBeenSentToX`: I18nKey = "class:anInvitationHasBeenSentToX" val `xAlreadyHasAPendingInvitation`: I18nKey = "class:xAlreadyHasAPendingInvitation" val `xIsAKidAccountWarning`: I18nKey = "class:xIsAKidAccountWarning" + val `move`: I18nKey = "class:move" + val `moveToClass`: I18nKey = "class:moveToClass" + val `moveToAnotherClass`: I18nKey = "class:moveToAnotherClass" val `nbPendingInvitations`: I18nKey = "class:nbPendingInvitations" val `nbTeachers`: I18nKey = "class:nbTeachers" val `nbStudents`: I18nKey = "class:nbStudents" diff --git a/translation/source/class.xml b/translation/source/class.xml index c037cb340751..afc4bc8ac546 100644 --- a/translation/source/class.xml +++ b/translation/source/class.xml @@ -114,4 +114,7 @@ It will display a horizontal line. An invitation has been sent to %s %s already has a pending invitation %1$s is a kid account and can't receive your message. You must give them the invitation URL manually: %2$s + Move + Move to %s + Move to another class diff --git a/ui/@types/lichess/i18n.d.ts b/ui/@types/lichess/i18n.d.ts index 8df1de4f1f73..2a524053f58d 100644 --- a/ui/@types/lichess/i18n.d.ts +++ b/ui/@types/lichess/i18n.d.ts @@ -515,6 +515,12 @@ interface I18n { maxStudentsNote: I18nFormat; /** Message all students about new class material */ messageAllStudents: string; + /** Move */ + move: string; + /** Move to another class */ + moveToAnotherClass: string; + /** Move to %s */ + moveToClass: I18nFormat; /** You can also %s to create multiple Lichess accounts from a list of student names. */ multipleAccsFormDescription: I18nFormat; /** N/A */ From ad9ce9c0d3b9f1bc96c9861c47f08981127c0ec3 Mon Sep 17 00:00:00 2001 From: YaFred Date: Wed, 18 Dec 2024 02:12:55 +0100 Subject: [PATCH 07/16] missing translations --- modules/clas/src/main/ui/DashboardUi.scala | 4 ++-- modules/coreI18n/src/main/key.scala | 3 +++ translation/source/class.xml | 3 +++ ui/@types/lichess/i18n.d.ts | 6 ++++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/modules/clas/src/main/ui/DashboardUi.scala b/modules/clas/src/main/ui/DashboardUi.scala index 7d36bbe73c43..68beb2432d01 100644 --- a/modules/clas/src/main/ui/DashboardUi.scala +++ b/modules/clas/src/main/ui/DashboardUi.scala @@ -139,11 +139,11 @@ final class DashboardUi(helpers: Helpers, ui: ClasUi)(using NetDomain): tr( td(userIdLink(i.userId.some)), td(i.realName), - td(if i.accepted.has(false) then "Declined" else "Pending"), + td(if i.accepted.has(false) then trans.clas.declined.txt() else trans.clas.pending.txt()), td(momentFromNow(i.created.at)), td: postForm(action := routes.Clas.invitationRevoke(i.id)): - submitButton(cls := "button button-red button-empty")("Revoke") + submitButton(cls := "button button-red button-empty")(trans.clas.revoke()) ) ) val archivedBox = diff --git a/modules/coreI18n/src/main/key.scala b/modules/coreI18n/src/main/key.scala index 5107ed9cfa43..33b1e7894f0a 100644 --- a/modules/coreI18n/src/main/key.scala +++ b/modules/coreI18n/src/main/key.scala @@ -282,6 +282,9 @@ object I18nKey: val `welcomeToClass`: I18nKey = "class:welcomeToClass" val `invitationToClass`: I18nKey = "class:invitationToClass" val `clickToViewInvitation`: I18nKey = "class:clickToViewInvitation" + val `pending`: I18nKey = "class:pending" + val `declined`: I18nKey = "class:declined" + val `revoke`: I18nKey = "class:revoke" val `onlyVisibleToTeachers`: I18nKey = "class:onlyVisibleToTeachers" val `lastActiveDate`: I18nKey = "class:lastActiveDate" val `managed`: I18nKey = "class:managed" diff --git a/translation/source/class.xml b/translation/source/class.xml index afc4bc8ac546..4d4997e2ea96 100644 --- a/translation/source/class.xml +++ b/translation/source/class.xml @@ -57,6 +57,9 @@ Here is the link to access the class. One pending invitation %s pending invitations + Pending + Declined + Revoke Only visible to the class teachers Active Managed diff --git a/ui/@types/lichess/i18n.d.ts b/ui/@types/lichess/i18n.d.ts index 2a524053f58d..3a842f58f4f5 100644 --- a/ui/@types/lichess/i18n.d.ts +++ b/ui/@types/lichess/i18n.d.ts @@ -471,6 +471,8 @@ interface I18n { createMultipleAccounts: string; /** Only create accounts for real students. Do not use this to make multiple accounts for yourself. You would get banned. */ createStudentWarning: string; + /** Declined */ + declined: string; /** Edit news */ editNews: string; /** Features */ @@ -561,6 +563,8 @@ interface I18n { overview: string; /** Password: %s */ passwordX: I18nFormat; + /** Pending */ + pending: string; /** Private. Will never be shown outside the class. Helps you remember who the student is. */ privateWillNeverBeShown: string; /** Progress */ @@ -589,6 +593,8 @@ interface I18n { reopen: string; /** Reset password */ resetPassword: string; + /** Revoke */ + revoke: string; /** Send a message to all students. */ sendAMessage: string; /** Student: %1$s */ From 8ce33077bb0ce8765889261a3beacf63720d9cb6 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 18 Dec 2024 09:11:03 +0100 Subject: [PATCH 08/16] sbt scalafmtAll --- modules/clas/src/main/ui/DashboardUi.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/clas/src/main/ui/DashboardUi.scala b/modules/clas/src/main/ui/DashboardUi.scala index 68beb2432d01..884a26b461c5 100644 --- a/modules/clas/src/main/ui/DashboardUi.scala +++ b/modules/clas/src/main/ui/DashboardUi.scala @@ -139,7 +139,9 @@ final class DashboardUi(helpers: Helpers, ui: ClasUi)(using NetDomain): tr( td(userIdLink(i.userId.some)), td(i.realName), - td(if i.accepted.has(false) then trans.clas.declined.txt() else trans.clas.pending.txt()), + td( + if i.accepted.has(false) then trans.clas.declined.txt() else trans.clas.pending.txt() + ), td(momentFromNow(i.created.at)), td: postForm(action := routes.Clas.invitationRevoke(i.id)): From f56652d25853cb2e3cdde7e3f90e2265e5f11336 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 18 Dec 2024 09:32:19 +0100 Subject: [PATCH 09/16] tweak student move controller code, no functional change --- app/controllers/Clas.scala | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/app/controllers/Clas.scala b/app/controllers/Clas.scala index 6c71f8f438c5..4957b9809183 100644 --- a/app/controllers/Clas.scala +++ b/app/controllers/Clas.scala @@ -475,18 +475,21 @@ final class Clas(env: Env, authC: Auth) extends LilaController(env): def studentMove(id: ClasId, username: UserStr) = Secure(_.Teacher) { ctx ?=> me ?=> WithClassAndStudents(id): (clas, students) => WithStudent(clas, username): s => - WithMyOtherClasses(clas): classes => - Ok.page(views.clas.student.move(clas, students, s, classes)) + for + classes <- env.clas.api.clas.of(me) + others = classes.filter(_.id != clas.id) + res <- Ok.page(views.clas.student.move(clas, students, s, others)) + yield res } def studentMovePost(id: ClasId, username: UserStr, to: ClasId) = SecureBody(_.Teacher) { ctx ?=> me ?=> WithClassAndStudents(id): (clas, students) => WithStudent(clas, username): s => WithClass(to): toClas => - (env.clas.api.student - .createCopy(toClas, s, me) >> - env.clas.api.student.closeAccount(s)) - .inject(Redirect(routes.Clas.show(clas.id))) + for + _ <- env.clas.api.student.createCopy(toClas, s, me) + _ <- env.clas.api.student.closeAccount(s) + yield Redirect(routes.Clas.show(clas.id)) } def becomeTeacher = AuthBody { ctx ?=> me ?=> @@ -540,14 +543,6 @@ final class Clas(env: Env, authC: Auth) extends LilaController(env): WithClass(clasId): c => env.clas.api.student.activeOf(c).flatMap { f(c, _) } - private def WithMyOtherClasses(clas: lila.clas.Clas)( - f: (List[lila.clas.Clas]) => Fu[Result] - )(using teacher: Me): Fu[Result] = - val res = - for classes <- env.clas.api.clas.of(teacher) - yield classes.filter(_.id != clas.id) - res.flatMap(f) - private def WithStudent(clas: lila.clas.Clas, username: UserStr)( f: lila.clas.Student.WithUser => Fu[Result] )(using Context): Fu[Result] = From c653943177bd0a429c7c600248ac763a73e55482 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 18 Dec 2024 09:42:34 +0100 Subject: [PATCH 10/16] move implementation to ClasApi --- app/controllers/Clas.scala | 4 +--- modules/clas/src/main/ClasApi.scala | 14 ++++++-------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/app/controllers/Clas.scala b/app/controllers/Clas.scala index 4957b9809183..5b5889469651 100644 --- a/app/controllers/Clas.scala +++ b/app/controllers/Clas.scala @@ -486,9 +486,7 @@ final class Clas(env: Env, authC: Auth) extends LilaController(env): WithClassAndStudents(id): (clas, students) => WithStudent(clas, username): s => WithClass(to): toClas => - for - _ <- env.clas.api.student.createCopy(toClas, s, me) - _ <- env.clas.api.student.closeAccount(s) + for _ <- env.clas.api.student.move(s, toClas) yield Redirect(routes.Clas.show(clas.id)) } diff --git a/modules/clas/src/main/ClasApi.scala b/modules/clas/src/main/ClasApi.scala index 015fd51e44d3..3c4ec90b6b9b 100644 --- a/modules/clas/src/main/ClasApi.scala +++ b/modules/clas/src/main/ClasApi.scala @@ -260,18 +260,16 @@ final class ClasApi( sendWelcomeMessage(teacher.id, user, clas)).inject(Student.WithPassword(student, password)) } - def createCopy( - clas: Clas, - s: WithUser, - teacher: Me - ): Fu[Option[Student]] = - val stu = Student.make(s.user, clas, teacher.userId, s.student.realName, s.student.managed) - colls.student.insert + def move(s: WithUser, toClas: Clas)(using teacher: Me): Fu[Option[Student]] = for + _ <- closeAccount(s) + stu = Student.make(s.user, toClas, teacher.userId, s.student.realName, s.student.managed) + moved <- colls.student.insert .one(stu) .inject(stu.some) .recoverWith(lila.db.recoverDuplicateKey { _ => - student.get(clas, s.user.id) + student.get(toClas, s.user.id) }) + yield moved def manyCreate( clas: Clas, From 6f96517ba8527d0515195c298d27eecc83b72dbd Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 18 Dec 2024 09:44:18 +0100 Subject: [PATCH 11/16] visual success feedback after moving a student --- app/controllers/Clas.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/Clas.scala b/app/controllers/Clas.scala index 5b5889469651..ce44029ffc7e 100644 --- a/app/controllers/Clas.scala +++ b/app/controllers/Clas.scala @@ -487,7 +487,7 @@ final class Clas(env: Env, authC: Auth) extends LilaController(env): WithStudent(clas, username): s => WithClass(to): toClas => for _ <- env.clas.api.student.move(s, toClas) - yield Redirect(routes.Clas.show(clas.id)) + yield Redirect(routes.Clas.show(clas.id)).flashSuccess } def becomeTeacher = AuthBody { ctx ?=> me ?=> From 9ebe29d0735720eb68aaa3fba6748d5c1c29a522 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 18 Dec 2024 09:51:28 +0100 Subject: [PATCH 12/16] don't override student notes and metadata when moving them --- modules/clas/src/main/ClasApi.scala | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/modules/clas/src/main/ClasApi.scala b/modules/clas/src/main/ClasApi.scala index 3c4ec90b6b9b..b37969700376 100644 --- a/modules/clas/src/main/ClasApi.scala +++ b/modules/clas/src/main/ClasApi.scala @@ -10,8 +10,6 @@ import lila.core.id.{ ClasId, ClasInviteId, StudentId } import lila.core.msg.MsgApi import lila.db.dsl.{ *, given } import lila.rating.{ Perf, PerfType, UserPerfs } -import lila.core.user.Me.userId -import lila.clas.Student.WithUser final class ClasApi( colls: ClasColls, @@ -260,9 +258,9 @@ final class ClasApi( sendWelcomeMessage(teacher.id, user, clas)).inject(Student.WithPassword(student, password)) } - def move(s: WithUser, toClas: Clas)(using teacher: Me): Fu[Option[Student]] = for + def move(s: Student.WithUser, toClas: Clas)(using teacher: Me): Fu[Option[Student]] = for _ <- closeAccount(s) - stu = Student.make(s.user, toClas, teacher.userId, s.student.realName, s.student.managed) + stu = s.student.copy(id = Student.makeId(s.user.id, toClas.id), clasId = toClas.id) moved <- colls.student.insert .one(stu) .inject(stu.some) From 0c12c471c54181f73c580dae1c74a6f8adfc6c57 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 18 Dec 2024 10:00:17 +0100 Subject: [PATCH 13/16] scala code golf --- modules/clas/src/main/ui/StudentFormUi.scala | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/modules/clas/src/main/ui/StudentFormUi.scala b/modules/clas/src/main/ui/StudentFormUi.scala index ea0016375b73..94df46d61773 100644 --- a/modules/clas/src/main/ui/StudentFormUi.scala +++ b/modules/clas/src/main/ui/StudentFormUi.scala @@ -264,18 +264,13 @@ final class StudentFormUi(helpers: Helpers, clasUi: ClasUi, studentUi: StudentUi cls := "student-show student-edit" ): - val classForms: Frag = - for (aClass <- otherClasses) - yield ( - postForm( - action := routes.Clas.studentMovePost(clas.id, s.student.userId, aClass.id) - )( - form3.submit(aClass.name, icon = Icon.Target.some)( - cls := "yes-no-confirm button-blue button-empty", - title := trans.clas.moveToClass.txt(aClass.name) - ) - ) + val classForms: Frag = otherClasses.map: toClass => + postForm(action := routes.Clas.studentMovePost(clas.id, s.student.userId, toClass.id))( + form3.submit(toClass.name, icon = Icon.InternalArrow.some)( + cls := "yes-no-confirm button-blue button-empty", + title := trans.clas.moveToClass.txt(toClass.name) ) + ) frag( studentUi.top(clas, s), From 3c2aed5f4f2a5b55e347019f488ffcab239488d9 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 18 Dec 2024 10:07:38 +0100 Subject: [PATCH 14/16] avoid adding unnecessary translation --- modules/clas/src/main/ui/DashboardUi.scala | 2 +- modules/coreI18n/src/main/key.scala | 1 - translation/source/class.xml | 1 - ui/@types/lichess/i18n.d.ts | 2 -- 4 files changed, 1 insertion(+), 5 deletions(-) diff --git a/modules/clas/src/main/ui/DashboardUi.scala b/modules/clas/src/main/ui/DashboardUi.scala index 884a26b461c5..532c19eac7f5 100644 --- a/modules/clas/src/main/ui/DashboardUi.scala +++ b/modules/clas/src/main/ui/DashboardUi.scala @@ -145,7 +145,7 @@ final class DashboardUi(helpers: Helpers, ui: ClasUi)(using NetDomain): td(momentFromNow(i.created.at)), td: postForm(action := routes.Clas.invitationRevoke(i.id)): - submitButton(cls := "button button-red button-empty")(trans.clas.revoke()) + submitButton(cls := "button button-red button-empty")(trans.site.delete()) ) ) val archivedBox = diff --git a/modules/coreI18n/src/main/key.scala b/modules/coreI18n/src/main/key.scala index 33b1e7894f0a..28538e522c02 100644 --- a/modules/coreI18n/src/main/key.scala +++ b/modules/coreI18n/src/main/key.scala @@ -284,7 +284,6 @@ object I18nKey: val `clickToViewInvitation`: I18nKey = "class:clickToViewInvitation" val `pending`: I18nKey = "class:pending" val `declined`: I18nKey = "class:declined" - val `revoke`: I18nKey = "class:revoke" val `onlyVisibleToTeachers`: I18nKey = "class:onlyVisibleToTeachers" val `lastActiveDate`: I18nKey = "class:lastActiveDate" val `managed`: I18nKey = "class:managed" diff --git a/translation/source/class.xml b/translation/source/class.xml index 4d4997e2ea96..34a02207ea46 100644 --- a/translation/source/class.xml +++ b/translation/source/class.xml @@ -59,7 +59,6 @@ Here is the link to access the class. Pending Declined - Revoke Only visible to the class teachers Active Managed diff --git a/ui/@types/lichess/i18n.d.ts b/ui/@types/lichess/i18n.d.ts index 3a842f58f4f5..7b93baaf32cf 100644 --- a/ui/@types/lichess/i18n.d.ts +++ b/ui/@types/lichess/i18n.d.ts @@ -593,8 +593,6 @@ interface I18n { reopen: string; /** Reset password */ resetPassword: string; - /** Revoke */ - revoke: string; /** Send a message to all students. */ sendAMessage: string; /** Student: %1$s */ From b307b1d9ab061b8a454c1ecd595fda1d90f1fabe Mon Sep 17 00:00:00 2001 From: YaFred Date: Wed, 18 Dec 2024 16:56:40 +0100 Subject: [PATCH 15/16] remove superfluous translation --- modules/clas/src/main/ui/StudentFormUi.scala | 5 ++--- modules/clas/src/main/ui/StudentUi.scala | 5 ++--- modules/coreI18n/src/main/key.scala | 1 - translation/source/class.xml | 1 - ui/@types/lichess/i18n.d.ts | 2 -- 5 files changed, 4 insertions(+), 10 deletions(-) diff --git a/modules/clas/src/main/ui/StudentFormUi.scala b/modules/clas/src/main/ui/StudentFormUi.scala index 94df46d61773..7bf632217891 100644 --- a/modules/clas/src/main/ui/StudentFormUi.scala +++ b/modules/clas/src/main/ui/StudentFormUi.scala @@ -221,9 +221,8 @@ final class StudentFormUi(helpers: Helpers, clasUi: ClasUi, studentUi: StudentUi (!s.student.managed).option( a( href := routes.Clas.studentMove(clas.id, s.user.username), - cls := "button button-empty", - title := trans.clas.move.txt() - )(trans.clas.move()) + cls := "button button-empty" + )(trans.clas.moveToAnotherClass()) ) ) ) diff --git a/modules/clas/src/main/ui/StudentUi.scala b/modules/clas/src/main/ui/StudentUi.scala index 34637847d8ba..6a13b9e4d340 100644 --- a/modules/clas/src/main/ui/StudentUi.scala +++ b/modules/clas/src/main/ui/StudentUi.scala @@ -67,9 +67,8 @@ final class StudentUi(helpers: Helpers, clasUi: ClasUi)(using NetDomain): )(trans.clas.release()), a( href := routes.Clas.studentMove(clas.id, s.user.username), - cls := "button button-empty", - title := trans.clas.move.txt() - )(trans.clas.move()) + cls := "button button-empty" + )(trans.clas.moveToAnotherClass()) ) ) ) diff --git a/modules/coreI18n/src/main/key.scala b/modules/coreI18n/src/main/key.scala index 28538e522c02..ef228129bf5f 100644 --- a/modules/coreI18n/src/main/key.scala +++ b/modules/coreI18n/src/main/key.scala @@ -332,7 +332,6 @@ object I18nKey: val `anInvitationHasBeenSentToX`: I18nKey = "class:anInvitationHasBeenSentToX" val `xAlreadyHasAPendingInvitation`: I18nKey = "class:xAlreadyHasAPendingInvitation" val `xIsAKidAccountWarning`: I18nKey = "class:xIsAKidAccountWarning" - val `move`: I18nKey = "class:move" val `moveToClass`: I18nKey = "class:moveToClass" val `moveToAnotherClass`: I18nKey = "class:moveToAnotherClass" val `nbPendingInvitations`: I18nKey = "class:nbPendingInvitations" diff --git a/translation/source/class.xml b/translation/source/class.xml index 34a02207ea46..5c2bccacb804 100644 --- a/translation/source/class.xml +++ b/translation/source/class.xml @@ -116,7 +116,6 @@ It will display a horizontal line. An invitation has been sent to %s %s already has a pending invitation %1$s is a kid account and can't receive your message. You must give them the invitation URL manually: %2$s - Move Move to %s Move to another class diff --git a/ui/@types/lichess/i18n.d.ts b/ui/@types/lichess/i18n.d.ts index 7b93baaf32cf..24829dbb28f5 100644 --- a/ui/@types/lichess/i18n.d.ts +++ b/ui/@types/lichess/i18n.d.ts @@ -517,8 +517,6 @@ interface I18n { maxStudentsNote: I18nFormat; /** Message all students about new class material */ messageAllStudents: string; - /** Move */ - move: string; /** Move to another class */ moveToAnotherClass: string; /** Move to %s */ From dda89a4999ce2fc687553b3f32e3ad91811b1f93 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 18 Dec 2024 17:30:30 +0100 Subject: [PATCH 16/16] student move button always in the same place --- modules/clas/src/main/ui/StudentFormUi.scala | 10 ++++------ modules/clas/src/main/ui/StudentUi.scala | 6 +----- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/modules/clas/src/main/ui/StudentFormUi.scala b/modules/clas/src/main/ui/StudentFormUi.scala index 7bf632217891..38095a7e77b6 100644 --- a/modules/clas/src/main/ui/StudentFormUi.scala +++ b/modules/clas/src/main/ui/StudentFormUi.scala @@ -218,12 +218,10 @@ final class StudentFormUi(helpers: Helpers, clasUi: ClasUi, studentUi: StudentUi title := trans.clas.closeDesc1.txt() )(trans.clas.closeStudent()) ), - (!s.student.managed).option( - a( - href := routes.Clas.studentMove(clas.id, s.user.username), - cls := "button button-empty" - )(trans.clas.moveToAnotherClass()) - ) + a( + href := routes.Clas.studentMove(clas.id, s.user.username), + cls := "button button-empty" + )(trans.clas.moveToAnotherClass()) ) ) ) diff --git a/modules/clas/src/main/ui/StudentUi.scala b/modules/clas/src/main/ui/StudentUi.scala index 6a13b9e4d340..e54f41201eb6 100644 --- a/modules/clas/src/main/ui/StudentUi.scala +++ b/modules/clas/src/main/ui/StudentUi.scala @@ -64,11 +64,7 @@ final class StudentUi(helpers: Helpers, clasUi: ClasUi)(using NetDomain): href := routes.Clas.studentRelease(clas.id, s.user.username), cls := "button button-empty", title := trans.clas.upgradeFromManaged.txt() - )(trans.clas.release()), - a( - href := routes.Clas.studentMove(clas.id, s.user.username), - cls := "button button-empty" - )(trans.clas.moveToAnotherClass()) + )(trans.clas.release()) ) ) )