Repository: ucfopen/canvasapi

Issue:
Authentications Log
**What resource needs additional coverage?**
Authentications Log

**What endpoints need to be covered?**
https://canvas.instructure.com/doc/api/authentications_log.html

 - [ ] Query by login
 - [ ] Query by account
 - [ ] Query by user

_New to working on CanvasAPI? Check out our [contributing guide](https://github.com/ucfopen/canvasapi/blob/develop/.github/CONTRIBUTING.md)!_

Discussion:

Patch:
diff --git a/canvasapi/account.py b/canvasapi/account.py
index d9b5524..d03cea3 100644
--- a/canvasapi/account.py
+++ b/canvasapi/account.py
@@ -642,6 +642,26 @@ class Account(CanvasObject):
             "accounts/{}/outcome_group_links".format(self.id),
         )
 
+    def get_authentication_events(self, **kwargs):
+        """
+        List authentication events for a given account.
+
+        :calls: `GET /api/v1/audit/authentication/accounts/:account_id \
+        <https://canvas.instructure.com/doc/api/authentications_log.html#method.authentication_audit_api.for_account>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+                :class:`canvasapi.authentication_event.AuthenticationEvent`
+        """
+        from canvasapi.authentication_event import AuthenticationEvent
+
+        return PaginatedList(
+            AuthenticationEvent,
+            self._requester,
+            "GET",
+            "audit/authentication/accounts/{}".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def get_authentication_provider(self, authentication_provider, **kwargs):
         """
         Get the specified authentication provider
diff --git a/canvasapi/authentication_event.py b/canvasapi/authentication_event.py
new file mode 100644
index 0000000..4ba6286
--- /dev/null
+++ b/canvasapi/authentication_event.py
@@ -0,0 +1,11 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from six import python_2_unicode_compatible
+
+from canvasapi.canvas_object import CanvasObject
+
+
+@python_2_unicode_compatible
+class AuthenticationEvent(CanvasObject):
+    def __str__(self):
+        return "{} {} ({})".format(self.created_at, self.event_type, self.pseudonym_id)
diff --git a/canvasapi/login.py b/canvasapi/login.py
index e43659e..d937e3f 100644
--- a/canvasapi/login.py
+++ b/canvasapi/login.py
@@ -3,6 +3,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 from six import python_2_unicode_compatible
 
 from canvasapi.canvas_object import CanvasObject
+from canvasapi.paginated_list import PaginatedList
 from canvasapi.util import combine_kwargs
 
 
@@ -40,3 +41,23 @@ class Login(CanvasObject):
             _kwargs=combine_kwargs(**kwargs),
         )
         return Login(self._requester, response.json())
+
+    def get_authentication_events(self, **kwargs):
+        """
+        List authentication events for a given login.
+
+        :calls: `GET /api/v1/audit/authentication/logins/:login_id \
+        <https://canvas.instructure.com/doc/api/authentications_log.html#method.authentication_audit_api.for_login>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+                :class:`canvasapi.authentication_event.AuthenticationEvent`
+        """
+        from canvasapi.authentication_event import AuthenticationEvent
+
+        return PaginatedList(
+            AuthenticationEvent,
+            self._requester,
+            "GET",
+            "audit/authentication/logins/{}".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
diff --git a/canvasapi/user.py b/canvasapi/user.py
index d560c72..92ee93a 100644
--- a/canvasapi/user.py
+++ b/canvasapi/user.py
@@ -185,6 +185,26 @@ class User(CanvasObject):
             _kwargs=combine_kwargs(**kwargs),
         )
 
+    def get_authentication_events(self, **kwargs):
+        """
+        List authentication events for a given user.
+
+        :calls: `GET /api/v1/audit/authentication/users/:user_id \
+        <https://canvas.instructure.com/doc/api/authentications_log.html#method.authentication_audit_api.for_user>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+                :class:`canvasapi.authentication_event.AuthenticationEvent`
+        """
+        from canvasapi.authentication_event import AuthenticationEvent
+
+        return PaginatedList(
+            AuthenticationEvent,
+            self._requester,
+            "GET",
+            "audit/authentication/users/{}".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def get_avatars(self):
         """
         Retrieve the possible user avatar options that can be set with the user update endpoint.
diff --git a/docs/authentication-event-ref.rst b/docs/authentication-event-ref.rst
new file mode 100644
index 0000000..6b28072
--- /dev/null
+++ b/docs/authentication-event-ref.rst
@@ -0,0 +1,6 @@
+===================
+AuthenticationEvent
+===================
+
+.. autoclass:: canvasapi.authentication_event.AuthenticationEvent
+    :members:
diff --git a/docs/class-reference.rst b/docs/class-reference.rst
index 6ef8966..319b81c 100644
--- a/docs/class-reference.rst
+++ b/docs/class-reference.rst
@@ -7,6 +7,7 @@ Class Reference
     account-ref
     appointment-group-ref
     assignment-ref
+    authentication-event-ref
     authentication-provider-ref
     avatar-ref
     blueprint-ref


Test patch:
diff --git a/tests/fixtures/account.json b/tests/fixtures/account.json
index 1c4302f..0d05a81 100644
--- a/tests/fixtures/account.json
+++ b/tests/fixtures/account.json
@@ -1330,7 +1330,7 @@
         	"path": "/api/v1/users/self/course_nicknames/:course_id",
         	"scope": "url:GET|/api/v1/users/self/course_nicknames/:course_id",
         	"resource": "users",
-        	"resource_name": "Users"	
+        	"resource_name": "Users"
 		}
 	],
 	"status_code": 200
