Skip to content

Commit

Permalink
Merge pull request #543 from edx/hasnain-naveed/ENT-2158
Browse files Browse the repository at this point in the history
ENT-2158 | Added XAPILearnerDataTransmissionAudit model and more logg…
  • Loading branch information
hasnain-naveed authored Aug 23, 2019
2 parents 0fc021c + b99b32e commit 8775119
Show file tree
Hide file tree
Showing 13 changed files with 362 additions and 73 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ Change Log
Unreleased
----------

[1.9.6] - 2019-08-23
--------------------

* Added XAPILearnerDataTransmissionAudit model for xapi integrated channel.

[1.9.5] - 2019-08-21
--------------------

Expand Down
2 changes: 1 addition & 1 deletion enterprise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@

from __future__ import absolute_import, unicode_literals

__version__ = "1.9.5"
__version__ = "1.9.6"

default_app_config = "enterprise.apps.EnterpriseConfig" # pylint: disable=invalid-name
25 changes: 25 additions & 0 deletions enterprise/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,31 @@ def is_audit_enrollment(self):
audit_modes = getattr(settings, 'ENTERPRISE_COURSE_ENROLLMENT_AUDIT_MODES', ['audit', 'honor'])
return course_enrollment and course_enrollment.get('mode') in audit_modes

@classmethod
def get_enterprise_course_enrollment_id(cls, user, course_id, enterprise_customer):
"""
Return the EnterpriseCourseEnrollment object for a given user in given course_id.
"""
enterprise_course_enrollment_id = None
try:
enterprise_course_enrollment_id = cls.objects.get(
enterprise_customer_user=EnterpriseCustomerUser.objects.get(
enterprise_customer=enterprise_customer,
user_id=user.id
),
course_id=course_id
).id
except ObjectDoesNotExist:
LOGGER.info(
'EnterpriseCourseEnrollment entry not found for user: {username}, course: {course_id}, '
'enterprise_customer: {enterprise_customer_name}'.format(
username=user.username,
course_id=course_id,
enterprise_customer_name=enterprise_customer.name
)
)
return enterprise_course_enrollment_id

