{"action":"created","comment":{"url":"https://api.github.com/repos/guardian/grid/pulls/comments/47086319","id":47086319,"diff_hunk":"@@ -1,53 +1,121 @@\n package controllers\n \n-import model._\n+import java.net.URI\n+\n+import scala.concurrent.ExecutionContext.Implicits.global\n+import scala.concurrent.Future\n+import scala.util.Try\n+\n+import play.api.libs.json.JsError\n+import play.api.Logger\n+import play.api.Logger\n+import play.api.mvc.Controller\n+import play.api.mvc.Results._\n+import play.api.mvc.Results._\n+import play.api.mvc.{Controller, BodyParsers}\n+import play.utils.UriEncoding\n+\n import rx.lang.scala.Observable\n \n import com.gu.mediaservice.lib.argo.ArgoHelpers\n-import com.gu.mediaservice.lib.argo.model.Link\n+import com.gu.mediaservice.lib.argo.model.{Action, Link, EntityReponse}\n import com.gu.mediaservice.lib.auth\n import com.gu.mediaservice.lib.auth.KeyStore\n import com.gu.mediaservice.lib.aws.NoItemFound\n-import lib.{Config, UsageRecorder}\n-\n-import play.api.Logger\n-import play.api.mvc.Controller\n-import play.api.mvc.Results._\n+import com.gu.mediaservice.model.Usage\n \n-import scala.concurrent.ExecutionContext.Implicits.global\n+import lib._\n+import model._\n \n \n object UsageApi extends Controller with ArgoHelpers {\n \n-  import Config.{awsCredentials, kahunaUri, keyStoreBucket, loginUriTemplate, rootUri}\n+  import Config._\n \n   val keyStore = new KeyStore(keyStoreBucket, awsCredentials)\n   val Authenticated = auth.Authenticated(keyStore, loginUriTemplate, kahunaUri)\n \n+  private def wrapUsage(usage: Usage): EntityReponse[Usage] = {\n+    EntityReponse(\n+      uri = usageUri(usage.id),\n+      data = usage\n+    )\n+  }\n+\n+  private def usageUri(usageId: String): Option[URI] = {\n+    val encodedUsageId = UriEncoding.encodePathSegment(usageId, \"UTF-8\")\n+    Try { new URI(s\"${Config.usageUri}/usages/${encodedUsageId}\") }.toOption\n+  }\n+\n   val indexResponse = {\n     val indexData = Map(\"description\" -> \"This is the Usage Recording service\")\n     val indexLinks = List(\n-      Link(\"usage\", s\"$rootUri/usage/media/{id}\")\n+      Link(\"usages-by-media\", s\"${Config.usageUri}/usages/media/{id}\"),\n+      Link(\"usages-by-id\", s\"${Config.usageUri}/usages/{id}\")\n     )\n-    respond(indexData, indexLinks)\n+\n+    val printPostUri = new URI(s\"${Config.usageUri}/usages/print\")\n+    val actions = List(\n+      Action(\"print-usage\", printPostUri, \"POST\")\n+    )\n+\n+    respond(indexData, indexLinks, actions)\n   }\n   def index = Authenticated { indexResponse }\n \n+  def forUsage(usageId: String) = Authenticated.async {\n+    val usageFuture = UsageTable.queryByUsageId(usageId)\n+\n+    usageFuture.map[play.api.mvc.Result]((mediaUsageOption: Option[MediaUsage]) => {\n+      mediaUsageOption.foldLeft(\n+        respondNotFound(\"No usages found.\")\n+      )((_, mediaUsage: MediaUsage) => {\n+        val usage = UsageBuilder.build(mediaUsage)\n+        val mediaId = mediaUsage.mediaId\n+\n+        val uri = usageUri(usage.id)\n+        val links = List(\n+          Link(\"media\", s\"${services.apiBaseUri}/images/${mediaId}\"),\n+          Link(\"media-usage\", s\"${services.usageBaseUri}/usages/media/${mediaId}\")\n+        )\n+\n+        respond[Usage](data = usage, uri = uri, links = links)\n+      })\n+    }).recover { case error: Exception => {\n+      Logger.error(\"UsageApi returned an error.\", error)\n+      respondError(InternalServerError, \"usage-retrieve-failed\", error.getMessage())\n+    }}\n+\n+  }\n+\n   def forMedia(mediaId: String) = Authenticated.async {\n     val usagesFuture = UsageTable.queryByImageId(mediaId)\n \n-    usagesFuture.map[play.api.mvc.Result](UsageResponseCollection.build).recover { case error: Exception => {\n+    usagesFuture.map[play.api.mvc.Result]((mediaUsages: Set[MediaUsage]) => {\n+      val usages = mediaUsages.toList.map(UsageBuilder.build)\n+\n+      usages match {\n+        case Nil => respondNotFound(\"No usages found.\")\n+        case usage :: _ => {\n+          val uri = Try { new URI(s\"${services.usageBaseUri}/usages/media/${mediaId}\") }.toOption","path":"usage/app/controllers/UsageApi.scala","position":112,"original_position":112,"commit_id":"21d4942b0a238814e4ba8f605f27695b5f731cf5","original_commit_id":"21d4942b0a238814e4ba8f605f27695b5f731cf5","user":{"login":"kenoir","id":953792,"avatar_url":"https://avatars.githubusercontent.com/u/953792?v=3","gravatar_id":"","url":"https://api.github.com/users/kenoir","html_url":"https://github.com/kenoir","followers_url":"https://api.github.com/users/kenoir/followers","following_url":"https://api.github.com/users/kenoir/following{/other_user}","gists_url":"https://api.github.com/users/kenoir/gists{/gist_id}","starred_url":"https://api.github.com/users/kenoir/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kenoir/subscriptions","organizations_url":"https://api.github.com/users/kenoir/orgs","repos_url":"https://api.github.com/users/kenoir/repos","events_url":"https://api.github.com/users/kenoir/events{/privacy}","received_events_url":"https://api.github.com/users/kenoir/received_events","type":"User","site_admin":false},"body":"`uri` is optional, and `java.net.URI` will throw if it can't parse the string to a URI. So although I don't expect this to throw, I prefer it to `Some(thing)`.","created_at":"2015-12-09T12:56:37Z","updated_at":"2015-12-09T12:56:37Z","html_url":"https://github.com/guardian/grid/pull/1447#discussion_r47086319","pull_request_url":"https://api.github.com/repos/guardian/grid/pulls/1447","_links":{"self":{"href":"https://api.github.com/repos/guardian/grid/pulls/comments/47086319"},"html":{"href":"https://github.com/guardian/grid/pull/1447#discussion_r47086319"},"pull_request":{"href":"https://api.github.com/repos/guardian/grid/pulls/1447"}}},"pull_request":{"url":"https://api.github.com/repos/guardian/grid/pulls/1447","id":50244559,"html_url":"https://github.com/guardian/grid/pull/1447","diff_url":"https://github.com/guardian/grid/pull/1447.diff","patch_url":"https://github.com/guardian/grid/pull/1447.patch","issue_url":"https://api.github.com/repos/guardian/grid/issues/1447","number":1447,"state":"open","locked":false,"title":"Store and present media usage from Elastic","user":{"login":"kenoir","id":953792,"avatar_url":"https://avatars.githubusercontent.com/u/953792?v=3","gravatar_id":"","url":"https://api.github.com/users/kenoir","html_url":"https://github.com/kenoir","followers_url":"https://api.github.com/users/kenoir/followers","following_url":"https://api.github.com/users/kenoir/following{/other_user}","gists_url":"https://api.github.com/users/kenoir/gists{/gist_id}","starred_url":"https://api.github.com/users/kenoir/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kenoir/subscriptions","organizations_url":"https://api.github.com/users/kenoir/orgs","repos_url":"https://api.github.com/users/kenoir/repos","events_url":"https://api.github.com/users/kenoir/events{/privacy}","received_events_url":"https://api.github.com/users/kenoir/received_events","type":"User","site_admin":false},"body":"This PR allows the grid to store usages in ES and present them via the Media API. This in turn allows us to sensibly present (and search) usage data across large numbers of images.\r\n\r\n**Image page:**\r\n![usage-image](https://cloud.githubusercontent.com/assets/953792/11657445/add21950-9db4-11e5-9978-9accc76b2a7e.png)\r\n\r\n**Search view:**\r\n![usage-search](https://cloud.githubusercontent.com/assets/953792/11657451/b6901286-9db4-11e5-88fe-cf8e1a7fbccf.png)\r\n\r\n[API Examples](https://github.com/guardian/grid/blob/usage-event-stream-lambda/usage/api-examples.md)\r\n\r\n**Deployment strategy:**\r\n\r\nApps should be deployed in the following manner to avoid service disruption:\r\n\r\nPreperation:\r\n- Turn off Usage by scaling ASG to 0\r\n- Drop & Recreate Usage Table\r\n\r\nDeployment:\r\n- Thrall\r\n- Image Loader\r\n- Media API\r\n- Kahuna\r\n- Usage\r\n\r\n\r\n","created_at":"2015-11-10T11:50:55Z","updated_at":"2015-12-09T12:56:37Z","closed_at":null,"merged_at":null,"merge_commit_sha":"98a758f8a3a58c249240dc77beeb570d788b5a2d","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/guardian/grid/pulls/1447/commits","review_comments_url":"https://api.github.com/repos/guardian/grid/pulls/1447/comments","review_comment_url":"https://api.github.com/repos/guardian/grid/pulls/comments{/number}","comments_url":"https://api.github.com/repos/guardian/grid/issues/1447/comments","statuses_url":"https://api.github.com/repos/guardian/grid/statuses/21d4942b0a238814e4ba8f605f27695b5f731cf5","head":{"label":"guardian:usage-event-stream-lambda","ref":"usage-event-stream-lambda","sha":"21d4942b0a238814e4ba8f605f27695b5f731cf5","user":{"login":"guardian","id":164318,"avatar_url":"https://avatars.githubusercontent.com/u/164318?v=3","gravatar_id":"","url":"https://api.github.com/users/guardian","html_url":"https://github.com/guardian","followers_url":"https://api.github.com/users/guardian/followers","following_url":"https://api.github.com/users/guardian/following{/other_user}","gists_url":"https://api.github.com/users/guardian/gists{/gist_id}","starred_url":"https://api.github.com/users/guardian/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/guardian/subscriptions","organizations_url":"https://api.github.com/users/guardian/orgs","repos_url":"https://api.github.com/users/guardian/repos","events_url":"https://api.github.com/users/guardian/events{/privacy}","received_events_url":"https://api.github.com/users/guardian/received_events","type":"Organization","site_admin":false},"repo":{"id":12298112,"name":"grid","full_name":"guardian/grid","owner":{"login":"guardian","id":164318,"avatar_url":"https://avatars.githubusercontent.com/u/164318?v=3","gravatar_id":"","url":"https://api.github.com/users/guardian","html_url":"https://github.com/guardian","followers_url":"https://api.github.com/users/guardian/followers","following_url":"https://api.github.com/users/guardian/following{/other_user}","gists_url":"https://api.github.com/users/guardian/gists{/gist_id}","starred_url":"https://api.github.com/users/guardian/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/guardian/subscriptions","organizations_url":"https://api.github.com/users/guardian/orgs","repos_url":"https://api.github.com/users/guardian/repos","events_url":"https://api.github.com/users/guardian/events{/privacy}","received_events_url":"https://api.github.com/users/guardian/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/guardian/grid","description":"The Guardian’s new image management system","fork":false,"url":"https://api.github.com/repos/guardian/grid","forks_url":"https://api.github.com/repos/guardian/grid/forks","keys_url":"https://api.github.com/repos/guardian/grid/keys{/key_id}","collaborators_url":"https://api.github.com/repos/guardian/grid/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/guardian/grid/teams","hooks_url":"https://api.github.com/repos/guardian/grid/hooks","issue_events_url":"https://api.github.com/repos/guardian/grid/issues/events{/number}","events_url":"https://api.github.com/repos/guardian/grid/events","assignees_url":"https://api.github.com/repos/guardian/grid/assignees{/user}","branches_url":"https://api.github.com/repos/guardian/grid/branches{/branch}","tags_url":"https://api.github.com/repos/guardian/grid/tags","blobs_url":"https://api.github.com/repos/guardian/grid/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/guardian/grid/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/guardian/grid/git/refs{/sha}","trees_url":"https://api.github.com/repos/guardian/grid/git/trees{/sha}","statuses_url":"https://api.github.com/repos/guardian/grid/statuses/{sha}","languages_url":"https://api.github.com/repos/guardian/grid/languages","stargazers_url":"https://api.github.com/repos/guardian/grid/stargazers","contributors_url":"https://api.github.com/repos/guardian/grid/contributors","subscribers_url":"https://api.github.com/repos/guardian/grid/subscribers","subscription_url":"https://api.github.com/repos/guardian/grid/subscription","commits_url":"https://api.github.com/repos/guardian/grid/commits{/sha}","git_commits_url":"https://api.github.com/repos/guardian/grid/git/commits{/sha}","comments_url":"https://api.github.com/repos/guardian/grid/comments{/number}","issue_comment_url":"https://api.github.com/repos/guardian/grid/issues/comments{/number}","contents_url":"https://api.github.com/repos/guardian/grid/contents/{+path}","compare_url":"https://api.github.com/repos/guardian/grid/compare/{base}...{head}","merges_url":"https://api.github.com/repos/guardian/grid/merges","archive_url":"https://api.github.com/repos/guardian/grid/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/guardian/grid/downloads","issues_url":"https://api.github.com/repos/guardian/grid/issues{/number}","pulls_url":"https://api.github.com/repos/guardian/grid/pulls{/number}","milestones_url":"https://api.github.com/repos/guardian/grid/milestones{/number}","notifications_url":"https://api.github.com/repos/guardian/grid/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/guardian/grid/labels{/name}","releases_url":"https://api.github.com/repos/guardian/grid/releases{/id}","created_at":"2013-08-22T13:49:31Z","updated_at":"2015-12-09T12:07:15Z","pushed_at":"2015-12-09T12:30:07Z","git_url":"git://github.com/guardian/grid.git","ssh_url":"git@github.com:guardian/grid.git","clone_url":"https://github.com/guardian/grid.git","svn_url":"https://github.com/guardian/grid","homepage":"","size":12938,"stargazers_count":977,"watchers_count":977,"language":"Scala","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":52,"mirror_url":null,"open_issues_count":64,"forks":52,"open_issues":64,"watchers":977,"default_branch":"master"}},"base":{"label":"guardian:master","ref":"master","sha":"5bd6d62d7d2b2fd505a04298750ed5addfb82df3","user":{"login":"guardian","id":164318,"avatar_url":"https://avatars.githubusercontent.com/u/164318?v=3","gravatar_id":"","url":"https://api.github.com/users/guardian","html_url":"https://github.com/guardian","followers_url":"https://api.github.com/users/guardian/followers","following_url":"https://api.github.com/users/guardian/following{/other_user}","gists_url":"https://api.github.com/users/guardian/gists{/gist_id}","starred_url":"https://api.github.com/users/guardian/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/guardian/subscriptions","organizations_url":"https://api.github.com/users/guardian/orgs","repos_url":"https://api.github.com/users/guardian/repos","events_url":"https://api.github.com/users/guardian/events{/privacy}","received_events_url":"https://api.github.com/users/guardian/received_events","type":"Organization","site_admin":false},"repo":{"id":12298112,"name":"grid","full_name":"guardian/grid","owner":{"login":"guardian","id":164318,"avatar_url":"https://avatars.githubusercontent.com/u/164318?v=3","gravatar_id":"","url":"https://api.github.com/users/guardian","html_url":"https://github.com/guardian","followers_url":"https://api.github.com/users/guardian/followers","following_url":"https://api.github.com/users/guardian/following{/other_user}","gists_url":"https://api.github.com/users/guardian/gists{/gist_id}","starred_url":"https://api.github.com/users/guardian/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/guardian/subscriptions","organizations_url":"https://api.github.com/users/guardian/orgs","repos_url":"https://api.github.com/users/guardian/repos","events_url":"https://api.github.com/users/guardian/events{/privacy}","received_events_url":"https://api.github.com/users/guardian/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/guardian/grid","description":"The Guardian’s new image management system","fork":false,"url":"https://api.github.com/repos/guardian/grid","forks_url":"https://api.github.com/repos/guardian/grid/forks","keys_url":"https://api.github.com/repos/guardian/grid/keys{/key_id}","collaborators_url":"https://api.github.com/repos/guardian/grid/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/guardian/grid/teams","hooks_url":"https://api.github.com/repos/guardian/grid/hooks","issue_events_url":"https://api.github.com/repos/guardian/grid/issues/events{/number}","events_url":"https://api.github.com/repos/guardian/grid/events","assignees_url":"https://api.github.com/repos/guardian/grid/assignees{/user}","branches_url":"https://api.github.com/repos/guardian/grid/branches{/branch}","tags_url":"https://api.github.com/repos/guardian/grid/tags","blobs_url":"https://api.github.com/repos/guardian/grid/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/guardian/grid/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/guardian/grid/git/refs{/sha}","trees_url":"https://api.github.com/repos/guardian/grid/git/trees{/sha}","statuses_url":"https://api.github.com/repos/guardian/grid/statuses/{sha}","languages_url":"https://api.github.com/repos/guardian/grid/languages","stargazers_url":"https://api.github.com/repos/guardian/grid/stargazers","contributors_url":"https://api.github.com/repos/guardian/grid/contributors","subscribers_url":"https://api.github.com/repos/guardian/grid/subscribers","subscription_url":"https://api.github.com/repos/guardian/grid/subscription","commits_url":"https://api.github.com/repos/guardian/grid/commits{/sha}","git_commits_url":"https://api.github.com/repos/guardian/grid/git/commits{/sha}","comments_url":"https://api.github.com/repos/guardian/grid/comments{/number}","issue_comment_url":"https://api.github.com/repos/guardian/grid/issues/comments{/number}","contents_url":"https://api.github.com/repos/guardian/grid/contents/{+path}","compare_url":"https://api.github.com/repos/guardian/grid/compare/{base}...{head}","merges_url":"https://api.github.com/repos/guardian/grid/merges","archive_url":"https://api.github.com/repos/guardian/grid/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/guardian/grid/downloads","issues_url":"https://api.github.com/repos/guardian/grid/issues{/number}","pulls_url":"https://api.github.com/repos/guardian/grid/pulls{/number}","milestones_url":"https://api.github.com/repos/guardian/grid/milestones{/number}","notifications_url":"https://api.github.com/repos/guardian/grid/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/guardian/grid/labels{/name}","releases_url":"https://api.github.com/repos/guardian/grid/releases{/id}","created_at":"2013-08-22T13:49:31Z","updated_at":"2015-12-09T12:07:15Z","pushed_at":"2015-12-09T12:30:07Z","git_url":"git://github.com/guardian/grid.git","ssh_url":"git@github.com:guardian/grid.git","clone_url":"https://github.com/guardian/grid.git","svn_url":"https://github.com/guardian/grid","homepage":"","size":12938,"stargazers_count":977,"watchers_count":977,"language":"Scala","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":52,"mirror_url":null,"open_issues_count":64,"forks":52,"open_issues":64,"watchers":977,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/guardian/grid/pulls/1447"},"html":{"href":"https://github.com/guardian/grid/pull/1447"},"issue":{"href":"https://api.github.com/repos/guardian/grid/issues/1447"},"comments":{"href":"https://api.github.com/repos/guardian/grid/issues/1447/comments"},"review_comments":{"href":"https://api.github.com/repos/guardian/grid/pulls/1447/comments"},"review_comment":{"href":"https://api.github.com/repos/guardian/grid/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/guardian/grid/pulls/1447/commits"},"statuses":{"href":"https://api.github.com/repos/guardian/grid/statuses/21d4942b0a238814e4ba8f605f27695b5f731cf5"}}}}