@@ -1732,5 +1732,22 @@
 			"state": "on"
 		},
 		"status_code": 200
+	},
+	"get_authentication_events": {
+		"method": "GET",
+		"endpoint": "audit/authentication/accounts/1?per_page=100",
+		"data": [
+			{
+				"created_at": "2012-07-19T15:00:00-06:00",
+				"event_type": "login",
+				"pseudonym_id": 9478
+			},
+			{
+				"created_at": "2012-07-20T15:00:00-06:00",
+				"event_type": "logout",
+				"pseudonym_id": 9478
+			}
+		],
+		"status_code": 200
 	}
 }
diff --git a/tests/fixtures/login.json b/tests/fixtures/login.json
index 8942470..53ffe6d 100644
--- a/tests/fixtures/login.json
+++ b/tests/fixtures/login.json
@@ -7,7 +7,7 @@
 			"id": 101,
 			"unique_id": "belieber@example.com",
 			"user_id": 1
-		}, 
+		},
 		"status_code": 200
 	},
 	"list_user_logins": {
@@ -62,5 +62,22 @@
 			"unique_id": "belieber@example.com",
 			"user_id": 1
 		}
+	},
+	"get_authentication_events": {
+		"method": "GET",
+		"endpoint": "audit/authentication/logins/101",
+		"data": [
+			{
+				"created_at": "2012-07-19T15:00:00-06:00",
+				"event_type": "login",
+				"pseudonym_id": 9478
+			},
+			{
+				"created_at": "2012-07-20T15:00:00-06:00",
+				"event_type": "logout",
+				"pseudonym_id": 9478
+			}
+		],
+		"status_code": 200
 	}
 }
diff --git a/tests/fixtures/user.json b/tests/fixtures/user.json
index 6c4129a..bf40e1e 100644
--- a/tests/fixtures/user.json
+++ b/tests/fixtures/user.json
@@ -918,5 +918,22 @@
 			"state": "allowed"
 		},
 		"status_code": 200
+	},
+	"get_authentication_events": {
+		"method": "GET",
+		"endpoint": "audit/authentication/users/1",
+		"data": [
+			{
+				"created_at": "2012-07-19T15:00:00-06:00",
+				"event_type": "login",
+				"pseudonym_id": 9478
+			},
+			{
+				"created_at": "2012-07-20T15:00:00-06:00",
+				"event_type": "logout",
+				"pseudonym_id": 9478
+			}
+		],
+		"status_code": 200
 	}
 }
diff --git a/tests/test_account.py b/tests/test_account.py
index 098d6e5..e79d275 100644
--- a/tests/test_account.py
+++ b/tests/test_account.py
@@ -16,6 +16,7 @@ from canvasapi.account import (
     SSOSettings,
 )
 from canvasapi.authentication_provider import AuthenticationProvider
+from canvasapi.authentication_event import AuthenticationEvent
 from canvasapi.course import Course
 from canvasapi.enrollment import Enrollment
 from canvasapi.enrollment_term import EnrollmentTerm
@@ -1332,6 +1333,23 @@ class TestAccount(unittest.TestCase):
         with self.assertRaises(RequiredFieldMissing):
             self.AccountNotification.update_global_notification(notif)
 
+    # get_authentication_events()
+    def test_get_authentication_events(self, m):
+        register_uris({"account": ["get_authentication_events"]}, m)
+
+        authentication_event = self.account.get_authentication_events()
+        event_list = [event for event in authentication_event]
+
+        self.assertEqual(len(event_list), 2)
+
+        self.assertIsInstance(event_list[0], AuthenticationEvent)
+        self.assertEqual(event_list[0].event_type, "login")
+        self.assertEqual(event_list[0].pseudonym_id, 9478)
+
+        self.assertIsInstance(event_list[1], AuthenticationEvent)
+        self.assertEqual(event_list[1].created_at, "2012-07-20T15:00:00-06:00")
+        self.assertEqual(event_list[1].event_type, "logout")
+
 
 @requests_mock.Mocker()
 class TestAccountNotification(unittest.TestCase):