def __str__(self):
"""
Create string representation of the enrollment.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from enterprise.models import EnterpriseCustomer
from enterprise.utils import NotConnectedToOpenEdX
from integrated_channels.exceptions import ClientError
from integrated_channels.xapi.models import XAPILRSConfiguration
from integrated_channels.xapi.models import XAPILearnerDataTransmissionAudit, XAPILRSConfiguration
from integrated_channels.xapi.utils import send_course_completion_statement

try:
Expand Down Expand Up @@ -133,18 +133,44 @@ def send_xapi_statements(self, lrs_configuration, days):
course_overviews = self.prefetch_courses(persistent_course_grades)

for persistent_course_grade in persistent_course_grades:
error_message = None
user = users.get(persistent_course_grade.user_id)
course_overview = course_overviews.get(persistent_course_grade.course_id)
course_grade = CourseGradeFactory().read(user, course_key=persistent_course_grade.course_id)
xapi_transmission_queryset = XAPILearnerDataTransmissionAudit.objects.filter(
user=user,
course_id=persistent_course_grade.course_id
)
if not xapi_transmission_queryset.exists():
LOGGER.warning(
'XAPILearnerDataTransmissionAudit object does not exist for user: {username}, course: '
'{course_id} so skipping the course completion statement to xapi.'
)
continue
try:
user = users.get(persistent_course_grade.user_id)
course_overview = course_overviews.get(persistent_course_grade.course_id)
course_grade = CourseGradeFactory().read(user, course_key=persistent_course_grade.course_id)
send_course_completion_statement(lrs_configuration, user, course_overview, course_grade)
except ClientError:
LOGGER.exception(
'Client error while sending course completion to xAPI for'
' enterprise customer {enterprise_customer}.'.format(
enterprise_customer=lrs_configuration.enterprise_customer.name
error_message = 'Client error while sending course completion to xAPI for ' \
'enterprise customer: {enterprise_customer}, user: {username} ' \
'and course: {course_id}'.format(
enterprise_customer=lrs_configuration.enterprise_customer.name,
username=user.username if user else '',
course_id=persistent_course_grade.course_id
)
LOGGER.exception(error_message)
status = 500
else:
LOGGER.info(
'Successfully sent course completion to xAPI for user: {username} for course: {course_id}'.format(
username=user.username if user else '',
course_id=persistent_course_grade.course_id
)
)
status = 200
fields = {'status': status, 'error_message': error_message}
if status == 200:
fields.update({'grade': course_grade.percent, 'timestamp': course_grade.passed_timestamp})
xapi_transmission_queryset.update(**fields)

def get_course_completions(self, enterprise_customer, days):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
import datetime
from logging import getLogger

import six

from django.core.management.base import BaseCommand, CommandError

from enterprise.models import EnterpriseCustomer
from enterprise.models import EnterpriseCourseEnrollment, EnterpriseCustomer
from enterprise.utils import NotConnectedToOpenEdX
from integrated_channels.exceptions import ClientError
from integrated_channels.xapi.models import XAPILRSConfiguration
from integrated_channels.xapi.models import XAPILearnerDataTransmissionAudit, XAPILRSConfiguration
from integrated_channels.xapi.utils import send_course_enrollment_statement

try:
Expand Down Expand Up @@ -118,13 +120,49 @@ def send_xapi_statements(self, lrs_configuration, days):
days (int): Include course enrollment of this number of days.
"""
for course_enrollment in self.get_course_enrollments(lrs_configuration.enterprise_customer, days):
error_message = None
course_id = six.text_type(course_enrollment.course.id)
try:
send_course_enrollment_statement(lrs_configuration, course_enrollment)
except ClientError:
LOGGER.exception(
'Client error while sending course enrollment to xAPI for'
' enterprise customer {enterprise_customer}.'.format(
enterprise_customer=lrs_configuration.enterprise_customer.name
error_message = 'Client error while sending course enrollment to xAPI for ' \
'enterprise customer: {enterprise_customer}, user: {username} ' \
'and course: {course_id}'.format(
enterprise_customer=lrs_configuration.enterprise_customer.name,
username=course_enrollment.user.username,
course_id=course_id
)
LOGGER.exception(error_message)
status = 500
else:
LOGGER.info(
'Successfully send the course enrollment to xAPI for user: {username} for course: '
"{course_id}".format(
username=course_enrollment.user.username,
course_id=course_id
)
)
status = 200
xapi_transmission, created = XAPILearnerDataTransmissionAudit.objects.get_or_create(
user=course_enrollment.user,
course_id=course_id,
defaults={
'enterprise_course_enrollment_id': EnterpriseCourseEnrollment.get_enterprise_course_enrollment_id(
course_enrollment.user,
course_id,
lrs_configuration.enterprise_customer
),
'status': status,
'error_message': error_message
}
)
if created:
LOGGER.info(
"Successfully created the XAPILearnerDataTransmissionAudit object with id: {id}, user: {username}"
" and course: {course_id}".format(
id=xapi_transmission.id,
username=xapi_transmission.user.username,
course_id=xapi_transmission.course_id
)
)

Expand Down
41 changes: 41 additions & 0 deletions integrated_channels/xapi/migrations/0003_auto_20190807_1006.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.22 on 2019-08-07 10:06
from __future__ import unicode_literals

import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models

import model_utils.fields


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('xapi', '0002_auto_20180726_0142'),
]

operations = [
migrations.CreateModel(
name='XAPILearnerDataTransmissionAudit',
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_course_enrollment_id', models.PositiveIntegerField(blank=True, db_index=True, null=True)),
('course_id', models.CharField(db_index=True, max_length=255)),
('course_completed', models.BooleanField(default=False)),
('completed_timestamp', models.DateTimeField(blank=True, null=True)),
('grade', models.CharField(blank=True, max_length=255, null=True)),
('status', models.CharField(max_length=100)),
('error_message', models.TextField(blank=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='xapi_transmission_audit', to=settings.AUTH_USER_MODEL)),
],
),
migrations.AlterUniqueTogether(
name='xapilearnerdatatransmissionaudit',
unique_together=set([('user', 'course_id')]),
),
]
43 changes: 43 additions & 0 deletions integrated_channels/xapi/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import base64

from django.contrib.auth.models import User
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
Expand Down Expand Up @@ -65,3 +66,45 @@ def authorization_header(self):
return 'Basic {}'.format(
base64.b64encode('{key}:{secret}'.format(key=self.key, secret=self.secret).encode()).decode()
)


