diff --git a/course_discovery/apps/course_metadata/data_loaders/api.py b/course_discovery/apps/course_metadata/data_loaders/api.py index 7484be4316..f336d63afb 100644 --- a/course_discovery/apps/course_metadata/data_loaders/api.py +++ b/course_discovery/apps/course_metadata/data_loaders/api.py @@ -20,7 +20,7 @@ from course_discovery.apps.course_metadata.data_loaders.course_type import calculate_course_type from course_discovery.apps.course_metadata.models import ( Course, CourseEntitlement, CourseRun, CourseRunType, CourseType, Organization, Program, ProgramType, Seat, SeatType, - Subject, Video + Subject, Video, Person, PersonSocialNetwork ) from course_discovery.apps.course_metadata.utils import push_to_ecommerce_for_course_run, subtract_deadline_delta @@ -953,6 +953,39 @@ def _load_data(self, page): response = self._make_request(page) self._process_response(response) + def _add_course_instructors(self, course_instructors, course_run): + """ + Create and add instructors to a course run. + """ + for course_instructor in course_instructors: + course_instructor['partner'] = course_run.course.partner + instructor_socials = course_instructor.pop('instructor_socials') + instructor, created = Person.objects.get_or_create( + marketing_id=course_instructor['marketing_id'], + defaults=course_instructor + ) + if created: + for instructor_social in instructor_socials: + PersonSocialNetwork.objects.create( + person=instructor, + type=instructor_social['field_name'], + title=instructor_social['field_name'].upper(), + url=instructor_social['url'], + ) + + course_run.staff.add(instructor) + else: + for key, value in course_instructor.items(): + setattr(instructor, key, value) + + instructor.save() + + socials = list(instructor.person_networks.all()) + for index, instructor_social in enumerate(socials): + instructor_social.url = instructor_socials[index]['url'] + + PersonSocialNetwork.objects.bulk_update(socials, ['url']) + def _process_response(self, response): """ Process the response from the WordPress. @@ -986,6 +1019,7 @@ def _process_response(self, response): course_run.course.subjects.add(subject) course_run.save() + self._add_course_instructors(body['course_instructors'], course_run) except CourseRun.DoesNotExist: logger.exception('Could not find course run [%s]', course_run_key) except Exception: # pylint: disable=broad-except diff --git a/course_discovery/apps/course_metadata/data_loaders/tests/mock_data.py b/course_discovery/apps/course_metadata/data_loaders/tests/mock_data.py index fc8ead6819..5d3a75488a 100644 --- a/course_discovery/apps/course_metadata/data_loaders/tests/mock_data.py +++ b/course_discovery/apps/course_metadata/data_loaders/tests/mock_data.py @@ -3093,6 +3093,25 @@ } ], 'tags': ['tag1', 'tag2', 'tag3'], - 'featured_image_url': 'http://example.com/demo-course-image.jpg' + 'featured_image_url': 'http://example.com/demo-course-image.jpg', + 'course_instructors': [ + { + 'given_name': 'Test instructor', + 'bio': 'This is a test instructor', + 'email': 'test@admin.com', + 'designation': 'SQA', + 'profile_image_url': 'http://example.com/demo-course-image.jpg', + 'marketing_id': 100, + 'marketing_url': 'http://example.com/demo-course-image', + 'phone_number': '12345', + 'website': 'http://example.com', + 'instructor_socials': [ + { + 'field_name': 'facebook', + 'url': 'http://facebook.com' + } + ] + } + ] } ] diff --git a/course_discovery/apps/course_metadata/data_loaders/tests/test_api.py b/course_discovery/apps/course_metadata/data_loaders/tests/test_api.py index 303855c3a7..1be630b43f 100644 --- a/course_discovery/apps/course_metadata/data_loaders/tests/test_api.py +++ b/course_discovery/apps/course_metadata/data_loaders/tests/test_api.py @@ -22,7 +22,7 @@ from course_discovery.apps.course_metadata.data_loaders.tests import JPEG, JSON, mock_data from course_discovery.apps.course_metadata.data_loaders.tests.mixins import DataLoaderTestMixin from course_discovery.apps.course_metadata.models import ( - Course, CourseEntitlement, CourseRun, CourseRunType, CourseType, Organization, Program, ProgramType, Seat, SeatType + Course, CourseEntitlement, CourseRun, CourseRunType, CourseType, Organization, Person, Program, ProgramType, Seat, SeatType ) from course_discovery.apps.course_metadata.tests.factories import ( CourseEntitlementFactory, CourseFactory, CourseRunFactory, OrganizationFactory, SeatFactory, SeatTypeFactory @@ -1136,6 +1136,19 @@ def test_ingest(self): assert subject.name == category['title'] assert subject.description == category['description'] + for course_instructor in expected_course['course_instructors']: + instructor = Person.objects.get(given_name=course_instructor['given_name']) + assert instructor.designation == course_instructor['designation'] + assert instructor.email == course_instructor['email'] + assert instructor.bio == course_instructor['bio'] + assert instructor.profile_image_url == course_instructor['profile_image_url'] + assert instructor.marketing_id == course_instructor['marketing_id'] + assert instructor.marketing_url == course_instructor['marketing_url'] + assert instructor.phone_number == course_instructor['phone_number'] + assert instructor.website == course_instructor['website'] + assert instructor.person_networks.first().type == course_instructor['instructor_socials'][0]['field_name'] + assert instructor.person_networks.first().url == course_instructor['instructor_socials'][0]['url'] + def test_ingest_with_fail(self): """ Test that data loader raise logs errors if course run not found. diff --git a/course_discovery/apps/course_metadata/migrations/0255_add_designation_and_profile_image_url_to_person.py b/course_discovery/apps/course_metadata/migrations/0255_add_designation_and_profile_image_url_to_person.py new file mode 100644 index 0000000000..0a81762aa0 --- /dev/null +++ b/course_discovery/apps/course_metadata/migrations/0255_add_designation_and_profile_image_url_to_person.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.16 on 2021-02-15 07:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('course_metadata', '0254_auto_20210201_1107'), + ] + + operations = [ + migrations.AddField( + model_name='person', + name='designation', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='person', + name='profile_image_url', + field=models.URLField(blank=True, null=True), + ), + ] diff --git a/course_discovery/apps/course_metadata/migrations/0256_add_instructor_info_fields_to_person.py b/course_discovery/apps/course_metadata/migrations/0256_add_instructor_info_fields_to_person.py new file mode 100644 index 0000000000..3fda8e4aac --- /dev/null +++ b/course_discovery/apps/course_metadata/migrations/0256_add_instructor_info_fields_to_person.py @@ -0,0 +1,34 @@ +# Generated by Django 2.2.16 on 2021-02-18 08:09 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('course_metadata', '0255_add_designation_and_profile_image_url_to_person'), + ] + + operations = [ + migrations.AddField( + model_name='person', + name='marketing_id', + field=models.PositiveIntegerField(blank=True, help_text='This field contains instructor post ID from wordpress.', null=True), + ), + migrations.AddField( + model_name='person', + name='marketing_url', + field=models.URLField(blank=True, null=True), + ), + migrations.AddField( + model_name='person', + name='phone_number', + field=models.CharField(blank=True, max_length=50, null=True, validators=[django.core.validators.RegexValidator(message='Phone number can only contain numbers.', regex='^\\+?1?\\d*$')]), + ), + migrations.AddField( + model_name='person', + name='website', + field=models.URLField(blank=True, null=True), + ), + ] diff --git a/course_discovery/apps/course_metadata/migrations/0257_add_social_media_type_choices.py b/course_discovery/apps/course_metadata/migrations/0257_add_social_media_type_choices.py new file mode 100644 index 0000000000..2c1dcf1b72 --- /dev/null +++ b/course_discovery/apps/course_metadata/migrations/0257_add_social_media_type_choices.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.16 on 2021-02-18 08:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('course_metadata', '0256_add_instructor_info_fields_to_person'), + ] + + operations = [ + migrations.AlterField( + model_name='personsocialnetwork', + name='type', + field=models.CharField(choices=[('blog', 'Blog'), ('dribbble', 'Dribbble'), ('facebook', 'Facebook'), ('github', 'github'), ('instagram', 'Instagram'), ('linkedin', 'LinkedIn'), ('medium', 'medium'), ('others', 'Others'), ('skype', 'Skype'), ('stackoverflow', 'stackoverflow'), ('twitter', 'Twitter'), ('youtube', 'Youtube')], db_index=True, max_length=15), + ), + ] diff --git a/course_discovery/apps/course_metadata/models.py b/course_discovery/apps/course_metadata/models.py index d65185c6ef..b0bcdc6ff5 100644 --- a/course_discovery/apps/course_metadata/models.py +++ b/course_discovery/apps/course_metadata/models.py @@ -12,7 +12,7 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.core.exceptions import ObjectDoesNotExist, ValidationError -from django.core.validators import FileExtensionValidator +from django.core.validators import FileExtensionValidator, RegexValidator from django.db import models, transaction from django.db.models import F, Q from django.utils.functional import cached_property @@ -629,6 +629,13 @@ class Person(TimeStampedModel): help_text=_('A list of major works by this person. Must be valid HTML.'), ) published = models.BooleanField(default=False) + designation = models.TextField(null=True, blank=True) + profile_image_url = models.URLField(null=True, blank=True) + marketing_id = models.PositiveIntegerField(null=True, blank=True, help_text=_('This field contains instructor post ID from wordpress.')) + marketing_url = models.URLField(null=True, blank=True) + phone_regex = RegexValidator(regex=r'^\+?1?\d*$', message="Phone number can only contain numbers.") + phone_number = models.CharField(validators=[phone_regex], null=True, blank=True, max_length=50) + website = models.URLField(null=True, blank=True) class Meta: unique_together = ( @@ -2741,10 +2748,26 @@ class PersonSocialNetwork(TimeStampedModel): FACEBOOK = 'facebook' TWITTER = 'twitter' BLOG = 'blog' + LINKEDIN = 'linkedin' + DRIBBBLE = 'dribbble' + YOUTUBE = 'youtube' + SKYPE = 'skype' + INSTAGRAM = 'instagram' + GITHUB = 'github' + STACKOVERFLOW = 'stackoverflow' + MEDIUM = 'medium' OTHERS = 'others' SOCIAL_NETWORK_CHOICES = { FACEBOOK: _('Facebook'), + LINKEDIN: _('LinkedIn'), + DRIBBBLE: _('Dribbble'), + YOUTUBE: _('Youtube'), + SKYPE: _('Skype'), + INSTAGRAM: _('Instagram'), + GITHUB: _('github'), + STACKOVERFLOW: _('stackoverflow'), + MEDIUM: _('medium'), TWITTER: _('Twitter'), BLOG: _('Blog'), OTHERS: _('Others'),