{"action":"created","comment":{"url":"https://api.github.com/repos/concord-consortium/rigse/pulls/comments/54778351","id":54778351,"diff_hunk":"@@ -0,0 +1,190 @@\n+class API::V1::ReportsController < API::APIController\n+  rescue_from Pundit::NotAuthorizedError, with: :pundit_user_not_authorized\n+\n+  def pundit_user_not_authorized(exception)\n+    unauthorized\n+  end\n+\n+  # GET api/v1/reports/offering?id=:id\n+  # Example response: https://gist.github.com/pjanik/b831b67f659e709931bf\n+  def offering\n+    offering = Portal::Offering.find(params[:id])\n+    authorize offering, :api_report?\n+    render json: response_json(offering)\n+  end\n+\n+  private\n+\n+  def response_json(offering)\n+    answers_hash = get_student_answers(offering)\n+    embeddable_filter = offering.report_embeddable_filter\n+    visibility_hash  = Hash[embeddable_filter.embeddables.collect { |v| [embeddable_key(v), true] }]\n+    {\n+      embeddables_filtering_enabled: !embeddable_filter.ignore,\n+      # Might be useful e.g. to filter out sections that doesn't make sense in LARA activities context.\n+      is_offering_external: offering.runnable.is_a?(ExternalActivity),\n+      class: class_json(offering),\n+      report: report_json(offering, answers_hash, visibility_hash)\n+    }\n+  end\n+\n+  # Helpers that provide class description:\n+\n+  def class_json(offering)\n+    clazz = offering.clazz\n+    section = clazz.section && clazz.section.length > 0 ? clazz.section : nil\n+    section = section ? \" (#{section})\" : \"\"\n+    {\n+      name: clazz.name + section,\n+      students: clazz.students.includes(:user).map { |s| student_json(s, offering) }\n+    }\n+  end\n+\n+  def student_json(student, offering)\n+    started_offering = student.report_learners.where(offering_id: offering.id).count > 0\n+    {\n+      id: student.id,\n+      name: student.name,\n+      started_offering:  started_offering\n+    }\n+  end\n+\n+  # Helpers that provide student answers grouped by embeddable key:\n+\n+  def get_student_answers(offering)\n+    # Collect all the student answers for given offering.\n+    answers = Report::Learner.where(offering_id: offering.id).map do |report_learner|\n+      student_name = report_learner.student_name\n+      student_id = report_learner.student_id\n+      answers = []\n+      report_learner.answers.map do |embeddable_key, answer|\n+        answer[:embeddable_key] = embeddable_key\n+        answer[:student_id] = student_id\n+        answer[:student_name] = student_name\n+        # Process some answers type to provide cleaner format, names, etc.\n+        question_type = embeddable_type(embeddable_key)\n+        if question_type == 'Embeddable::MultipleChoice'\n+          process_multiple_choice_answer(answer)\n+        elsif question_type == 'Embeddable::ImageQuestion'\n+          process_image_question_answer(answer)\n+        end\n+        answers.push(answer)\n+      end\n+      answers\n+    end\n+    # Flatten answers and group them by the embeddable key.\n+    answers.flatten.group_by { |a| a[:embeddable_key] }\n+  end\n+\n+  def process_multiple_choice_answer(hash)\n+    # Make naming more consistent, otherwise we would have crazy sequence of [:answers][:answer][:answer] keys.\n+    hash[:answer] = hash[:answer].map do |a|\n+      {\n+        choice: a[:answer],\n+        is_correct: a[:correct]\n+      }\n+    end\n+  end\n+\n+  def process_image_question_answer(hash)\n+    # Filter out useless Blob description, provide image URL instead (and note / annotation).\n+    ans = hash[:answer]\n+    if ans[:type] === 'Dataservice::Blob'\n+      ans[:image_url] = dataservice_blob_raw_url(id: ans[:id], token: ans[:token])\n+      # Skip unnecessary attributes, leave only image url and note.\n+      ans.slice!(:image_url, :note)","path":"app/controllers/api/v1/reports_controller.rb","position":95,"original_position":95,"commit_id":"59d23fd92d88bc5468c4adbe1c17f1509de6c13b","original_commit_id":"59d23fd92d88bc5468c4adbe1c17f1509de6c13b","user":{"login":"knowuh","id":22908,"avatar_url":"https://avatars.githubusercontent.com/u/22908?v=3","gravatar_id":"","url":"https://api.github.com/users/knowuh","html_url":"https://github.com/knowuh","followers_url":"https://api.github.com/users/knowuh/followers","following_url":"https://api.github.com/users/knowuh/following{/other_user}","gists_url":"https://api.github.com/users/knowuh/gists{/gist_id}","starred_url":"https://api.github.com/users/knowuh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/knowuh/subscriptions","organizations_url":"https://api.github.com/users/knowuh/orgs","repos_url":"https://api.github.com/users/knowuh/repos","events_url":"https://api.github.com/users/knowuh/events{/privacy}","received_events_url":"https://api.github.com/users/knowuh/received_events","type":"User","site_admin":false},"body":"Wow. I didn't know about Hash#slice.  ❤","created_at":"2016-03-02T19:39:39Z","updated_at":"2016-03-02T19:39:39Z","html_url":"https://github.com/concord-consortium/rigse/pull/217#discussion_r54778351","pull_request_url":"https://api.github.com/repos/concord-consortium/rigse/pulls/217","_links":{"self":{"href":"https://api.github.com/repos/concord-consortium/rigse/pulls/comments/54778351"},"html":{"href":"https://github.com/concord-consortium/rigse/pull/217#discussion_r54778351"},"pull_request":{"href":"https://api.github.com/repos/concord-consortium/rigse/pulls/217"}}},"pull_request":{"url":"https://api.github.com/repos/concord-consortium/rigse/pulls/217","id":61351276,"html_url":"https://github.com/concord-consortium/rigse/pull/217","diff_url":"https://github.com/concord-consortium/rigse/pull/217.diff","patch_url":"https://github.com/concord-consortium/rigse/pull/217.patch","issue_url":"https://api.github.com/repos/concord-consortium/rigse/issues/217","number":217,"state":"open","locked":false,"title":"New report API","user":{"login":"pjanik","id":767857,"avatar_url":"https://avatars.githubusercontent.com/u/767857?v=3","gravatar_id":"","url":"https://api.github.com/users/pjanik","html_url":"https://github.com/pjanik","followers_url":"https://api.github.com/users/pjanik/followers","following_url":"https://api.github.com/users/pjanik/following{/other_user}","gists_url":"https://api.github.com/users/pjanik/gists{/gist_id}","starred_url":"https://api.github.com/users/pjanik/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pjanik/subscriptions","organizations_url":"https://api.github.com/users/pjanik/orgs","repos_url":"https://api.github.com/users/pjanik/repos","events_url":"https://api.github.com/users/pjanik/events{/privacy}","received_events_url":"https://api.github.com/users/pjanik/received_events","type":"User","site_admin":false},"body":"`GET api/v1/reports/offering?id=:id`\r\nExample response: https://gist.github.com/pjanik/b831b67f659e709931bf\r\n\r\nIt provides some basic information, students list and finally the report structure. It's a tree: (investigation) -> activities -> sections -> pages -> questions -> answers. If offering is an activity, the root will be different (activity). It includes filters settings too (`visible_in_report`).\r\n\r\nWe don't necessarily need to merge it very soon, things might change a few times once I start working on the client side. However some initial feedback, especially regarding structure and general approach, would be useful. \r\n\r\n@scytacki, @knowuh, a few general notes following our discussion on Monday:\r\nI decided not to use Doug's `api/v1/answer_controller`. It provides students answer, but the format wouldn't be really useful for me. Also, it seems to have lots of raw SQL, maybe it makes sense due to performance reasons in this case (as it provides way more data than my single report), but it wouldn't be too easy to customize it. Also, obtaining students answer was actually the easiest part of this work (just read `Report::Learners`). \r\n\r\nI was trying to follow what's going on when we render the current report and base implementation on it, so hopefully things still work in the same way. The old controller itself is actually very simple, but it passes a bunch of objects to view and then the craziness begins. Views call helpers, partials, then some models, then you go back to view, plus there are some weird intermediate objects (Saveables, Standins) and so on. I tried to simplify it as much as I could, provide minimum information which is necessary to generate current report and move rest of the logic to React components. Yes, I might have missed something important, but I feel it's better to test it in practice rather than try to untangle the old code. Once we're sure that it's working, I hope we can get rid of many files, e.g. [report/util.js](https://github.com/concord-consortium/rigse/blob/master/app/models/report/util.rb). I feel the old code is overly complicated as it randomly tries to cache and pre-calculate various things, so views don't hit DB too many times. JSON structure should solve that issue. I tried not to use old helpers at all, even if some methods were useful, and to keep all the logic in this one controller. If it works and we could limit report generation to < 200 lines, it would be great.\r\n\r\nPerformance should also be way better, probably we can even implement live reload of the report (easy with React).","created_at":"2016-03-02T04:56:23Z","updated_at":"2016-03-02T19:39:39Z","closed_at":null,"merged_at":null,"merge_commit_sha":"3fda28f6bec7b6f5ef2369fc12939f5c528cbe33","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/concord-consortium/rigse/pulls/217/commits","review_comments_url":"https://api.github.com/repos/concord-consortium/rigse/pulls/217/comments","review_comment_url":"https://api.github.com/repos/concord-consortium/rigse/pulls/comments{/number}","comments_url":"https://api.github.com/repos/concord-consortium/rigse/issues/217/comments","statuses_url":"https://api.github.com/repos/concord-consortium/rigse/statuses/59d23fd92d88bc5468c4adbe1c17f1509de6c13b","head":{"label":"concord-consortium:report-api","ref":"report-api","sha":"59d23fd92d88bc5468c4adbe1c17f1509de6c13b","user":{"login":"concord-consortium","id":319219,"avatar_url":"https://avatars.githubusercontent.com/u/319219?v=3","gravatar_id":"","url":"https://api.github.com/users/concord-consortium","html_url":"https://github.com/concord-consortium","followers_url":"https://api.github.com/users/concord-consortium/followers","following_url":"https://api.github.com/users/concord-consortium/following{/other_user}","gists_url":"https://api.github.com/users/concord-consortium/gists{/gist_id}","starred_url":"https://api.github.com/users/concord-consortium/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/concord-consortium/subscriptions","organizations_url":"https://api.github.com/users/concord-consortium/orgs","repos_url":"https://api.github.com/users/concord-consortium/repos","events_url":"https://api.github.com/users/concord-consortium/events{/privacy}","received_events_url":"https://api.github.com/users/concord-consortium/received_events","type":"Organization","site_admin":false},"repo":{"id":2056634,"name":"rigse","full_name":"concord-consortium/rigse","owner":{"login":"concord-consortium","id":319219,"avatar_url":"https://avatars.githubusercontent.com/u/319219?v=3","gravatar_id":"","url":"https://api.github.com/users/concord-consortium","html_url":"https://github.com/concord-consortium","followers_url":"https://api.github.com/users/concord-consortium/followers","following_url":"https://api.github.com/users/concord-consortium/following{/other_user}","gists_url":"https://api.github.com/users/concord-consortium/gists{/gist_id}","starred_url":"https://api.github.com/users/concord-consortium/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/concord-consortium/subscriptions","organizations_url":"https://api.github.com/users/concord-consortium/orgs","repos_url":"https://api.github.com/users/concord-consortium/repos","events_url":"https://api.github.com/users/concord-consortium/events{/privacy}","received_events_url":"https://api.github.com/users/concord-consortium/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/concord-consortium/rigse","description":"Concord Consortium Portal somewhat like an LMS","fork":false,"url":"https://api.github.com/repos/concord-consortium/rigse","forks_url":"https://api.github.com/repos/concord-consortium/rigse/forks","keys_url":"https://api.github.com/repos/concord-consortium/rigse/keys{/key_id}","collaborators_url":"https://api.github.com/repos/concord-consortium/rigse/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/concord-consortium/rigse/teams","hooks_url":"https://api.github.com/repos/concord-consortium/rigse/hooks","issue_events_url":"https://api.github.com/repos/concord-consortium/rigse/issues/events{/number}","events_url":"https://api.github.com/repos/concord-consortium/rigse/events","assignees_url":"https://api.github.com/repos/concord-consortium/rigse/assignees{/user}","branches_url":"https://api.github.com/repos/concord-consortium/rigse/branches{/branch}","tags_url":"https://api.github.com/repos/concord-consortium/rigse/tags","blobs_url":"https://api.github.com/repos/concord-consortium/rigse/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/concord-consortium/rigse/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/concord-consortium/rigse/git/refs{/sha}","trees_url":"https://api.github.com/repos/concord-consortium/rigse/git/trees{/sha}","statuses_url":"https://api.github.com/repos/concord-consortium/rigse/statuses/{sha}","languages_url":"https://api.github.com/repos/concord-consortium/rigse/languages","stargazers_url":"https://api.github.com/repos/concord-consortium/rigse/stargazers","contributors_url":"https://api.github.com/repos/concord-consortium/rigse/contributors","subscribers_url":"https://api.github.com/repos/concord-consortium/rigse/subscribers","subscription_url":"https://api.github.com/repos/concord-consortium/rigse/subscription","commits_url":"https://api.github.com/repos/concord-consortium/rigse/commits{/sha}","git_commits_url":"https://api.github.com/repos/concord-consortium/rigse/git/commits{/sha}","comments_url":"https://api.github.com/repos/concord-consortium/rigse/comments{/number}","issue_comment_url":"https://api.github.com/repos/concord-consortium/rigse/issues/comments{/number}","contents_url":"https://api.github.com/repos/concord-consortium/rigse/contents/{+path}","compare_url":"https://api.github.com/repos/concord-consortium/rigse/compare/{base}...{head}","merges_url":"https://api.github.com/repos/concord-consortium/rigse/merges","archive_url":"https://api.github.com/repos/concord-consortium/rigse/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/concord-consortium/rigse/downloads","issues_url":"https://api.github.com/repos/concord-consortium/rigse/issues{/number}","pulls_url":"https://api.github.com/repos/concord-consortium/rigse/pulls{/number}","milestones_url":"https://api.github.com/repos/concord-consortium/rigse/milestones{/number}","notifications_url":"https://api.github.com/repos/concord-consortium/rigse/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/concord-consortium/rigse/labels{/name}","releases_url":"https://api.github.com/repos/concord-consortium/rigse/releases{/id}","deployments_url":"https://api.github.com/repos/concord-consortium/rigse/deployments","created_at":"2011-07-16T03:22:38Z","updated_at":"2016-01-07T14:50:08Z","pushed_at":"2016-03-02T04:56:23Z","git_url":"git://github.com/concord-consortium/rigse.git","ssh_url":"git@github.com:concord-consortium/rigse.git","clone_url":"https://github.com/concord-consortium/rigse.git","svn_url":"https://github.com/concord-consortium/rigse","homepage":"","size":76107,"stargazers_count":5,"watchers_count":5,"language":"Ruby","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":6,"mirror_url":null,"open_issues_count":3,"forks":6,"open_issues":3,"watchers":5,"default_branch":"master"}},"base":{"label":"concord-consortium:master","ref":"master","sha":"2cd16b4a1517c6ca140ec00ac4cf9c82a198f819","user":{"login":"concord-consortium","id":319219,"avatar_url":"https://avatars.githubusercontent.com/u/319219?v=3","gravatar_id":"","url":"https://api.github.com/users/concord-consortium","html_url":"https://github.com/concord-consortium","followers_url":"https://api.github.com/users/concord-consortium/followers","following_url":"https://api.github.com/users/concord-consortium/following{/other_user}","gists_url":"https://api.github.com/users/concord-consortium/gists{/gist_id}","starred_url":"https://api.github.com/users/concord-consortium/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/concord-consortium/subscriptions","organizations_url":"https://api.github.com/users/concord-consortium/orgs","repos_url":"https://api.github.com/users/concord-consortium/repos","events_url":"https://api.github.com/users/concord-consortium/events{/privacy}","received_events_url":"https://api.github.com/users/concord-consortium/received_events","type":"Organization","site_admin":false},"repo":{"id":2056634,"name":"rigse","full_name":"concord-consortium/rigse","owner":{"login":"concord-consortium","id":319219,"avatar_url":"https://avatars.githubusercontent.com/u/319219?v=3","gravatar_id":"","url":"https://api.github.com/users/concord-consortium","html_url":"https://github.com/concord-consortium","followers_url":"https://api.github.com/users/concord-consortium/followers","following_url":"https://api.github.com/users/concord-consortium/following{/other_user}","gists_url":"https://api.github.com/users/concord-consortium/gists{/gist_id}","starred_url":"https://api.github.com/users/concord-consortium/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/concord-consortium/subscriptions","organizations_url":"https://api.github.com/users/concord-consortium/orgs","repos_url":"https://api.github.com/users/concord-consortium/repos","events_url":"https://api.github.com/users/concord-consortium/events{/privacy}","received_events_url":"https://api.github.com/users/concord-consortium/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/concord-consortium/rigse","description":"Concord Consortium Portal somewhat like an LMS","fork":false,"url":"https://api.github.com/repos/concord-consortium/rigse","forks_url":"https://api.github.com/repos/concord-consortium/rigse/forks","keys_url":"https://api.github.com/repos/concord-consortium/rigse/keys{/key_id}","collaborators_url":"https://api.github.com/repos/concord-consortium/rigse/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/concord-consortium/rigse/teams","hooks_url":"https://api.github.com/repos/concord-consortium/rigse/hooks","issue_events_url":"https://api.github.com/repos/concord-consortium/rigse/issues/events{/number}","events_url":"https://api.github.com/repos/concord-consortium/rigse/events","assignees_url":"https://api.github.com/repos/concord-consortium/rigse/assignees{/user}","branches_url":"https://api.github.com/repos/concord-consortium/rigse/branches{/branch}","tags_url":"https://api.github.com/repos/concord-consortium/rigse/tags","blobs_url":"https://api.github.com/repos/concord-consortium/rigse/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/concord-consortium/rigse/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/concord-consortium/rigse/git/refs{/sha}","trees_url":"https://api.github.com/repos/concord-consortium/rigse/git/trees{/sha}","statuses_url":"https://api.github.com/repos/concord-consortium/rigse/statuses/{sha}","languages_url":"https://api.github.com/repos/concord-consortium/rigse/languages","stargazers_url":"https://api.github.com/repos/concord-consortium/rigse/stargazers","contributors_url":"https://api.github.com/repos/concord-consortium/rigse/contributors","subscribers_url":"https://api.github.com/repos/concord-consortium/rigse/subscribers","subscription_url":"https://api.github.com/repos/concord-consortium/rigse/subscription","commits_url":"https://api.github.com/repos/concord-consortium/rigse/commits{/sha}","git_commits_url":"https://api.github.com/repos/concord-consortium/rigse/git/commits{/sha}","comments_url":"https://api.github.com/repos/concord-consortium/rigse/comments{/number}","issue_comment_url":"https://api.github.com/repos/concord-consortium/rigse/issues/comments{/number}","contents_url":"https://api.github.com/repos/concord-consortium/rigse/contents/{+path}","compare_url":"https://api.github.com/repos/concord-consortium/rigse/compare/{base}...{head}","merges_url":"https://api.github.com/repos/concord-consortium/rigse/merges","archive_url":"https://api.github.com/repos/concord-consortium/rigse/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/concord-consortium/rigse/downloads","issues_url":"https://api.github.com/repos/concord-consortium/rigse/issues{/number}","pulls_url":"https://api.github.com/repos/concord-consortium/rigse/pulls{/number}","milestones_url":"https://api.github.com/repos/concord-consortium/rigse/milestones{/number}","notifications_url":"https://api.github.com/repos/concord-consortium/rigse/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/concord-consortium/rigse/labels{/name}","releases_url":"https://api.github.com/repos/concord-consortium/rigse/releases{/id}","deployments_url":"https://api.github.com/repos/concord-consortium/rigse/deployments","created_at":"2011-07-16T03:22:38Z","updated_at":"2016-01-07T14:50:08Z","pushed_at":"2016-03-02T04:56:23Z","git_url":"git://github.com/concord-consortium/rigse.git","ssh_url":"git@github.com:concord-consortium/rigse.git","clone_url":"https://github.com/concord-consortium/rigse.git","svn_url":"https://github.com/concord-consortium/rigse","homepage":"","size":76107,"stargazers_count":5,"watchers_count":5,"language":"Ruby","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":6,"mirror_url":null,"open_issues_count":3,"forks":6,"open_issues":3,"watchers":5,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/concord-consortium/rigse/pulls/217"},"html":{"href":"https://github.com/concord-consortium/rigse/pull/217"},"issue":{"href":"https://api.github.com/repos/concord-consortium/rigse/issues/217"},"comments":{"href":"https://api.github.com/repos/concord-consortium/rigse/issues/217/comments"},"review_comments":{"href":"https://api.github.com/repos/concord-consortium/rigse/pulls/217/comments"},"review_comment":{"href":"https://api.github.com/repos/concord-consortium/rigse/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/concord-consortium/rigse/pulls/217/commits"},"statuses":{"href":"https://api.github.com/repos/concord-consortium/rigse/statuses/59d23fd92d88bc5468c4adbe1c17f1509de6c13b"}}}}