{"action":"created","issue":{"url":"https://api.github.com/repos/radremedy/radremedy/issues/216","labels_url":"https://api.github.com/repos/radremedy/radremedy/issues/216/labels{/name}","comments_url":"https://api.github.com/repos/radremedy/radremedy/issues/216/comments","events_url":"https://api.github.com/repos/radremedy/radremedy/issues/216/events","html_url":"https://github.com/radremedy/radremedy/issues/216","id":80789510,"number":216,"title":"Track login history","user":{"login":"radioprotector","id":8010294,"avatar_url":"https://avatars.githubusercontent.com/u/8010294?v=3","gravatar_id":"","url":"https://api.github.com/users/radioprotector","html_url":"https://github.com/radioprotector","followers_url":"https://api.github.com/users/radioprotector/followers","following_url":"https://api.github.com/users/radioprotector/following{/other_user}","gists_url":"https://api.github.com/users/radioprotector/gists{/gist_id}","starred_url":"https://api.github.com/users/radioprotector/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/radioprotector/subscriptions","organizations_url":"https://api.github.com/users/radioprotector/orgs","repos_url":"https://api.github.com/users/radioprotector/repos","events_url":"https://api.github.com/users/radioprotector/events{/privacy}","received_events_url":"https://api.github.com/users/radioprotector/received_events","type":"User","site_admin":false},"labels":[{"url":"https://api.github.com/repos/radremedy/radremedy/labels/data%20management","name":"data management","color":"009800"},{"url":"https://api.github.com/repos/radremedy/radremedy/labels/database","name":"database","color":"006b75"},{"url":"https://api.github.com/repos/radremedy/radremedy/labels/enhancement","name":"enhancement","color":"84b6eb"},{"url":"https://api.github.com/repos/radremedy/radremedy/labels/in%20progress","name":"in progress","color":"fad8c7"}],"state":"open","locked":false,"assignee":{"login":"radioprotector","id":8010294,"avatar_url":"https://avatars.githubusercontent.com/u/8010294?v=3","gravatar_id":"","url":"https://api.github.com/users/radioprotector","html_url":"https://github.com/radioprotector","followers_url":"https://api.github.com/users/radioprotector/followers","following_url":"https://api.github.com/users/radioprotector/following{/other_user}","gists_url":"https://api.github.com/users/radioprotector/gists{/gist_id}","starred_url":"https://api.github.com/users/radioprotector/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/radioprotector/subscriptions","organizations_url":"https://api.github.com/users/radioprotector/orgs","repos_url":"https://api.github.com/users/radioprotector/repos","events_url":"https://api.github.com/users/radioprotector/events{/privacy}","received_events_url":"https://api.github.com/users/radioprotector/received_events","type":"User","site_admin":false},"milestone":{"url":"https://api.github.com/repos/radremedy/radremedy/milestones/1","html_url":"https://github.com/radremedy/radremedy/milestones/0.1","labels_url":"https://api.github.com/repos/radremedy/radremedy/milestones/1/labels","id":676852,"number":1,"title":"0.1","description":null,"creator":{"login":"zitterbewegung","id":30265,"avatar_url":"https://avatars.githubusercontent.com/u/30265?v=3","gravatar_id":"","url":"https://api.github.com/users/zitterbewegung","html_url":"https://github.com/zitterbewegung","followers_url":"https://api.github.com/users/zitterbewegung/followers","following_url":"https://api.github.com/users/zitterbewegung/following{/other_user}","gists_url":"https://api.github.com/users/zitterbewegung/gists{/gist_id}","starred_url":"https://api.github.com/users/zitterbewegung/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zitterbewegung/subscriptions","organizations_url":"https://api.github.com/users/zitterbewegung/orgs","repos_url":"https://api.github.com/users/zitterbewegung/repos","events_url":"https://api.github.com/users/zitterbewegung/events{/privacy}","received_events_url":"https://api.github.com/users/zitterbewegung/received_events","type":"User","site_admin":false},"open_issues":8,"closed_issues":91,"state":"open","created_at":"2014-06-01T00:44:46Z","updated_at":"2015-05-28T01:06:23Z","due_on":null,"closed_at":null},"comments":1,"created_at":"2015-05-26T04:46:39Z","updated_at":"2015-05-30T03:34:24Z","closed_at":null,"body":"Track the UTC date/time of the login, the IP, the attempted username, and whether it was successful. Eventually we may want some sort of automatic lock-out feature but I don't think it's necessary for the initial version."},"comment":{"url":"https://api.github.com/repos/radremedy/radremedy/issues/comments/106981814","html_url":"https://github.com/radremedy/radremedy/issues/216#issuecomment-106981814","issue_url":"https://api.github.com/repos/radremedy/radremedy/issues/216","id":106981814,"user":{"login":"radioprotector","id":8010294,"avatar_url":"https://avatars.githubusercontent.com/u/8010294?v=3","gravatar_id":"","url":"https://api.github.com/users/radioprotector","html_url":"https://github.com/radioprotector","followers_url":"https://api.github.com/users/radioprotector/followers","following_url":"https://api.github.com/users/radioprotector/following{/other_user}","gists_url":"https://api.github.com/users/radioprotector/gists{/gist_id}","starred_url":"https://api.github.com/users/radioprotector/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/radioprotector/subscriptions","organizations_url":"https://api.github.com/users/radioprotector/orgs","repos_url":"https://api.github.com/users/radioprotector/repos","events_url":"https://api.github.com/users/radioprotector/events{/privacy}","received_events_url":"https://api.github.com/users/radioprotector/received_events","type":"User","site_admin":false},"created_at":"2015-05-30T03:34:24Z","updated_at":"2015-05-30T03:34:24Z","body":"There will be a new LoginHistory model with the following fields:\r\n- `id` (Integer, Primary Key)\r\n- `login_date` (DateTime, non-null, default to `datetime.utcnow`)\r\n- `ip` (Unicode(45), non-null)\r\n- `username` (Unicode(50), non-null)\r\n- `successful` (Boolean, non-null)\r\n- `failure_reason` (Unicode(20), null)\r\n\r\nIf a login attempt fails, the `failure_reason` will be one of the following:\r\n- No User\r\n- Bad Password\r\n- Deactivated\r\n- Not Confirmed\r\n\r\nA new `remedy_utils` module will be added to the remedy folder. It will have a `get_ip()` method that will be extracted from `email_utils` and will be used in the following places:\r\n- Recording login information\r\n- Emails that include IP addresses\r\n- Recording the IP of reviews (when that gets added)\r\n\r\nThe `user_auth.sign_in` method will be updated to create new LoginHistory entries if the submitted form is valid (so login attempts without a username or password won't go in).\r\n\r\nA new `LoginHistoryView` will be added to the Admin views and will be available as a \"Login History\" item under the \"User\" menu option. This view will not allow creation, editing, or deletion and will list out all of the columns in the model save for `id`.\r\n\r\nThe existing \"Users\" view will be available as an \"Users\" option under this list. Setting up the menu stuff will be handled by `admin.py`.\r\n\r\nI thought about trying to store the IP address as an actual binary field, but it doesn't appear that Python has a good way out-of-the-box to handle this ([`socket.getaddrinfo`](https://docs.python.org/2/library/socket.html#socket.getaddrinfo) still returns a string) and conversion back in user-facing land would be complicated. So, I'm using just a string field to store it instead."}}