Skip to content

Commit

Permalink
Merge pull request #686 from edx/ENT-871-TotalHours-field-for-Success…
Browse files Browse the repository at this point in the history
…Factors-data-export

ENT-871 Added total hours field for SuccessFactors export
  • Loading branch information
irfanuddinahmad authored Jan 27, 2020
2 parents 7123cae + 3fdfcb8 commit b79ea4d
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 31 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ Change Log

.. There should always be an "Unreleased" section for changes pending release.
[2.1.5] - 2020-01-27
---------------------

* Added totalHours field for successfactors export

[2.1.4] - 2020-01-24
---------------------

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__ = "2.1.4"
__version__ = "2.1.5"

default_app_config = "enterprise.apps.EnterpriseConfig" # pylint: disable=invalid-name
9 changes: 2 additions & 7 deletions integrated_channels/cornerstone/exporters/content_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from __future__ import absolute_import, unicode_literals

import datetime
import math
from logging import getLogger

import pytz
Expand All @@ -15,7 +14,7 @@

from enterprise.utils import get_closest_course_run, get_language_code
from integrated_channels.integrated_channel.exporters.content_metadata import ContentMetadataExporter
from integrated_channels.utils import get_image_url
from integrated_channels.utils import get_duration_from_estimated_hours, get_image_url

LOGGER = getLogger(__name__)

Expand Down Expand Up @@ -91,11 +90,7 @@ def transform_estimated_hours(self, content_metadata_item):
if course_runs:
closest_course_run = get_closest_course_run(course_runs)
estimated_hours = closest_course_run.get('estimated_hours')
if estimated_hours and isinstance(estimated_hours, (int, float)):
fraction, whole_number = math.modf(estimated_hours)
hours = "{:02d}".format(int(whole_number))
minutes = "{:02d}".format(int(60 * fraction))
duration = "{hours}:{minutes}:00".format(hours=hours, minutes=minutes)
duration = get_duration_from_estimated_hours(estimated_hours)

return duration

Expand Down
1 change: 1 addition & 0 deletions integrated_channels/sap_success_factors/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class SAPSuccessFactorsEnterpriseCustomerConfigurationAdmin(admin.ModelAdmin):
'sapsf_user_id',
'user_type',
'has_access_token',
'show_total_hours',
'transmission_chunk_size',
'additional_locales',
'catalogs_to_transmit',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
UNIX_MAX_DATE_STRING,
UNIX_MIN_DATE_STRING,
current_time_is_in_interval,
get_duration_from_estimated_hours,
get_image_url,
parse_datetime_to_epoch_millis,
)
Expand All @@ -40,6 +41,7 @@ class SapSuccessFactorsContentMetadataExporter(ContentMetadataExporter): # pyli
'title': 'title',
'description': 'description',
'thumbnailURI': 'image',
'totalHours': 'estimated_hours',
'content': 'launch_points',
'revisionNumber': 'revision_number',
'schedule': 'schedule',
Expand Down Expand Up @@ -109,6 +111,19 @@ def transform_description(self, content_metadata_item):

return description_with_locales

def transform_estimated_hours(self, content_metadata_item):
"""
Return the duration of course in hh:mm:ss format.
"""
duration = 'Not-Available'
if self.enterprise_configuration.show_total_hours:
course_runs = content_metadata_item.get('course_runs')
if course_runs:
closest_course_run = get_closest_course_run(course_runs)
estimated_hours = closest_course_run.get('estimated_hours')
duration = get_duration_from_estimated_hours(estimated_hours)
return duration

def transform_image(self, content_metadata_item):
"""
Return the image URI of the content item.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.27 on 2020-01-21 13:41
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('sap_success_factors', '0020_sapsuccessfactorsenterprisecustomerconfiguration_catalogs_to_transmit'),
]

operations = [
migrations.AddField(
model_name='sapsuccessfactorsenterprisecustomerconfiguration',
name='show_total_hours',
field=models.BooleanField(default=False, verbose_name='Show Total Hours'),
),
]
1 change: 1 addition & 0 deletions integrated_channels/sap_success_factors/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ class SAPSuccessFactorsEnterpriseCustomerConfiguration(EnterpriseCustomerPluginC
help_text=_("A comma-separated list of additional locales.")
)
show_course_price = models.BooleanField(default=False)
show_total_hours = models.BooleanField(default=False, verbose_name=_("Show Total Hours"))

def get_locales(self, default_locale=None):
"""
Expand Down
15 changes: 15 additions & 0 deletions integrated_channels/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from __future__ import absolute_import, unicode_literals

import datetime
import math
import re
from itertools import islice
from string import Formatter
Expand Down Expand Up @@ -178,3 +179,17 @@ def is_already_transmitted(transmission, enterprise_enrollment_id, grade):
pass

