{"action":"created","comment":{"url":"https://api.github.com/repos/edx/edx-platform/pulls/comments/30831878","id":30831878,"diff_hunk":"@@ -139,3 +157,1222 @@ def test_publish_draft_delete(self):\n             self.draft_mongo.get_item(location)\n         self.assertNotIn(other_child_loc, item.children)\n         self.assertTrue(self.draft_mongo.has_item(other_child_loc), \"Oops, lost moved item\")\n+\n+\n+class UniversalTestSetup(unittest.TestCase):\n+    \"\"\"\n+    This class exists to test XML import and export between different modulestore\n+    classes.\n+\n+    Requires from subclasses:\n+        self.user_id - fake user_id\n+    \"\"\"\n+\n+    def _create_course(self, store, course_key):\n+        \"\"\"\n+        Create the course that'll be published below. The course has a binary structure, meaning:\n+        The course has two chapters (chapter_0 & chapter_1),\n+        each of which has two sequentials (sequential_0/1 & sequential_2/3),\n+        each of which has two verticals (vertical_0/1 - vertical_6/7),\n+        each of which has two units (unit_0/1 - unit_14/15).\n+        \"\"\"\n+        def _create_child(parent, block_type, block_id):\n+            \"\"\"\n+            Create a child block within the course.\n+            \"\"\"\n+            return store.create_child(\n+                self.user_id, parent.location, block_type, block_id=block_id\n+            )\n+\n+        def _make_block_id(block_type, num):\n+            \"\"\"\n+            Given a block_type/num, return a block id.\n+            \"\"\"\n+            return '{}{:02d}'.format(block_type, num)\n+\n+        def _make_course_db_entry(parent_type, parent_id, block_id, idx, child_block_type, child_block_id_base):\n+            \"\"\"\n+            Make a single entry for the course DB.\n+            \"\"\"\n+            return {\n+                'parent_type': parent_type,\n+                'parent_id': parent_id,\n+                'index_in_children_list': idx % 2,\n+                'filename': block_id,\n+                'child_ids': (\n+                    (child_block_type, _make_block_id(child_block_id_base, idx * 2)),\n+                    (child_block_type, _make_block_id(child_block_id_base, idx * 2 + 1)),\n+                )\n+            }\n+\n+        # Create all the course items on the draft branch.\n+        with store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):\n+            # Create course.\n+            self.course = store.create_course(course_key.org, course_key.course, course_key.run, self.user_id)\n+\n+            # Create chapters.\n+            block_type = 'chapter'\n+            for idx in xrange(0, 2):\n+                parent_type = 'course'\n+                parent_id = 'course'\n+                block_id = _make_block_id(block_type, idx)\n+                setattr(self, block_id, _create_child(self.course, block_type, block_id))\n+                db_entry = {\n+                    (block_type, block_id): _make_course_db_entry(\n+                        parent_type, parent_id, block_id, idx, 'sequential', 'sequential'\n+                    )\n+                }\n+                self.course_db.update(db_entry)\n+\n+            # Create sequentials.\n+            block_type = 'sequential'\n+            for idx in xrange(0, 4):\n+                parent_type = 'chapter'\n+                parent_id = _make_block_id(parent_type, idx / 2)\n+                parent_item = getattr(self, parent_id)\n+                block_id = _make_block_id(block_type, idx)\n+                setattr(self, block_id, _create_child(parent_item, block_type, block_id))\n+                db_entry = {\n+                    (block_type, block_id): _make_course_db_entry(\n+                        parent_type, parent_id, block_id, idx, 'vertical', 'vertical'\n+                    )\n+                }\n+                self.course_db.update(db_entry)\n+\n+            # Create verticals.\n+            block_type = 'vertical'\n+            for idx in xrange(0, 8):\n+                parent_type = 'sequential'\n+                parent_id = _make_block_id(parent_type, idx / 2)\n+                parent_item = getattr(self, parent_id)\n+                block_id = _make_block_id(block_type, idx)\n+                setattr(self, block_id, _create_child(parent_item, block_type, block_id))\n+                db_entry = {\n+                    (block_type, block_id): _make_course_db_entry(\n+                        parent_type, parent_id, block_id, idx, 'html', 'unit'\n+                    )\n+                }\n+                self.course_db.update(db_entry)\n+                self.all_verticals.append((block_type, block_id))\n+\n+            # Create html units.\n+            block_type = 'html'\n+            for idx in xrange(0, 16):\n+                parent_type = 'vertical'\n+                parent_id = _make_block_id(parent_type, idx / 2)\n+                parent_item = getattr(self, parent_id)\n+                block_id = _make_block_id('unit', idx)\n+                setattr(self, block_id, _create_child(parent_item, 'html', block_id))\n+                db_entry = {\n+                    (block_type, block_id): _make_course_db_entry(\n+                        parent_type, parent_id, block_id, idx, '', ''\n+                    )\n+                }\n+                self.course_db.update(db_entry)\n+                self.all_units.append((block_type, block_id))\n+\n+    def setUp(self):\n+        self.user_id = -3\n+        self.course_key = CourseLocator('test_org', 'test_course', 'test_run')\n+        self.course = None\n+\n+        # For convenience, maintain a list of (block_type, block_id) pairs for all verticals/units.\n+        self.all_verticals = []\n+        self.all_units = []\n+\n+        # Course block database is keyed on (block_type, block_id) pairs.\n+        # It's built during the course creation below and contains all the parent/child\n+        # data needed to check the OLX.\n+        self.course_db = {}\n+\n+        super(UniversalTestSetup, self).setUp()\n+\n+\n+class OLXFormatChecker(unittest.TestCase):\n+    \"\"\"\n+    Examines the on-disk course export to verify that specific items are present/missing\n+    in the course export.\n+    Currently assumes that the course is broken up into different subdirs.\n+\n+    Requires from subclasses:\n+        self.root_export_dir - absolute root directory of course exports\n+        self.export_dir - top-level course export directory name\n+    \"\"\"\n+    unittest.TestCase.longMessage = True\n+\n+    def _get_course_export_dir(self):\n+        \"\"\"\n+        Ensure that the course has been exported and return course export dir.\n+        \"\"\"\n+        block_path = os.path.join(self.root_export_dir, self.export_dir)  # pylint: disable=no-member\n+        self.assertTrue(\n+            os.path.isdir(block_path),\n+            msg='{} is not a dir.'.format(block_path)\n+        )\n+        return block_path\n+\n+    def _get_block_type_path(self, course_export_dir, block_type, draft):\n+        \"\"\"\n+        Return the path to the block type subdirectory, factoring in drafts.\n+        \"\"\"\n+        block_path = course_export_dir\n+        if draft:\n+            block_path = os.path.join(block_path, 'drafts')\n+        return os.path.join(block_path, block_type)\n+\n+    def _get_block_filename(self, block_id):\n+        \"\"\"\n+        Return the course export filename for a block.\n+        \"\"\"\n+        return '{}.xml'.format(block_id)\n+\n+    def _get_block_contents(self, block_subdir_path, block_id):\n+        \"\"\"\n+        Determine the filename containing the block info.\n+        Return the file contents.\n+        \"\"\"\n+        block_file = self._get_block_filename(block_id)\n+        block_file_path = os.path.join(block_subdir_path, block_file)\n+        self.assertTrue(\n+            os.path.isfile(block_file_path),\n+            msg='{} is not an existing file.'.format(block_file_path)\n+        )\n+        with open(block_file_path, \"r\") as file_handle:\n+            return file_handle.read()\n+\n+    def _assert_parsed_xml(self, block_contents, checklist):\n+        \"\"\"\n+        Using a dictionary with the following format:\n+        {   'tag' : block_type,\n+            'attrs' : attrs,\n+            'children' : {\n+                'tag' : child_type,\n+                'attrs' : {'url_name' : child_id_regex},\n+            }\n+        }\n+        , verify the XML of a particular block in a course export.\n+        In the above dictionary, 'attrs' is a dict with {attribute:regex} pairs.\n+        \"\"\"\n+        def _assert_xml_level(xml_root, tag, attrs, children):\n+            \"\"\"\n+            Verify one level of the block XML. Called recursively to check other levels (children).\n+            \"\"\"\n+            self.assertEqual(xml_root.tag, tag)\n+            if attrs:\n+                for attr_name, attr_re in attrs.iteritems():\n+                    if attr_re:\n+                        self.assertIn(attr_name, xml_root.attrib)\n+                        attr_re_comp = re.compile(attr_re)\n+                        self.assertIsNotNone(\n+                            attr_re_comp.search(xml_root.attrib[attr_name]),\n+                            msg='Attr {} of tag {} doesn\\'t match regex of:\\n{}'.format(\n+                                attr_name, tag, attr_re\n+                            )\n+                        )\n+            else:\n+                # If not checking the attrs, there should be *no* attrs in the XML.\n+                self.assertEqual(xml_root.attrib, {})\n+            if children:\n+                for child in xml_root:\n+                    _assert_xml_level(child, children['tag'], children.get('attrs'), children.get('children'))\n+            else:\n+                # If not checking the children, there should be *no* children in the XML.\n+                self.assertEqual([child for child in xml_root], [])\n+\n+        # Parse the XML string into an ElementTree.\n+        block_tree = ET.fromstring(block_contents)\n+        # Now go through and verify the individual tags/attributes/children.\n+        _assert_xml_level(block_tree, checklist['tag'], checklist.get('attrs'), checklist.get('children'))\n+\n+    def assertOLXContent(self, block_type, block_id, **kwargs):\n+        \"\"\"\n+        Assert that a particular block exists in a course export in the proper draft/published location\n+        and that the format is correct.\n+        \"\"\"\n+        course_export_dir = self._get_course_export_dir()\n+        is_draft = kwargs.pop('draft', False)\n+        xml_to_check = kwargs.pop('xml', None)\n+        xml_re_to_check = kwargs.pop('xml_re', None)\n+        xml_parse = kwargs.pop('xml_parse', None)\n+\n+        block_path = self._get_block_type_path(course_export_dir, block_type, is_draft)\n+        block_contents = self._get_block_contents(block_path, block_id)\n+        if xml_to_check:\n+            self.assertIn(xml_to_check, block_contents)\n+        if xml_re_to_check:\n+            xml_re = re.compile(xml_re_to_check)\n+            self.assertIsNotNone(\n+                xml_re.search(block_contents),\n+                msg='Block ({}, {}) contents of:\\n{}\\n don\\'t match regex of:\\n{}'.format(\n+                    block_type, block_id, block_contents, xml_re_to_check\n+                )\n+            )\n+        if xml_parse:\n+            self._assert_parsed_xml(block_contents, xml_parse)\n+\n+    def assertOLXMissing(self, block_type, block_id, **kwargs):\n+        \"\"\"\n+        Assert that a particular block does not exist in a particular draft/published location.\n+        \"\"\"\n+        course_export_dir = self._get_course_export_dir()\n+        is_draft = kwargs.pop('draft', False)\n+        block_path = self._get_block_type_path(course_export_dir, block_type, is_draft)\n+        block_file_path = os.path.join(block_path, self._get_block_filename(block_id))\n+        self.assertFalse(\n+            os.path.exists(block_file_path),\n+            msg='{} exists but should not!'.format(block_file_path)\n+        )\n+\n+    def _make_deprecated_block_key(self, course_key, block_type, block_id):\n+        \"\"\"\n+        Return a block key in the deprecated format.\n+        \"\"\"\n+        return r'i4x://{ORG}/{COURSE}/{BLOCK_TYPE}/{BLOCK_ID}'.format(\n+            ORG=course_key.org,\n+            COURSE=course_key.course,\n+            BLOCK_TYPE=block_type,\n+            BLOCK_ID=block_id,\n+        )\n+\n+    def _make_block_key(self, course_key, block_type, block_id):\n+        \"\"\"\n+        Return a block key in the latest format.\n+        \"\"\"\n+        return r'block-v1:{ORG}\\+{COURSE}\\+{RUN}\\+type@{BLOCK_TYPE}\\+block@{BLOCK_ID}'.format(\n+            ORG=course_key.org,\n+            COURSE=course_key.course,\n+            RUN=course_key.run,\n+            BLOCK_TYPE=block_type,\n+            BLOCK_ID=block_id,\n+        )\n+\n+    def _make_xml_parse_regex(self, block_type, course_key, draft, **kwargs):\n+        \"\"\"\n+        Construct a dictionary containing regular expressions that will\n+        be used to validate block XML.\n+        \"\"\"\n+        parent_url_regex = None\n+        child_index_regex = None\n+        if draft:\n+            # Draft items are expected to have certain XML attributes.\n+            parent_type = kwargs.pop('parent_type', None)\n+            parent_id = kwargs.pop('parent_id', None)\n+            index_in_children_list = kwargs.pop('index_in_children_list', None)\n+            self.assertIsNotNone(\n+                parent_type,\n+                msg=\"Parent block type must be passed for draft {} item!\".format(block_type)\n+            )\n+            self.assertIsNotNone(\n+                parent_id,\n+                msg=\"Parent block id must be passed for draft {} item!\".format(block_type)\n+            )\n+            self.assertIsNotNone(\n+                index_in_children_list,\n+                msg=\"Index within {} must be passed for draft {} item!\".format(parent_type, block_type)\n+            )\n+            parent_url_regex = '({DEPRECATED_PARENT_KEY}|{PARENT_KEY})'.format(\n+                DEPRECATED_PARENT_KEY=self._make_deprecated_block_key(course_key, parent_type, parent_id),\n+                PARENT_KEY=self._make_block_key(course_key, parent_type, parent_id),\n+            )\n+            child_index_regex = '{}'.format(index_in_children_list)\n+\n+        # Form the checked attributes based on the block type.\n+        attrs = {}\n+        if block_type == 'html':\n+            filename = kwargs.pop('filename', None)\n+            attrs.update({'filename': filename})\n+        else:\n+            attrs.update({\n+                'parent_url': parent_url_regex,\n+                'index_in_children_list': child_index_regex\n+            })\n+\n+        # If children exist, construct regular expressions to check them.\n+        child_id_regex = None\n+        child_type = None\n+        child_types_ids = kwargs.pop('child_ids', None)\n+        if child_types_ids:\n+            # Grab the type of the first child as the type of all the children.\n+            child_type = child_types_ids[0][0]\n+            # Construct regex out of all the child_ids that are included.\n+            child_id_regex = '|'.join([child[1] for child in child_types_ids])\n+\n+        return {\n+            'tag': block_type,\n+            'attrs': attrs,\n+            'children': {\n+                'tag': child_type,\n+                'attrs': {'url_name': child_id_regex},\n+            }\n+        }\n+\n+    def _assertOLXBase(self, block_list, draft, published):  # pylint: disable=invalid-name\n+        \"\"\"\n+        Check that all blocks in the list are draft blocks in the OLX format when the course is exported.\n+        \"\"\"\n+        for block_data in block_list:\n+            block_params = self.course_db.get(block_data)\n+            self.assertIsNotNone(block_params)\n+            (block_type, block_id) = block_data\n+            if draft:\n+                xml_parse_regex = self._make_xml_parse_regex(block_type, self.course_key, draft=True, **block_params)\n+                self.assertOLXContent(block_type, block_id, draft=True, xml_parse=xml_parse_regex)\n+            else:\n+                self.assertOLXMissing(block_type, block_id, draft=True)\n+            if published:\n+                xml_parse_regex = self._make_xml_parse_regex(block_type, self.course_key, draft=False, **block_params)\n+                self.assertOLXContent(block_type, block_id, draft=False, xml_parse=xml_parse_regex)\n+            else:\n+                self.assertOLXMissing(block_type, block_id, draft=False)","path":"common/lib/xmodule/xmodule/modulestore/tests/test_publish.py","position":384,"original_position":407,"commit_id":"63f7b939c87e67ebabb581cc840dea4c9ba1bd29","original_commit_id":"245e8d38d8fee0df55fae6694e7a14e78d2f097c","user":{"login":"doctoryes","id":7285237,"avatar_url":"https://avatars.githubusercontent.com/u/7285237?v=3","gravatar_id":"","url":"https://api.github.com/users/doctoryes","html_url":"https://github.com/doctoryes","followers_url":"https://api.github.com/users/doctoryes/followers","following_url":"https://api.github.com/users/doctoryes/following{/other_user}","gists_url":"https://api.github.com/users/doctoryes/gists{/gist_id}","starred_url":"https://api.github.com/users/doctoryes/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/doctoryes/subscriptions","organizations_url":"https://api.github.com/users/doctoryes/orgs","repos_url":"https://api.github.com/users/doctoryes/repos","events_url":"https://api.github.com/users/doctoryes/events{/privacy}","received_events_url":"https://api.github.com/users/doctoryes/received_events","type":"User","site_admin":false},"body":"We could - but will it add value in this case? Multiple failures will likely map to the same fix - for example, if all child units show up as draft when they shouldn't, the same fix will clear them all up.","created_at":"2015-05-21T18:26:49Z","updated_at":"2015-05-21T18:26:49Z","html_url":"https://github.com/edx/edx-platform/pull/7948#discussion_r30831878","pull_request_url":"https://api.github.com/repos/edx/edx-platform/pulls/7948","_links":{"self":{"href":"https://api.github.com/repos/edx/edx-platform/pulls/comments/30831878"},"html":{"href":"https://github.com/edx/edx-platform/pull/7948#discussion_r30831878"},"pull_request":{"href":"https://api.github.com/repos/edx/edx-platform/pulls/7948"}}},"pull_request":{"url":"https://api.github.com/repos/edx/edx-platform/pulls/7948","id":34825472,"html_url":"https://github.com/edx/edx-platform/pull/7948","diff_url":"https://github.com/edx/edx-platform/pull/7948.diff","patch_url":"https://github.com/edx/edx-platform/pull/7948.patch","issue_url":"https://api.github.com/repos/edx/edx-platform/issues/7948","number":7948,"state":"open","locked":false,"title":"WIP: Add tests for publishing operations and OLX export.","user":{"login":"doctoryes","id":7285237,"avatar_url":"https://avatars.githubusercontent.com/u/7285237?v=3","gravatar_id":"","url":"https://api.github.com/users/doctoryes","html_url":"https://github.com/doctoryes","followers_url":"https://api.github.com/users/doctoryes/followers","following_url":"https://api.github.com/users/doctoryes/following{/other_user}","gists_url":"https://api.github.com/users/doctoryes/gists{/gist_id}","starred_url":"https://api.github.com/users/doctoryes/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/doctoryes/subscriptions","organizations_url":"https://api.github.com/users/doctoryes/orgs","repos_url":"https://api.github.com/users/doctoryes/repos","events_url":"https://api.github.com/users/doctoryes/events{/privacy}","received_events_url":"https://api.github.com/users/doctoryes/received_events","type":"User","site_admin":false},"body":"During the fix for https://openedx.atlassian.net/browse/PLAT-452 , we discovered that the platform is missing tests which perform specific elemental publishing operations and then confirm those changes in the resulting course export XML. This PR adds those tests.\r\n\r\nThis PR is for this story: https://openedx.atlassian.net/browse/PLAT-618","created_at":"2015-05-06T14:59:59Z","updated_at":"2015-05-21T18:26:49Z","closed_at":null,"merged_at":null,"merge_commit_sha":"66966ea767621de91c47eed9952e4380a9ea2621","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/edx/edx-platform/pulls/7948/commits","review_comments_url":"https://api.github.com/repos/edx/edx-platform/pulls/7948/comments","review_comment_url":"https://api.github.com/repos/edx/edx-platform/pulls/comments{/number}","comments_url":"https://api.github.com/repos/edx/edx-platform/issues/7948/comments","statuses_url":"https://api.github.com/repos/edx/edx-platform/statuses/63f7b939c87e67ebabb581cc840dea4c9ba1bd29","head":{"label":"edx:jeskew/PLAT_618_elemental_publish_tests","ref":"jeskew/PLAT_618_elemental_publish_tests","sha":"63f7b939c87e67ebabb581cc840dea4c9ba1bd29","user":{"login":"edx","id":3179841,"avatar_url":"https://avatars.githubusercontent.com/u/3179841?v=3","gravatar_id":"","url":"https://api.github.com/users/edx","html_url":"https://github.com/edx","followers_url":"https://api.github.com/users/edx/followers","following_url":"https://api.github.com/users/edx/following{/other_user}","gists_url":"https://api.github.com/users/edx/gists{/gist_id}","starred_url":"https://api.github.com/users/edx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/edx/subscriptions","organizations_url":"https://api.github.com/users/edx/orgs","repos_url":"https://api.github.com/users/edx/repos","events_url":"https://api.github.com/users/edx/events{/privacy}","received_events_url":"https://api.github.com/users/edx/received_events","type":"Organization","site_admin":false},"repo":{"id":10391073,"name":"edx-platform","full_name":"edx/edx-platform","owner":{"login":"edx","id":3179841,"avatar_url":"https://avatars.githubusercontent.com/u/3179841?v=3","gravatar_id":"","url":"https://api.github.com/users/edx","html_url":"https://github.com/edx","followers_url":"https://api.github.com/users/edx/followers","following_url":"https://api.github.com/users/edx/following{/other_user}","gists_url":"https://api.github.com/users/edx/gists{/gist_id}","starred_url":"https://api.github.com/users/edx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/edx/subscriptions","organizations_url":"https://api.github.com/users/edx/orgs","repos_url":"https://api.github.com/users/edx/repos","events_url":"https://api.github.com/users/edx/events{/privacy}","received_events_url":"https://api.github.com/users/edx/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/edx/edx-platform","description":"Open edX, the edX platform that powers http://edx.org","fork":false,"url":"https://api.github.com/repos/edx/edx-platform","forks_url":"https://api.github.com/repos/edx/edx-platform/forks","keys_url":"https://api.github.com/repos/edx/edx-platform/keys{/key_id}","collaborators_url":"https://api.github.com/repos/edx/edx-platform/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/edx/edx-platform/teams","hooks_url":"https://api.github.com/repos/edx/edx-platform/hooks","issue_events_url":"https://api.github.com/repos/edx/edx-platform/issues/events{/number}","events_url":"https://api.github.com/repos/edx/edx-platform/events","assignees_url":"https://api.github.com/repos/edx/edx-platform/assignees{/user}","branches_url":"https://api.github.com/repos/edx/edx-platform/branches{/branch}","tags_url":"https://api.github.com/repos/edx/edx-platform/tags","blobs_url":"https://api.github.com/repos/edx/edx-platform/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/edx/edx-platform/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/edx/edx-platform/git/refs{/sha}","trees_url":"https://api.github.com/repos/edx/edx-platform/git/trees{/sha}","statuses_url":"https://api.github.com/repos/edx/edx-platform/statuses/{sha}","languages_url":"https://api.github.com/repos/edx/edx-platform/languages","stargazers_url":"https://api.github.com/repos/edx/edx-platform/stargazers","contributors_url":"https://api.github.com/repos/edx/edx-platform/contributors","subscribers_url":"https://api.github.com/repos/edx/edx-platform/subscribers","subscription_url":"https://api.github.com/repos/edx/edx-platform/subscription","commits_url":"https://api.github.com/repos/edx/edx-platform/commits{/sha}","git_commits_url":"https://api.github.com/repos/edx/edx-platform/git/commits{/sha}","comments_url":"https://api.github.com/repos/edx/edx-platform/comments{/number}","issue_comment_url":"https://api.github.com/repos/edx/edx-platform/issues/comments{/number}","contents_url":"https://api.github.com/repos/edx/edx-platform/contents/{+path}","compare_url":"https://api.github.com/repos/edx/edx-platform/compare/{base}...{head}","merges_url":"https://api.github.com/repos/edx/edx-platform/merges","archive_url":"https://api.github.com/repos/edx/edx-platform/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/edx/edx-platform/downloads","issues_url":"https://api.github.com/repos/edx/edx-platform/issues{/number}","pulls_url":"https://api.github.com/repos/edx/edx-platform/pulls{/number}","milestones_url":"https://api.github.com/repos/edx/edx-platform/milestones{/number}","notifications_url":"https://api.github.com/repos/edx/edx-platform/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/edx/edx-platform/labels{/name}","releases_url":"https://api.github.com/repos/edx/edx-platform/releases{/id}","created_at":"2013-05-30T20:20:38Z","updated_at":"2015-05-21T16:55:36Z","pushed_at":"2015-05-21T18:25:29Z","git_url":"git://github.com/edx/edx-platform.git","ssh_url":"git@github.com:edx/edx-platform.git","clone_url":"https://github.com/edx/edx-platform.git","svn_url":"https://github.com/edx/edx-platform","homepage":"http://openedx.org/","size":1944196,"stargazers_count":1934,"watchers_count":1934,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":1113,"mirror_url":null,"open_issues_count":166,"forks":1113,"open_issues":166,"watchers":1934,"default_branch":"master"}},"base":{"label":"edx:master","ref":"master","sha":"225f442623578c7181bb7fee3c8535e68f1c57f2","user":{"login":"edx","id":3179841,"avatar_url":"https://avatars.githubusercontent.com/u/3179841?v=3","gravatar_id":"","url":"https://api.github.com/users/edx","html_url":"https://github.com/edx","followers_url":"https://api.github.com/users/edx/followers","following_url":"https://api.github.com/users/edx/following{/other_user}","gists_url":"https://api.github.com/users/edx/gists{/gist_id}","starred_url":"https://api.github.com/users/edx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/edx/subscriptions","organizations_url":"https://api.github.com/users/edx/orgs","repos_url":"https://api.github.com/users/edx/repos","events_url":"https://api.github.com/users/edx/events{/privacy}","received_events_url":"https://api.github.com/users/edx/received_events","type":"Organization","site_admin":false},"repo":{"id":10391073,"name":"edx-platform","full_name":"edx/edx-platform","owner":{"login":"edx","id":3179841,"avatar_url":"https://avatars.githubusercontent.com/u/3179841?v=3","gravatar_id":"","url":"https://api.github.com/users/edx","html_url":"https://github.com/edx","followers_url":"https://api.github.com/users/edx/followers","following_url":"https://api.github.com/users/edx/following{/other_user}","gists_url":"https://api.github.com/users/edx/gists{/gist_id}","starred_url":"https://api.github.com/users/edx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/edx/subscriptions","organizations_url":"https://api.github.com/users/edx/orgs","repos_url":"https://api.github.com/users/edx/repos","events_url":"https://api.github.com/users/edx/events{/privacy}","received_events_url":"https://api.github.com/users/edx/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/edx/edx-platform","description":"Open edX, the edX platform that powers http://edx.org","fork":false,"url":"https://api.github.com/repos/edx/edx-platform","forks_url":"https://api.github.com/repos/edx/edx-platform/forks","keys_url":"https://api.github.com/repos/edx/edx-platform/keys{/key_id}","collaborators_url":"https://api.github.com/repos/edx/edx-platform/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/edx/edx-platform/teams","hooks_url":"https://api.github.com/repos/edx/edx-platform/hooks","issue_events_url":"https://api.github.com/repos/edx/edx-platform/issues/events{/number}","events_url":"https://api.github.com/repos/edx/edx-platform/events","assignees_url":"https://api.github.com/repos/edx/edx-platform/assignees{/user}","branches_url":"https://api.github.com/repos/edx/edx-platform/branches{/branch}","tags_url":"https://api.github.com/repos/edx/edx-platform/tags","blobs_url":"https://api.github.com/repos/edx/edx-platform/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/edx/edx-platform/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/edx/edx-platform/git/refs{/sha}","trees_url":"https://api.github.com/repos/edx/edx-platform/git/trees{/sha}","statuses_url":"https://api.github.com/repos/edx/edx-platform/statuses/{sha}","languages_url":"https://api.github.com/repos/edx/edx-platform/languages","stargazers_url":"https://api.github.com/repos/edx/edx-platform/stargazers","contributors_url":"https://api.github.com/repos/edx/edx-platform/contributors","subscribers_url":"https://api.github.com/repos/edx/edx-platform/subscribers","subscription_url":"https://api.github.com/repos/edx/edx-platform/subscription","commits_url":"https://api.github.com/repos/edx/edx-platform/commits{/sha}","git_commits_url":"https://api.github.com/repos/edx/edx-platform/git/commits{/sha}","comments_url":"https://api.github.com/repos/edx/edx-platform/comments{/number}","issue_comment_url":"https://api.github.com/repos/edx/edx-platform/issues/comments{/number}","contents_url":"https://api.github.com/repos/edx/edx-platform/contents/{+path}","compare_url":"https://api.github.com/repos/edx/edx-platform/compare/{base}...{head}","merges_url":"https://api.github.com/repos/edx/edx-platform/merges","archive_url":"https://api.github.com/repos/edx/edx-platform/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/edx/edx-platform/downloads","issues_url":"https://api.github.com/repos/edx/edx-platform/issues{/number}","pulls_url":"https://api.github.com/repos/edx/edx-platform/pulls{/number}","milestones_url":"https://api.github.com/repos/edx/edx-platform/milestones{/number}","notifications_url":"https://api.github.com/repos/edx/edx-platform/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/edx/edx-platform/labels{/name}","releases_url":"https://api.github.com/repos/edx/edx-platform/releases{/id}","created_at":"2013-05-30T20:20:38Z","updated_at":"2015-05-21T16:55:36Z","pushed_at":"2015-05-21T18:25:29Z","git_url":"git://github.com/edx/edx-platform.git","ssh_url":"git@github.com:edx/edx-platform.git","clone_url":"https://github.com/edx/edx-platform.git","svn_url":"https://github.com/edx/edx-platform","homepage":"http://openedx.org/","size":1944196,"stargazers_count":1934,"watchers_count":1934,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":1113,"mirror_url":null,"open_issues_count":166,"forks":1113,"open_issues":166,"watchers":1934,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/edx/edx-platform/pulls/7948"},"html":{"href":"https://github.com/edx/edx-platform/pull/7948"},"issue":{"href":"https://api.github.com/repos/edx/edx-platform/issues/7948"},"comments":{"href":"https://api.github.com/repos/edx/edx-platform/issues/7948/comments"},"review_comments":{"href":"https://api.github.com/repos/edx/edx-platform/pulls/7948/comments"},"review_comment":{"href":"https://api.github.com/repos/edx/edx-platform/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/edx/edx-platform/pulls/7948/commits"},"statuses":{"href":"https://api.github.com/repos/edx/edx-platform/statuses/63f7b939c87e67ebabb581cc840dea4c9ba1bd29"}}}}