{"action":"created","comment":{"url":"https://api.github.com/repos/hypothesis/lms/pulls/comments/214143667","pull_request_review_id":151134917,"id":214143667,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIxNDE0MzY2Nw==","diff_hunk":"@@ -122,3 +117,122 @@ def wrapper(request, jwt):  # pylint: disable=too-many-branches\n         return wrapped(request, jwt)\n \n     return wrapper\n+\n+\n+def create_course_group(wrapped):\n+    \"\"\"\n+    Create a Hypothesis group for the LTI course, if one doesn't exist already.\n+\n+    Call the h API to create a group for the LTI course, if one doesn't exist\n+    already.\n+\n+    Groups can only be created if the LTI user is allowed to create Hypothesis\n+    groups (for example instructors are allowed to create groups). If the group\n+    for the course hasn't been created yet, and if the user isn't allowed to\n+    create groups (e.g. if they're just a student) then show an error page\n+    instead of continuing with the LTI launch.\n+\n+    Use this function as a view decorator rather than calling it directly.\n+    The decorated view must take ``request`` and ``jwt`` arguments::\n+\n+      @view_config(...)\n+      @create_course_group\n+      def my_view(request, jwt):\n+          ...\n+\n+    \"\"\"\n+    # FIXME: This function doesn't do anything with the ``jwt`` argument,\n+    # other than pass it through to the wrapped view (or to the next decorator\n+    # on the wrapped view). The ``jwt`` argument has to be here because:\n+    #\n+    # - This decorator is called by the ``@lti_launch`` decorator (because\n+    #   ``@lti_launch`` is always placed above this decorator on decorated views)\n+    #   and ``@lti_launch`` passes a ``jwt_token`` argument when it calls this\n+    #   decorator.\n+    #\n+    # - The views that this decorator decorates expect a ``jwt`` argument so this\n+    #   decorator has to pass it to them (or rather it has to pass it down to the\n+    #   next decorator down in the stack, and it eventually gets passed to the\n+    #   view).\n+    #\n+    # This should all be refactored so that views and view decorators aren't\n+    # tightly coupled and arguments don't need to be passed through multiple\n+    # decorators to the view.\n+    def wrapper(request, jwt):\n+        _maybe_create_group(request)\n+        return wrapped(request, jwt)\n+    return wrapper\n+\n+\n+def _maybe_create_group(request):\n+    \"\"\"Create a Hypothesis group for the LTI course, if one doesn't exist.\"\"\"\n+    # Only create groups for application instances that we've enabled the\n+    # auto provisioning features for.\n+    if not _auto_provisioning_feature_enabled(request):\n+        return\n+\n+    get_param = functools.partial(_get_param, request)\n+\n+    # Show the user an error page if the group hasn't been created yet and\n+    # the user isn't allowed to create groups.\n+    if not any(role in get_param(\"roles\").lower() for role in (\"administrator\", \"instructor\", \"teachingassisstant\")):\n+        raise HTTPBadRequest(\"Instructor must launch assignment first\")\n+\n+    tool_consumer_instance_guid = get_param(\"tool_consumer_instance_guid\")\n+    context_id = get_param(\"context_id\")\n+\n+    group = models.CourseGroup.get(request.db, tool_consumer_instance_guid, context_id)\n+\n+    if group:\n+        # The group has already been created in h, nothing more to do here.\n+        return\n+\n+    create_group_api_url = request.registry.settings[\"h_api_url\"] + \"/groups\"\n+    client_id = request.registry.settings[\"h_client_id\"]\n+    client_secret = request.registry.settings[\"h_client_secret\"]\n+\n+    # Generate the name for the new group.\n+    try:\n+        name = util.generate_group_name(request.params)\n+    except MissingContextTitleError:\n+        raise HTTPBadRequest('Required parameter \"context_title\" missing from LTI params')\n+\n+    authority = request.registry.settings[\"h_authority\"]\n+    username = util.generate_username(request.params)\n+\n+    # Create the group in h.\n+    try:\n+        response = requests.post(\n+            create_group_api_url,\n+            auth=(client_id, client_secret),\n+            data=json.dumps({\"name\": name}),\n+            headers={\n+                \"X-Forwarded-User\": \"acct:{username}@{authority}\".format(username=username, authority=authority),\n+            },\n+            timeout=1,\n+        )\n+        response.raise_for_status()\n+    except requests.RequestException:\n+        raise HTTPGatewayTimeout(explanation=\"Connecting to Hypothesis failed\")\n+\n+    # Save a record of the group's pubid in the DB so that we can find it\n+    # again later.\n+    request.db.add(models.CourseGroup(\n+        tool_consumer_instance_guid=tool_consumer_instance_guid,\n+        context_id=context_id,\n+        pubid=response.json()[\"id\"],\n+    ))\n+\n+\n+def _get_param(request, param_name):","path":"lms/views/decorators/h_api.py","position":148,"original_position":148,"commit_id":"b29b56fb8eba18ddf4db1f84be82979207e02408","original_commit_id":"b29b56fb8eba18ddf4db1f84be82979207e02408","user":{"login":"lyzadanger","id":439947,"node_id":"MDQ6VXNlcjQzOTk0Nw==","avatar_url":"https://avatars2.githubusercontent.com/u/439947?v=4","gravatar_id":"","url":"https://api.github.com/users/lyzadanger","html_url":"https://github.com/lyzadanger","followers_url":"https://api.github.com/users/lyzadanger/followers","following_url":"https://api.github.com/users/lyzadanger/following{/other_user}","gists_url":"https://api.github.com/users/lyzadanger/gists{/gist_id}","starred_url":"https://api.github.com/users/lyzadanger/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lyzadanger/subscriptions","organizations_url":"https://api.github.com/users/lyzadanger/orgs","repos_url":"https://api.github.com/users/lyzadanger/repos","events_url":"https://api.github.com/users/lyzadanger/events{/privacy}","received_events_url":"https://api.github.com/users/lyzadanger/received_events","type":"User","site_admin":false},"body":"Something for later: the naming of this function raises some confusion between \"get\" as in \"getter\" and \"get\" as in \"GET\" (versus POST).","created_at":"2018-08-30T18:54:25Z","updated_at":"2018-08-30T18:56:22Z","html_url":"https://github.com/hypothesis/lms/pull/193#discussion_r214143667","pull_request_url":"https://api.github.com/repos/hypothesis/lms/pulls/193","author_association":"CONTRIBUTOR","_links":{"self":{"href":"https://api.github.com/repos/hypothesis/lms/pulls/comments/214143667"},"html":{"href":"https://github.com/hypothesis/lms/pull/193#discussion_r214143667"},"pull_request":{"href":"https://api.github.com/repos/hypothesis/lms/pulls/193"}}},"pull_request":{"url":"https://api.github.com/repos/hypothesis/lms/pulls/193","id":212051072,"node_id":"MDExOlB1bGxSZXF1ZXN0MjEyMDUxMDcy","html_url":"https://github.com/hypothesis/lms/pull/193","diff_url":"https://github.com/hypothesis/lms/pull/193.diff","patch_url":"https://github.com/hypothesis/lms/pull/193.patch","issue_url":"https://api.github.com/repos/hypothesis/lms/issues/193","number":193,"state":"open","locked":false,"title":"[Create groups 5/n]: Create groups on LTI launches","user":{"login":"seanh","id":22498,"node_id":"MDQ6VXNlcjIyNDk4","avatar_url":"https://avatars3.githubusercontent.com/u/22498?v=4","gravatar_id":"","url":"https://api.github.com/users/seanh","html_url":"https://github.com/seanh","followers_url":"https://api.github.com/users/seanh/followers","following_url":"https://api.github.com/users/seanh/following{/other_user}","gists_url":"https://api.github.com/users/seanh/gists{/gist_id}","starred_url":"https://api.github.com/users/seanh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/seanh/subscriptions","organizations_url":"https://api.github.com/users/seanh/orgs","repos_url":"https://api.github.com/users/seanh/repos","events_url":"https://api.github.com/users/seanh/events{/privacy}","received_events_url":"https://api.github.com/users/seanh/received_events","type":"User","site_admin":false},"body":"On LTI launch (either assignment launch or content item selection launch), if the LTI user is allowed to create groups (e.g. if they're a teacher), then create a group for the LTI course in h if one doesn't exist already.\r\n\r\n## Depends on\r\n\r\n- [ ] https://github.com/hypothesis/lms/pull/191\r\n- [ ] https://github.com/hypothesis/lms/pull/192\r\n\r\n## Todo (before merging this PR)\r\n\r\n- [ ] Create group on content item selection, not just one assignment launch\r\n- [ ] Review and tidy up\r\n- [ ] Test manually\r\n\r\n## Known limitations (not intending to address in this PR)\r\n\r\n- Doesn't update group names if the course name is changed (h has no API for doing this yet)\r\n- Completely breaks if the groups table in the LMS app's DB gets out of sync with the one in h's DB. For example if a group is deleted from h's DB but still exists in the LMS app's DB, or vice-versa. One or the other of the DBs would require manual fixing up to correct such a situation if it ever happened. (Fortunately LMS users don't have access to h's group pages so they _can't_ delete groups from h's DB, and groups are never deleted from the LMS app's DB either.)","created_at":"2018-08-30T11:48:37Z","updated_at":"2018-08-30T18:56:22Z","closed_at":null,"merged_at":null,"merge_commit_sha":"e6733e5890861782688df42d7afe76944dd1d492","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":776543438,"node_id":"MDU6TGFiZWw3NzY1NDM0Mzg=","url":"https://api.github.com/repos/hypothesis/lms/labels/WIP","name":"WIP","color":"fbca04","default":false}],"milestone":null,"commits_url":"https://api.github.com/repos/hypothesis/lms/pulls/193/commits","review_comments_url":"https://api.github.com/repos/hypothesis/lms/pulls/193/comments","review_comment_url":"https://api.github.com/repos/hypothesis/lms/pulls/comments{/number}","comments_url":"https://api.github.com/repos/hypothesis/lms/issues/193/comments","statuses_url":"https://api.github.com/repos/hypothesis/lms/statuses/b29b56fb8eba18ddf4db1f84be82979207e02408","head":{"label":"hypothesis:create-groups-on-LTI-launches","ref":"create-groups-on-LTI-launches","sha":"b29b56fb8eba18ddf4db1f84be82979207e02408","user":{"login":"hypothesis","id":1243215,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEyNDMyMTU=","avatar_url":"https://avatars1.githubusercontent.com/u/1243215?v=4","gravatar_id":"","url":"https://api.github.com/users/hypothesis","html_url":"https://github.com/hypothesis","followers_url":"https://api.github.com/users/hypothesis/followers","following_url":"https://api.github.com/users/hypothesis/following{/other_user}","gists_url":"https://api.github.com/users/hypothesis/gists{/gist_id}","starred_url":"https://api.github.com/users/hypothesis/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hypothesis/subscriptions","organizations_url":"https://api.github.com/users/hypothesis/orgs","repos_url":"https://api.github.com/users/hypothesis/repos","events_url":"https://api.github.com/users/hypothesis/events{/privacy}","received_events_url":"https://api.github.com/users/hypothesis/received_events","type":"Organization","site_admin":false},"repo":{"id":108909388,"node_id":"MDEwOlJlcG9zaXRvcnkxMDg5MDkzODg=","name":"lms","full_name":"hypothesis/lms","owner":{"login":"hypothesis","id":1243215,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEyNDMyMTU=","avatar_url":"https://avatars1.githubusercontent.com/u/1243215?v=4","gravatar_id":"","url":"https://api.github.com/users/hypothesis","html_url":"https://github.com/hypothesis","followers_url":"https://api.github.com/users/hypothesis/followers","following_url":"https://api.github.com/users/hypothesis/following{/other_user}","gists_url":"https://api.github.com/users/hypothesis/gists{/gist_id}","starred_url":"https://api.github.com/users/hypothesis/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hypothesis/subscriptions","organizations_url":"https://api.github.com/users/hypothesis/orgs","repos_url":"https://api.github.com/users/hypothesis/repos","events_url":"https://api.github.com/users/hypothesis/events{/privacy}","received_events_url":"https://api.github.com/users/hypothesis/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/hypothesis/lms","description":"LTI app for integrating with learning management systems","fork":false,"url":"https://api.github.com/repos/hypothesis/lms","forks_url":"https://api.github.com/repos/hypothesis/lms/forks","keys_url":"https://api.github.com/repos/hypothesis/lms/keys{/key_id}","collaborators_url":"https://api.github.com/repos/hypothesis/lms/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/hypothesis/lms/teams","hooks_url":"https://api.github.com/repos/hypothesis/lms/hooks","issue_events_url":"https://api.github.com/repos/hypothesis/lms/issues/events{/number}","events_url":"https://api.github.com/repos/hypothesis/lms/events","assignees_url":"https://api.github.com/repos/hypothesis/lms/assignees{/user}","branches_url":"https://api.github.com/repos/hypothesis/lms/branches{/branch}","tags_url":"https://api.github.com/repos/hypothesis/lms/tags","blobs_url":"https://api.github.com/repos/hypothesis/lms/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/hypothesis/lms/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/hypothesis/lms/git/refs{/sha}","trees_url":"https://api.github.com/repos/hypothesis/lms/git/trees{/sha}","statuses_url":"https://api.github.com/repos/hypothesis/lms/statuses/{sha}","languages_url":"https://api.github.com/repos/hypothesis/lms/languages","stargazers_url":"https://api.github.com/repos/hypothesis/lms/stargazers","contributors_url":"https://api.github.com/repos/hypothesis/lms/contributors","subscribers_url":"https://api.github.com/repos/hypothesis/lms/subscribers","subscription_url":"https://api.github.com/repos/hypothesis/lms/subscription","commits_url":"https://api.github.com/repos/hypothesis/lms/commits{/sha}","git_commits_url":"https://api.github.com/repos/hypothesis/lms/git/commits{/sha}","comments_url":"https://api.github.com/repos/hypothesis/lms/comments{/number}","issue_comment_url":"https://api.github.com/repos/hypothesis/lms/issues/comments{/number}","contents_url":"https://api.github.com/repos/hypothesis/lms/contents/{+path}","compare_url":"https://api.github.com/repos/hypothesis/lms/compare/{base}...{head}","merges_url":"https://api.github.com/repos/hypothesis/lms/merges","archive_url":"https://api.github.com/repos/hypothesis/lms/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/hypothesis/lms/downloads","issues_url":"https://api.github.com/repos/hypothesis/lms/issues{/number}","pulls_url":"https://api.github.com/repos/hypothesis/lms/pulls{/number}","milestones_url":"https://api.github.com/repos/hypothesis/lms/milestones{/number}","notifications_url":"https://api.github.com/repos/hypothesis/lms/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/hypothesis/lms/labels{/name}","releases_url":"https://api.github.com/repos/hypothesis/lms/releases{/id}","deployments_url":"https://api.github.com/repos/hypothesis/lms/deployments","created_at":"2017-10-30T21:15:37Z","updated_at":"2018-08-30T17:47:06Z","pushed_at":"2018-08-30T18:05:41Z","git_url":"git://github.com/hypothesis/lms.git","ssh_url":"git@github.com:hypothesis/lms.git","clone_url":"https://github.com/hypothesis/lms.git","svn_url":"https://github.com/hypothesis/lms","homepage":null,"size":4702,"stargazers_count":2,"watchers_count":2,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"archived":false,"open_issues_count":31,"license":{"key":"bsd-2-clause","name":"BSD 2-Clause \"Simplified\" License","spdx_id":"BSD-2-Clause","url":"https://api.github.com/licenses/bsd-2-clause","node_id":"MDc6TGljZW5zZTQ="},"forks":1,"open_issues":31,"watchers":2,"default_branch":"master"}},"base":{"label":"hypothesis:master","ref":"master","sha":"42a4c6922995a14dd010296d748766a88e617ab9","user":{"login":"hypothesis","id":1243215,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEyNDMyMTU=","avatar_url":"https://avatars1.githubusercontent.com/u/1243215?v=4","gravatar_id":"","url":"https://api.github.com/users/hypothesis","html_url":"https://github.com/hypothesis","followers_url":"https://api.github.com/users/hypothesis/followers","following_url":"https://api.github.com/users/hypothesis/following{/other_user}","gists_url":"https://api.github.com/users/hypothesis/gists{/gist_id}","starred_url":"https://api.github.com/users/hypothesis/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hypothesis/subscriptions","organizations_url":"https://api.github.com/users/hypothesis/orgs","repos_url":"https://api.github.com/users/hypothesis/repos","events_url":"https://api.github.com/users/hypothesis/events{/privacy}","received_events_url":"https://api.github.com/users/hypothesis/received_events","type":"Organization","site_admin":false},"repo":{"id":108909388,"node_id":"MDEwOlJlcG9zaXRvcnkxMDg5MDkzODg=","name":"lms","full_name":"hypothesis/lms","owner":{"login":"hypothesis","id":1243215,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEyNDMyMTU=","avatar_url":"https://avatars1.githubusercontent.com/u/1243215?v=4","gravatar_id":"","url":"https://api.github.com/users/hypothesis","html_url":"https://github.com/hypothesis","followers_url":"https://api.github.com/users/hypothesis/followers","following_url":"https://api.github.com/users/hypothesis/following{/other_user}","gists_url":"https://api.github.com/users/hypothesis/gists{/gist_id}","starred_url":"https://api.github.com/users/hypothesis/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hypothesis/subscriptions","organizations_url":"https://api.github.com/users/hypothesis/orgs","repos_url":"https://api.github.com/users/hypothesis/repos","events_url":"https://api.github.com/users/hypothesis/events{/privacy}","received_events_url":"https://api.github.com/users/hypothesis/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/hypothesis/lms","description":"LTI app for integrating with learning management systems","fork":false,"url":"https://api.github.com/repos/hypothesis/lms","forks_url":"https://api.github.com/repos/hypothesis/lms/forks","keys_url":"https://api.github.com/repos/hypothesis/lms/keys{/key_id}","collaborators_url":"https://api.github.com/repos/hypothesis/lms/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/hypothesis/lms/teams","hooks_url":"https://api.github.com/repos/hypothesis/lms/hooks","issue_events_url":"https://api.github.com/repos/hypothesis/lms/issues/events{/number}","events_url":"https://api.github.com/repos/hypothesis/lms/events","assignees_url":"https://api.github.com/repos/hypothesis/lms/assignees{/user}","branches_url":"https://api.github.com/repos/hypothesis/lms/branches{/branch}","tags_url":"https://api.github.com/repos/hypothesis/lms/tags","blobs_url":"https://api.github.com/repos/hypothesis/lms/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/hypothesis/lms/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/hypothesis/lms/git/refs{/sha}","trees_url":"https://api.github.com/repos/hypothesis/lms/git/trees{/sha}","statuses_url":"https://api.github.com/repos/hypothesis/lms/statuses/{sha}","languages_url":"https://api.github.com/repos/hypothesis/lms/languages","stargazers_url":"https://api.github.com/repos/hypothesis/lms/stargazers","contributors_url":"https://api.github.com/repos/hypothesis/lms/contributors","subscribers_url":"https://api.github.com/repos/hypothesis/lms/subscribers","subscription_url":"https://api.github.com/repos/hypothesis/lms/subscription","commits_url":"https://api.github.com/repos/hypothesis/lms/commits{/sha}","git_commits_url":"https://api.github.com/repos/hypothesis/lms/git/commits{/sha}","comments_url":"https://api.github.com/repos/hypothesis/lms/comments{/number}","issue_comment_url":"https://api.github.com/repos/hypothesis/lms/issues/comments{/number}","contents_url":"https://api.github.com/repos/hypothesis/lms/contents/{+path}","compare_url":"https://api.github.com/repos/hypothesis/lms/compare/{base}...{head}","merges_url":"https://api.github.com/repos/hypothesis/lms/merges","archive_url":"https://api.github.com/repos/hypothesis/lms/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/hypothesis/lms/downloads","issues_url":"https://api.github.com/repos/hypothesis/lms/issues{/number}","pulls_url":"https://api.github.com/repos/hypothesis/lms/pulls{/number}","milestones_url":"https://api.github.com/repos/hypothesis/lms/milestones{/number}","notifications_url":"https://api.github.com/repos/hypothesis/lms/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/hypothesis/lms/labels{/name}","releases_url":"https://api.github.com/repos/hypothesis/lms/releases{/id}","deployments_url":"https://api.github.com/repos/hypothesis/lms/deployments","created_at":"2017-10-30T21:15:37Z","updated_at":"2018-08-30T17:47:06Z","pushed_at":"2018-08-30T18:05:41Z","git_url":"git://github.com/hypothesis/lms.git","ssh_url":"git@github.com:hypothesis/lms.git","clone_url":"https://github.com/hypothesis/lms.git","svn_url":"https://github.com/hypothesis/lms","homepage":null,"size":4702,"stargazers_count":2,"watchers_count":2,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"archived":false,"open_issues_count":31,"license":{"key":"bsd-2-clause","name":"BSD 2-Clause \"Simplified\" License","spdx_id":"BSD-2-Clause","url":"https://api.github.com/licenses/bsd-2-clause","node_id":"MDc6TGljZW5zZTQ="},"forks":1,"open_issues":31,"watchers":2,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/hypothesis/lms/pulls/193"},"html":{"href":"https://github.com/hypothesis/lms/pull/193"},"issue":{"href":"https://api.github.com/repos/hypothesis/lms/issues/193"},"comments":{"href":"https://api.github.com/repos/hypothesis/lms/issues/193/comments"},"review_comments":{"href":"https://api.github.com/repos/hypothesis/lms/pulls/193/comments"},"review_comment":{"href":"https://api.github.com/repos/hypothesis/lms/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/hypothesis/lms/pulls/193/commits"},"statuses":{"href":"https://api.github.com/repos/hypothesis/lms/statuses/b29b56fb8eba18ddf4db1f84be82979207e02408"}},"author_association":"COLLABORATOR"}}
{"actor":{"display_login":"lyzadanger"}}