return False


def get_duration_from_estimated_hours(estimated_hours):
"""
Return the duration in {hours}:{minutes}:00 corresponding to estimated hours as int or float.
"""
if estimated_hours and isinstance(estimated_hours, (int, float)):
fraction, whole_number = math.modf(estimated_hours)
hours = "{:02d}".format(int(whole_number))
minutes = "{:02d}".format(int(60 * fraction))
duration = "{hours}:{minutes}:00".format(hours=hours, minutes=minutes)
return duration

return None
24 changes: 24 additions & 0 deletions test_utils/integrated_channels_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
"""
Miscellaneous utils for tests.
"""

from __future__ import absolute_import, unicode_literals

import copy


def merge_dicts(dict1, dict2):
"""
Merge dict1 and dict2 and returns merged dict.
If dict2 has a key with value set to `undefined` it removes that key from dict1
"""
merged_dict = copy.deepcopy(dict1)
if dict2:
for key, val in dict2.items():
if val == 'undefined' and key in merged_dict:
del merged_dict[key]
else:
merged_dict.update(dict2)
return merged_dict
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

from __future__ import absolute_import, unicode_literals, with_statement

import copy
import datetime
import unittest

Expand All @@ -20,6 +19,7 @@
from test_utils import FAKE_UUIDS, factories
from test_utils.fake_catalog_api import FAKE_SEARCH_ALL_COURSE_RESULT_3
from test_utils.fake_enterprise_api import EnterpriseMockMixin
from test_utils.integrated_channels_utils import merge_dicts

NOW = datetime.datetime.now(pytz.UTC)
DEFAULT_OWNER = {
Expand Down Expand Up @@ -57,21 +57,6 @@ def setUp(self):
self.addCleanup(jwt_builder.stop)
super(TestCornerstoneContentMetadataExporter, self).setUp()

def _merge_dicts(self, dict1, dict2):
"""
Merges dict1 and dict2 and returns merged dict.
If dict2 has a key with value set to `undefined`
it removes that key from dict1
"""
merged_dict = copy.deepcopy(dict1)
if dict2:
for key, val in dict2.items():
if val == 'undefined' and key in merged_dict:
del merged_dict[key]
else:
merged_dict.update(dict2)
return merged_dict

@responses.activate
def test_content_exporter_export(self):
"""
Expand Down Expand Up @@ -120,7 +105,7 @@ def test_transform_description(self, item_description, expected_description):
course short description or course title should be returned
content type of the provided `content_metadata_item`.
"""
item_content_metadata = self._merge_dicts(FAKE_SEARCH_ALL_COURSE_RESULT_3, item_description)
item_content_metadata = merge_dicts(FAKE_SEARCH_ALL_COURSE_RESULT_3, item_description)
exporter = CornerstoneContentMetadataExporter('fake-user', self.config)
exporter.LONG_STRING_LIMIT = 100
assert exporter.transform_description(item_content_metadata) == expected_description
Expand Down Expand Up @@ -220,7 +205,7 @@ def test_transform_is_active(self, item_course_runs, expected_is_active):
"""
Test transforms for is_active status of course.
"""
item_content_metadata = self._merge_dicts(FAKE_SEARCH_ALL_COURSE_RESULT_3, item_course_runs)
item_content_metadata = merge_dicts(FAKE_SEARCH_ALL_COURSE_RESULT_3, item_course_runs)
exporter = CornerstoneContentMetadataExporter('fake-user', self.config)
assert exporter.transform_is_active(item_content_metadata) == expected_is_active

Expand Down Expand Up @@ -302,7 +287,7 @@ def test_transform_estimated_hours(self, item_course_runs, expected_duration):
"""
Test transformation of estimated_hours into course duration.
"""
item_content_metadata = self._merge_dicts(FAKE_SEARCH_ALL_COURSE_RESULT_3, item_course_runs)
item_content_metadata = merge_dicts(FAKE_SEARCH_ALL_COURSE_RESULT_3, item_course_runs)
exporter = CornerstoneContentMetadataExporter('fake-user', self.config)
assert exporter.transform_estimated_hours(item_content_metadata) == expected_duration

Expand Down Expand Up @@ -420,7 +405,7 @@ def test_transform_modified(self, item_course_runs, expected_modified_datetime):
"""
Test transformation for LastModifiedUTC field.
"""
item_content_metadata = self._merge_dicts(FAKE_SEARCH_ALL_COURSE_RESULT_3, item_course_runs)
item_content_metadata = merge_dicts(FAKE_SEARCH_ALL_COURSE_RESULT_3, item_course_runs)
exporter = CornerstoneContentMetadataExporter('fake-user', self.config)
assert exporter.transform_modified(item_content_metadata) == expected_modified_datetime

Expand Down Expand Up @@ -460,7 +445,7 @@ def test_transform_languages(self, item_languages, expected_languages):
"""
Test transforming languages should return a list of languages for course.
"""
item_content_metadata = self._merge_dicts(FAKE_SEARCH_ALL_COURSE_RESULT_3, item_languages)
item_content_metadata = merge_dicts(FAKE_SEARCH_ALL_COURSE_RESULT_3, item_languages)
exporter = CornerstoneContentMetadataExporter('fake-user', self.config)
transformed_languages = exporter.transform_languages(item_content_metadata)
assert sorted(transformed_languages) == sorted(expected_languages)
Expand Down Expand Up @@ -497,7 +482,7 @@ def test_transform_organizations(self, item_organizations, expected_organization
"""
Transforming organizations gives back the a list of dict {"Name": "Org Name"}.
"""
item_content_metadata = self._merge_dicts(FAKE_SEARCH_ALL_COURSE_RESULT_3, item_organizations)
item_content_metadata = merge_dicts(FAKE_SEARCH_ALL_COURSE_RESULT_3, item_organizations)
exporter = CornerstoneContentMetadataExporter('fake-user', self.config)
assert exporter.transform_organizations(item_content_metadata) == expected_organizations

Expand Down Expand Up @@ -533,7 +518,7 @@ def test_transform_subjects(self, item_subjects, expected_subjects):
"""
Transforming subjects gives back the a list of cornerstone's subjects.
"""
item_content_metadata = self._merge_dicts(FAKE_SEARCH_ALL_COURSE_RESULT_3, item_subjects)
item_content_metadata = merge_dicts(FAKE_SEARCH_ALL_COURSE_RESULT_3, item_subjects)
exporter = CornerstoneContentMetadataExporter('fake-user', self.config)
assert sorted(exporter.transform_subjects(item_content_metadata)) == sorted(expected_subjects)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
from enterprise.api_client.lms import parse_lms_api_datetime
from integrated_channels.sap_success_factors.exporters.content_metadata import SapSuccessFactorsContentMetadataExporter
from test_utils import factories
from test_utils.fake_catalog_api import FAKE_SEARCH_ALL_COURSE_RESULT_3
from test_utils.fake_enterprise_api import EnterpriseMockMixin
from test_utils.integrated_channels_utils import merge_dicts


@mark.django_db
Expand Down Expand Up @@ -423,3 +425,91 @@ def test_transform_course_description(self, course, expected_description):
'value': expected_description
}
]