diff --git a/tests/test_authentication_event.py b/tests/test_authentication_event.py
new file mode 100644
index 0000000..2849060
--- /dev/null
+++ b/tests/test_authentication_event.py
@@ -0,0 +1,39 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+import unittest
+
+import requests_mock
+
+from canvasapi import Canvas
+from tests import settings
+from tests.util import register_uris
+
+
+@requests_mock.Mocker()
+class TestAuthenticationEvent(unittest.TestCase):
+    def setUp(self):
+        self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
+
+        with requests_mock.Mocker() as m:
+            requires = {
+                "account": ["get_by_id", "get_authentication_events"],
+                "login": ["create_user_login", "get_authentication_events"],
+                "user": ["get_by_id", "get_authentication_events"],
+            }
+            register_uris(requires, m)
+
+            self.account = self.canvas.get_account(1)
+            self.login = self.account.create_user_login(
+                user={"id": 1}, login={"unique_id": "belieber@example.com"}
+            )
+            self.user = self.canvas.get_user(1)
+
+            self.authentication_event_account = self.account.get_authentication_events()[
+                0
+            ]
+            self.authentication_event_login = self.login.get_authentication_events()[0]
+            self.authentication_event_user = self.user.get_authentication_events()[0]
+
+    # __str__()
+    def test__str__(self, m):
+        string = str(self.authentication_event_account)
+        self.assertIsInstance(string, str)
diff --git a/tests/test_login.py b/tests/test_login.py
index 2921481..d74198b 100644
--- a/tests/test_login.py
+++ b/tests/test_login.py
@@ -4,6 +4,7 @@ import unittest
 import requests_mock
 
 from canvasapi import Canvas
+from canvasapi.authentication_event import AuthenticationEvent
 from canvasapi.login import Login
 from tests import settings
 from tests.util import register_uris
@@ -49,3 +50,20 @@ class TestLogin(unittest.TestCase):
     def test__str__(self, m):
         string = str(self.login)
         self.assertIsInstance(string, str)
+
+    # get_authentication_events()
+    def test_get_authentication_events(self, m):
+        register_uris({"login": ["get_authentication_events"]}, m)
+
+        authentication_event = self.login.get_authentication_events()
+        event_list = [event for event in authentication_event]
+
+        self.assertEqual(len(event_list), 2)
+
+        self.assertIsInstance(event_list[0], AuthenticationEvent)
+        self.assertEqual(event_list[0].event_type, "login")
+        self.assertEqual(event_list[0].pseudonym_id, 9478)
+
+        self.assertIsInstance(event_list[1], AuthenticationEvent)
+        self.assertEqual(event_list[1].created_at, "2012-07-20T15:00:00-06:00")
+        self.assertEqual(event_list[1].event_type, "logout")
diff --git a/tests/test_user.py b/tests/test_user.py
index 5c0d29e..6a32251 100644
--- a/tests/test_user.py
+++ b/tests/test_user.py
@@ -7,6 +7,7 @@ import requests_mock
 
 from canvasapi import Canvas
 from canvasapi.assignment import Assignment
+from canvasapi.authentication_event import AuthenticationEvent
 from canvasapi.avatar import Avatar
 from canvasapi.calendar_event import CalendarEvent
 from canvasapi.communication_channel import CommunicationChannel
@@ -563,6 +564,23 @@ class TestUser(unittest.TestCase):
         self.assertIsInstance(feature_flag, FeatureFlag)
         self.assertEqual(feature_flag.feature, "high_contrast")
 
+    # get_authentication_events()
+    def test_get_authentication_events(self, m):
+        register_uris({"user": ["get_authentication_events"]}, m)
+
+        authentication_event = self.user.get_authentication_events()
+        event_list = [event for event in authentication_event]
+
+        self.assertEqual(len(event_list), 2)
+
+        self.assertIsInstance(event_list[0], AuthenticationEvent)
+        self.assertEqual(event_list[0].event_type, "login")
+        self.assertEqual(event_list[0].pseudonym_id, 9478)
+
+        self.assertIsInstance(event_list[1], AuthenticationEvent)
+        self.assertEqual(event_list[1].created_at, "2012-07-20T15:00:00-06:00")
+        self.assertEqual(event_list[1].event_type, "logout")
+
 
 @requests_mock.Mocker()
 class TestUserDisplay(unittest.TestCase):