Thursday, 5 September 2013

Building an assessment app in 2+ɛ days -- 16th commit

Right, back to it then… After the fun of the CEO's visit this morning, (and a big long sleep last night) back to work on the assessment app.

The students are going to be using it on Monday, so that's a deadline I can't let whoosh past me, as it's not just me making up a deadline for myself.

User updates in a functional world

The commit I just pushed changes course pre-enrolments so they happen automatically when the browser asks for the user's courses.

That means this is a request that changes something about the user -- it changes the user's registration.

I've written the app in a way that is functional and typically works with immutable data. That means there's one small additional wrinkle in this request.

DataAction.returning.one automatically converts an item to JSON for the requesting user. It uses an Approval[User] that typically has a LazyId for the user, fetched the first time it's needed.

But, I happen to have written this particular app in a functional style -- with immutable data objects. (You don't have to write your app with immutable data types, I just did for this one.) And this request modifies the user.

If your data types are immutable, you can find yourself with a small bug where this happens:

  1. We ask for the user, because to look up any pre-enrolments, we need a list of their social identities

  2. This triggers the lazy reference to the user to load.

  3. We find a pre-enrolment in the database, and update the user's registrations.

  4. We return the course from DataAction.returning.one

  5. But the Approval in the request has an immutable representation of the user that was fetched before we registered them to the new course, and the JSON comes out as if they weren't registered.

The solution involves a change to one line of code, and the addition of two more:

  1. Change the method from DataAction.returning.one to DataAction.returning.json

    (or from DataAction.returning.many to DataAction.returning.manyJson)

  2. Create a new approval for the updated user.

  3. Call the JsonConverter with the new Approval

We can see this in CourseController.myCourses

def myCourses = DataAction.returning.manyJson 
{ implicit request =>

  val userAfterUpdates = for (
    u <- request.user; 
    updated <- doPreenrolments(u)
  ) yield updated

  // As we've updated the user, we'll need a new Approval
  val approval = Approval(userAfterUpdates)

And at the end of the method:

  approved <- approval ask Permissions.ViewCourse(c.itself);
  j <- CourseToJson.toJsonFor(c, approval)
) yield j

Wednesday, 4 September 2013

Building an assessment app in 2+ɛ days -- 13th commit

This is a little picture of where the app is up to at the moment. (This is the admin screen for a course.) After the lecture this morning, and a little more coding, I'm going to pop home and take a kip, and resume updates a little later.

The last few commits haven't been especially interesting to blog about -- just adding more of the DAO classes, services, and controllers, in much the same style as the previous ones.

However, I have been making a few design decisions for how the app will behave that might make interesting reading later on today.

Is that a whooshing noise?

A couple more commits have gone in, though I haven't blogged them.

Course pre-enrolments happen, but I haven't yet set up group pre-enrolments or the critique task itself. Those will need to happen later today instead.

So, it looks like it'll need to be an assessment app in three days.

Building an assessment app in two days -- 8th commit

A few yawns are creeping in here, it's getting late…

The eighth commit is up, and now we can create courses. The interesting part of this commit, however is security.

Security in Assessory

If you have a look in CourseController, you'll see controllers that look like this:

/**
 * Retrieves a course
 */  
def get(id:String) = dataAction.one { 
  implicit request =>     
    val cache = request.approval.cache
    for (
      course <- cache(refCourse(id));
      approved <- request.approval ask 
                  Permissions.ViewCourse(course.itself)
    ) yield course
}

The permissions check is chained right there in the for loop (which is syntactic sugar for chaining flatMap calls on the Refs)

The way of thinking about it is that at any stage you can ask for approval to do something. That approval might be given; it might take some time to work out (involve looking something up in the database) and it might fail or be refused. All those fit neatly into the functionality of Ref, so we treat is asking for a Ref[Approved].

This also means it's independent of the database or Play classes, and I've declared the permission rules in the assessory-api module.

Security is in the API

If you look in Permissions, you can see the different permissions that an Approval[User] can ask to be approved.

Sometimes these are straightforward objects:

case object CreateCourse extends Perm[User] {    
  def resolve(prior:Approval[User]) = {
    Approved("Anyone may create a course")
  }
}

And sometimes they are approvals on an item:

  case class ViewCourse(course:Ref[Course]) 
       extends PermOnIdRef[User, Course](course) 
  {
    def resolve(prior:Approval[User]) = 
      hasRole(
         course, prior.who, 
         CourseRole.student, prior.cache
      )
  }

Approvals on an item (PermOnIdRef) are clever enough to realise that if you ask for an approval on Course(id=1).itself, and you ask for an approval on LazyId(classOf[Course], "1"), those are the same approval and it doesn't need to look up the ID the second time.

And it can do that independently of what kind of database you've wired up, or whether or not it's in a Play app.

Cache

As well as remembering granted approvals, Approval also contains a cache for Ref lookups.

The Approval tends to be present in all three of the controller, the security check, and the JSON conversion -- and these are the three places where you typically need to look up Refs. So having a cache attached to it is rather handy.

JSON conversion is seemless with security too

All this flatMapping on Refs has another payoff in the JSON conversion.

If you have a look at CourseToJSON, you'll see that it embeds a permissions block into the JSON it returns

def toJsonFor(c:Course, a:Approval[User]) = {

  val permissions = for (
    view <- optionally(
      a ask Permissions.EditCourse(c.itself)
    );
    edit <- optionally(
      a ask Permissions.EditCourse(c.itself)
    )
  ) yield Json.obj(
    "view" -> view.isDefined,
    "edit" -> edit.isDefined
  )

  for (p <- permissions) yield {
    courseFormat.writes(c) ++ 
    Json.obj("permissions" -> p)
  }
}

This happens asynchronously -- the user reference in the approval is asynchronous and may or may not already have been retrieved.

The JSON block that this produces ends up looking like something this:

{
  "id":"52275e6a9acb3a4500d7c2ea",
  "title":"Design Computing Studio 2",
  "shortName":"DECO2800",
  "shortDescription":"…",
  "addedBy":"522732099acb3a4d00fedff9",
  "created":1378311786259,
  "permissions": {
    "view":true,
    "edit":true
  }
}

Because the permissions block is in the item, on the client it is easy to enable and disable components using Angular.js.

Say, for instance, we might have an edit link that only shows if the edit permission is present:

<a href="edit" ng-show="course.permissions.edit">
  Edit
</a>

Building an assessment app in two days -- 7th commit

After another long delay, the seventh commit is up. This is the last of the plumbing commits, and adds OAuth login.

The OAuth authentication is handled by handy-play-oauth, which is about the smallest possible OAuth library. It doesn't itself do user management or any of that. All it does is:

  1. Forward the request to the relevant authentication service (in this case, GitHub's OAuth URL)

  2. Extract the returned OAuth repsonse

  3. Use the authentication token to call the service's REST API to get the user's details

  4. Call whatever action you've configured

In this case, the action I've configured is InterstitialController.onAuth. It either logs you in (if you've already set up your account), or shows an "interstitial" confirmation page if you haven't.

The reason for the confirmation page is that you can attach multiple identities to your account. So before Assessory creates a new account for you, it asks you to confirm that's what you want to do -- in case what you really wanted to do was add the login to an existing account.

(The long delay was me having dinner and getting over a migraine, by the way. Plus staring for far too long at a stupidly trivial bug while my head ached.)

Building an assessment app in two days -- 6th commit

After a long pause while I fixed that bug, back to getting the app up and running.

The sixth commit has basic sign-in, sign-up, and sign-out functionality working, which means we have our first proper controller on the server and our first proper service on the client.

Earlier I said I wasn't going put in email/password log in. Well, I then realised I would need to.

Later on, I'm going to need to try out creating a course (as staff) and then preenrolling a different user (as a student). That means I need two accounts, but I only have one GitHub log in.

Handling users on the server

UserController contains the various concise actions that we've defined on the server:

  • self to return JSON for the currenlty logged in user
  • signUp to sign up
  • logIn to log in
  • logOut to log out.

At the top of the controller, there is the interesting line

implicit val userToJson = UserToJson

This is used to convert the responses to JSON format.

If you then have a look at UserToJson, you'll see it's a slightly different strategy for converting to JSON than most libraries. The key function is:

def toJsonFor(u:User, a:Approval[User]) = { 
  // etc

This is from recognising that very often we want to give different information depending on who's asking. For instance, in this case we only want to give out the JSON data for the user if they are asking about themselves, not someone else.

And, it returns a Ref[JsValue] rather than just a JsValue, in case it has any other work it needs to do before it can return a result.

Handling users on the client

UserService is the corresponding component on the client. Again, it has self, signUp, logIn, and logOut actions which are all neatly concise.

Angular.js has built into it promises. These are very much like the way Promises and Futures work in Scala on the server.

Which is perhaps why I quite like working with Angular.js in the browser!

Building an assessment app in two days -- bug resolved

Ouch that was a painfully long delay...

The pause in committing code was because I hit an issue between DataAction and Play that took a little while to resolve. So there's a commit on the handy project that's just been pushed to resolve it.

The gory details can wait for another time, but the short version is that Play has a distinction between EssentialAction and Action, and these two Play classes don't work quite as nicely together as I naively assumed. This was causing problems in DataAction.one(parse.json). It was a bit of a fiddly workaround that was needed.

So, back to it, but having taken a few hours out to deal with that, I might be having a late night after all.