{"action":"closed","number":177,"pull_request":{"url":"https://api.github.com/repos/swcarpentry/amy/pulls/177","id":28744927,"html_url":"https://github.com/swcarpentry/amy/pull/177","diff_url":"https://github.com/swcarpentry/amy/pull/177.diff","patch_url":"https://github.com/swcarpentry/amy/pull/177.patch","issue_url":"https://api.github.com/repos/swcarpentry/amy/issues/177","number":177,"state":"closed","locked":false,"title":"workshops/util.py: Expect a Unicode stream in upload_person_task_csv","user":{"login":"wking","id":209920,"avatar_url":"https://avatars.githubusercontent.com/u/209920?v=3","gravatar_id":"","url":"https://api.github.com/users/wking","html_url":"https://github.com/wking","followers_url":"https://api.github.com/users/wking/followers","following_url":"https://api.github.com/users/wking/following{/other_user}","gists_url":"https://api.github.com/users/wking/gists{/gist_id}","starred_url":"https://api.github.com/users/wking/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/wking/subscriptions","organizations_url":"https://api.github.com/users/wking/orgs","repos_url":"https://api.github.com/users/wking/repos","events_url":"https://api.github.com/users/wking/events{/privacy}","received_events_url":"https://api.github.com/users/wking/received_events","type":"User","site_admin":false},"body":"Instead of passing in an encoding and doing some issubclass stuff,\njust require a Unicode stream in upload_person_task_csv.  In general,\ndecoding to Unicode as soon as possible and encoding to bytes as late\nas possible makes for cleaner code (who better to know the proper\nencoding than the first/last code that touches the content?).  We see\nthat here, with a simpler upload_person_task_csv implementation and\nonly a two more lines in person_bulk_add to wrap the incoming file in\na StreamReader.\n\nThe accept-charset business is a hack to work around shortcomings in\nmodern browsers.  For example, the data posted from my Firefox 24.8\nlooks like:\n\n    Content-Type: multipart/form-data; boundary=---------------------------196554893016147250581211961313\n    Content-Length: 477\n\n    -----------------------------196554893016147250581211961313\n\n    Content-Disposition: form-data; name=\"csrfmiddlewaretoken\"\n\n    D8gooO2bYBXwsVQndknbnHUkJbs9ZRHp\n\n    -----------------------------196554893016147250581211961313\n\n    Content-Disposition: form-data; name=\"file\"; filename=\"BulkPersonAddTemplate.csv\"\n    Content-Type: text/csv\n\n    personal,middle,family,email,event,role\n    αβ,γδ,Ε,a@b.com,2015-02-05-esu,student\n\n    -----------------------------196554893016147250581211961313--\n\nIt's setting a per-file Content-Type (yay!), but that Content-Type\ndoesn't have charset information (boo!).  It looks like [the Mozilla\nfolks tried to fix this][1], but [backed it out in 2007][2] because it\n[broke][3] some [crummy parsers][4].\n\nI see nothing in RFC 2388 that is file-specific, but [their only\nContent-Type example does show a charset][5].  The only workable,\nspec-compliant solution I can come up with is to explictly set\n[accept-charset][6].  Otherwise, the user agent is supposed to use\nsome [wonky fallbacks][7] (although if `DEFAULT_CHARSET` was UTF-8,\nthose would end up as UTF-8 too).\n\nThe current approach is to respect the charset set by the user-agent\n(which we can access via the [.charset property][8]).  If it doesn't\nset one, we expect it to respect the form's accept-charset and use\n`DEFAULT_CHARSET`.\n\n[1]: https://bugzilla.mozilla.org/show_bug.cgi?id=116346\n[2]: https://bugzilla.mozilla.org/show_bug.cgi?id=116346#c79\n[3]: https://bugzilla.mozilla.org/show_bug.cgi?id=116346#c74\n[4]: https://bugzilla.mozilla.org/show_bug.cgi?id=392982#c45\n[5]: https://tools.ietf.org/html/rfc2388#section-4.5\n[6]: http://www.w3.org/TR/html5/forms.html#attr-form-accept-charset\n[7]: http://www.w3.org/TR/html5/forms.html#multipart/form-data-encoding-algorithm\n[8]: https://docs.djangoproject.com/en/1.7/ref/files/uploads/#django.core.files.uploadedfile.UploadedFile.charset","created_at":"2015-02-05T17:42:11Z","updated_at":"2015-02-05T22:59:07Z","closed_at":"2015-02-05T22:59:07Z","merged_at":"2015-02-05T22:59:07Z","merge_commit_sha":"c1851184b54428d87c893119576f2a01d190c08a","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/swcarpentry/amy/pulls/177/commits","review_comments_url":"https://api.github.com/repos/swcarpentry/amy/pulls/177/comments","review_comment_url":"https://api.github.com/repos/swcarpentry/amy/pulls/comments/{number}","comments_url":"https://api.github.com/repos/swcarpentry/amy/issues/177/comments","statuses_url":"https://api.github.com/repos/swcarpentry/amy/statuses/09a6c421cffde7b161c88ee1590572d988baeb2a","head":{"label":"wking:drop-py27-changes","ref":"drop-py27-changes","sha":"09a6c421cffde7b161c88ee1590572d988baeb2a","user":{"login":"wking","id":209920,"avatar_url":"https://avatars.githubusercontent.com/u/209920?v=3","gravatar_id":"","url":"https://api.github.com/users/wking","html_url":"https://github.com/wking","followers_url":"https://api.github.com/users/wking/followers","following_url":"https://api.github.com/users/wking/following{/other_user}","gists_url":"https://api.github.com/users/wking/gists{/gist_id}","starred_url":"https://api.github.com/users/wking/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/wking/subscriptions","organizations_url":"https://api.github.com/users/wking/orgs","repos_url":"https://api.github.com/users/wking/repos","events_url":"https://api.github.com/users/wking/events{/privacy}","received_events_url":"https://api.github.com/users/wking/received_events","type":"User","site_admin":false},"repo":{"id":28017695,"name":"swc-amy","full_name":"wking/swc-amy","owner":{"login":"wking","id":209920,"avatar_url":"https://avatars.githubusercontent.com/u/209920?v=3","gravatar_id":"","url":"https://api.github.com/users/wking","html_url":"https://github.com/wking","followers_url":"https://api.github.com/users/wking/followers","following_url":"https://api.github.com/users/wking/following{/other_user}","gists_url":"https://api.github.com/users/wking/gists{/gist_id}","starred_url":"https://api.github.com/users/wking/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/wking/subscriptions","organizations_url":"https://api.github.com/users/wking/orgs","repos_url":"https://api.github.com/users/wking/repos","events_url":"https://api.github.com/users/wking/events{/privacy}","received_events_url":"https://api.github.com/users/wking/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/wking/swc-amy","description":"A web-based workshop administration application built using Django.","fork":true,"url":"https://api.github.com/repos/wking/swc-amy","forks_url":"https://api.github.com/repos/wking/swc-amy/forks","keys_url":"https://api.github.com/repos/wking/swc-amy/keys{/key_id}","collaborators_url":"https://api.github.com/repos/wking/swc-amy/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/wking/swc-amy/teams","hooks_url":"https://api.github.com/repos/wking/swc-amy/hooks","issue_events_url":"https://api.github.com/repos/wking/swc-amy/issues/events{/number}","events_url":"https://api.github.com/repos/wking/swc-amy/events","assignees_url":"https://api.github.com/repos/wking/swc-amy/assignees{/user}","branches_url":"https://api.github.com/repos/wking/swc-amy/branches{/branch}","tags_url":"https://api.github.com/repos/wking/swc-amy/tags","blobs_url":"https://api.github.com/repos/wking/swc-amy/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/wking/swc-amy/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/wking/swc-amy/git/refs{/sha}","trees_url":"https://api.github.com/repos/wking/swc-amy/git/trees{/sha}","statuses_url":"https://api.github.com/repos/wking/swc-amy/statuses/{sha}","languages_url":"https://api.github.com/repos/wking/swc-amy/languages","stargazers_url":"https://api.github.com/repos/wking/swc-amy/stargazers","contributors_url":"https://api.github.com/repos/wking/swc-amy/contributors","subscribers_url":"https://api.github.com/repos/wking/swc-amy/subscribers","subscription_url":"https://api.github.com/repos/wking/swc-amy/subscription","commits_url":"https://api.github.com/repos/wking/swc-amy/commits{/sha}","git_commits_url":"https://api.github.com/repos/wking/swc-amy/git/commits{/sha}","comments_url":"https://api.github.com/repos/wking/swc-amy/comments{/number}","issue_comment_url":"https://api.github.com/repos/wking/swc-amy/issues/comments/{number}","contents_url":"https://api.github.com/repos/wking/swc-amy/contents/{+path}","compare_url":"https://api.github.com/repos/wking/swc-amy/compare/{base}...{head}","merges_url":"https://api.github.com/repos/wking/swc-amy/merges","archive_url":"https://api.github.com/repos/wking/swc-amy/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/wking/swc-amy/downloads","issues_url":"https://api.github.com/repos/wking/swc-amy/issues{/number}","pulls_url":"https://api.github.com/repos/wking/swc-amy/pulls{/number}","milestones_url":"https://api.github.com/repos/wking/swc-amy/milestones{/number}","notifications_url":"https://api.github.com/repos/wking/swc-amy/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/wking/swc-amy/labels{/name}","releases_url":"https://api.github.com/repos/wking/swc-amy/releases{/id}","created_at":"2014-12-15T02:52:42Z","updated_at":"2015-01-31T19:23:49Z","pushed_at":"2015-02-05T22:37:09Z","git_url":"git://github.com/wking/swc-amy.git","ssh_url":"git@github.com:wking/swc-amy.git","clone_url":"https://github.com/wking/swc-amy.git","svn_url":"https://github.com/wking/swc-amy","homepage":null,"size":4579,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"swcarpentry:master","ref":"master","sha":"9d85ac0dbdfad794020ad769adb156bf979cf2cb","user":{"login":"swcarpentry","id":2238358,"avatar_url":"https://avatars.githubusercontent.com/u/2238358?v=3","gravatar_id":"","url":"https://api.github.com/users/swcarpentry","html_url":"https://github.com/swcarpentry","followers_url":"https://api.github.com/users/swcarpentry/followers","following_url":"https://api.github.com/users/swcarpentry/following{/other_user}","gists_url":"https://api.github.com/users/swcarpentry/gists{/gist_id}","starred_url":"https://api.github.com/users/swcarpentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/swcarpentry/subscriptions","organizations_url":"https://api.github.com/users/swcarpentry/orgs","repos_url":"https://api.github.com/users/swcarpentry/repos","events_url":"https://api.github.com/users/swcarpentry/events{/privacy}","received_events_url":"https://api.github.com/users/swcarpentry/received_events","type":"Organization","site_admin":false},"repo":{"id":27387287,"name":"amy","full_name":"swcarpentry/amy","owner":{"login":"swcarpentry","id":2238358,"avatar_url":"https://avatars.githubusercontent.com/u/2238358?v=3","gravatar_id":"","url":"https://api.github.com/users/swcarpentry","html_url":"https://github.com/swcarpentry","followers_url":"https://api.github.com/users/swcarpentry/followers","following_url":"https://api.github.com/users/swcarpentry/following{/other_user}","gists_url":"https://api.github.com/users/swcarpentry/gists{/gist_id}","starred_url":"https://api.github.com/users/swcarpentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/swcarpentry/subscriptions","organizations_url":"https://api.github.com/users/swcarpentry/orgs","repos_url":"https://api.github.com/users/swcarpentry/repos","events_url":"https://api.github.com/users/swcarpentry/events{/privacy}","received_events_url":"https://api.github.com/users/swcarpentry/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/swcarpentry/amy","description":"A web-based workshop administration application built using Django.","fork":false,"url":"https://api.github.com/repos/swcarpentry/amy","forks_url":"https://api.github.com/repos/swcarpentry/amy/forks","keys_url":"https://api.github.com/repos/swcarpentry/amy/keys{/key_id}","collaborators_url":"https://api.github.com/repos/swcarpentry/amy/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/swcarpentry/amy/teams","hooks_url":"https://api.github.com/repos/swcarpentry/amy/hooks","issue_events_url":"https://api.github.com/repos/swcarpentry/amy/issues/events{/number}","events_url":"https://api.github.com/repos/swcarpentry/amy/events","assignees_url":"https://api.github.com/repos/swcarpentry/amy/assignees{/user}","branches_url":"https://api.github.com/repos/swcarpentry/amy/branches{/branch}","tags_url":"https://api.github.com/repos/swcarpentry/amy/tags","blobs_url":"https://api.github.com/repos/swcarpentry/amy/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/swcarpentry/amy/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/swcarpentry/amy/git/refs{/sha}","trees_url":"https://api.github.com/repos/swcarpentry/amy/git/trees{/sha}","statuses_url":"https://api.github.com/repos/swcarpentry/amy/statuses/{sha}","languages_url":"https://api.github.com/repos/swcarpentry/amy/languages","stargazers_url":"https://api.github.com/repos/swcarpentry/amy/stargazers","contributors_url":"https://api.github.com/repos/swcarpentry/amy/contributors","subscribers_url":"https://api.github.com/repos/swcarpentry/amy/subscribers","subscription_url":"https://api.github.com/repos/swcarpentry/amy/subscription","commits_url":"https://api.github.com/repos/swcarpentry/amy/commits{/sha}","git_commits_url":"https://api.github.com/repos/swcarpentry/amy/git/commits{/sha}","comments_url":"https://api.github.com/repos/swcarpentry/amy/comments{/number}","issue_comment_url":"https://api.github.com/repos/swcarpentry/amy/issues/comments/{number}","contents_url":"https://api.github.com/repos/swcarpentry/amy/contents/{+path}","compare_url":"https://api.github.com/repos/swcarpentry/amy/compare/{base}...{head}","merges_url":"https://api.github.com/repos/swcarpentry/amy/merges","archive_url":"https://api.github.com/repos/swcarpentry/amy/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/swcarpentry/amy/downloads","issues_url":"https://api.github.com/repos/swcarpentry/amy/issues{/number}","pulls_url":"https://api.github.com/repos/swcarpentry/amy/pulls{/number}","milestones_url":"https://api.github.com/repos/swcarpentry/amy/milestones{/number}","notifications_url":"https://api.github.com/repos/swcarpentry/amy/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/swcarpentry/amy/labels{/name}","releases_url":"https://api.github.com/repos/swcarpentry/amy/releases{/id}","created_at":"2014-12-01T16:07:14Z","updated_at":"2015-02-05T22:59:07Z","pushed_at":"2015-02-05T22:59:07Z","git_url":"git://github.com/swcarpentry/amy.git","ssh_url":"git@github.com:swcarpentry/amy.git","clone_url":"https://github.com/swcarpentry/amy.git","svn_url":"https://github.com/swcarpentry/amy","homepage":null,"size":5672,"stargazers_count":8,"watchers_count":8,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":21,"mirror_url":null,"open_issues_count":41,"forks":21,"open_issues":41,"watchers":8,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/swcarpentry/amy/pulls/177"},"html":{"href":"https://github.com/swcarpentry/amy/pull/177"},"issue":{"href":"https://api.github.com/repos/swcarpentry/amy/issues/177"},"comments":{"href":"https://api.github.com/repos/swcarpentry/amy/issues/177/comments"},"review_comments":{"href":"https://api.github.com/repos/swcarpentry/amy/pulls/177/comments"},"review_comment":{"href":"https://api.github.com/repos/swcarpentry/amy/pulls/comments/{number}"},"commits":{"href":"https://api.github.com/repos/swcarpentry/amy/pulls/177/commits"},"statuses":{"href":"https://api.github.com/repos/swcarpentry/amy/statuses/09a6c421cffde7b161c88ee1590572d988baeb2a"}},"merged":true,"mergeable":null,"mergeable_state":"unknown","merged_by":{"login":"pbanaszkiewicz","id":72821,"avatar_url":"https://avatars.githubusercontent.com/u/72821?v=3","gravatar_id":"","url":"https://api.github.com/users/pbanaszkiewicz","html_url":"https://github.com/pbanaszkiewicz","followers_url":"https://api.github.com/users/pbanaszkiewicz/followers","following_url":"https://api.github.com/users/pbanaszkiewicz/following{/other_user}","gists_url":"https://api.github.com/users/pbanaszkiewicz/gists{/gist_id}","starred_url":"https://api.github.com/users/pbanaszkiewicz/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pbanaszkiewicz/subscriptions","organizations_url":"https://api.github.com/users/pbanaszkiewicz/orgs","repos_url":"https://api.github.com/users/pbanaszkiewicz/repos","events_url":"https://api.github.com/users/pbanaszkiewicz/events{/privacy}","received_events_url":"https://api.github.com/users/pbanaszkiewicz/received_events","type":"User","site_admin":false},"comments":7,"review_comments":9,"commits":2,"additions":21,"deletions":31,"changed_files":3}}