@ddt.data(
(
{'course_runs': []},
'Not-Available',
False,
),
(
{
'course_runs': [
{
"enrollment_end": None,
"enrollment_mode": "audit",
"key": "course-v1:edX+DemoX+Demo_Course",
"enrollment_start": None,
"end": "2013-02-05T05:00:00Z",
"start": "2013-02-05T05:00:00Z",
"availability": "Current"
}
]
},
'Not-Available',
False,
),
(
{
'course_runs': [
{
"enrollment_end": None,
"enrollment_mode": "audit",
"key": "course-v1:edX+DemoX+Demo_Course",
"enrollment_start": None,
"pacing_type": "instructor_paced",
"end": "2013-02-05T05:00:00Z",
"start": "2013-02-05T05:00:00Z",
"go_live_date": None,
"estimated_hours": 5.5,
"availability": "Archived"
},
{
"enrollment_end": None,
"enrollment_mode": "verified",
"key": "course-v1:edX+DemoX+Demo_Course",
"enrollment_start": None,
"pacing_type": "instructor_paced",
"end": None,
"start": "2019-02-05T05:00:00Z",
"go_live_date": None,
"estimated_hours": 6.5,
"availability": "Current"
},
]
},
"06:30:00",
True,
),
(
{
'course_runs': [
{
"enrollment_end": None,
"enrollment_mode": "audit",
"key": "course-v1:edX+DemoX+Demo_Course",
"enrollment_start": None,
"pacing_type": "instructor_paced",
"end": "2013-02-05T05:00:00Z",
"start": "2013-02-05T05:00:00Z",
"go_live_date": None,
"estimated_hours": 100,
"availability": "Archived"
},
]
},
"100:00:00",
True,
),
)
@responses.activate
@ddt.unpack
def test_transform_estimated_hours(self, item_course_runs, expected_hours, show_total_hours):
"""
Test transformation of estimated_hours into total hours.
"""
self.config.show_total_hours = show_total_hours
self.config.save()
item_content_metadata = merge_dicts(FAKE_SEARCH_ALL_COURSE_RESULT_3, item_course_runs)
exporter = SapSuccessFactorsContentMetadataExporter('fake-user', self.config)
assert exporter.transform_estimated_hours(item_content_metadata) == expected_hours

0 comments on commit b79ea4d

Please sign in to comment.