From a9734d80b5bb6016a7f6639af06e854f0aa7bb92 Mon Sep 17 00:00:00 2001 From: hamzawaleed01 Date: Mon, 22 Jan 2024 18:09:24 +0500 Subject: [PATCH 01/11] feat: WIP - update cornerstone client to store cornerstone API calls in db --- .../cornerstone/admin/__init__.py | 10 ++ integrated_channels/cornerstone/client.py | 12 ++- ...alter_cornerstoneapirequestlogs_options.py | 17 ++++ integrated_channels/cornerstone/models.py | 1 + integrated_channels/cornerstone/utils.py | 32 +++++++ .../migrations/0032_auto_20240122_1206.py | 27 ++++++ .../integrated_channel/models.py | 14 +-- .../test_cornerstone/test_client.py | 91 +++++++++++++++++++ 8 files changed, 197 insertions(+), 7 deletions(-) create mode 100644 integrated_channels/cornerstone/migrations/0033_alter_cornerstoneapirequestlogs_options.py create mode 100644 integrated_channels/integrated_channel/migrations/0032_auto_20240122_1206.py create mode 100644 tests/test_integrated_channels/test_cornerstone/test_client.py diff --git a/integrated_channels/cornerstone/admin/__init__.py b/integrated_channels/cornerstone/admin/__init__.py index cb9c791e49..413da0e385 100644 --- a/integrated_channels/cornerstone/admin/__init__.py +++ b/integrated_channels/cornerstone/admin/__init__.py @@ -13,6 +13,7 @@ CornerstoneEnterpriseCustomerConfiguration, CornerstoneGlobalConfiguration, CornerstoneLearnerDataTransmissionAudit, + CornerstoneAPIRequestLogs ) from integrated_channels.integrated_channel.admin import BaseLearnerDataTransmissionAuditAdmin @@ -138,3 +139,12 @@ def user_email(self, obj): being rendered with this admin form. """ return obj.user.email + +@admin.register(CornerstoneAPIRequestLogs) +class CornerstoneAPIRequestLogAdmin(DjangoObjectActions, admin.ModelAdmin): + """ + Django admin model for CornerstoneAPIRequestLogs. + """ + + class Meta: + model = CornerstoneAPIRequestLogs diff --git a/integrated_channels/cornerstone/client.py b/integrated_channels/cornerstone/client.py index 5a9bed5270..a5ae41c09e 100644 --- a/integrated_channels/cornerstone/client.py +++ b/integrated_channels/cornerstone/client.py @@ -10,7 +10,7 @@ from django.apps import apps -from integrated_channels.cornerstone.utils import get_or_create_key_pair +from integrated_channels.cornerstone.utils import get_or_create_key_pair, store_cornerstone_api_calls from integrated_channels.integrated_channel.client import IntegratedChannelApiClient from integrated_channels.utils import generate_formatted_log @@ -113,6 +113,16 @@ def create_course_completion(self, user_id, payload): 'Content-Type': 'application/json' } ) + store_cornerstone_api_calls( + user_agent='asdf', + user_ip='123', + enterprise_customer=self.enterprise_configuration.enterprise_customer, + endpoint=url, + payload=json_payload['data'], + time_taken=100, + status_code=200, + response_body='{}' + ) return response.status_code, response.text def create_assessment_reporting(self, user_id, payload): diff --git a/integrated_channels/cornerstone/migrations/0033_alter_cornerstoneapirequestlogs_options.py b/integrated_channels/cornerstone/migrations/0033_alter_cornerstoneapirequestlogs_options.py new file mode 100644 index 0000000000..016d210399 --- /dev/null +++ b/integrated_channels/cornerstone/migrations/0033_alter_cornerstoneapirequestlogs_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.23 on 2024-01-22 12:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('cornerstone', '0032_cornerstonelearnerdatatransmissionaudit_transmission_status'), + ] + + operations = [ + migrations.AlterModelOptions( + name='cornerstoneapirequestlogs', + options={'verbose_name_plural': 'Cornerstone API request logs'}, + ), + ] diff --git a/integrated_channels/cornerstone/models.py b/integrated_channels/cornerstone/models.py index 46592bc4ea..aa4b696cd7 100644 --- a/integrated_channels/cornerstone/models.py +++ b/integrated_channels/cornerstone/models.py @@ -330,6 +330,7 @@ class CornerstoneAPIRequestLogs(IntegratedChannelAPIRequestLogs): class Meta: app_label = 'cornerstone' + verbose_name_plural = "Cornerstone API request logs" def __str__(self): """ diff --git a/integrated_channels/cornerstone/utils.py b/integrated_channels/cornerstone/utils.py index e6c1defbb9..37b9d5aed5 100644 --- a/integrated_channels/cornerstone/utils.py +++ b/integrated_channels/cornerstone/utils.py @@ -22,6 +22,13 @@ def cornerstone_course_key_model(): return apps.get_model('cornerstone', 'CornerstoneCourseKey') +def cornerstone_request_log__model(): + """ + Returns the ``CornerstoneAPIRequestLogs`` class. + """ + return apps.get_model('cornerstone', 'CornerstoneAPIRequestLogs') + + LOGGER = getLogger(__name__) @@ -80,3 +87,28 @@ def get_or_create_key_pair(course_id): internal_course_id=course_id, defaults={ 'external_course_id': str(uuid4())}) return key_mapping + + +def store_cornerstone_api_calls( + user_agent, + user_ip, + enterprise_customer, + endpoint, + payload, + time_taken, + status_code, + response_body, +): + """ + Creates new record in CornerstoneAPIRequestLogs table. + """ + cornerstone_request_log__model().objects.create( + user_agent=user_agent, + user_ip=user_ip, + enterprise_customer=enterprise_customer, + endpoint=endpoint, + payload=payload, + time_taken=time_taken, + status_code=status_code, + response_body=response_body, + ) diff --git a/integrated_channels/integrated_channel/migrations/0032_auto_20240122_1206.py b/integrated_channels/integrated_channel/migrations/0032_auto_20240122_1206.py new file mode 100644 index 0000000000..c45ba9431f --- /dev/null +++ b/integrated_channels/integrated_channel/migrations/0032_auto_20240122_1206.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.23 on 2024-01-22 12:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('integrated_channel', '0031_genericlearnerdatatransmissionaudit_transmission_status'), + ] + + operations = [ + migrations.RemoveField( + model_name='integratedchannelapirequestlogs', + name='api_record', + ), + migrations.AddField( + model_name='integratedchannelapirequestlogs', + name='response_body', + field=models.TextField(blank=True, help_text='TAPI call response body', null=True), + ), + migrations.AddField( + model_name='integratedchannelapirequestlogs', + name='status_code', + field=models.PositiveIntegerField(blank=True, help_text='API call response HTTP status code', null=True), + ), + ] diff --git a/integrated_channels/integrated_channel/models.py b/integrated_channels/integrated_channel/models.py index cc0d60fc64..436b7258ac 100644 --- a/integrated_channels/integrated_channel/models.py +++ b/integrated_channels/integrated_channel/models.py @@ -910,13 +910,15 @@ class IntegratedChannelAPIRequestLogs(TimeStampedModel): endpoint = models.TextField(blank=False, null=False) payload = models.TextField(blank=False, null=False) time_taken = models.DurationField(blank=False, null=False) - api_record = models.OneToOneField( - ApiResponseRecord, + status_code = models.PositiveIntegerField( + help_text='API call response HTTP status code', blank=True, - null=True, - on_delete=models.CASCADE, - help_text=_( - 'Data pertaining to the transmissions API request response.') + null=True + ) + response_body = models.TextField( + help_text='API call response body', + blank=True, + null=True ) class Meta: diff --git a/tests/test_integrated_channels/test_cornerstone/test_client.py b/tests/test_integrated_channels/test_cornerstone/test_client.py new file mode 100644 index 0000000000..92cfb21bad --- /dev/null +++ b/tests/test_integrated_channels/test_cornerstone/test_client.py @@ -0,0 +1,91 @@ +""" +Tests for Degreed2 client for integrated_channels. +""" + +import datetime +import json +import unittest + +import mock +import pytest +import requests +import responses +from freezegun import freeze_time +from six.moves.urllib.parse import urljoin + +from django.apps.registry import apps + +from enterprise.api_client.enterprise_catalog import EnterpriseCatalogApiClient +from enterprise.models import EnterpriseCustomerUser +from integrated_channels.cornerstone.client import CornerstoneAPIClient +from integrated_channels.exceptions import ClientError +from test_utils import factories + +NOW = datetime.datetime(2017, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc) +NOW_TIMESTAMP_FORMATTED = NOW.strftime("%F") + + +def create_course_payload(): + return json.dumps( + { + "courses": [ + { + "title": "title", + "summary": "description", + "image-url": "image", + "url": "enrollment_url", + "language": "content_language", + "external-id": "key", + "duration": "duration", + "duration-type": "Days", + } + ], + }, + sort_keys=True, + ).encode("utf-8") + + +@pytest.mark.django_db +@freeze_time(NOW) +class TestCornerstoneApiClient(unittest.TestCase): + """ + Test Degreed2 API client methods. + """ + + def setUp(self): + super().setUp() + self.cornerstone_base_url = "https://edx.example.com/" + self.csod_config = factories.CornerstoneEnterpriseCustomerConfigurationFactory( + cornerstone_base_url=self.cornerstone_base_url + ) + + @responses.activate + def test_create_course_completion(self): + """ + ``create_course_completion`` should use the appropriate URLs for transmission. + """ + cornerstone_api_client = CornerstoneAPIClient(self.csod_config) + callbackUrl = "dummy_callback_url" + sessionToken = "dummy_session_oken" + payload = { + "data": { + "userGuid": "dummy_id", + "sessionToken": sessionToken, + "callbackUrl": callbackUrl, + "subdomain": "dummy_subdomain", + } + } + responses.add( + responses.POST, + f"{self.cornerstone_base_url}{callbackUrl}?sessionToken={sessionToken}", + json="{}", + status=200, + ) + output = cornerstone_api_client.create_course_completion( + "test-learner@example.com", json.dumps(payload) + ) + + assert output == (200, '"{}"') + # assert len(responses.calls) == 2 + # assert responses.calls[0].request.url == cornerstone_api_client.get_oauth_url() + # assert responses.calls[1].request.url == cornerstone_api_client.get_completions_url() From 95071abb991ecb1cb261da01dd8e37db872a439c Mon Sep 17 00:00:00 2001 From: hamzawaleed01 Date: Tue, 23 Jan 2024 16:01:46 +0500 Subject: [PATCH 02/11] feat: introduced base abstract model --- integrated_channels/cornerstone/client.py | 17 +++--- .../0031_cornerstoneapirequestlogs.py | 23 ++++++-- ...alter_cornerstoneapirequestlogs_options.py | 17 ------ integrated_channels/cornerstone/models.py | 29 +++++----- integrated_channels/cornerstone/utils.py | 6 ++- .../0030_integratedchannelapirequestlogs.py | 9 ++-- .../migrations/0032_auto_20240122_1206.py | 27 ---------- .../integrated_channel/models.py | 54 ++++++++++++------- 8 files changed, 85 insertions(+), 97 deletions(-) delete mode 100644 integrated_channels/cornerstone/migrations/0033_alter_cornerstoneapirequestlogs_options.py delete mode 100644 integrated_channels/integrated_channel/migrations/0032_auto_20240122_1206.py diff --git a/integrated_channels/cornerstone/client.py b/integrated_channels/cornerstone/client.py index a5ae41c09e..b003276ea9 100644 --- a/integrated_channels/cornerstone/client.py +++ b/integrated_channels/cornerstone/client.py @@ -5,6 +5,7 @@ import base64 import json import logging +import time import requests @@ -104,7 +105,7 @@ def create_course_completion(self, user_id, payload): completion_path=self.global_cornerstone_config.completion_status_api_path, session_token=session_token, ) - + start_time = time.time() response = requests.post( url, json=[json_payload['data']], @@ -113,16 +114,16 @@ def create_course_completion(self, user_id, payload): 'Content-Type': 'application/json' } ) + duration_seconds = time.time() - start_time store_cornerstone_api_calls( - user_agent='asdf', - user_ip='123', enterprise_customer=self.enterprise_configuration.enterprise_customer, + enterprise_customer_configuration_id=self.enterprise_configuration.id, endpoint=url, - payload=json_payload['data'], - time_taken=100, - status_code=200, - response_body='{}' - ) + payload=json_payload["data"], + time_taken=duration_seconds, + status_code=response.status_code, + response_body=response.text, + ) return response.status_code, response.text def create_assessment_reporting(self, user_id, payload): diff --git a/integrated_channels/cornerstone/migrations/0031_cornerstoneapirequestlogs.py b/integrated_channels/cornerstone/migrations/0031_cornerstoneapirequestlogs.py index 35a3c66b68..914b533ef8 100644 --- a/integrated_channels/cornerstone/migrations/0031_cornerstoneapirequestlogs.py +++ b/integrated_channels/cornerstone/migrations/0031_cornerstoneapirequestlogs.py @@ -1,13 +1,15 @@ -# Generated by Django 3.2.23 on 2024-01-15 07:54 +# Generated by Django 3.2.23 on 2024-01-23 10:59 from django.db import migrations, models import django.db.models.deletion +import django.utils.timezone +import model_utils.fields class Migration(migrations.Migration): dependencies = [ - ('integrated_channel', '0030_integratedchannelapirequestlogs'), + ('enterprise', '0198_alter_enterprisecourseenrollment_options'), ('cornerstone', '0030_auto_20231010_1654'), ] @@ -15,10 +17,21 @@ class Migration(migrations.Migration): migrations.CreateModel( name='CornerstoneAPIRequestLogs', fields=[ - ('integratedchannelapirequestlogs_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='integrated_channel.integratedchannelapirequestlogs')), - ('user_agent', models.CharField(max_length=255)), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('enterprise_customer_configuration_id', models.IntegerField(help_text='ID from the EnterpriseCustomerConfiguration model')), + ('endpoint', models.TextField()), + ('payload', models.TextField()), + ('time_taken', models.FloatField()), + ('status_code', models.PositiveIntegerField(blank=True, help_text='API call response HTTP status code', null=True)), + ('response_body', models.TextField(blank=True, help_text='API call response body', null=True)), + ('user_agent', models.CharField(blank=True, max_length=255, null=True)), ('user_ip', models.GenericIPAddressField(blank=True, null=True)), + ('enterprise_customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='enterprise.enterprisecustomer')), ], - bases=('integrated_channel.integratedchannelapirequestlogs',), + options={ + 'verbose_name_plural': 'Cornerstone API request logs', + }, ), ] diff --git a/integrated_channels/cornerstone/migrations/0033_alter_cornerstoneapirequestlogs_options.py b/integrated_channels/cornerstone/migrations/0033_alter_cornerstoneapirequestlogs_options.py deleted file mode 100644 index 016d210399..0000000000 --- a/integrated_channels/cornerstone/migrations/0033_alter_cornerstoneapirequestlogs_options.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.2.23 on 2024-01-22 12:06 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('cornerstone', '0032_cornerstonelearnerdatatransmissionaudit_transmission_status'), - ] - - operations = [ - migrations.AlterModelOptions( - name='cornerstoneapirequestlogs', - options={'verbose_name_plural': 'Cornerstone API request logs'}, - ), - ] diff --git a/integrated_channels/cornerstone/models.py b/integrated_channels/cornerstone/models.py index aa4b696cd7..e51999135f 100644 --- a/integrated_channels/cornerstone/models.py +++ b/integrated_channels/cornerstone/models.py @@ -18,8 +18,8 @@ from integrated_channels.cornerstone.transmitters.content_metadata import CornerstoneContentMetadataTransmitter from integrated_channels.cornerstone.transmitters.learner_data import CornerstoneLearnerTransmitter from integrated_channels.integrated_channel.models import ( + BaseIntegratedChannelAPIRequestLogs, EnterpriseCustomerPluginConfiguration, - IntegratedChannelAPIRequestLogs, LearnerDataTransmissionAudit, ) from integrated_channels.utils import is_valid_url @@ -321,15 +321,16 @@ class Meta: app_label = 'cornerstone' -class CornerstoneAPIRequestLogs(IntegratedChannelAPIRequestLogs): +class CornerstoneAPIRequestLogs(BaseIntegratedChannelAPIRequestLogs): """ - A model to track basic information about every API call we make from the integrated channels. + A model to track basic information about every API call we make from the integrated channels. """ - user_agent = models.CharField(max_length=255) + + user_agent = models.CharField(blank=True, null=True, max_length=255) user_ip = models.GenericIPAddressField(blank=True, null=True) class Meta: - app_label = 'cornerstone' + app_label = "cornerstone" verbose_name_plural = "Cornerstone API request logs" def __str__(self): @@ -337,15 +338,15 @@ def __str__(self): Return a human-readable string representation of the object. """ return ( - f'' - f', endpoint: {self.endpoint}' - f', time_taken: {self.time_taken}' - f', user_agent: {self.user_agent}' - f', user_ip: {self.user_ip}' - f', api_record.body: {self.api_record.body}' - f', api_record.status_code: {self.api_record.status_code}' + f"" + f", endpoint: {self.endpoint}" + f", time_taken: {self.time_taken}" + f", user_agent: {self.user_agent}" + f", user_ip: {self.user_ip}" + f", api_record.body: {self.api_record.body}" + f", api_record.status_code: {self.api_record.status_code}" ) def __repr__(self): diff --git a/integrated_channels/cornerstone/utils.py b/integrated_channels/cornerstone/utils.py index 37b9d5aed5..55c15d27d5 100644 --- a/integrated_channels/cornerstone/utils.py +++ b/integrated_channels/cornerstone/utils.py @@ -90,14 +90,15 @@ def get_or_create_key_pair(course_id): def store_cornerstone_api_calls( - user_agent, - user_ip, enterprise_customer, + enterprise_customer_configuration_id, endpoint, payload, time_taken, status_code, response_body, + user_agent=None, + user_ip=None, ): """ Creates new record in CornerstoneAPIRequestLogs table. @@ -106,6 +107,7 @@ def store_cornerstone_api_calls( user_agent=user_agent, user_ip=user_ip, enterprise_customer=enterprise_customer, + enterprise_customer_configuration_id=enterprise_customer_configuration_id, endpoint=endpoint, payload=payload, time_taken=time_taken, diff --git a/integrated_channels/integrated_channel/migrations/0030_integratedchannelapirequestlogs.py b/integrated_channels/integrated_channel/migrations/0030_integratedchannelapirequestlogs.py index db8f99c3e5..1e187aeb8c 100644 --- a/integrated_channels/integrated_channel/migrations/0030_integratedchannelapirequestlogs.py +++ b/integrated_channels/integrated_channel/migrations/0030_integratedchannelapirequestlogs.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.23 on 2024-01-12 07:24 +# Generated by Django 3.2.23 on 2024-01-23 10:51 from django.db import migrations, models import django.db.models.deletion @@ -9,7 +9,7 @@ class Migration(migrations.Migration): dependencies = [ - ('enterprise', '0197_auto_20231130_2239'), + ('enterprise', '0198_alter_enterprisecourseenrollment_options'), ('integrated_channel', '0029_genericenterprisecustomerpluginconfiguration_show_course_price'), ] @@ -23,8 +23,9 @@ class Migration(migrations.Migration): ('enterprise_customer_configuration_id', models.IntegerField(help_text='ID from the EnterpriseCustomerConfiguration model')), ('endpoint', models.TextField()), ('payload', models.TextField()), - ('time_taken', models.DurationField()), - ('api_record', models.OneToOneField(blank=True, help_text='Data pertaining to the transmissions API request response.', null=True, on_delete=django.db.models.deletion.CASCADE, to='integrated_channel.apiresponserecord')), + ('time_taken', models.FloatField()), + ('status_code', models.PositiveIntegerField(blank=True, help_text='API call response HTTP status code', null=True)), + ('response_body', models.TextField(blank=True, help_text='API call response body', null=True)), ('enterprise_customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='enterprise.enterprisecustomer')), ], ), diff --git a/integrated_channels/integrated_channel/migrations/0032_auto_20240122_1206.py b/integrated_channels/integrated_channel/migrations/0032_auto_20240122_1206.py deleted file mode 100644 index c45ba9431f..0000000000 --- a/integrated_channels/integrated_channel/migrations/0032_auto_20240122_1206.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 3.2.23 on 2024-01-22 12:06 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('integrated_channel', '0031_genericlearnerdatatransmissionaudit_transmission_status'), - ] - - operations = [ - migrations.RemoveField( - model_name='integratedchannelapirequestlogs', - name='api_record', - ), - migrations.AddField( - model_name='integratedchannelapirequestlogs', - name='response_body', - field=models.TextField(blank=True, help_text='TAPI call response body', null=True), - ), - migrations.AddField( - model_name='integratedchannelapirequestlogs', - name='status_code', - field=models.PositiveIntegerField(blank=True, help_text='API call response HTTP status code', null=True), - ), - ] diff --git a/integrated_channels/integrated_channel/models.py b/integrated_channels/integrated_channel/models.py index 17e7dea872..d00a9521c8 100644 --- a/integrated_channels/integrated_channel/models.py +++ b/integrated_channels/integrated_channel/models.py @@ -881,43 +881,57 @@ class Meta: resolved = models.BooleanField(default=False) -class IntegratedChannelAPIRequestLogs(TimeStampedModel): +class BaseIntegratedChannelAPIRequestLogs(TimeStampedModel): """ - A model to track basic information about every API call we make from the integrated channels. + A model to track basic information about every API call we make from the integrated channels. """ + enterprise_customer = models.ForeignKey( - EnterpriseCustomer, on_delete=models.CASCADE) + EnterpriseCustomer, on_delete=models.CASCADE + ) enterprise_customer_configuration_id = models.IntegerField( - blank=False, null=False, help_text='ID from the EnterpriseCustomerConfiguration model') - endpoint = models.TextField(blank=False, null=False) + blank=False, + null=False, + help_text="ID from the EnterpriseCustomerConfiguration model", + ) + endpoint = models.TextField( + blank=False, + null=False, + ) payload = models.TextField(blank=False, null=False) - time_taken = models.DurationField(blank=False, null=False) + time_taken = models.FloatField(blank=False, null=False) status_code = models.PositiveIntegerField( - help_text='API call response HTTP status code', - blank=True, - null=True + help_text="API call response HTTP status code", blank=True, null=True ) response_body = models.TextField( - help_text='API call response body', - blank=True, - null=True + help_text="API call response body", blank=True, null=True ) class Meta: - app_label = 'integrated_channel' + app_label = "integrated_channel" + abstract = True + + +class IntegratedChannelAPIRequestLogs(BaseIntegratedChannelAPIRequestLogs): + """ + A model to track basic information about every API call we make from the integrated channels. + """ + + class Meta: + app_label = "integrated_channel" def __str__(self): """ Return a human-readable string representation of the object. """ return ( - f'' - f', endpoint: {self.endpoint}' - f', time_taken: {self.time_taken}' - f', api_record.body: {self.api_record.body}' - f', api_record.status_code: {self.api_record.status_code}' + f"" + f", endpoint: {self.endpoint}" + f", time_taken: {self.time_taken}" + f", api_record.body: {self.api_record.body}" + f", api_record.status_code: {self.api_record.status_code}" ) def __repr__(self): From 0ef6ae697e571231e98ad4d426d2a2d56bb3861e Mon Sep 17 00:00:00 2001 From: hamzawaleed01 Date: Wed, 24 Jan 2024 14:43:50 +0500 Subject: [PATCH 03/11] feat: update tests --- integrated_channels/cornerstone/utils.py | 36 ++++++++++++------- .../test_cornerstone/test_client.py | 35 +----------------- 2 files changed, 25 insertions(+), 46 deletions(-) diff --git a/integrated_channels/cornerstone/utils.py b/integrated_channels/cornerstone/utils.py index 55c15d27d5..23fc4698a2 100644 --- a/integrated_channels/cornerstone/utils.py +++ b/integrated_channels/cornerstone/utils.py @@ -22,7 +22,7 @@ def cornerstone_course_key_model(): return apps.get_model('cornerstone', 'CornerstoneCourseKey') -def cornerstone_request_log__model(): +def cornerstone_request_log_model(): """ Returns the ``CornerstoneAPIRequestLogs`` class. """ @@ -103,14 +103,26 @@ def store_cornerstone_api_calls( """ Creates new record in CornerstoneAPIRequestLogs table. """ - cornerstone_request_log__model().objects.create( - user_agent=user_agent, - user_ip=user_ip, - enterprise_customer=enterprise_customer, - enterprise_customer_configuration_id=enterprise_customer_configuration_id, - endpoint=endpoint, - payload=payload, - time_taken=time_taken, - status_code=status_code, - response_body=response_body, - ) + try: + cornerstone_request_log_model().objects.create( + user_agent=user_agent, + user_ip=user_ip, + enterprise_customer=enterprise_customer, + enterprise_customer_configuration_id=enterprise_customer_configuration_id, + endpoint=endpoint, + payload=payload, + time_taken=time_taken, + status_code=status_code, + response_body=response_body, + ) + except Exception as e: # pylint: disable=broad-except + LOGGER.error( + f"[Cornerstone]: Error occurred while storing API call: {e}" + f"user_agent={user_agent}, user_ip={user_ip}, enterprise_customer={enterprise_customer}" + f"enterprise_customer_configuration_id={enterprise_customer_configuration_id}," + f"endpoint={endpoint}" + f"payload={payload}" + f"time_taken={time_taken}" + f"status_code={status_code}" + f"response_body={response_body}" + ) diff --git a/tests/test_integrated_channels/test_cornerstone/test_client.py b/tests/test_integrated_channels/test_cornerstone/test_client.py index 92cfb21bad..af2dcdb9b4 100644 --- a/tests/test_integrated_channels/test_cornerstone/test_client.py +++ b/tests/test_integrated_channels/test_cornerstone/test_client.py @@ -2,51 +2,20 @@ Tests for Degreed2 client for integrated_channels. """ -import datetime import json import unittest -import mock import pytest -import requests import responses from freezegun import freeze_time -from six.moves.urllib.parse import urljoin from django.apps.registry import apps -from enterprise.api_client.enterprise_catalog import EnterpriseCatalogApiClient -from enterprise.models import EnterpriseCustomerUser from integrated_channels.cornerstone.client import CornerstoneAPIClient -from integrated_channels.exceptions import ClientError from test_utils import factories -NOW = datetime.datetime(2017, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc) -NOW_TIMESTAMP_FORMATTED = NOW.strftime("%F") - - -def create_course_payload(): - return json.dumps( - { - "courses": [ - { - "title": "title", - "summary": "description", - "image-url": "image", - "url": "enrollment_url", - "language": "content_language", - "external-id": "key", - "duration": "duration", - "duration-type": "Days", - } - ], - }, - sort_keys=True, - ).encode("utf-8") - @pytest.mark.django_db -@freeze_time(NOW) class TestCornerstoneApiClient(unittest.TestCase): """ Test Degreed2 API client methods. @@ -85,7 +54,5 @@ def test_create_course_completion(self): "test-learner@example.com", json.dumps(payload) ) + assert len(responses.calls) == 1 assert output == (200, '"{}"') - # assert len(responses.calls) == 2 - # assert responses.calls[0].request.url == cornerstone_api_client.get_oauth_url() - # assert responses.calls[1].request.url == cornerstone_api_client.get_completions_url() From 2365f54eadf0e11dd2b923586b27182717636e47 Mon Sep 17 00:00:00 2001 From: hamzawaleed01 Date: Wed, 24 Jan 2024 16:04:59 +0500 Subject: [PATCH 04/11] feat: store cornerstone api calls from views --- integrated_channels/cornerstone/views.py | 14 +++++++++++++- .../test_cornerstone/test_client.py | 3 --- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/integrated_channels/cornerstone/views.py b/integrated_channels/cornerstone/views.py index 38d7445392..0117cbefde 100644 --- a/integrated_channels/cornerstone/views.py +++ b/integrated_channels/cornerstone/views.py @@ -4,6 +4,7 @@ import datetime from logging import getLogger +import time from dateutil import parser from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication @@ -16,6 +17,7 @@ from enterprise.api.throttles import ServiceUserThrottle from enterprise.utils import get_enterprise_customer, get_enterprise_worker_user, get_oauth2authentication_class from integrated_channels.cornerstone.models import CornerstoneEnterpriseCustomerConfiguration +from integrated_channels.cornerstone.utils import store_cornerstone_api_calls from integrated_channels.integrated_channel.constants import ISO_8601_DATE_FORMAT logger = getLogger(__name__) @@ -98,6 +100,7 @@ class CornerstoneCoursesListView(BaseViewSet): """ def get(self, request, *args, **kwargs): + start_time = time.time() enterprise_customer_uuid = request.GET.get('ciid') if not enterprise_customer_uuid: return Response( @@ -156,5 +159,14 @@ def get(self, request, *args, **kwargs): f'fixing modified/header mismatch') if_modified_since_dt = datetime.datetime.fromtimestamp(if_modified_since) item['LastModifiedUTC'] = if_modified_since_dt.strftime(ISO_8601_DATE_FORMAT) - + duration_seconds = time.time() - start_time + store_cornerstone_api_calls( + enterprise_customer=enterprise_customer, + enterprise_customer_configuration_id=enterprise_config.id, + endpoint=request.get_full_path(), + payload=f"Request Headers: {request.headers}", + time_taken=duration_seconds, + status_code=200, + response_body=data, + ) return Response(data) diff --git a/tests/test_integrated_channels/test_cornerstone/test_client.py b/tests/test_integrated_channels/test_cornerstone/test_client.py index af2dcdb9b4..06a1f0ddb4 100644 --- a/tests/test_integrated_channels/test_cornerstone/test_client.py +++ b/tests/test_integrated_channels/test_cornerstone/test_client.py @@ -7,9 +7,6 @@ import pytest import responses -from freezegun import freeze_time - -from django.apps.registry import apps from integrated_channels.cornerstone.client import CornerstoneAPIClient from test_utils import factories From 46ebbc6eee5172b75fded47348914b9d640b5ada Mon Sep 17 00:00:00 2001 From: hamzawaleed01 Date: Wed, 24 Jan 2024 18:17:59 +0500 Subject: [PATCH 05/11] refactor: common util to save api records --- integrated_channels/cornerstone/client.py | 9 +-- integrated_channels/cornerstone/models.py | 6 +- integrated_channels/cornerstone/utils.py | 48 -------------- integrated_channels/cornerstone/views.py | 26 ++++---- .../integrated_channel/models.py | 4 +- integrated_channels/utils.py | 62 +++++++++++++++++++ 6 files changed, 87 insertions(+), 68 deletions(-) diff --git a/integrated_channels/cornerstone/client.py b/integrated_channels/cornerstone/client.py index b003276ea9..760667dfe0 100644 --- a/integrated_channels/cornerstone/client.py +++ b/integrated_channels/cornerstone/client.py @@ -11,9 +11,9 @@ from django.apps import apps -from integrated_channels.cornerstone.utils import get_or_create_key_pair, store_cornerstone_api_calls +from integrated_channels.cornerstone.utils import get_or_create_key_pair from integrated_channels.integrated_channel.client import IntegratedChannelApiClient -from integrated_channels.utils import generate_formatted_log +from integrated_channels.utils import generate_formatted_log, store_api_call LOGGER = logging.getLogger(__name__) @@ -115,12 +115,13 @@ def create_course_completion(self, user_id, payload): } ) duration_seconds = time.time() - start_time - store_cornerstone_api_calls( + store_api_call( enterprise_customer=self.enterprise_configuration.enterprise_customer, enterprise_customer_configuration_id=self.enterprise_configuration.id, endpoint=url, - payload=json_payload["data"], + payload=json.dumps(json_payload["data"]), time_taken=duration_seconds, + channel_code=self.enterprise_configuration.channel_code(), status_code=response.status_code, response_body=response.text, ) diff --git a/integrated_channels/cornerstone/models.py b/integrated_channels/cornerstone/models.py index e51999135f..be18cba9d5 100644 --- a/integrated_channels/cornerstone/models.py +++ b/integrated_channels/cornerstone/models.py @@ -339,14 +339,14 @@ def __str__(self): """ return ( f"" f", endpoint: {self.endpoint}" f", time_taken: {self.time_taken}" f", user_agent: {self.user_agent}" f", user_ip: {self.user_ip}" - f", api_record.body: {self.api_record.body}" - f", api_record.status_code: {self.api_record.status_code}" + f", response_body: {self.response_body}" + f", status_code: {self.status_code}" ) def __repr__(self): diff --git a/integrated_channels/cornerstone/utils.py b/integrated_channels/cornerstone/utils.py index 23fc4698a2..c3ea08ad7e 100644 --- a/integrated_channels/cornerstone/utils.py +++ b/integrated_channels/cornerstone/utils.py @@ -21,17 +21,8 @@ def cornerstone_course_key_model(): """ return apps.get_model('cornerstone', 'CornerstoneCourseKey') - -def cornerstone_request_log_model(): - """ - Returns the ``CornerstoneAPIRequestLogs`` class. - """ - return apps.get_model('cornerstone', 'CornerstoneAPIRequestLogs') - - LOGGER = getLogger(__name__) - def create_cornerstone_learner_data(request, cornerstone_customer_configuration, course_id): """ updates or creates CornerstoneLearnerDataTransmissionAudit @@ -87,42 +78,3 @@ def get_or_create_key_pair(course_id): internal_course_id=course_id, defaults={ 'external_course_id': str(uuid4())}) return key_mapping - - -def store_cornerstone_api_calls( - enterprise_customer, - enterprise_customer_configuration_id, - endpoint, - payload, - time_taken, - status_code, - response_body, - user_agent=None, - user_ip=None, -): - """ - Creates new record in CornerstoneAPIRequestLogs table. - """ - try: - cornerstone_request_log_model().objects.create( - user_agent=user_agent, - user_ip=user_ip, - enterprise_customer=enterprise_customer, - enterprise_customer_configuration_id=enterprise_customer_configuration_id, - endpoint=endpoint, - payload=payload, - time_taken=time_taken, - status_code=status_code, - response_body=response_body, - ) - except Exception as e: # pylint: disable=broad-except - LOGGER.error( - f"[Cornerstone]: Error occurred while storing API call: {e}" - f"user_agent={user_agent}, user_ip={user_ip}, enterprise_customer={enterprise_customer}" - f"enterprise_customer_configuration_id={enterprise_customer_configuration_id}," - f"endpoint={endpoint}" - f"payload={payload}" - f"time_taken={time_taken}" - f"status_code={status_code}" - f"response_body={response_body}" - ) diff --git a/integrated_channels/cornerstone/views.py b/integrated_channels/cornerstone/views.py index 52b2317796..5ff9e84be0 100644 --- a/integrated_channels/cornerstone/views.py +++ b/integrated_channels/cornerstone/views.py @@ -3,6 +3,7 @@ """ import datetime +import json from logging import getLogger import time @@ -17,7 +18,7 @@ from enterprise.api.throttles import ServiceUserThrottle from enterprise.utils import get_enterprise_customer, get_enterprise_worker_user, get_oauth2authentication_class from integrated_channels.cornerstone.models import CornerstoneEnterpriseCustomerConfiguration -from integrated_channels.cornerstone.utils import store_cornerstone_api_calls +from integrated_channels.utils import store_api_call from integrated_channels.integrated_channel.constants import ISO_8601_DATE_FORMAT logger = getLogger(__name__) @@ -160,16 +161,7 @@ def get(self, request, *args, **kwargs): if_modified_since_dt = datetime.datetime.fromtimestamp(if_modified_since) item['LastModifiedUTC'] = if_modified_since_dt.strftime(ISO_8601_DATE_FORMAT) duration_seconds = time.time() - start_time - store_cornerstone_api_calls( - enterprise_customer=enterprise_customer, - enterprise_customer_configuration_id=enterprise_config.id, - endpoint=request.get_full_path(), - payload=f"Request Headers: {request.headers}", - time_taken=duration_seconds, - status_code=200, - response_body=data, - ) - # TODO remove following logs (temporarily added) + # TODO remove following log (temporarily added) logger.info( f"[Cornerstone]: request.headers={request.headers}" f"GET params={request.GET}" @@ -177,4 +169,16 @@ def get(self, request, *args, **kwargs): f"enterprise_config.id={enterprise_config.id}" f"data={data}" ) + headers_dict = dict(request.headers) + headers_json = json.dumps(headers_dict) + store_api_call( + enterprise_customer=enterprise_customer, + enterprise_customer_configuration_id=enterprise_config.id, + endpoint=request.get_full_path(), + payload=f"Request Headers: {headers_json}", + time_taken=duration_seconds, + status_code=200, + response_body=json.dumps(data), + channel_code="CSOD" + ) return Response(data) diff --git a/integrated_channels/integrated_channel/models.py b/integrated_channels/integrated_channel/models.py index d00a9521c8..d0b57d5fff 100644 --- a/integrated_channels/integrated_channel/models.py +++ b/integrated_channels/integrated_channel/models.py @@ -930,8 +930,8 @@ def __str__(self): f", enterprise_customer_configuration_id: {self.enterprise_customer_configuration_id}>" f", endpoint: {self.endpoint}" f", time_taken: {self.time_taken}" - f", api_record.body: {self.api_record.body}" - f", api_record.status_code: {self.api_record.status_code}" + f", response_body: {self.response_body}" + f", status_code: {self.status_code}" ) def __repr__(self): diff --git a/integrated_channels/utils.py b/integrated_channels/utils.py index 62ba54fa71..1205db3c87 100644 --- a/integrated_channels/utils.py +++ b/integrated_channels/utils.py @@ -476,6 +476,18 @@ def get_enterprise_customer_model(): """ return apps.get_model('enterprise', 'EnterpriseCustomer') +def cornerstone_request_log_model(): + """ + Returns the ``CornerstoneAPIRequestLogs`` class. + """ + return apps.get_model("cornerstone", "CornerstoneAPIRequestLogs") + + +def integrated_channel_request_log_model(): + """ + Returns the ``IntegratedChannelAPIRequestLogs`` class. + """ + return apps.get_model("integrated_channel", "IntegratedChannelAPIRequestLogs") def get_enterprise_customer_from_enterprise_enrollment(enrollment_id): """ @@ -501,3 +513,53 @@ def get_enterprise_client_by_channel_code(channel_code): 'canvas': CanvasAPIClient, } return _enterprise_client_model_by_channel_code[channel_code] + +def store_api_call( + enterprise_customer, + enterprise_customer_configuration_id, + endpoint, + payload, + time_taken, + status_code, + response_body, + channel_code="", + user_agent=None, + user_ip=None, +): + """ + Creates new record in CornerstoneAPIRequestLogs table. + """ + try: + if channel_code == "CSOD": + cornerstone_request_log_model().objects.create( + user_agent=user_agent, + user_ip=user_ip, + enterprise_customer=enterprise_customer, + enterprise_customer_configuration_id=enterprise_customer_configuration_id, + endpoint=endpoint, + payload=payload, + time_taken=time_taken, + status_code=status_code, + response_body=response_body, + ) + else: + integrated_channel_request_log_model().objects.create( + enterprise_customer=enterprise_customer, + enterprise_customer_configuration_id=enterprise_customer_configuration_id, + endpoint=endpoint, + payload=payload, + time_taken=time_taken, + status_code=status_code, + response_body=response_body, + ) + except Exception as e: # pylint: disable=broad-except + LOGGER.error( + f"[{channel_code}]: store_api_call raised error while storing API call: {e}" + f"user_agent={user_agent}, user_ip={user_ip}, enterprise_customer={enterprise_customer}" + f"enterprise_customer_configuration_id={enterprise_customer_configuration_id}," + f"endpoint={endpoint}" + f"payload={payload}" + f"time_taken={time_taken}" + f"status_code={status_code}" + f"response_body={response_body}" + ) From b5d6b33e8713802b549d8ff1e61f5dc1ef4c9aec Mon Sep 17 00:00:00 2001 From: hamzawaleed01 Date: Thu, 25 Jan 2024 14:04:31 +0500 Subject: [PATCH 06/11] feat: use same model for integrated channels API logs --- .../cornerstone/admin/__init__.py | 10 ---- integrated_channels/cornerstone/client.py | 1 - .../0031_cornerstoneapirequestlogs.py | 23 ++-------- .../0032_delete_cornerstoneapirequestlogs.py | 16 +++++++ integrated_channels/cornerstone/models.py | 36 --------------- integrated_channels/cornerstone/utils.py | 2 + integrated_channels/cornerstone/views.py | 15 ++---- .../integrated_channel/admin/__init__.py | 16 ++++++- .../0030_integratedchannelapirequestlogs.py | 9 ++-- ...integratedchannelapirequestlogs_options.py | 17 +++++++ .../migrations/0032_auto_20240125_0936.py | 32 +++++++++++++ .../integrated_channel/models.py | 1 + integrated_channels/utils.py | 46 ++++++------------- 13 files changed, 108 insertions(+), 116 deletions(-) create mode 100644 integrated_channels/cornerstone/migrations/0032_delete_cornerstoneapirequestlogs.py create mode 100644 integrated_channels/integrated_channel/migrations/0031_alter_integratedchannelapirequestlogs_options.py create mode 100644 integrated_channels/integrated_channel/migrations/0032_auto_20240125_0936.py diff --git a/integrated_channels/cornerstone/admin/__init__.py b/integrated_channels/cornerstone/admin/__init__.py index 413da0e385..cb9c791e49 100644 --- a/integrated_channels/cornerstone/admin/__init__.py +++ b/integrated_channels/cornerstone/admin/__init__.py @@ -13,7 +13,6 @@ CornerstoneEnterpriseCustomerConfiguration, CornerstoneGlobalConfiguration, CornerstoneLearnerDataTransmissionAudit, - CornerstoneAPIRequestLogs ) from integrated_channels.integrated_channel.admin import BaseLearnerDataTransmissionAuditAdmin @@ -139,12 +138,3 @@ def user_email(self, obj): being rendered with this admin form. """ return obj.user.email - -@admin.register(CornerstoneAPIRequestLogs) -class CornerstoneAPIRequestLogAdmin(DjangoObjectActions, admin.ModelAdmin): - """ - Django admin model for CornerstoneAPIRequestLogs. - """ - - class Meta: - model = CornerstoneAPIRequestLogs diff --git a/integrated_channels/cornerstone/client.py b/integrated_channels/cornerstone/client.py index 760667dfe0..40e33bd863 100644 --- a/integrated_channels/cornerstone/client.py +++ b/integrated_channels/cornerstone/client.py @@ -121,7 +121,6 @@ def create_course_completion(self, user_id, payload): endpoint=url, payload=json.dumps(json_payload["data"]), time_taken=duration_seconds, - channel_code=self.enterprise_configuration.channel_code(), status_code=response.status_code, response_body=response.text, ) diff --git a/integrated_channels/cornerstone/migrations/0031_cornerstoneapirequestlogs.py b/integrated_channels/cornerstone/migrations/0031_cornerstoneapirequestlogs.py index 914b533ef8..35a3c66b68 100644 --- a/integrated_channels/cornerstone/migrations/0031_cornerstoneapirequestlogs.py +++ b/integrated_channels/cornerstone/migrations/0031_cornerstoneapirequestlogs.py @@ -1,15 +1,13 @@ -# Generated by Django 3.2.23 on 2024-01-23 10:59 +# Generated by Django 3.2.23 on 2024-01-15 07:54 from django.db import migrations, models import django.db.models.deletion -import django.utils.timezone -import model_utils.fields class Migration(migrations.Migration): dependencies = [ - ('enterprise', '0198_alter_enterprisecourseenrollment_options'), + ('integrated_channel', '0030_integratedchannelapirequestlogs'), ('cornerstone', '0030_auto_20231010_1654'), ] @@ -17,21 +15,10 @@ class Migration(migrations.Migration): migrations.CreateModel( name='CornerstoneAPIRequestLogs', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('enterprise_customer_configuration_id', models.IntegerField(help_text='ID from the EnterpriseCustomerConfiguration model')), - ('endpoint', models.TextField()), - ('payload', models.TextField()), - ('time_taken', models.FloatField()), - ('status_code', models.PositiveIntegerField(blank=True, help_text='API call response HTTP status code', null=True)), - ('response_body', models.TextField(blank=True, help_text='API call response body', null=True)), - ('user_agent', models.CharField(blank=True, max_length=255, null=True)), + ('integratedchannelapirequestlogs_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='integrated_channel.integratedchannelapirequestlogs')), + ('user_agent', models.CharField(max_length=255)), ('user_ip', models.GenericIPAddressField(blank=True, null=True)), - ('enterprise_customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='enterprise.enterprisecustomer')), ], - options={ - 'verbose_name_plural': 'Cornerstone API request logs', - }, + bases=('integrated_channel.integratedchannelapirequestlogs',), ), ] diff --git a/integrated_channels/cornerstone/migrations/0032_delete_cornerstoneapirequestlogs.py b/integrated_channels/cornerstone/migrations/0032_delete_cornerstoneapirequestlogs.py new file mode 100644 index 0000000000..932d71c798 --- /dev/null +++ b/integrated_channels/cornerstone/migrations/0032_delete_cornerstoneapirequestlogs.py @@ -0,0 +1,16 @@ +# Generated by Django 3.2.22 on 2024-01-25 09:25 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('cornerstone', '0031_cornerstoneapirequestlogs'), + ] + + operations = [ + migrations.DeleteModel( + name='CornerstoneAPIRequestLogs', + ), + ] diff --git a/integrated_channels/cornerstone/models.py b/integrated_channels/cornerstone/models.py index be18cba9d5..88b0f24e93 100644 --- a/integrated_channels/cornerstone/models.py +++ b/integrated_channels/cornerstone/models.py @@ -18,7 +18,6 @@ from integrated_channels.cornerstone.transmitters.content_metadata import CornerstoneContentMetadataTransmitter from integrated_channels.cornerstone.transmitters.learner_data import CornerstoneLearnerTransmitter from integrated_channels.integrated_channel.models import ( - BaseIntegratedChannelAPIRequestLogs, EnterpriseCustomerPluginConfiguration, LearnerDataTransmissionAudit, ) @@ -319,38 +318,3 @@ class CornerstoneCourseKey(models.Model): class Meta: app_label = 'cornerstone' - - -class CornerstoneAPIRequestLogs(BaseIntegratedChannelAPIRequestLogs): - """ - A model to track basic information about every API call we make from the integrated channels. - """ - - user_agent = models.CharField(blank=True, null=True, max_length=255) - user_ip = models.GenericIPAddressField(blank=True, null=True) - - class Meta: - app_label = "cornerstone" - verbose_name_plural = "Cornerstone API request logs" - - def __str__(self): - """ - Return a human-readable string representation of the object. - """ - return ( - f"" - f", endpoint: {self.endpoint}" - f", time_taken: {self.time_taken}" - f", user_agent: {self.user_agent}" - f", user_ip: {self.user_ip}" - f", response_body: {self.response_body}" - f", status_code: {self.status_code}" - ) - - def __repr__(self): - """ - Return uniquely identifying string representation. - """ - return self.__str__() diff --git a/integrated_channels/cornerstone/utils.py b/integrated_channels/cornerstone/utils.py index c3ea08ad7e..e6c1defbb9 100644 --- a/integrated_channels/cornerstone/utils.py +++ b/integrated_channels/cornerstone/utils.py @@ -21,8 +21,10 @@ def cornerstone_course_key_model(): """ return apps.get_model('cornerstone', 'CornerstoneCourseKey') + LOGGER = getLogger(__name__) + def create_cornerstone_learner_data(request, cornerstone_customer_configuration, course_id): """ updates or creates CornerstoneLearnerDataTransmissionAudit diff --git a/integrated_channels/cornerstone/views.py b/integrated_channels/cornerstone/views.py index 5ff9e84be0..363e2ab2a4 100644 --- a/integrated_channels/cornerstone/views.py +++ b/integrated_channels/cornerstone/views.py @@ -4,8 +4,8 @@ import datetime import json -from logging import getLogger import time +from logging import getLogger from dateutil import parser from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication @@ -18,8 +18,8 @@ from enterprise.api.throttles import ServiceUserThrottle from enterprise.utils import get_enterprise_customer, get_enterprise_worker_user, get_oauth2authentication_class from integrated_channels.cornerstone.models import CornerstoneEnterpriseCustomerConfiguration -from integrated_channels.utils import store_api_call from integrated_channels.integrated_channel.constants import ISO_8601_DATE_FORMAT +from integrated_channels.utils import store_api_call logger = getLogger(__name__) @@ -161,14 +161,6 @@ def get(self, request, *args, **kwargs): if_modified_since_dt = datetime.datetime.fromtimestamp(if_modified_since) item['LastModifiedUTC'] = if_modified_since_dt.strftime(ISO_8601_DATE_FORMAT) duration_seconds = time.time() - start_time - # TODO remove following log (temporarily added) - logger.info( - f"[Cornerstone]: request.headers={request.headers}" - f"GET params={request.GET}" - f"enterprise_config={enterprise_config}" - f"enterprise_config.id={enterprise_config.id}" - f"data={data}" - ) headers_dict = dict(request.headers) headers_json = json.dumps(headers_dict) store_api_call( @@ -178,7 +170,6 @@ def get(self, request, *args, **kwargs): payload=f"Request Headers: {headers_json}", time_taken=duration_seconds, status_code=200, - response_body=json.dumps(data), - channel_code="CSOD" + response_body=json.dumps(data) ) return Response(data) diff --git a/integrated_channels/integrated_channel/admin/__init__.py b/integrated_channels/integrated_channel/admin/__init__.py index f79b1659bb..960b49c182 100644 --- a/integrated_channels/integrated_channel/admin/__init__.py +++ b/integrated_channels/integrated_channel/admin/__init__.py @@ -4,7 +4,11 @@ from django.contrib import admin -from integrated_channels.integrated_channel.models import ApiResponseRecord, ContentMetadataItemTransmission +from integrated_channels.integrated_channel.models import ( + ApiResponseRecord, + ContentMetadataItemTransmission, + IntegratedChannelAPIRequestLogs, +) from integrated_channels.utils import get_enterprise_customer_from_enterprise_enrollment @@ -85,3 +89,13 @@ class ApiResponseRecordAdmin(admin.ModelAdmin): ) list_per_page = 1000 + + +@admin.register(IntegratedChannelAPIRequestLogs) +class CornerstoneAPIRequestLogAdmin(admin.ModelAdmin): + """ + Django admin model for IntegratedChannelAPIRequestLogs. + """ + + class Meta: + model = IntegratedChannelAPIRequestLogs diff --git a/integrated_channels/integrated_channel/migrations/0030_integratedchannelapirequestlogs.py b/integrated_channels/integrated_channel/migrations/0030_integratedchannelapirequestlogs.py index 1e187aeb8c..db8f99c3e5 100644 --- a/integrated_channels/integrated_channel/migrations/0030_integratedchannelapirequestlogs.py +++ b/integrated_channels/integrated_channel/migrations/0030_integratedchannelapirequestlogs.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.23 on 2024-01-23 10:51 +# Generated by Django 3.2.23 on 2024-01-12 07:24 from django.db import migrations, models import django.db.models.deletion @@ -9,7 +9,7 @@ class Migration(migrations.Migration): dependencies = [ - ('enterprise', '0198_alter_enterprisecourseenrollment_options'), + ('enterprise', '0197_auto_20231130_2239'), ('integrated_channel', '0029_genericenterprisecustomerpluginconfiguration_show_course_price'), ] @@ -23,9 +23,8 @@ class Migration(migrations.Migration): ('enterprise_customer_configuration_id', models.IntegerField(help_text='ID from the EnterpriseCustomerConfiguration model')), ('endpoint', models.TextField()), ('payload', models.TextField()), - ('time_taken', models.FloatField()), - ('status_code', models.PositiveIntegerField(blank=True, help_text='API call response HTTP status code', null=True)), - ('response_body', models.TextField(blank=True, help_text='API call response body', null=True)), + ('time_taken', models.DurationField()), + ('api_record', models.OneToOneField(blank=True, help_text='Data pertaining to the transmissions API request response.', null=True, on_delete=django.db.models.deletion.CASCADE, to='integrated_channel.apiresponserecord')), ('enterprise_customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='enterprise.enterprisecustomer')), ], ), diff --git a/integrated_channels/integrated_channel/migrations/0031_alter_integratedchannelapirequestlogs_options.py b/integrated_channels/integrated_channel/migrations/0031_alter_integratedchannelapirequestlogs_options.py new file mode 100644 index 0000000000..5b11a9f695 --- /dev/null +++ b/integrated_channels/integrated_channel/migrations/0031_alter_integratedchannelapirequestlogs_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.22 on 2024-01-25 09:25 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('integrated_channel', '0030_integratedchannelapirequestlogs'), + ] + + operations = [ + migrations.AlterModelOptions( + name='integratedchannelapirequestlogs', + options={'verbose_name_plural': 'Integrated channels API request logs'}, + ), + ] diff --git a/integrated_channels/integrated_channel/migrations/0032_auto_20240125_0936.py b/integrated_channels/integrated_channel/migrations/0032_auto_20240125_0936.py new file mode 100644 index 0000000000..077a1af02d --- /dev/null +++ b/integrated_channels/integrated_channel/migrations/0032_auto_20240125_0936.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.22 on 2024-01-25 09:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('integrated_channel', '0031_alter_integratedchannelapirequestlogs_options'), + ] + + operations = [ + migrations.RemoveField( + model_name='integratedchannelapirequestlogs', + name='api_record', + ), + migrations.AddField( + model_name='integratedchannelapirequestlogs', + name='response_body', + field=models.TextField(blank=True, help_text='API call response body', null=True), + ), + migrations.AddField( + model_name='integratedchannelapirequestlogs', + name='status_code', + field=models.PositiveIntegerField(blank=True, help_text='API call response HTTP status code', null=True), + ), + migrations.AlterField( + model_name='integratedchannelapirequestlogs', + name='time_taken', + field=models.FloatField(), + ), + ] diff --git a/integrated_channels/integrated_channel/models.py b/integrated_channels/integrated_channel/models.py index d0b57d5fff..c5700eb446 100644 --- a/integrated_channels/integrated_channel/models.py +++ b/integrated_channels/integrated_channel/models.py @@ -919,6 +919,7 @@ class IntegratedChannelAPIRequestLogs(BaseIntegratedChannelAPIRequestLogs): class Meta: app_label = "integrated_channel" + verbose_name_plural = "Integrated channels API request logs" def __str__(self): """ diff --git a/integrated_channels/utils.py b/integrated_channels/utils.py index 1205db3c87..bf728c94a9 100644 --- a/integrated_channels/utils.py +++ b/integrated_channels/utils.py @@ -476,12 +476,6 @@ def get_enterprise_customer_model(): """ return apps.get_model('enterprise', 'EnterpriseCustomer') -def cornerstone_request_log_model(): - """ - Returns the ``CornerstoneAPIRequestLogs`` class. - """ - return apps.get_model("cornerstone", "CornerstoneAPIRequestLogs") - def integrated_channel_request_log_model(): """ @@ -489,6 +483,7 @@ def integrated_channel_request_log_model(): """ return apps.get_model("integrated_channel", "IntegratedChannelAPIRequestLogs") + def get_enterprise_customer_from_enterprise_enrollment(enrollment_id): """ Returns the Django ORM enterprise customer object that is associated with an enterprise enrollment ID @@ -514,6 +509,7 @@ def get_enterprise_client_by_channel_code(channel_code): } return _enterprise_client_model_by_channel_code[channel_code] + def store_api_call( enterprise_customer, enterprise_customer_configuration_id, @@ -522,40 +518,24 @@ def store_api_call( time_taken, status_code, response_body, - channel_code="", - user_agent=None, - user_ip=None, ): """ Creates new record in CornerstoneAPIRequestLogs table. """ try: - if channel_code == "CSOD": - cornerstone_request_log_model().objects.create( - user_agent=user_agent, - user_ip=user_ip, - enterprise_customer=enterprise_customer, - enterprise_customer_configuration_id=enterprise_customer_configuration_id, - endpoint=endpoint, - payload=payload, - time_taken=time_taken, - status_code=status_code, - response_body=response_body, - ) - else: - integrated_channel_request_log_model().objects.create( - enterprise_customer=enterprise_customer, - enterprise_customer_configuration_id=enterprise_customer_configuration_id, - endpoint=endpoint, - payload=payload, - time_taken=time_taken, - status_code=status_code, - response_body=response_body, - ) + integrated_channel_request_log_model().objects.create( + enterprise_customer=enterprise_customer, + enterprise_customer_configuration_id=enterprise_customer_configuration_id, + endpoint=endpoint, + payload=payload, + time_taken=time_taken, + status_code=status_code, + response_body=response_body, + ) except Exception as e: # pylint: disable=broad-except LOGGER.error( - f"[{channel_code}]: store_api_call raised error while storing API call: {e}" - f"user_agent={user_agent}, user_ip={user_ip}, enterprise_customer={enterprise_customer}" + f"store_api_call raised error while storing API call: {e}" + f"enterprise_customer={enterprise_customer}" f"enterprise_customer_configuration_id={enterprise_customer_configuration_id}," f"endpoint={endpoint}" f"payload={payload}" From 419100032a606740fd20eb4785b1bd8c58703d90 Mon Sep 17 00:00:00 2001 From: hamzawaleed01 Date: Thu, 25 Jan 2024 15:22:04 +0500 Subject: [PATCH 07/11] fix: update tests --- .../test_integrated_channel/test_models.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_integrated_channels/test_integrated_channel/test_models.py b/tests/test_integrated_channels/test_integrated_channel/test_models.py index 49c4c21bc9..8625ae51d3 100644 --- a/tests/test_integrated_channels/test_integrated_channel/test_models.py +++ b/tests/test_integrated_channels/test_integrated_channel/test_models.py @@ -274,9 +274,8 @@ def setUp(self): self.endpoint = 'https://example.com/endpoint' self.payload = "{}" self.time_taken = 500 - api_record = ApiResponseRecord(status_code=200, body='SUCCESS') - api_record.save() - self.api_record = api_record + self.response_body = "{}" + self.status_code = 200 super().setUp() def test_content_meta_data_string_representation(self): @@ -285,12 +284,12 @@ def test_content_meta_data_string_representation(self): """ expected_string = ( f'' f', endpoint: {self.endpoint}' f', time_taken: {self.time_taken}' - f', api_record.body: {self.api_record.body}' - f', api_record.status_code: {self.api_record.status_code}' + f", response_body: {self.response_body}" + f", status_code: {self.status_code}" ) request_log = IntegratedChannelAPIRequestLogs( @@ -300,6 +299,7 @@ def test_content_meta_data_string_representation(self): endpoint=self.endpoint, payload=self.payload, time_taken=self.time_taken, - api_record=self.api_record + response_body=self.response_body, + status_code=self.status_code ) assert expected_string == repr(request_log) From 317e4ee3139bc69fcd32d13435941215f518969a Mon Sep 17 00:00:00 2001 From: hamzawaleed01 Date: Thu, 25 Jan 2024 16:40:51 +0500 Subject: [PATCH 08/11] chore: bump version + add changelog --- CHANGELOG.rst | 5 +++++ enterprise/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dbe6228f6c..77bf5295e0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,11 @@ Change Log Unreleased ---------- +[4.11.0] +-------- + +feat: update cornerstone client to store API calls in DB + [4.10.9] -------- diff --git a/enterprise/__init__.py b/enterprise/__init__.py index dd3b6160f6..f2d38d70bf 100644 --- a/enterprise/__init__.py +++ b/enterprise/__init__.py @@ -2,4 +2,4 @@ Your project description goes here. """ -__version__ = "4.10.9" +__version__ = "4.11.0" From c9bff319439481ccde6c46d38cd892ab4d769706 Mon Sep 17 00:00:00 2001 From: hamzawaleed01 Date: Tue, 30 Jan 2024 13:04:50 +0500 Subject: [PATCH 09/11] refactor: move util function to classmethod --- integrated_channels/cornerstone/client.py | 7 ++- integrated_channels/cornerstone/views.py | 7 ++- .../integrated_channel/admin/__init__.py | 10 ---- ...ntegratedchannelapirequestlogs_endpoint.py | 18 +++++++ .../integrated_channel/models.py | 53 ++++++++++++++----- integrated_channels/utils.py | 42 --------------- 6 files changed, 69 insertions(+), 68 deletions(-) create mode 100644 integrated_channels/integrated_channel/migrations/0033_alter_integratedchannelapirequestlogs_endpoint.py diff --git a/integrated_channels/cornerstone/client.py b/integrated_channels/cornerstone/client.py index 40e33bd863..72362bd4ee 100644 --- a/integrated_channels/cornerstone/client.py +++ b/integrated_channels/cornerstone/client.py @@ -13,7 +13,7 @@ from integrated_channels.cornerstone.utils import get_or_create_key_pair from integrated_channels.integrated_channel.client import IntegratedChannelApiClient -from integrated_channels.utils import generate_formatted_log, store_api_call +from integrated_channels.utils import generate_formatted_log LOGGER = logging.getLogger(__name__) @@ -36,6 +36,9 @@ def __init__(self, enterprise_configuration): """ super().__init__(enterprise_configuration) self.global_cornerstone_config = apps.get_model('cornerstone', 'CornerstoneGlobalConfiguration').current() + self.IntegratedChannelAPIRequestLogs = apps.get_model( + "integrated_channel", "IntegratedChannelAPIRequestLogs" + ) self.session = None self.expires_at = None @@ -115,7 +118,7 @@ def create_course_completion(self, user_id, payload): } ) duration_seconds = time.time() - start_time - store_api_call( + self.IntegratedChannelAPIRequestLogs.store_api_call( enterprise_customer=self.enterprise_configuration.enterprise_customer, enterprise_customer_configuration_id=self.enterprise_configuration.id, endpoint=url, diff --git a/integrated_channels/cornerstone/views.py b/integrated_channels/cornerstone/views.py index 363e2ab2a4..9ef4b9e015 100644 --- a/integrated_channels/cornerstone/views.py +++ b/integrated_channels/cornerstone/views.py @@ -15,11 +15,11 @@ from django.utils.http import parse_http_date_safe +from django.apps import apps from enterprise.api.throttles import ServiceUserThrottle from enterprise.utils import get_enterprise_customer, get_enterprise_worker_user, get_oauth2authentication_class from integrated_channels.cornerstone.models import CornerstoneEnterpriseCustomerConfiguration from integrated_channels.integrated_channel.constants import ISO_8601_DATE_FORMAT -from integrated_channels.utils import store_api_call logger = getLogger(__name__) @@ -103,6 +103,9 @@ class CornerstoneCoursesListView(BaseViewSet): def get(self, request, *args, **kwargs): start_time = time.time() enterprise_customer_uuid = request.GET.get('ciid') + IntegratedChannelAPIRequestLogs = apps.get_model( + "integrated_channel", "IntegratedChannelAPIRequestLogs" + ) if not enterprise_customer_uuid: return Response( status=status.HTTP_400_BAD_REQUEST, @@ -163,7 +166,7 @@ def get(self, request, *args, **kwargs): duration_seconds = time.time() - start_time headers_dict = dict(request.headers) headers_json = json.dumps(headers_dict) - store_api_call( + IntegratedChannelAPIRequestLogs.store_api_call( enterprise_customer=enterprise_customer, enterprise_customer_configuration_id=enterprise_config.id, endpoint=request.get_full_path(), diff --git a/integrated_channels/integrated_channel/admin/__init__.py b/integrated_channels/integrated_channel/admin/__init__.py index 960b49c182..27dd98e384 100644 --- a/integrated_channels/integrated_channel/admin/__init__.py +++ b/integrated_channels/integrated_channel/admin/__init__.py @@ -89,13 +89,3 @@ class ApiResponseRecordAdmin(admin.ModelAdmin): ) list_per_page = 1000 - - -@admin.register(IntegratedChannelAPIRequestLogs) -class CornerstoneAPIRequestLogAdmin(admin.ModelAdmin): - """ - Django admin model for IntegratedChannelAPIRequestLogs. - """ - - class Meta: - model = IntegratedChannelAPIRequestLogs diff --git a/integrated_channels/integrated_channel/migrations/0033_alter_integratedchannelapirequestlogs_endpoint.py b/integrated_channels/integrated_channel/migrations/0033_alter_integratedchannelapirequestlogs_endpoint.py new file mode 100644 index 0000000000..b5220703ed --- /dev/null +++ b/integrated_channels/integrated_channel/migrations/0033_alter_integratedchannelapirequestlogs_endpoint.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.22 on 2024-01-29 13:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('integrated_channel', '0032_auto_20240125_0936'), + ] + + operations = [ + migrations.AlterField( + model_name='integratedchannelapirequestlogs', + name='endpoint', + field=models.URLField(max_length=255), + ), + ] diff --git a/integrated_channels/integrated_channel/models.py b/integrated_channels/integrated_channel/models.py index c5700eb446..1eaf5e4ee1 100644 --- a/integrated_channels/integrated_channel/models.py +++ b/integrated_channels/integrated_channel/models.py @@ -881,7 +881,7 @@ class Meta: resolved = models.BooleanField(default=False) -class BaseIntegratedChannelAPIRequestLogs(TimeStampedModel): +class IntegratedChannelAPIRequestLogs(TimeStampedModel): """ A model to track basic information about every API call we make from the integrated channels. """ @@ -894,8 +894,9 @@ class BaseIntegratedChannelAPIRequestLogs(TimeStampedModel): null=False, help_text="ID from the EnterpriseCustomerConfiguration model", ) - endpoint = models.TextField( + endpoint = models.URLField( blank=False, + max_length=255, null=False, ) payload = models.TextField(blank=False, null=False) @@ -907,16 +908,6 @@ class BaseIntegratedChannelAPIRequestLogs(TimeStampedModel): help_text="API call response body", blank=True, null=True ) - class Meta: - app_label = "integrated_channel" - abstract = True - - -class IntegratedChannelAPIRequestLogs(BaseIntegratedChannelAPIRequestLogs): - """ - A model to track basic information about every API call we make from the integrated channels. - """ - class Meta: app_label = "integrated_channel" verbose_name_plural = "Integrated channels API request logs" @@ -940,3 +931,41 @@ def __repr__(self): Return uniquely identifying string representation. """ return self.__str__() + + + @classmethod + def store_api_call( + cls, + enterprise_customer, + enterprise_customer_configuration_id, + endpoint, + payload, + time_taken, + status_code, + response_body, + ): + """ + Creates new record in IntegratedChannelAPIRequestLogs table. + """ + try: + record = cls( + enterprise_customer=enterprise_customer, + enterprise_customer_configuration_id=enterprise_customer_configuration_id, + endpoint=endpoint, + payload=payload, + time_taken=time_taken, + status_code=status_code, + response_body=response_body, + ) + record.save() + except Exception as e: # pylint: disable=broad-except + LOGGER.error( + f"store_api_call raised error while storing API call: {e}" + f"enterprise_customer={enterprise_customer}" + f"enterprise_customer_configuration_id={enterprise_customer_configuration_id}," + f"endpoint={endpoint}" + f"payload={payload}" + f"time_taken={time_taken}" + f"status_code={status_code}" + f"response_body={response_body}" + ) diff --git a/integrated_channels/utils.py b/integrated_channels/utils.py index bf728c94a9..62ba54fa71 100644 --- a/integrated_channels/utils.py +++ b/integrated_channels/utils.py @@ -477,13 +477,6 @@ def get_enterprise_customer_model(): return apps.get_model('enterprise', 'EnterpriseCustomer') -def integrated_channel_request_log_model(): - """ - Returns the ``IntegratedChannelAPIRequestLogs`` class. - """ - return apps.get_model("integrated_channel", "IntegratedChannelAPIRequestLogs") - - def get_enterprise_customer_from_enterprise_enrollment(enrollment_id): """ Returns the Django ORM enterprise customer object that is associated with an enterprise enrollment ID @@ -508,38 +501,3 @@ def get_enterprise_client_by_channel_code(channel_code): 'canvas': CanvasAPIClient, } return _enterprise_client_model_by_channel_code[channel_code] - - -def store_api_call( - enterprise_customer, - enterprise_customer_configuration_id, - endpoint, - payload, - time_taken, - status_code, - response_body, -): - """ - Creates new record in CornerstoneAPIRequestLogs table. - """ - try: - integrated_channel_request_log_model().objects.create( - enterprise_customer=enterprise_customer, - enterprise_customer_configuration_id=enterprise_customer_configuration_id, - endpoint=endpoint, - payload=payload, - time_taken=time_taken, - status_code=status_code, - response_body=response_body, - ) - except Exception as e: # pylint: disable=broad-except - LOGGER.error( - f"store_api_call raised error while storing API call: {e}" - f"enterprise_customer={enterprise_customer}" - f"enterprise_customer_configuration_id={enterprise_customer_configuration_id}," - f"endpoint={endpoint}" - f"payload={payload}" - f"time_taken={time_taken}" - f"status_code={status_code}" - f"response_body={response_body}" - ) From d5bd99d3f5b21f785adc0c9ba0429de8259e0094 Mon Sep 17 00:00:00 2001 From: hamzawaleed01 Date: Tue, 30 Jan 2024 13:17:24 +0500 Subject: [PATCH 10/11] refactor: squash migrations --- integrated_channels/cornerstone/views.py | 2 +- ...integratedchannelapirequestlogs_options.py | 21 +++++++++++- ...tegratedchannelapirequestlogs_endpoint.py} | 2 +- .../migrations/0032_auto_20240125_0936.py | 32 ------------------- .../integrated_channel/models.py | 1 - .../test_cornerstone/test_client.py | 11 +++++-- 6 files changed, 31 insertions(+), 38 deletions(-) rename integrated_channels/integrated_channel/migrations/{0033_alter_integratedchannelapirequestlogs_endpoint.py => 0032_alter_integratedchannelapirequestlogs_endpoint.py} (80%) delete mode 100644 integrated_channels/integrated_channel/migrations/0032_auto_20240125_0936.py diff --git a/integrated_channels/cornerstone/views.py b/integrated_channels/cornerstone/views.py index 9ef4b9e015..47308b86ff 100644 --- a/integrated_channels/cornerstone/views.py +++ b/integrated_channels/cornerstone/views.py @@ -13,9 +13,9 @@ from rest_framework.authentication import SessionAuthentication from rest_framework.response import Response +from django.apps import apps from django.utils.http import parse_http_date_safe -from django.apps import apps from enterprise.api.throttles import ServiceUserThrottle from enterprise.utils import get_enterprise_customer, get_enterprise_worker_user, get_oauth2authentication_class from integrated_channels.cornerstone.models import CornerstoneEnterpriseCustomerConfiguration diff --git a/integrated_channels/integrated_channel/migrations/0031_alter_integratedchannelapirequestlogs_options.py b/integrated_channels/integrated_channel/migrations/0031_alter_integratedchannelapirequestlogs_options.py index 5b11a9f695..7f04148866 100644 --- a/integrated_channels/integrated_channel/migrations/0031_alter_integratedchannelapirequestlogs_options.py +++ b/integrated_channels/integrated_channel/migrations/0031_alter_integratedchannelapirequestlogs_options.py @@ -1,6 +1,6 @@ # Generated by Django 3.2.22 on 2024-01-25 09:25 -from django.db import migrations +from django.db import migrations, models class Migration(migrations.Migration): @@ -14,4 +14,23 @@ class Migration(migrations.Migration): name='integratedchannelapirequestlogs', options={'verbose_name_plural': 'Integrated channels API request logs'}, ), + migrations.RemoveField( + model_name='integratedchannelapirequestlogs', + name='api_record', + ), + migrations.AddField( + model_name='integratedchannelapirequestlogs', + name='response_body', + field=models.TextField(blank=True, help_text='API call response body', null=True), + ), + migrations.AddField( + model_name='integratedchannelapirequestlogs', + name='status_code', + field=models.PositiveIntegerField(blank=True, help_text='API call response HTTP status code', null=True), + ), + migrations.AlterField( + model_name='integratedchannelapirequestlogs', + name='time_taken', + field=models.FloatField(), + ), ] diff --git a/integrated_channels/integrated_channel/migrations/0033_alter_integratedchannelapirequestlogs_endpoint.py b/integrated_channels/integrated_channel/migrations/0032_alter_integratedchannelapirequestlogs_endpoint.py similarity index 80% rename from integrated_channels/integrated_channel/migrations/0033_alter_integratedchannelapirequestlogs_endpoint.py rename to integrated_channels/integrated_channel/migrations/0032_alter_integratedchannelapirequestlogs_endpoint.py index b5220703ed..ccd5d2b95c 100644 --- a/integrated_channels/integrated_channel/migrations/0033_alter_integratedchannelapirequestlogs_endpoint.py +++ b/integrated_channels/integrated_channel/migrations/0032_alter_integratedchannelapirequestlogs_endpoint.py @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ('integrated_channel', '0032_auto_20240125_0936'), + ('integrated_channel', '0031_alter_integratedchannelapirequestlogs_options'), ] operations = [ diff --git a/integrated_channels/integrated_channel/migrations/0032_auto_20240125_0936.py b/integrated_channels/integrated_channel/migrations/0032_auto_20240125_0936.py deleted file mode 100644 index 077a1af02d..0000000000 --- a/integrated_channels/integrated_channel/migrations/0032_auto_20240125_0936.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 3.2.22 on 2024-01-25 09:36 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('integrated_channel', '0031_alter_integratedchannelapirequestlogs_options'), - ] - - operations = [ - migrations.RemoveField( - model_name='integratedchannelapirequestlogs', - name='api_record', - ), - migrations.AddField( - model_name='integratedchannelapirequestlogs', - name='response_body', - field=models.TextField(blank=True, help_text='API call response body', null=True), - ), - migrations.AddField( - model_name='integratedchannelapirequestlogs', - name='status_code', - field=models.PositiveIntegerField(blank=True, help_text='API call response HTTP status code', null=True), - ), - migrations.AlterField( - model_name='integratedchannelapirequestlogs', - name='time_taken', - field=models.FloatField(), - ), - ] diff --git a/integrated_channels/integrated_channel/models.py b/integrated_channels/integrated_channel/models.py index 1eaf5e4ee1..44ab495e35 100644 --- a/integrated_channels/integrated_channel/models.py +++ b/integrated_channels/integrated_channel/models.py @@ -932,7 +932,6 @@ def __repr__(self): """ return self.__str__() - @classmethod def store_api_call( cls, diff --git a/tests/test_integrated_channels/test_cornerstone/test_client.py b/tests/test_integrated_channels/test_cornerstone/test_client.py index 06a1f0ddb4..ffc5b07aed 100644 --- a/tests/test_integrated_channels/test_cornerstone/test_client.py +++ b/tests/test_integrated_channels/test_cornerstone/test_client.py @@ -8,9 +8,15 @@ import pytest import responses +from django.apps import apps + from integrated_channels.cornerstone.client import CornerstoneAPIClient from test_utils import factories +IntegratedChannelAPIRequestLogs = apps.get_model( + "integrated_channel", "IntegratedChannelAPIRequestLogs" +) + @pytest.mark.django_db class TestCornerstoneApiClient(unittest.TestCase): @@ -26,7 +32,7 @@ def setUp(self): ) @responses.activate - def test_create_course_completion(self): + def test_create_course_completion_stores_api_record(self): """ ``create_course_completion`` should use the appropriate URLs for transmission. """ @@ -47,9 +53,10 @@ def test_create_course_completion(self): json="{}", status=200, ) + assert IntegratedChannelAPIRequestLogs.objects.count() == 0 output = cornerstone_api_client.create_course_completion( "test-learner@example.com", json.dumps(payload) ) - + assert IntegratedChannelAPIRequestLogs.objects.count() == 1 assert len(responses.calls) == 1 assert output == (200, '"{}"') From 716a0cd9b9ea1144dab77387ed2ad95e2943d118 Mon Sep 17 00:00:00 2001 From: hamzawaleed01 Date: Wed, 31 Jan 2024 14:55:43 +0500 Subject: [PATCH 11/11] refactor: IntegratedChannelAPIRequestLogs not a class attribute anymore --- integrated_channels/cornerstone/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/integrated_channels/cornerstone/client.py b/integrated_channels/cornerstone/client.py index 72362bd4ee..6f4a74244e 100644 --- a/integrated_channels/cornerstone/client.py +++ b/integrated_channels/cornerstone/client.py @@ -36,9 +36,6 @@ def __init__(self, enterprise_configuration): """ super().__init__(enterprise_configuration) self.global_cornerstone_config = apps.get_model('cornerstone', 'CornerstoneGlobalConfiguration').current() - self.IntegratedChannelAPIRequestLogs = apps.get_model( - "integrated_channel", "IntegratedChannelAPIRequestLogs" - ) self.session = None self.expires_at = None @@ -91,6 +88,9 @@ def create_course_completion(self, user_id, payload): Raises: HTTPError: if we received a failure response code from Cornerstone """ + IntegratedChannelAPIRequestLogs = apps.get_model( + "integrated_channel", "IntegratedChannelAPIRequestLogs" + ) json_payload = json.loads(payload) callback_url = json_payload['data'].pop('callbackUrl') session_token = self.enterprise_configuration.session_token @@ -118,7 +118,7 @@ def create_course_completion(self, user_id, payload): } ) duration_seconds = time.time() - start_time - self.IntegratedChannelAPIRequestLogs.store_api_call( + IntegratedChannelAPIRequestLogs.store_api_call( enterprise_customer=self.enterprise_configuration.enterprise_customer, enterprise_customer_configuration_id=self.enterprise_configuration.id, endpoint=url,