{"action":"created","comment":{"url":"https://api.github.com/repos/mitodl/edx-sga/pulls/comments/24821887","id":24821887,"diff_hunk":"@@ -0,0 +1,530 @@\n+\"\"\"\n+This block defines a Staff Graded Assignment.  Students are shown a rubric\n+and invited to upload a file which is then graded by staff.\n+\"\"\"\n+import datetime\n+import hashlib\n+import json\n+import logging\n+import mimetypes\n+import os\n+import pkg_resources\n+import pytz\n+\n+from functools import partial\n+\n+from courseware.models import StudentModule\n+\n+from django.core.exceptions import PermissionDenied\n+from django.core.files import File\n+from django.core.files.storage import default_storage\n+from django.template import Context, Template\n+\n+from student.models import user_by_anonymous_id\n+from submissions import api as submissions_api\n+from submissions.models import StudentItem as SubmissionsStudent\n+\n+from webob.response import Response\n+\n+from xblock.core import XBlock\n+from xblock.exceptions import JsonHandlerError\n+from xblock.fields import DateTime, Scope, String, Float, Integer\n+from xblock.fragment import Fragment\n+\n+from xmodule.util.duedate import get_extended_due_date\n+\n+\n+log = logging.getLogger(__name__)\n+\n+\n+def reify(meth):\n+    \"\"\"\n+    Property which caches value so it is only computed once.\n+    \"\"\"\n+    def getter(inst):\n+        value = meth(inst)\n+        inst.__dict__[meth.__name__] = value\n+        return value\n+    return property(getter)\n+\n+\n+class StaffGradedAssignmentXBlock(XBlock):\n+    \"\"\"\n+    This block defines a Staff Graded Assignment.  Students are shown a rubric\n+    and invited to upload a file which is then graded by staff.\n+    \"\"\"\n+    has_score = True\n+    icon_class = 'problem'\n+\n+    display_name = String(\n+        default='Staff Graded Assignment', scope=Scope.settings,\n+        help=\"This name appears in the horizontal navigation at the top of \"\n+             \"the page.\"\n+    )\n+\n+    weight = Float(\n+        display_name=\"Problem Weight\",\n+        help=(\"Defines the number of points each problem is worth. \"\n+              \"If the value is not set, the problem is worth the sum of the \"\n+              \"option point values.\"),\n+        values={\"min\": 0, \"step\": .1},\n+        scope=Scope.settings\n+    )\n+\n+    points = Integer(\n+        display_name=\"Maximum score\",\n+        help=(\"Maximum grade score given to assignment by staff.\"),\n+        default=100,\n+        scope=Scope.settings\n+    )\n+\n+    staff_score = Integer(\n+        display_name=\"Score assigned by non-instructor staff\",\n+        help=(\"Score will need to be approved by instructor before being \"\n+              \"published.\"),\n+        default=None,\n+        scope=Scope.settings\n+    )\n+\n+    comment = String(\n+        display_name=\"Instructor comment\",\n+        default='',\n+        scope=Scope.user_state,\n+        help=\"Feedback given to student by instructor.\"\n+    )\n+\n+    annotated_sha1 = String(\n+        display_name=\"Annotated SHA1\",\n+        scope=Scope.user_state,\n+        default=None,\n+        help=(\"sha1 of the annotated file uploaded by the instructor for \"\n+              \"this assignment.\")\n+    )\n+\n+    annotated_filename = String(\n+        display_name=\"Annotated file name\",\n+        scope=Scope.user_state,\n+        default=None,\n+        help=\"The name of the annotated file uploaded for this assignment.\"\n+    )\n+\n+    annotated_mimetype = String(\n+        display_name=\"Mime type of annotated file\",\n+        scope=Scope.user_state,\n+        default=None,\n+        help=\"The mimetype of the annotated file uploaded for this assignment.\"\n+    )\n+\n+    annotated_timestamp = DateTime(\n+        display_name=\"Timestamp\",\n+        scope=Scope.user_state,\n+        default=None,\n+        help=\"When the annotated file was uploaded\")\n+\n+    def max_score(self):\n+        return self.points\n+\n+    @reify\n+    def block_id(self):\n+        # cargo culted gibberish\n+        return self.scope_ids.usage_id\n+\n+    def student_submission_id(self, id=None):\n+        \"\"\"\n+        Returns dict required by the submissions app for creating and\n+        retrieving submissions for a particular student.\n+        \"\"\"\n+        if id is None:\n+            id = self.xmodule_runtime.anonymous_student_id\n+            assert id != 'MOCK', \"Forgot to call 'personalize' in test.\"\n+        return {\n+            \"student_id\": id,\n+            \"course_id\": self.course_id,\n+            \"item_id\": self.block_id,\n+            \"item_type\": 'sga',  # ???\n+        }\n+\n+    def get_submission(self, id=None):\n+        \"\"\"\n+        Get student's most recent submission.\n+        \"\"\"\n+        submissions = submissions_api.get_submissions(\n+            self.student_submission_id(id))\n+        if submissions:\n+            # If I understand docs correctly, most recent submission should\n+            # be first\n+            return submissions[0]\n+\n+    def get_score(self, id=None):\n+        \"\"\"\n+        Get student's current score.\n+        \"\"\"\n+        score = submissions_api.get_score(self.student_submission_id(id))\n+        if score:\n+            return score['points_earned']\n+\n+    @reify\n+    def score(self):\n+        return self.get_score()\n+\n+    def student_view(self, context=None):\n+        \"\"\"\n+        The primary view of the StaffGradedAssignmentXBlock, shown to students\n+        when viewing courses.\n+        \"\"\"\n+        context = {\n+            \"student_state\": json.dumps(self.student_state()),\n+            \"id\": self.location.name.replace('.', '_')\n+        }\n+        if self.show_staff_grading_interface():\n+            context['is_course_staff'] = True\n+            self.update_staff_debug_context(context)\n+\n+        fragment = Fragment()\n+        fragment.add_content(\n+            render_template(\n+                'templates/staff_graded_assignment/show.html',\n+                context\n+            )\n+        )\n+        fragment.add_css(_resource(\"static/css/edx_sga.css\"))\n+        fragment.add_javascript(_resource(\"static/js/src/edx_sga.js\"))\n+        fragment.initialize_js('StaffGradedAssignmentXBlock')\n+        return fragment\n+\n+    def update_staff_debug_context(self, context):\n+        published = self.start\n+        context['is_released'] = published and published < _now()\n+        context['location'] = self.location\n+        context['category'] = type(self).__name__\n+        context['fields'] = [\n+            (name, field.read_from(self))\n+            for name, field in self.fields.items()]\n+\n+    def student_state(self):\n+        \"\"\"\n+        Returns a JSON serializable representation of student's state for\n+        rendering in client view.\n+        \"\"\"\n+        submission = self.get_submission()\n+        if submission:\n+            uploaded = {\"filename\": submission['answer']['filename']}\n+        else:\n+            uploaded = None\n+\n+        if self.annotated_sha1:\n+            annotated = {\"filename\": self.annotated_filename}\n+        else:\n+            annotated = None\n+\n+        score = self.score\n+        if score is not None:\n+            graded = {'score': score, 'comment': self.comment}\n+        else:\n+            graded = None\n+\n+        return {\n+            \"uploaded\": uploaded,\n+            \"annotated\": annotated,\n+            \"graded\": graded,\n+            \"max_score\": self.max_score(),\n+            \"upload_allowed\": self.upload_allowed(),\n+        }\n+\n+    def staff_grading_data(self):\n+        def get_student_data():\n+            # Submissions doesn't have API for this, just use model directly\n+            students = SubmissionsStudent.objects.filter(\n+                course_id=self.course_id,\n+                item_id=self.block_id)\n+            for student in students:\n+                submission = self.get_submission(student.student_id)\n+                if not submission:\n+                    continue\n+                user = user_by_anonymous_id(student.student_id)\n+                module, _ = StudentModule.objects.get_or_create(\n+                    course_id=self.course_id,\n+                    module_state_key=self.location,\n+                    student=user,\n+                    defaults={\n+                        'state': '{}',\n+                        'module_type': self.category,\n+                    })\n+                state = json.loads(module.state)\n+                score = self.get_score(student.student_id)\n+                approved = score is not None\n+                if score is None:\n+                    score = state.get('staff_score')\n+                    needs_approval = score is not None\n+                else:\n+                    needs_approval = False\n+                instructor = self.is_instructor()\n+                yield {\n+                    'module_id': module.id,\n+                    'student_id': student.student_id,\n+                    'submission_id': submission['uuid'],\n+                    'username': module.student.username,\n+                    'fullname': module.student.profile.name,\n+                    'filename': submission['answer'][\"filename\"],\n+                    'timestamp': submission['created_at'].strftime(\n+                        DateTime.DATETIME_FORMAT\n+                    ),\n+                    'score': score,\n+                    'approved': approved,\n+                    'needs_approval': instructor and needs_approval,\n+                    'may_grade': instructor or not approved,\n+                    'annotated': state.get(\"annotated_filename\"),\n+                    'comment': state.get(\"comment\", ''),\n+                }\n+\n+        return {\n+            'assignments': list(get_student_data()),\n+            'max_score': self.max_score(),\n+        }\n+\n+    def studio_view(self, context=None):\n+        try:\n+            cls = type(self)\n+\n+            def none_to_empty(x):\n+                return x if x is not None else ''\n+            edit_fields = (\n+                (field, none_to_empty(getattr(self, field.name)), validator)\n+                for field, validator in (\n+                    (cls.display_name, 'string'),\n+                    (cls.points, 'number'),\n+                    (cls.weight, 'number'))\n+            )\n+\n+            context = {\n+                'fields': edit_fields\n+            }\n+            fragment = Fragment()\n+            fragment.add_content(\n+                render_template(\n+                    'templates/staff_graded_assignment/edit.html',\n+                    context\n+                )\n+            )\n+            fragment.add_javascript(_resource(\"static/js/src/studio.js\"))\n+            fragment.initialize_js('StaffGradedAssignmentXBlock')\n+            return fragment\n+        except:  # pragma: NO COVER\n+            log.error(\"Don't swallow my exceptions\", exc_info=True)\n+            raise\n+\n+    @XBlock.json_handler\n+    def save_sga(self, data, suffix=''):\n+        self.display_name = data.get('display_name', self.display_name)\n+        self.weight = data.get('weight', self.weight)\n+\n+        # Validate points before saving\n+        points = data.get('points', self.points)\n+        # Check that we are an int\n+        try:\n+            points = int(points)\n+        except ValueError:\n+            raise JsonHandlerError(400, 'Points must be an integer')\n+        # Check that we are positive\n+        if points < 0:\n+            raise JsonHandlerError(400, 'Points must be a positive integer')\n+        self.points = points\n+\n+    @XBlock.handler\n+    def upload_assignment(self, request, suffix=''):\n+        require(self.upload_allowed())\n+        upload = request.params['assignment']\n+        sha1 = _get_sha1(upload.file)\n+        answer = {\n+            \"sha1\": sha1,\n+            \"filename\": upload.file.name,\n+            \"mimetype\": mimetypes.guess_type(upload.file.name)[0],\n+        }\n+        student_id = self.student_submission_id()\n+        submissions_api.create_submission(student_id, answer)\n+        path = self._file_storage_path(sha1, upload.file.name)\n+        if not default_storage.exists(path):\n+            default_storage.save(path, File(upload.file))\n+        return Response(json_body=self.student_state())\n+\n+    @XBlock.handler\n+    def staff_upload_annotated(self, request, suffix=''):\n+        require(self.is_course_staff())\n+        upload = request.params['annotated']\n+        module = StudentModule.objects.get(pk=request.params['module_id'])\n+        state = json.loads(module.state)\n+        state['annotated_sha1'] = sha1 = _get_sha1(upload.file)\n+        state['annotated_filename'] = filename = upload.file.name\n+        state['annotated_mimetype'] = mimetypes.guess_type(upload.file.name)[0]\n+        state['annotated_timestamp'] = _now().strftime(\n+            DateTime.DATETIME_FORMAT\n+        )\n+        path = self._file_storage_path(sha1, filename)\n+        if not default_storage.exists(path):\n+            default_storage.save(path, File(upload.file))\n+        module.state = json.dumps(state)\n+        module.save()\n+        return Response(json_body=self.staff_grading_data())\n+\n+    @XBlock.handler\n+    def download_assignment(self, request, suffix=''):\n+        answer = self.get_submission()['answer']\n+        path = self._file_storage_path(answer['sha1'], answer['filename'])\n+        return self.download(path, answer['mimetype'], answer['filename'])\n+\n+    @XBlock.handler\n+    def download_annotated(self, request, suffix=''):\n+        path = self._file_storage_path(\n+            self.annotated_sha1,\n+            self.annotated_filename,\n+        )\n+        return self.download(\n+            path,\n+            self.annotated_mimetype,\n+            self.annotated_filename\n+        )\n+\n+    @XBlock.handler\n+    def staff_download(self, request, suffix=''):\n+        require(self.is_course_staff())\n+        submission = self.get_submission(request.params['student_id'])\n+        answer = submission['answer']\n+        path = self._file_storage_path(answer['sha1'], answer['filename'])\n+        return self.download(path, answer['mimetype'], answer['filename'])\n+\n+    @XBlock.handler\n+    def staff_download_annotated(self, request, suffix=''):\n+        require(self.is_course_staff())\n+        module = StudentModule.objects.get(pk=request.params['module_id'])\n+        state = json.loads(module.state)\n+        path = self._file_storage_path(\n+            state['annotated_sha1'],\n+            state['annotated_filename']\n+        )\n+        return self.download(\n+            path,\n+            state['annotated_mimetype'],\n+            state['annotated_filename']\n+        )\n+\n+    def download(self, path, mimetype, filename):\n+        BLOCK_SIZE = (1 << 10) * 8  # 8kb\n+        file = default_storage.open(path)\n+        app_iter = iter(partial(file.read, BLOCK_SIZE), '')\n+        return Response(\n+            app_iter=app_iter,\n+            content_type=mimetype,\n+            content_disposition=\"attachment; filename=\" + filename)\n+\n+    @XBlock.handler\n+    def get_staff_grading_data(self, request, suffix=''):\n+        require(self.is_course_staff())","path":"edx_sga/sga.py","position":421,"original_position":421,"commit_id":"004807ec0d09d46ac6377909160e505844c715a0","original_commit_id":"004807ec0d09d46ac6377909160e505844c715a0","user":{"login":"stephensanchez","id":235752,"avatar_url":"https://avatars.githubusercontent.com/u/235752?v=3","gravatar_id":"","url":"https://api.github.com/users/stephensanchez","html_url":"https://github.com/stephensanchez","followers_url":"https://api.github.com/users/stephensanchez/followers","following_url":"https://api.github.com/users/stephensanchez/following{/other_user}","gists_url":"https://api.github.com/users/stephensanchez/gists{/gist_id}","starred_url":"https://api.github.com/users/stephensanchez/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stephensanchez/subscriptions","organizations_url":"https://api.github.com/users/stephensanchez/orgs","repos_url":"https://api.github.com/users/stephensanchez/repos","events_url":"https://api.github.com/users/stephensanchez/events{/privacy}","received_events_url":"https://api.github.com/users/stephensanchez/received_events","type":"User","site_admin":false},"body":"nitpick. Could make a decorator for required privileges on functions and handlers.\r\nhttps://github.com/edx/edx-ora2/blob/master/openassessment/xblock/staff_info_mixin.py#L206-206","created_at":"2015-02-17T15:15:21Z","updated_at":"2015-02-17T15:15:21Z","html_url":"https://github.com/mitodl/edx-sga/pull/52#discussion_r24821887","pull_request_url":"https://api.github.com/repos/mitodl/edx-sga/pulls/52","_links":{"self":{"href":"https://api.github.com/repos/mitodl/edx-sga/pulls/comments/24821887"},"html":{"href":"https://github.com/mitodl/edx-sga/pull/52#discussion_r24821887"},"pull_request":{"href":"https://api.github.com/repos/mitodl/edx-sga/pulls/52"}}},"pull_request":{"url":"https://api.github.com/repos/mitodl/edx-sga/pulls/52","id":29194096,"html_url":"https://github.com/mitodl/edx-sga/pull/52","diff_url":"https://github.com/mitodl/edx-sga/pull/52.diff","patch_url":"https://github.com/mitodl/edx-sga/pull/52.patch","issue_url":"https://api.github.com/repos/mitodl/edx-sga/issues/52","number":52,"state":"open","locked":false,"title":"edX Review ","user":{"login":"pdpinch","id":430126,"avatar_url":"https://avatars.githubusercontent.com/u/430126?v=3","gravatar_id":"","url":"https://api.github.com/users/pdpinch","html_url":"https://github.com/pdpinch","followers_url":"https://api.github.com/users/pdpinch/followers","following_url":"https://api.github.com/users/pdpinch/following{/other_user}","gists_url":"https://api.github.com/users/pdpinch/gists{/gist_id}","starred_url":"https://api.github.com/users/pdpinch/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pdpinch/subscriptions","organizations_url":"https://api.github.com/users/pdpinch/orgs","repos_url":"https://api.github.com/users/pdpinch/repos","events_url":"https://api.github.com/users/pdpinch/events{/privacy}","received_events_url":"https://api.github.com/users/pdpinch/received_events","type":"User","site_admin":false},"body":"@nedbat is this what you had in mind? I'm not sure how it's different from https://github.com/mitodl/edx-sga/compare/995b18ed2273291d8f51f7b805fe2573844e6f71...master\r\n\r\n@carsongee @Ferdi @skleinfeldt FYI","created_at":"2015-02-12T16:37:57Z","updated_at":"2015-02-17T15:15:22Z","closed_at":null,"merged_at":null,"merge_commit_sha":"24cb223107bf972d9333df0f2614beca0290f96a","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/mitodl/edx-sga/pulls/52/commits","review_comments_url":"https://api.github.com/repos/mitodl/edx-sga/pulls/52/comments","review_comment_url":"https://api.github.com/repos/mitodl/edx-sga/pulls/comments/{number}","comments_url":"https://api.github.com/repos/mitodl/edx-sga/issues/52/comments","statuses_url":"https://api.github.com/repos/mitodl/edx-sga/statuses/004807ec0d09d46ac6377909160e505844c715a0","head":{"label":"mitodl:master","ref":"master","sha":"004807ec0d09d46ac6377909160e505844c715a0","user":{"login":"mitodl","id":7918516,"avatar_url":"https://avatars.githubusercontent.com/u/7918516?v=3","gravatar_id":"","url":"https://api.github.com/users/mitodl","html_url":"https://github.com/mitodl","followers_url":"https://api.github.com/users/mitodl/followers","following_url":"https://api.github.com/users/mitodl/following{/other_user}","gists_url":"https://api.github.com/users/mitodl/gists{/gist_id}","starred_url":"https://api.github.com/users/mitodl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mitodl/subscriptions","organizations_url":"https://api.github.com/users/mitodl/orgs","repos_url":"https://api.github.com/users/mitodl/repos","events_url":"https://api.github.com/users/mitodl/events{/privacy}","received_events_url":"https://api.github.com/users/mitodl/received_events","type":"Organization","site_admin":false},"repo":{"id":20945615,"name":"edx-sga","full_name":"mitodl/edx-sga","owner":{"login":"mitodl","id":7918516,"avatar_url":"https://avatars.githubusercontent.com/u/7918516?v=3","gravatar_id":"","url":"https://api.github.com/users/mitodl","html_url":"https://github.com/mitodl","followers_url":"https://api.github.com/users/mitodl/followers","following_url":"https://api.github.com/users/mitodl/following{/other_user}","gists_url":"https://api.github.com/users/mitodl/gists{/gist_id}","starred_url":"https://api.github.com/users/mitodl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mitodl/subscriptions","organizations_url":"https://api.github.com/users/mitodl/orgs","repos_url":"https://api.github.com/users/mitodl/repos","events_url":"https://api.github.com/users/mitodl/events{/privacy}","received_events_url":"https://api.github.com/users/mitodl/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/mitodl/edx-sga","description":"Staff Graded Assignment XBlock for the edX platform","fork":true,"url":"https://api.github.com/repos/mitodl/edx-sga","forks_url":"https://api.github.com/repos/mitodl/edx-sga/forks","keys_url":"https://api.github.com/repos/mitodl/edx-sga/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mitodl/edx-sga/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mitodl/edx-sga/teams","hooks_url":"https://api.github.com/repos/mitodl/edx-sga/hooks","issue_events_url":"https://api.github.com/repos/mitodl/edx-sga/issues/events{/number}","events_url":"https://api.github.com/repos/mitodl/edx-sga/events","assignees_url":"https://api.github.com/repos/mitodl/edx-sga/assignees{/user}","branches_url":"https://api.github.com/repos/mitodl/edx-sga/branches{/branch}","tags_url":"https://api.github.com/repos/mitodl/edx-sga/tags","blobs_url":"https://api.github.com/repos/mitodl/edx-sga/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mitodl/edx-sga/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mitodl/edx-sga/git/refs{/sha}","trees_url":"https://api.github.com/repos/mitodl/edx-sga/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mitodl/edx-sga/statuses/{sha}","languages_url":"https://api.github.com/repos/mitodl/edx-sga/languages","stargazers_url":"https://api.github.com/repos/mitodl/edx-sga/stargazers","contributors_url":"https://api.github.com/repos/mitodl/edx-sga/contributors","subscribers_url":"https://api.github.com/repos/mitodl/edx-sga/subscribers","subscription_url":"https://api.github.com/repos/mitodl/edx-sga/subscription","commits_url":"https://api.github.com/repos/mitodl/edx-sga/commits{/sha}","git_commits_url":"https://api.github.com/repos/mitodl/edx-sga/git/commits{/sha}","comments_url":"https://api.github.com/repos/mitodl/edx-sga/comments{/number}","issue_comment_url":"https://api.github.com/repos/mitodl/edx-sga/issues/comments/{number}","contents_url":"https://api.github.com/repos/mitodl/edx-sga/contents/{+path}","compare_url":"https://api.github.com/repos/mitodl/edx-sga/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mitodl/edx-sga/merges","archive_url":"https://api.github.com/repos/mitodl/edx-sga/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mitodl/edx-sga/downloads","issues_url":"https://api.github.com/repos/mitodl/edx-sga/issues{/number}","pulls_url":"https://api.github.com/repos/mitodl/edx-sga/pulls{/number}","milestones_url":"https://api.github.com/repos/mitodl/edx-sga/milestones{/number}","notifications_url":"https://api.github.com/repos/mitodl/edx-sga/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mitodl/edx-sga/labels{/name}","releases_url":"https://api.github.com/repos/mitodl/edx-sga/releases{/id}","created_at":"2014-06-18T01:46:49Z","updated_at":"2015-02-12T21:46:06Z","pushed_at":"2015-02-12T21:46:43Z","git_url":"git://github.com/mitodl/edx-sga.git","ssh_url":"git@github.com:mitodl/edx-sga.git","clone_url":"https://github.com/mitodl/edx-sga.git","svn_url":"https://github.com/mitodl/edx-sga","homepage":null,"size":2559,"stargazers_count":8,"watchers_count":8,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":14,"mirror_url":null,"open_issues_count":10,"forks":14,"open_issues":10,"watchers":8,"default_branch":"master"}},"base":{"label":"mitodl:empty","ref":"empty","sha":"995b18ed2273291d8f51f7b805fe2573844e6f71","user":{"login":"mitodl","id":7918516,"avatar_url":"https://avatars.githubusercontent.com/u/7918516?v=3","gravatar_id":"","url":"https://api.github.com/users/mitodl","html_url":"https://github.com/mitodl","followers_url":"https://api.github.com/users/mitodl/followers","following_url":"https://api.github.com/users/mitodl/following{/other_user}","gists_url":"https://api.github.com/users/mitodl/gists{/gist_id}","starred_url":"https://api.github.com/users/mitodl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mitodl/subscriptions","organizations_url":"https://api.github.com/users/mitodl/orgs","repos_url":"https://api.github.com/users/mitodl/repos","events_url":"https://api.github.com/users/mitodl/events{/privacy}","received_events_url":"https://api.github.com/users/mitodl/received_events","type":"Organization","site_admin":false},"repo":{"id":20945615,"name":"edx-sga","full_name":"mitodl/edx-sga","owner":{"login":"mitodl","id":7918516,"avatar_url":"https://avatars.githubusercontent.com/u/7918516?v=3","gravatar_id":"","url":"https://api.github.com/users/mitodl","html_url":"https://github.com/mitodl","followers_url":"https://api.github.com/users/mitodl/followers","following_url":"https://api.github.com/users/mitodl/following{/other_user}","gists_url":"https://api.github.com/users/mitodl/gists{/gist_id}","starred_url":"https://api.github.com/users/mitodl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mitodl/subscriptions","organizations_url":"https://api.github.com/users/mitodl/orgs","repos_url":"https://api.github.com/users/mitodl/repos","events_url":"https://api.github.com/users/mitodl/events{/privacy}","received_events_url":"https://api.github.com/users/mitodl/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/mitodl/edx-sga","description":"Staff Graded Assignment XBlock for the edX platform","fork":true,"url":"https://api.github.com/repos/mitodl/edx-sga","forks_url":"https://api.github.com/repos/mitodl/edx-sga/forks","keys_url":"https://api.github.com/repos/mitodl/edx-sga/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mitodl/edx-sga/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mitodl/edx-sga/teams","hooks_url":"https://api.github.com/repos/mitodl/edx-sga/hooks","issue_events_url":"https://api.github.com/repos/mitodl/edx-sga/issues/events{/number}","events_url":"https://api.github.com/repos/mitodl/edx-sga/events","assignees_url":"https://api.github.com/repos/mitodl/edx-sga/assignees{/user}","branches_url":"https://api.github.com/repos/mitodl/edx-sga/branches{/branch}","tags_url":"https://api.github.com/repos/mitodl/edx-sga/tags","blobs_url":"https://api.github.com/repos/mitodl/edx-sga/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mitodl/edx-sga/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mitodl/edx-sga/git/refs{/sha}","trees_url":"https://api.github.com/repos/mitodl/edx-sga/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mitodl/edx-sga/statuses/{sha}","languages_url":"https://api.github.com/repos/mitodl/edx-sga/languages","stargazers_url":"https://api.github.com/repos/mitodl/edx-sga/stargazers","contributors_url":"https://api.github.com/repos/mitodl/edx-sga/contributors","subscribers_url":"https://api.github.com/repos/mitodl/edx-sga/subscribers","subscription_url":"https://api.github.com/repos/mitodl/edx-sga/subscription","commits_url":"https://api.github.com/repos/mitodl/edx-sga/commits{/sha}","git_commits_url":"https://api.github.com/repos/mitodl/edx-sga/git/commits{/sha}","comments_url":"https://api.github.com/repos/mitodl/edx-sga/comments{/number}","issue_comment_url":"https://api.github.com/repos/mitodl/edx-sga/issues/comments/{number}","contents_url":"https://api.github.com/repos/mitodl/edx-sga/contents/{+path}","compare_url":"https://api.github.com/repos/mitodl/edx-sga/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mitodl/edx-sga/merges","archive_url":"https://api.github.com/repos/mitodl/edx-sga/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mitodl/edx-sga/downloads","issues_url":"https://api.github.com/repos/mitodl/edx-sga/issues{/number}","pulls_url":"https://api.github.com/repos/mitodl/edx-sga/pulls{/number}","milestones_url":"https://api.github.com/repos/mitodl/edx-sga/milestones{/number}","notifications_url":"https://api.github.com/repos/mitodl/edx-sga/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mitodl/edx-sga/labels{/name}","releases_url":"https://api.github.com/repos/mitodl/edx-sga/releases{/id}","created_at":"2014-06-18T01:46:49Z","updated_at":"2015-02-12T21:46:06Z","pushed_at":"2015-02-12T21:46:43Z","git_url":"git://github.com/mitodl/edx-sga.git","ssh_url":"git@github.com:mitodl/edx-sga.git","clone_url":"https://github.com/mitodl/edx-sga.git","svn_url":"https://github.com/mitodl/edx-sga","homepage":null,"size":2559,"stargazers_count":8,"watchers_count":8,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":14,"mirror_url":null,"open_issues_count":10,"forks":14,"open_issues":10,"watchers":8,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/mitodl/edx-sga/pulls/52"},"html":{"href":"https://github.com/mitodl/edx-sga/pull/52"},"issue":{"href":"https://api.github.com/repos/mitodl/edx-sga/issues/52"},"comments":{"href":"https://api.github.com/repos/mitodl/edx-sga/issues/52/comments"},"review_comments":{"href":"https://api.github.com/repos/mitodl/edx-sga/pulls/52/comments"},"review_comment":{"href":"https://api.github.com/repos/mitodl/edx-sga/pulls/comments/{number}"},"commits":{"href":"https://api.github.com/repos/mitodl/edx-sga/pulls/52/commits"},"statuses":{"href":"https://api.github.com/repos/mitodl/edx-sga/statuses/004807ec0d09d46ac6377909160e505844c715a0"}}}}