class WIP

pull/5938/head
Thibault Duplessis 2020-01-18 19:59:05 -06:00
parent 5510460d69
commit 5a2a8ea80a
11 changed files with 167 additions and 75 deletions

View File

@ -52,15 +52,15 @@ final class Clas(
isGranted(_.Teacher).??(env.clas.api.clas.isTeacherOf(me, lila.clas.Clas.Id(id))) flatMap {
case true =>
WithClass(me, id) { _ => clas =>
env.clas.api.student.allOfWithUsers(clas) map { students =>
env.clas.api.student.activeWithUsers(clas) map { students =>
preloadStudentUsers(students)
views.html.clas.teacherDashboard(clas, students)
views.html.clas.teacherDashboard.active(clas, students)
}
}
case _ =>
env.clas.api.clas.byId(lila.clas.Clas.Id(id)) flatMap {
_ ?? { clas =>
env.clas.api.student.activeOfWithUsers(clas) flatMap { students =>
env.clas.api.student.activeWithUsers(clas) flatMap { students =>
if (students.exists(_.student is me)) {
preloadStudentUsers(students)
Ok(views.html.clas.studentDashboard(clas, students)).fuccess
@ -71,6 +71,24 @@ final class Clas(
}
}
def archived(id: String) = Secure(_.Teacher) { implicit ctx => me =>
WithClass(me, id) { _ => clas =>
env.clas.api.student.allWithUsers(clas) map { students =>
views.html.clas.teacherDashboard.archived(clas, students)
}
}
}
def perfType(id: String, key: String) = Secure(_.Teacher) { implicit ctx => me =>
lila.rating.PerfType(key) ?? { perfType =>
WithClass(me, id) { _ => clas =>
env.clas.api.student.activeWithUsers(clas) map { students =>
views.html.clas.teacherDashboard.perf(clas, students, perfType)
}
}
}
}
def edit(id: String) = Secure(_.Teacher) { implicit ctx => me =>
WithClass(me, id) { _ => clas =>
Ok(html.clas.clas.edit(clas, env.clas.forms.edit(clas))).fuccess

View File

@ -184,7 +184,7 @@ trait UserHelper { self: I18nHelper with StringHelper with NumberHelper =>
withTitle: Boolean = true,
withBestRating: Boolean = false,
withPerfRating: Option[PerfType] = None,
text: Option[String] = None
name: Option[Frag] = None
): Frag =
span(
cls := userClass(user.id, cssClass, withOnline, withPowerTip),
@ -192,7 +192,7 @@ trait UserHelper { self: I18nHelper with StringHelper with NumberHelper =>
)(
withOnline ?? lineIcon(user),
withTitle option titleTag(user.title),
text | user.username,
name | user.username,
userRating(user, withPerfRating, withBestRating)
)

View File

@ -1,10 +1,10 @@
package views.html.clas
import controllers.routes
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.clas.{ Clas, Student }
import controllers.routes
object bits {

View File

@ -14,7 +14,7 @@ object studentDashboard {
students: List[Student.WithUser]
)(implicit ctx: Context) =
bits.layout(c.name, Left(c withStudents Nil))(
cls := "clas-show student-dashboard",
cls := "clas-show dashboard dashboard-student",
div(cls := "box__top")(
h1(dataIcon := "f", cls := "text")(c.name)
),

View File

@ -2,6 +2,7 @@ package views.html.clas
import controllers.routes
import lila.api.Context
import lila.rating.PerfType
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.clas.{ Clas, Student }
@ -9,12 +10,13 @@ import lila.common.String.html.richText
object teacherDashboard {
def apply(
private def dashboard(
c: Clas,
students: List[Student.WithUser]
)(implicit ctx: Context) =
students: List[Student.WithUser],
active: String
)(content: Frag)(implicit ctx: Context) =
bits.layout(c.name, Left(c withStudents students.map(_.student)))(
cls := "clas-show teacher-dashboard",
cls := "clas-show dashboard dashboard-teacher",
div(cls := "box__top")(
h1(dataIcon := "f", cls := "text")(c.name),
div(cls := "box__top__actions")(
@ -45,49 +47,113 @@ object teacherDashboard {
},
clas.teachers(c)
),
students.partition(_.student.isArchived) match {
case (archived, active) =>
frag(
div(cls := "students")(studentList(c, active)("Students")),
archived.nonEmpty option div(cls := "students students-archived")(
studentList(c, archived)("Archived students")
)
)
}
st.nav(cls := "dashboard-nav tabs-horiz")(
a(cls := active.active("students"), href := routes.Clas.show(c.id.value))("Students"),
List(PerfType.Bullet, PerfType.Blitz, PerfType.Rapid, PerfType.Classical, PerfType.Correspondence)
.map { pt =>
a(cls := active.active(pt.key), href := routes.Clas.perfType(c.id.value, pt.key))(pt.name),
},
a(cls := active.active("archived"), href := routes.Clas.archived(c.id.value))("Archived")
),
content
)
def studentList(c: Clas, students: List[Student.WithUser])(title: Frag)(implicit ctx: Context) =
def active(
c: Clas,
students: List[Student.WithUser]
)(implicit ctx: Context) =
dashboard(c, students, "students")(
studentList(c, students)
)
def archived(
c: Clas,
students: List[Student.WithUser]
)(implicit ctx: Context) =
dashboard(c, students.filter(_.student.isActive), "archived")(
studentList(c, students.filter(_.student.isArchived))
)
def perf(
c: Clas,
students: List[Student.WithUser],
perfType: PerfType
)(implicit ctx: Context) =
dashboard(c, students, perfType.key)(
div(cls := "students")(
table(cls := "slist slist-pad sortable")(
thead(
tr(
th(attr("data-sort-default") := "1")(),
sortNumberTh("Rating"),
sortNumberTh("Games"),
sortNumberTh("Progress")
)
),
tbody(
students.sortBy(_.user.username).map {
case s @ Student.WithUser(_, user) =>
tr(
studentTd(c, s),
td(dataSort := user.perfs(perfType).intRating, cls := "rating")(
user.perfs(perfType).showRatingProvisional
),
td(user.perfs(perfType).nb),
td(dataSort := user.perfs(perfType).progress)(user.perfs(perfType).progress)
)
}
)
)
)
)
private def studentList(c: Clas, students: List[Student.WithUser])(implicit ctx: Context) =
if (students.isEmpty)
frag(hr, p(cls := "box__pad students__empty")("No students in the class, yet."))
else
table(cls := "slist slist-pad sortable")(
thead(
tr(
th(attr("data-sort-default") := "1")(title),
th("Real name"),
sortNumberTh("Rating"),
sortNumberTh("Games"),
sortNumberTh("Active")
div(cls := "students")(
table(cls := "slist slist-pad sortable")(
thead(
tr(
th(attr("data-sort-default") := "1")("Student"),
th("Real name"),
sortNumberTh("Rating"),
sortNumberTh("Games"),
sortNumberTh("Puzzles"),
sortNumberTh("Active")
)
),
tbody(
students.sortBy(_.user.username).map {
case s @ Student.WithUser(student, user) =>
tr(
studentTd(c, s),
td(student.realName),
td(dataSort := user.perfs.bestRating, cls := "rating")(user.best3Perfs.map {
showPerfRating(user, _)
}),
td(user.count.game.localize),
td(user.perfs.puzzle.nb),
td(dataSort := user.seenAt.map(_.getMillis.toString))(user.seenAt.map(momentFromNowOnce))
)
}
)
),
tbody(
students.sortBy(_.user.username).map {
case Student.WithUser(student, user) =>
tr(
td(
a(href := routes.Clas.studentShow(c.id.value, user.username))(
userSpan(user)
)
),
td(student.realName),
td(user.perfs.bestRating),
td(user.count.game.localize),
td(dataSort := user.seenAt.map(_.getMillis.toString))(user.seenAt.map(momentFromNowOnce))
)
}
)
)
private def studentTd(c: Clas, s: Student.WithUser) =
td(
a(href := routes.Clas.studentShow(c.id.value, s.user.username))(
userSpan(
s.user,
name = span(
strong(s.user.username),
em(s.student.realName)
).some
)
)
)
private val sortNumberTh = th(attr("data-sort-method") := "number")
private val dataSort = attr("data-sort")
}

View File

@ -445,6 +445,8 @@ GET /class/$id<\w{8}> controllers.Clas.show(id: String)
GET /class/$id<\w{8}>/edit controllers.Clas.edit(id: String)
POST /class/$id<\w{8}>/edit controllers.Clas.update(id: String)
POST /class/$id<\w{8}>/archive controllers.Clas.archive(id: String, v: Boolean)
GET /class/$id<\w{8}>/archived controllers.Clas.archived(id: String)
GET /class/$id<\w{8}>/perf/:pt controllers.Clas.perfType(id: String, pt: String)
GET /class/$id<\w{8}>/student/add controllers.Clas.studentForm(id: String)
POST /class/$id<\w{8}>/student/new controllers.Clas.studentCreate(id: String)
POST /class/$id<\w{8}>/student/invite controllers.Clas.studentInvite(id: String)

View File

@ -87,9 +87,9 @@ final class ClasApi(
def activeOf(clas: Clas): Fu[List[Student]] =
of($doc("clasId" -> clas.id, "archived" $exists false))
def allOfWithUsers(clas: Clas): Fu[List[Student.WithUser]] =
def allWithUsers(clas: Clas): Fu[List[Student.WithUser]] =
of($doc("clasId" -> clas.id)) flatMap withUsers
def activeOfWithUsers(clas: Clas): Fu[List[Student.WithUser]] =
def activeWithUsers(clas: Clas): Fu[List[Student.WithUser]] =
of($doc("clasId" -> clas.id, "archived" $exists false)) flatMap withUsers
private def of(selector: Bdoc): Fu[List[Student]] =

View File

@ -69,6 +69,8 @@ case class Perf(
def rankable(variant: chess.variant.Variant) = glicko.rankable(variant)
def provisional = glicko.provisional
def established = glicko.established
def showRatingProvisional = s"$intRating${provisional ?? "?"}"
}
case object Perf {

View File

@ -6,7 +6,7 @@
@include breakpoint($mq-not-xx-small) {
font-size: .9em;
}
span {
span, > a {
@extend %roboto;
flex: 1 1 auto;
text-align: center;

View File

@ -101,44 +101,47 @@ $clas-color: rgb(127, 90, 240);
}
}
.student-dashboard {
.dashboard {
.students {
.user-link {
@extend %flex-center;
.line {
font-size: 2em;
}
em {
@extend %roboto;
display: block;
}
}
.rating span {
&::before {
margin: 0;
}
margin: 0 .3em;
}
.button {
font-size: 1.2em;
}
&-student {
.students {
.button {
font-size: 1.2em;
}
}
}
}
.teacher-dashboard {
&-dashboard {
.students {
&-archived {
margin-top: 2em;
}
&__empty {
margin-bottom: 8em;
}
td:first-child {
font-weight: bold;
}
td:nth-child(2) {
color: $c-font-dim;
.students {
.user-link {
@extend %flex-center;
.line {
font-size: 2em;
}
em {
@extend %roboto;
display: block;
}
}
&__empty {
margin-bottom: 8em;
}
td:first-child {
font-weight: bold;
}
td:nth-child(2) {
color: $c-font-dim;
}
}
}
}

View File

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