Skip to content

Commit

Permalink
Add tests for Courses.
Browse files Browse the repository at this point in the history
Add methods to get the domain class from a Ref class.
  • Loading branch information
Lasering committed May 18, 2020
1 parent e0a8444 commit 9639b95
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 37 deletions.
24 changes: 14 additions & 10 deletions src/main/scala/org/fenixedu/sdk/models/Course.scala
@@ -1,7 +1,10 @@
package org.fenixedu.sdk.models

import io.circe.Decoder
import cats.effect.Sync
import io.circe.Decoder.Result
import io.circe.{Decoder, HCursor}
import io.circe.derivation.deriveDecoder
import org.fenixedu.sdk.FenixEduClient
import org.http4s.Uri
import org.http4s.circe.decodeUri

Expand All @@ -12,7 +15,7 @@ case class Course(
acronym: String,
name: String,
academicTerm: String,
evaluationMethod: String,
evaluationMethod: Option[String],
numberOfAttendingStudents: Int,
summaryLink: String,
announcementLink: String,
Expand All @@ -24,19 +27,20 @@ case class Course(
object Competence {
implicit val decoder: Decoder[Competence] = deriveDecoder(identity)
}
case class Competence(id: String, program: String, bibliographicReferences: List[BibliographicReference], degrees: List[DegreeRef])
case class Competence(id: String, program: Option[String], bibliographicReferences: List[BibliographicReference], degrees: List[DegreeRef])

object BibliographicReference {
// Nuclear option to handle malformed URLs like these. We simply ignore them.
// "url" : "http://DOI 10.1007/978-3-642-28616-2 1"
// "url" : "http://fundamentals-of-bpm.org/ ; ISBN: 978-3-642-33142-8 ; DOI: 10.1007/978-3-642-33143-5"
implicit val decoderOptionUri: Decoder[Option[Uri]] = Decoder.decodeOption[Uri](decodeUri) or ((_: HCursor) => Right(None))
implicit val decoder: Decoder[BibliographicReference] = deriveDecoder(identity)
}
case class BibliographicReference(`type`: String, author: String, reference: String, title: String, year: String, url: Uri)

object CourseLoad {
implicit val decoder: Decoder[CourseLoad] = deriveDecoder(identity)
}
case class CourseLoad(`type`: String, totalQuantity: Int, unitQuantity: Int)
case class BibliographicReference(`type`: String, author: String, reference: String, title: String, year: String, url: Option[Uri])

object CourseRef {
implicit val decoder: Decoder[CourseRef] = deriveDecoder(identity)
}
case class CourseRef(id: String, acronym: String, name: String, academicTerm: String, url: Option[Uri])
case class CourseRef(id: String, acronym: String, name: String, academicTerm: String, url: Option[Uri]) {
def course[F[_]: Sync](implicit client: FenixEduClient[F]): F[Course] = client.course(id).get()
}
6 changes: 5 additions & 1 deletion src/main/scala/org/fenixedu/sdk/models/Degree.scala
@@ -1,7 +1,9 @@
package org.fenixedu.sdk.models

import cats.effect.Sync
import io.circe.Decoder
import io.circe.derivation.deriveDecoder
import org.fenixedu.sdk.FenixEduClient
import org.http4s.Uri
import org.http4s.circe.decodeUri

Expand Down Expand Up @@ -40,4 +42,6 @@ case class Degree(
object DegreeRef {
implicit val decoder: Decoder[DegreeRef] = deriveDecoder(identity)
}
case class DegreeRef(id: String, name: String, acronym: String)
case class DegreeRef(id: String, name: String, acronym: String) {
def degree[F[_]: Sync](implicit client: FenixEduClient[F]): F[Degree] = client.degrees.get(id)
}
44 changes: 38 additions & 6 deletions src/main/scala/org/fenixedu/sdk/models/Evaluation.scala
@@ -1,13 +1,45 @@
package org.fenixedu.sdk.models

import io.circe.Decoder
import io.circe.derivation.deriveDecoder
import io.circe.{Decoder, DecodingFailure, HCursor}

object Evaluation {
implicit val decoder: Decoder[Evaluation] = deriveDecoder(identity)
}
implicit val decoderProject: Decoder[Project] = deriveDecoder(identity)
implicit val decoderOnlineTest: Decoder[OnlineTest] = deriveDecoder(identity)
implicit val decoderFinalEvaluation: Decoder[FinalEvaluation] = deriveDecoder(identity)
implicit val decoderAdHoc: Decoder[AdHoc] = deriveDecoder(identity)
implicit val decoderExamOrTest: Decoder[ExamOrTest] = deriveDecoder(identity)

case class Evaluation(
implicit val decoder: Decoder[Evaluation] = { cursor: HCursor =>
val downType = cursor.downField("type")
downType.as[String].flatMap {
case "PROJECT" => cursor.as[Project]
case "ONLINE_TEST" => cursor.as[OnlineTest]
case "FINAL_EVALUATION" => cursor.as[FinalEvaluation]
case "AD_HOC" => cursor.as[AdHoc]
case "EXAM" | "TEST" => cursor.as[ExamOrTest]
case t => Left(DecodingFailure(s"Unknown evaluation type $t", downType.history))
}
}
}
sealed trait Evaluation {
def `type`: String
def name: String
def evaluationPeriod: Period
}
case class Project(name: String, evaluationPeriod: Period) extends Evaluation {
final val `type`: String = "PROJECT"
}
case class OnlineTest(name: String, evaluationPeriod: Period) extends Evaluation {
final val `type`: String = "ONLINE_TEST"
}
case class FinalEvaluation(name: String, evaluationPeriod: Period) extends Evaluation {
final val `type`: String = "FINAL_EVALUATION"
}
case class AdHoc(name: String, evaluationPeriod: Period, description: Option[String]) extends Evaluation {
final val `type`: String = "AD_HOC"
}
case class ExamOrTest(
id: String,
`type`: String,
name: String,
Expand All @@ -16,5 +48,5 @@ case class Evaluation(
isInEnrolmentPeriod: Boolean,
courses: List[CourseRef],
rooms: List[SpaceRef],
assignedRoom: SpaceRef
)
assignedRoom: Option[SpaceRef]
) extends Evaluation
8 changes: 4 additions & 4 deletions src/main/scala/org/fenixedu/sdk/models/Group.scala
Expand Up @@ -11,9 +11,9 @@ case class Group(
description: String,
enrolmentPeriod: Period,
enrolmentPolicy: String,
minimumCapacity: Int,
maximumCapacity: Int,
idealCapacity: Int,
minimumCapacity: Option[Int],
maximumCapacity: Option[Int],
idealCapacity: Option[Int],
associatedCourses: List[AssociatedCourse],
associatedGroups: List[AssociatedGroup]
)
Expand All @@ -26,7 +26,7 @@ case class AssociatedCourse(id: String, name: String, degrees: Seq[DegreeRef])
object AssociatedGroup{
implicit val decoder: Decoder[AssociatedGroup] = deriveDecoder (identity)
}
case class AssociatedGroup(groupNumber: Int, shift: String, members: Seq[Member])
case class AssociatedGroup(groupNumber: Int, shift: Option[String], members: Seq[Member])

object Member {
implicit val decoder: Decoder[Member] = deriveDecoder(identity)
Expand Down
17 changes: 12 additions & 5 deletions src/main/scala/org/fenixedu/sdk/models/Period.scala
Expand Up @@ -2,12 +2,19 @@ package org.fenixedu.sdk.models

import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import io.circe.Decoder
import io.circe.derivation.deriveDecoder
import io.circe.{Decoder, Json}

object Period {
val dateTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm")
implicit val decoderLocalDateTime: Decoder[LocalDateTime] = Decoder.decodeLocalDateTimeWithFormatter(dateTimeFormatter)
implicit val decoder: Decoder[Period] = deriveDecoder(identity)
private def forPattern(pattern: String): Decoder[LocalDateTime] =
Decoder.decodeLocalDateTimeWithFormatter(DateTimeFormatter.ofPattern(pattern))

implicit val decoderLocalDateTime: Decoder[LocalDateTime] =
forPattern("dd/MM/yyyy HH:mm") or forPattern("dd/MM/yyyy HH:mm:ss") or forPattern("yyyy-MM-dd HH:mm:ss")

implicit val decoder: Decoder[Period] =
Decoder.forProduct2("start", "end")(Period.apply).prepare(_.withFocus(_.mapObject(_.mapValues(_.withString{
case s if s.isEmpty => Json.Null
case s => Json.fromString(s)
}))))
}
case class Period(start: Option[LocalDateTime], end: Option[LocalDateTime])
7 changes: 6 additions & 1 deletion src/main/scala/org/fenixedu/sdk/models/Schedule.scala
Expand Up @@ -8,6 +8,11 @@ object Schedule {
}
case class Schedule(lessonPeriods: List[Period], courseLoads: List[CourseLoad], shifts: List[Shift])

object CourseLoad {
implicit val decoder: Decoder[CourseLoad] = deriveDecoder(identity)
}
case class CourseLoad(`type`: String, totalQuantity: Float, unitQuantity: Float)

object Shift {
implicit val decoder: Decoder[Shift] = deriveDecoder(identity)
}
Expand All @@ -21,7 +26,7 @@ case class Occupation(current: Int, max: Int)
object Lesson {
implicit val decoder: Decoder[Lesson] = deriveDecoder(identity)
}
case class Lesson(start: String, end: String, room: SpaceRef)
case class Lesson(start: String, end: String, room: Option[SpaceRef])



Expand Down
2 changes: 0 additions & 2 deletions src/main/scala/org/fenixedu/sdk/models/Shuttle.scala
Expand Up @@ -33,5 +33,3 @@ object Trip {
implicit val decoder: Decoder[Trip] = deriveDecoder(identity)
}
case class Trip(`type`: String, stations: List[Trip.Stop])


12 changes: 10 additions & 2 deletions src/main/scala/org/fenixedu/sdk/models/Space.scala
@@ -1,12 +1,20 @@
package org.fenixedu.sdk.models

import cats.effect.Sync
import io.circe.derivation.deriveDecoder
import io.circe.{Decoder, DecodingFailure, HCursor}
import io.circe.{Decoder, DecodingFailure, HCursor, Json}
import org.fenixedu.sdk.FenixEduClient

object SpaceRef {
implicit val decoder: Decoder[SpaceRef] = deriveDecoder(identity)
implicit val decoderOpt: Decoder[Option[SpaceRef]] = Decoder.decodeOption(decoder).prepare(_.withFocus(_.withObject{ obj =>
if (obj("id").exists(_.isNull)) Json.Null
else Json.fromJsonObject(obj)
}))
}
case class SpaceRef(`type`: String, id: String, name: String) {
def space[F[_]: Sync](implicit client: FenixEduClient[F]): F[Space] = client.spaces.get(id)
}
case class SpaceRef(`type`: String, id: String, name: String)

object Capacity {
implicit val decoder: Decoder[Capacity] = deriveDecoder(identity)
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/org/fenixedu/sdk/services/Course.scala
Expand Up @@ -13,7 +13,7 @@ final class Course[F[_]: Sync](val id: String, baseUri: Uri)(implicit client: Cl
* The same course may be lectured simultaneously in multiple degrees during the same academic term.
* The “competences” field holds curricular information for each set of degrees in which the course is lectured.
* Usually this information is the same for all the associated degrees. */
def apply(): F[CouseModel] = client.expect(uri)
def get(): F[CouseModel] = client.expect(uri)

/** An evaluation is a component of a course in which the teacher determines the extent of the students understanding of the program.
* Current known implementations of evaluations are: tests, exams, projects, online tests and ad-hoc evaluations. */
Expand All @@ -26,7 +26,7 @@ final class Course[F[_]: Sync](val id: String, baseUri: Uri)(implicit client: Cl
/** Each course is lectured during a specific set of intervals. These intervals make up the lesson period for that course.
* Each course also has a curricular load that specifies the time each student will expend with the course. Each shift is the possible
* schedule in which a student should enrol. */
val schedule: F[List[Schedule]] = client.expect(uri / "schedule")
val schedule: F[Schedule] = client.expect(uri / "schedule")

/** This endpoint lists all the students attending the specified course. For each student it indicates the corresponding degree.
* The endpoint also returns the number of students officially enrolled in the course. */
Expand Down
51 changes: 51 additions & 0 deletions src/test/scala/org/fenixedu/sdk/CoursesSpec.scala
@@ -0,0 +1,51 @@
package org.fenixedu.sdk

import cats.instances.list._
import cats.syntax.parallel._
import cats.syntax.applicativeError._
import org.http4s.Status.InternalServerError
import org.http4s.client.UnexpectedStatus

class CoursesSpec extends Utils {
def forAcademicTerm(year: Int) = {
val academicTerm = s"$year/${year + 1}"
client.degrees.list(academicTerm = Some(academicTerm))
.flatMap(_.parTraverse(degree => client.degrees.courses(degree.id, Some(academicTerm)).map(courses => (degree, courses.take(3)))))
.unsafeRunSync()
.foreach { case (degree, courses) =>
s"the degree is ${degree.acronym} (${degree.id}) on academic term $academicTerm" should {
courses.foreach { courseRef =>
val courseService = client.course(courseRef.id)
s"get the course ${courseRef.acronym} (${courseRef.id})" in {
courseService.get().value(_.academicTerm should include (academicTerm))
}
s"get the course ${courseRef.acronym} (${courseRef.id}) evaluations" in {
courseService.evaluations.value(_.length should be >= 0)
}
s"get the course groups ${courseRef.acronym} (${courseRef.id})" in {
courseService.groups.value(_.length should be >= 0)
}
s"get the course schedule ${courseRef.acronym} (${courseRef.id})" in {
courseService.schedule.value(_.lessonPeriods.length should be >= 0).recover {
case UnexpectedStatus(_: InternalServerError.type) =>
// There is an error on the server, since we cannot easily exclude the Courses with the error we simply ignore it
assert(true)
}
}
s"get the course students ${courseRef.acronym} (${courseRef.id})" in {
courseService.students.value(_.students.length should be >= 0)
}
}
}
}
}

"Courses service" when {
// These tests take very long. So we run them for just some years, it probably covers most scenarios.
forAcademicTerm(2000) // 0 tests, all the Degrees from this year have 0 courses. I don't know why.
// From 2002 to 2007 the API returns degrees with the same id
forAcademicTerm(2007) // 1205 tests
forAcademicTerm(2019) // 1155 tests
forAcademicTerm(2020) // As of 18/05/2020 0 tests
}
}
10 changes: 6 additions & 4 deletions src/test/scala/org/fenixedu/sdk/Utils.scala
Expand Up @@ -15,7 +15,12 @@ import org.scalatest.exceptions.TestFailedException
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AsyncWordSpec

abstract class Utils extends AsyncWordSpec with Matchers with BeforeAndAfterAll {
trait LowPriority {
import scala.language.implicitConversions
implicit def io2Future[T](io: IO[T]): Future[T] = io.unsafeToFuture()
}

abstract class Utils extends AsyncWordSpec with Matchers with BeforeAndAfterAll with LowPriority {
val logger: Logger = getLogger

implicit override def executionContext = ExecutionContext.global
Expand Down Expand Up @@ -71,9 +76,6 @@ abstract class Utils extends AsyncWordSpec with Matchers with BeforeAndAfterAll
def valueShouldIdempotentlyBe(value: T): IO[Assertion] = idempotently(_ shouldBe value)
}

import scala.language.implicitConversions
implicit def io2Future[T](io: IO[T]): Future[T] = io.unsafeToFuture()

private def ordinalSuffix(number: Int): String = {
number % 100 match {
case 1 => "st"
Expand Down

0 comments on commit 9639b95

Please sign in to comment.