@python_2_unicode_compatible
class XAPILearnerDataTransmissionAudit(TimeStampedModel):
"""
The payload we sent to XAPI at a given point in time for an enterprise course enrollment.
.. no_pii:
"""

user = models.ForeignKey(
User,
blank=False,
null=False,
related_name='xapi_transmission_audit',
on_delete=models.CASCADE,
)
enterprise_course_enrollment_id = models.PositiveIntegerField(db_index=True, blank=True, null=True)
course_id = models.CharField(max_length=255, blank=False, null=False, db_index=True)
course_completed = models.BooleanField(default=False)
completed_timestamp = models.DateTimeField(null=True, blank=True)
grade = models.CharField(max_length=255, null=True, blank=True)
status = models.CharField(max_length=100, blank=False, null=False)
error_message = models.TextField(blank=True)

class Meta:
app_label = 'xapi'
unique_together = ("user", "course_id")

def __str__(self):
"""
Return a human-readable string representation of the object.
"""
return (
'<XAPILearnerDataTransmissionAudit {transmission_id} for enterprise enrollment '
'{enterprise_course_enrollment_id}, XAPI user {user_id}, and course {course_id}>'.format(
transmission_id=self.id,
enterprise_course_enrollment_id=self.enterprise_course_enrollment_id,
user_id=self.user.id,
course_id=self.course_id
)
)
21 changes: 19 additions & 2 deletions integrated_channels/xapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@

from __future__ import absolute_import, unicode_literals

import logging

import six

from integrated_channels.xapi.client import EnterpriseXAPIClient
from integrated_channels.xapi.serializers import CourseInfoSerializer, LearnerInfoSerializer
from integrated_channels.xapi.statements.learner_course_completion import LearnerCourseCompletionStatement
from integrated_channels.xapi.statements.learner_course_enrollment import LearnerCourseEnrollmentStatement

LOGGER = logging.getLogger(__name__)


def send_course_enrollment_statement(lrs_configuration, course_enrollment):
"""
Expand All @@ -25,7 +31,12 @@ def send_course_enrollment_statement(lrs_configuration, course_enrollment):
course_enrollment.course,
context={'site': lrs_configuration.enterprise_customer.site}
)

LOGGER.info(
'Sending course enrollment to xAPI for user: {username} for course: {course_key}'.format(
username=course_enrollment.user.username,
course_key=six.text_type(course_enrollment.course.id)
)
)
statement = LearnerCourseEnrollmentStatement(
course_enrollment.user,
course_enrollment.course,
Expand All @@ -50,7 +61,13 @@ def send_course_completion_statement(lrs_configuration, user, course_overview, c
course_overview,
context={'site': lrs_configuration.enterprise_customer.site}
)

LOGGER.info(
'Sending course completion to xAPI for user: {username}, course: {course_key} with {percentage}%'.format(
username=user.username if user else '',
course_key=six.text_type(course_overview.id),
percentage=course_grade.percent * 100
)
)
statement = LearnerCourseCompletionStatement(
user,
course_overview,
Expand Down
20 changes: 19 additions & 1 deletion test_utils/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
SAPSuccessFactorsGlobalConfiguration,
SapSuccessFactorsLearnerDataTransmissionAudit,
)
from integrated_channels.xapi.models import XAPILRSConfiguration
from integrated_channels.xapi.models import XAPILearnerDataTransmissionAudit, XAPILRSConfiguration

FAKER = FakerFactory.create()

Expand Down Expand Up @@ -590,3 +590,21 @@ class Meta(object):
key = factory.LazyAttribute(lambda x: FAKER.slug())
secret = factory.LazyAttribute(lambda x: FAKER.uuid4())
active = True


class XAPILearnerDataTransmissionAuditFactory(factory.django.DjangoModelFactory):
"""
``XAPILearnerDataTransmissionAudit`` factory.
Creates an instance of ``XAPILearnerDataTransmissionAudit`` with minimal boilerplate.
"""

class Meta(object):
"""
Meta for ``XAPILearnerDataTransmissionAuditFactory``.
"""

model = XAPILearnerDataTransmissionAudit

user_id = factory.LazyAttribute(lambda x: FAKER.pyint())
course_id = factory.LazyAttribute(lambda x: FAKER.slug())
Loading

0 comments on commit 8775119

Please sign in to comment.