Commit a9c5877c by Clinton Blackburn Committed by GitHub

Updated Person model and data loader (#243)

ECOM-5193 and ECOM-5194
parent d5a47469
...@@ -149,11 +149,10 @@ class SeatSerializer(serializers.ModelSerializer): ...@@ -149,11 +149,10 @@ class SeatSerializer(serializers.ModelSerializer):
class PersonSerializer(serializers.ModelSerializer): class PersonSerializer(serializers.ModelSerializer):
"""Serializer for the ``Person`` model.""" """Serializer for the ``Person`` model."""
profile_image = ImageSerializer()
class Meta(object): class Meta(object):
model = Person model = Person
fields = ('key', 'name', 'title', 'bio', 'profile_image',) fields = ('uuid', 'given_name', 'family_name', 'bio', 'profile_image_url', 'slug',)
class OrganizationSerializer(TaggitSerializer, serializers.ModelSerializer): class OrganizationSerializer(TaggitSerializer, serializers.ModelSerializer):
......
...@@ -423,15 +423,15 @@ class SeatSerializerTests(TestCase): ...@@ -423,15 +423,15 @@ class SeatSerializerTests(TestCase):
class PersonSerializerTests(TestCase): class PersonSerializerTests(TestCase):
def test_data(self): def test_data(self):
person = PersonFactory() person = PersonFactory()
image = person.profile_image
serializer = PersonSerializer(person) serializer = PersonSerializer(person)
expected = { expected = {
'key': person.key, 'uuid': str(person.uuid),
'name': person.name, 'given_name': person.given_name,
'title': person.title, 'family_name': person.family_name,
'bio': person.bio, 'bio': person.bio,
'profile_image': ImageSerializer(image).data 'profile_image_url': person.profile_image_url,
'slug': person.slug,
} }
self.assertDictEqual(serializer.data, expected) self.assertDictEqual(serializer.data, expected)
......
...@@ -14,6 +14,11 @@ class SeatInline(admin.TabularInline): ...@@ -14,6 +14,11 @@ class SeatInline(admin.TabularInline):
extra = 1 extra = 1
class PositionInline(admin.TabularInline):
model = Position
extra = 0
@admin.register(Course) @admin.register(Course)
class CourseAdmin(admin.ModelAdmin): class CourseAdmin(admin.ModelAdmin):
inlines = (CourseOrganizationInline,) inlines = (CourseOrganizationInline,)
...@@ -82,10 +87,14 @@ class SubjectAdmin(admin.ModelAdmin): ...@@ -82,10 +87,14 @@ class SubjectAdmin(admin.ModelAdmin):
search_fields = ('uuid', 'name', 'slug',) search_fields = ('uuid', 'name', 'slug',)
class KeyNameAdmin(admin.ModelAdmin): @admin.register(Person)
list_display = ('key', 'name',) class PersonAdmin(admin.ModelAdmin):
ordering = ('key', 'name',) inlines = (PositionInline,)
search_fields = ('key', 'name',) list_display = ('uuid', 'family_name', 'given_name', 'slug',)
list_filter = ('partner',)
ordering = ('family_name', 'given_name', 'uuid',)
readonly_fields = ('uuid',)
search_fields = ('uuid', 'family_name', 'given_name', 'slug',)
class NamedModelAdmin(admin.ModelAdmin): class NamedModelAdmin(admin.ModelAdmin):
...@@ -94,12 +103,8 @@ class NamedModelAdmin(admin.ModelAdmin): ...@@ -94,12 +103,8 @@ class NamedModelAdmin(admin.ModelAdmin):
search_fields = ('name',) search_fields = ('name',)
# Register key-name models
for model in (Person,):
admin.site.register(model, KeyNameAdmin)
# Register children of AbstractNamedModel # Register children of AbstractNamedModel
for model in (LevelType, Prerequisite, Expertise, MajorWork): for model in (LevelType, Prerequisite,):
admin.site.register(model, NamedModelAdmin) admin.site.register(model, NamedModelAdmin)
# Register remaining models using basic ModelAdmin classes # Register remaining models using basic ModelAdmin classes
......
...@@ -121,7 +121,7 @@ class AbstractDataLoader(metaclass=abc.ABCMeta): ...@@ -121,7 +121,7 @@ class AbstractDataLoader(metaclass=abc.ABCMeta):
@classmethod @classmethod
def delete_orphans(cls): def delete_orphans(cls):
""" Remove orphaned objects from the database. """ """ Remove orphaned objects from the database. """
for model in (Image, Person, Video): for model in (Image, Video):
delete_orphans(model) delete_orphans(model)
@classmethod @classmethod
......
import abc import abc
import logging import logging
from urllib.parse import urljoin, urlencode from urllib.parse import urljoin, urlencode
from uuid import UUID
import requests import requests
from django.db.models import Q
from django.utils.functional import cached_property from django.utils.functional import cached_property
from course_discovery.apps.course_metadata.data_loaders import AbstractDataLoader from course_discovery.apps.course_metadata.data_loaders import AbstractDataLoader
from course_discovery.apps.course_metadata.models import ( from course_discovery.apps.course_metadata.models import (
Course, CourseOrganization, CourseRun, Image, LanguageTag, LevelType, Organization, Person, Subject, Program, Course, CourseOrganization, CourseRun, Image, LanguageTag, LevelType, Organization, Person, Subject, Program,
Position,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -122,15 +125,9 @@ class DrupalApiDataLoader(AbstractDataLoader): ...@@ -122,15 +125,9 @@ class DrupalApiDataLoader(AbstractDataLoader):
def set_staff(self, course_run, body): def set_staff(self, course_run, body):
"""Update `course_run` with staff from `body`.""" """Update `course_run` with staff from `body`."""
course_run.staff.clear() course_run.staff.clear()
for staff_body in body['staff']: uuids = [staff['uuid'] for staff in body['staff']]
image, __ = Image.objects.get_or_create(src=staff_body['image']) staff = Person.objects.filter(uuid_in=uuids)
defaults = { course_run.staff.add(*staff)
'name': staff_body['title'],
'profile_image': image,
'title': staff_body['display_position']['title'],
}
person, __ = Person.objects.update_or_create(key=staff_body['uuid'], defaults=defaults)
course_run.staff.add(person)
def get_language_tag(self, body): def get_language_tag(self, body):
"""Get a language tag from Drupal data given by `body`.""" """Get a language tag from Drupal data given by `body`."""
...@@ -366,3 +363,72 @@ class SponsorMarketingSiteDataLoader(AbstractMarketingSiteDataLoader): ...@@ -366,3 +363,72 @@ class SponsorMarketingSiteDataLoader(AbstractMarketingSiteDataLoader):
logger.info('Processed sponsor with UUID [%s].', uuid) logger.info('Processed sponsor with UUID [%s].', uuid)
return sponsor return sponsor
class PersonMarketingSiteDataLoader(AbstractMarketingSiteDataLoader):
@property
def node_type(self):
return 'person'
def process_node(self, data):
uuid = UUID(data['uuid'])
defaults = {
'given_name': data['field_person_first_middle_name'],
'family_name': data['field_person_last_name'],
'bio': self.clean_html(data['field_person_resume']['value']),
'profile_image_url': self._get_nested_url(data.get('field_person_image')),
}
person, created = Person.objects.update_or_create(uuid=uuid, partner=self.partner, defaults=defaults)
# NOTE (CCB): The AutoSlug field kicks in at creation time. We need to apply overrides in a separate
# operation.
if created:
person.slug = data['url'].split('/')[-1]
person.save()
self.set_position(person, data)
logger.info('Processed person with UUID [%s].', uuid)
return person
def set_position(self, person, data):
uuid = data['uuid']
try:
data = data.get('field_person_positions', [])
if data:
data = data[0]
# NOTE (CCB): This is not a typo. The field is misspelled on the marketing site.
titles = data['field_person_position_tiltes']
if titles:
title = titles[0]
# NOTE (CCB): Not all positions are associated with organizations.
organization = None
organization_name = (data.get('field_person_position_org_link', {}) or {}).get('title')
if organization_name:
try:
# TODO Consider using Elasticsearch as a method of finding better inexact matches.
organization = Organization.objects.get(
Q(name__iexact=organization_name) | Q(key__iexact=organization_name) & Q(
partner=self.partner))
except Organization.DoesNotExist:
pass
defaults = {
'title': title,
'organization': None,
'organization_override': None,
}
if organization:
defaults['organization'] = organization
else:
defaults['organization_override'] = organization_name
Position.objects.update_or_create(person=person, defaults=defaults)
except: # pylint: disable=bare-except
logger.exception('Failed to set position for person with UUID [%s]!', uuid)
...@@ -18,7 +18,7 @@ from course_discovery.apps.course_metadata.models import ( ...@@ -18,7 +18,7 @@ from course_discovery.apps.course_metadata.models import (
) )
from course_discovery.apps.course_metadata.tests import mock_data from course_discovery.apps.course_metadata.tests import mock_data
from course_discovery.apps.course_metadata.tests.factories import ( from course_discovery.apps.course_metadata.tests.factories import (
CourseRunFactory, SeatFactory, ImageFactory, PersonFactory, VideoFactory, OrganizationFactory, CourseFactory, CourseRunFactory, SeatFactory, ImageFactory, VideoFactory, OrganizationFactory, CourseFactory,
) )
LOGGER_PATH = 'course_discovery.apps.course_metadata.data_loaders.api.logger' LOGGER_PATH = 'course_discovery.apps.course_metadata.data_loaders.api.logger'
...@@ -52,7 +52,7 @@ class AbstractDataLoaderTest(TestCase): ...@@ -52,7 +52,7 @@ class AbstractDataLoaderTest(TestCase):
def test_delete_orphans(self): def test_delete_orphans(self):
""" Verify the delete_orphans method deletes orphaned instances. """ """ Verify the delete_orphans method deletes orphaned instances. """
instances = (ImageFactory(), PersonFactory(), VideoFactory(),) instances = (ImageFactory(), VideoFactory(),)
AbstractDataLoader.delete_orphans() AbstractDataLoader.delete_orphans()
for instance in instances: for instance in instances:
......
...@@ -10,15 +10,15 @@ from opaque_keys.edx.keys import CourseKey ...@@ -10,15 +10,15 @@ from opaque_keys.edx.keys import CourseKey
from course_discovery.apps.course_metadata.data_loaders.marketing_site import ( from course_discovery.apps.course_metadata.data_loaders.marketing_site import (
DrupalApiDataLoader, XSeriesMarketingSiteDataLoader, SubjectMarketingSiteDataLoader, SchoolMarketingSiteDataLoader, DrupalApiDataLoader, XSeriesMarketingSiteDataLoader, SubjectMarketingSiteDataLoader, SchoolMarketingSiteDataLoader,
SponsorMarketingSiteDataLoader, SponsorMarketingSiteDataLoader, PersonMarketingSiteDataLoader,
) )
from course_discovery.apps.course_metadata.data_loaders.tests import JSON from course_discovery.apps.course_metadata.data_loaders.tests import JSON
from course_discovery.apps.course_metadata.data_loaders.tests.mixins import ApiClientTestMixin, DataLoaderTestMixin from course_discovery.apps.course_metadata.data_loaders.tests.mixins import ApiClientTestMixin, DataLoaderTestMixin
from course_discovery.apps.course_metadata.models import ( from course_discovery.apps.course_metadata.models import (
Course, CourseOrganization, CourseRun, Organization, Person, Subject, Program, Video, Course, CourseOrganization, CourseRun, Organization, Subject, Program, Video, Person,
) )
from course_discovery.apps.course_metadata.tests import mock_data from course_discovery.apps.course_metadata.tests import mock_data
from course_discovery.apps.course_metadata.tests.factories import ProgramFactory from course_discovery.apps.course_metadata.tests.factories import ProgramFactory, OrganizationFactory
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
ENGLISH_LANGUAGE_TAG = LanguageTag(code='en-us', name='English - United States') ENGLISH_LANGUAGE_TAG = LanguageTag(code='en-us', name='English - United States')
...@@ -37,15 +37,13 @@ class DrupalApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCase ...@@ -37,15 +37,13 @@ class DrupalApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCase
super(DrupalApiDataLoaderTests, self).setUp() super(DrupalApiDataLoaderTests, self).setUp()
for course_dict in mock_data.EXISTING_COURSE_AND_RUN_DATA: for course_dict in mock_data.EXISTING_COURSE_AND_RUN_DATA:
course = Course.objects.create(key=course_dict['course_key'], title=course_dict['title']) course = Course.objects.create(key=course_dict['course_key'], title=course_dict['title'])
course_run = CourseRun.objects.create( CourseRun.objects.create(
key=course_dict['course_run_key'], key=course_dict['course_run_key'],
language=self.loader.get_language_tag(course_dict), language=self.loader.get_language_tag(course_dict),
course=course course=course
) )
# Add some data that doesn't exist in Drupal already # Add some data that doesn't exist in Drupal already
person = Person.objects.create(key='orphan_staff_' + course_run.key)
course_run.staff.add(person)
organization = Organization.objects.create(key='orphan_org_' + course.key) organization = Organization.objects.create(key='orphan_org_' + course.key)
CourseOrganization.objects.create( CourseOrganization.objects.create(
organization=organization, organization=organization,
...@@ -54,7 +52,6 @@ class DrupalApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCase ...@@ -54,7 +52,6 @@ class DrupalApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCase
) )
Course.objects.create(key=mock_data.EXISTING_COURSE['course_key'], title=mock_data.EXISTING_COURSE['title']) Course.objects.create(key=mock_data.EXISTING_COURSE['course_key'], title=mock_data.EXISTING_COURSE['title'])
Person.objects.create(key=mock_data.ORPHAN_STAFF_KEY)
Organization.objects.create(key=mock_data.ORPHAN_ORGANIZATION_KEY) Organization.objects.create(key=mock_data.ORPHAN_ORGANIZATION_KEY)
def create_mock_subjects(self, course_runs): def create_mock_subjects(self, course_runs):
...@@ -93,23 +90,12 @@ class DrupalApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCase ...@@ -93,23 +90,12 @@ class DrupalApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCase
self.assertEqual(course_run.course, course) self.assertEqual(course_run.course, course)
self.assert_course_loaded(course, body) self.assert_course_loaded(course, body)
self.assert_staff_loaded(course_run, body)
if course_run.language: if course_run.language:
self.assertEqual(course_run.language.code, body['current_language']) self.assertEqual(course_run.language.code, body['current_language'])
else: else:
self.assertEqual(body['current_language'], '') self.assertEqual(body['current_language'], '')
def assert_staff_loaded(self, course_run, body):
"""Verify that staff have been loaded correctly."""
course_run_staff = course_run.staff.all()
api_staff = body['staff']
self.assertEqual(len(course_run_staff), len(api_staff))
for api_staff_member in api_staff:
loaded_staff_member = Person.objects.get(key=api_staff_member['uuid'])
self.assertIn(loaded_staff_member, course_run_staff)
def assert_course_loaded(self, course, body): def assert_course_loaded(self, course, body):
"""Verify that the course has been loaded correctly.""" """Verify that the course has been loaded correctly."""
self.assertEqual(course.title, body['title']) self.assertEqual(course.title, body['title'])
...@@ -162,9 +148,7 @@ class DrupalApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCase ...@@ -162,9 +148,7 @@ class DrupalApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCase
self.loader.ingest() self.loader.ingest()
# Verify that orphan data is deleted # Verify that orphan data is deleted
self.assertFalse(Person.objects.filter(key=mock_data.ORPHAN_STAFF_KEY).exists())
self.assertFalse(Organization.objects.filter(key=mock_data.ORPHAN_ORGANIZATION_KEY).exists()) self.assertFalse(Organization.objects.filter(key=mock_data.ORPHAN_ORGANIZATION_KEY).exists())
self.assertFalse(Person.objects.filter(key__startswith='orphan_staff_').exists())
self.assertFalse(Organization.objects.filter(key__startswith='orphan_org_').exists()) self.assertFalse(Organization.objects.filter(key__startswith='orphan_org_').exists())
@responses.activate @responses.activate
...@@ -506,3 +490,56 @@ class SponsorMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMix ...@@ -506,3 +490,56 @@ class SponsorMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMix
for sponsor in sponsors: for sponsor in sponsors:
self.assert_sponsor_loaded(sponsor) self.assert_sponsor_loaded(sponsor)
class PersonMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMixin, TestCase):
loader_class = PersonMarketingSiteDataLoader
def mock_api(self):
bodies = mock_data.MARKETING_SITE_API_PERSON_BODIES
url = self.api_url + 'node.json'
responses.add_callback(
responses.GET,
url,
callback=self.mock_api_callback(url, bodies),
content_type=JSON
)
return bodies
def assert_person_loaded(self, data):
uuid = data['uuid']
person = Person.objects.get(uuid=uuid, partner=self.partner)
expected_values = {
'given_name': data['field_person_first_middle_name'],
'family_name': data['field_person_last_name'],
'bio': self.loader.clean_html(data['field_person_resume']['value']),
'profile_image_url': data['field_person_image']['url'],
'slug': data['url'].split('/')[-1],
}
for field, value in expected_values.items():
self.assertEqual(getattr(person, field), value)
positions = data['field_person_positions']
if positions:
position_data = positions[0]
titles = position_data['field_person_position_tiltes']
if titles:
self.assertEqual(person.position.title, titles[0])
self.assertEqual(person.position.organization_name,
(position_data.get('field_person_position_org_link') or {}).get('title'))
@responses.activate
def test_ingest(self):
self.mock_login_response()
people = self.mock_api()
OrganizationFactory(name='MIT')
self.loader.ingest()
for person in people:
self.assert_person_loaded(person)
...@@ -9,7 +9,7 @@ from course_discovery.apps.course_metadata.data_loaders.api import ( ...@@ -9,7 +9,7 @@ from course_discovery.apps.course_metadata.data_loaders.api import (
) )
from course_discovery.apps.course_metadata.data_loaders.marketing_site import ( from course_discovery.apps.course_metadata.data_loaders.marketing_site import (
DrupalApiDataLoader, XSeriesMarketingSiteDataLoader, SubjectMarketingSiteDataLoader, SchoolMarketingSiteDataLoader, DrupalApiDataLoader, XSeriesMarketingSiteDataLoader, SubjectMarketingSiteDataLoader, SchoolMarketingSiteDataLoader,
SponsorMarketingSiteDataLoader, SponsorMarketingSiteDataLoader, PersonMarketingSiteDataLoader,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -82,6 +82,7 @@ class Command(BaseCommand): ...@@ -82,6 +82,7 @@ class Command(BaseCommand):
(partner.marketing_site_url_root, SubjectMarketingSiteDataLoader,), (partner.marketing_site_url_root, SubjectMarketingSiteDataLoader,),
(partner.marketing_site_url_root, SchoolMarketingSiteDataLoader,), (partner.marketing_site_url_root, SchoolMarketingSiteDataLoader,),
(partner.marketing_site_url_root, SponsorMarketingSiteDataLoader,), (partner.marketing_site_url_root, SponsorMarketingSiteDataLoader,),
(partner.marketing_site_url_root, PersonMarketingSiteDataLoader,),
(partner.organizations_api_url, OrganizationsApiDataLoader,), (partner.organizations_api_url, OrganizationsApiDataLoader,),
(partner.courses_api_url, CoursesApiDataLoader,), (partner.courses_api_url, CoursesApiDataLoader,),
(partner.ecommerce_api_url, EcommerceApiDataLoader,), (partner.ecommerce_api_url, EcommerceApiDataLoader,),
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django_extensions.db.fields
import uuid
import django.db.models.deletion
def delete_people(apps, schema_editor):
Person = apps.get_model('course_metadata', 'Person')
Person.objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('core', '0010_auto_20160731_0023'),
('course_metadata', '0017_auto_20160815_2135'),
]
operations = [
migrations.RunPython(delete_people, reverse_code=migrations.RunPython.noop),
migrations.CreateModel(
name='Position',
fields=[
('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
('title', models.CharField(max_length=255)),
('organization_override', models.CharField(max_length=255, blank=True, null=True)),
('organization', models.ForeignKey(null=True, to='course_metadata.Organization', blank=True)),
],
options={
'ordering': ('-modified', '-created'),
'abstract': False,
'get_latest_by': 'modified',
},
),
migrations.RemoveField(
model_name='historicalperson',
name='email',
),
migrations.RemoveField(
model_name='historicalperson',
name='key',
),
migrations.RemoveField(
model_name='historicalperson',
name='name',
),
migrations.RemoveField(
model_name='historicalperson',
name='profile_image',
),
migrations.RemoveField(
model_name='historicalperson',
name='title',
),
migrations.RemoveField(
model_name='historicalperson',
name='username',
),
migrations.AddField(
model_name='historicalperson',
name='family_name',
field=models.CharField(max_length=255, blank=True, null=True),
),
migrations.AddField(
model_name='historicalperson',
name='given_name',
field=models.CharField(default='', max_length=255),
preserve_default=False,
),
migrations.AddField(
model_name='historicalperson',
name='partner',
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, null=True, db_constraint=False, to='core.Partner', related_name='+', blank=True),
),
migrations.AddField(
model_name='historicalperson',
name='profile_image_url',
field=models.URLField(blank=True, null=True),
),
migrations.AddField(
model_name='historicalperson',
name='slug',
field=django_extensions.db.fields.AutoSlugField(populate_from=('given_name', 'family_name'), blank=True, editable=False),
),
migrations.AddField(
model_name='historicalperson',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, verbose_name='UUID', editable=False),
),
migrations.AddField(
model_name='person',
name='family_name',
field=models.CharField(max_length=255, blank=True, null=True),
),
migrations.AddField(
model_name='person',
name='given_name',
field=models.CharField(default='', max_length=255),
preserve_default=False,
),
migrations.AddField(
model_name='person',
name='partner',
field=models.ForeignKey(null=True, to='core.Partner'),
),
migrations.AddField(
model_name='person',
name='profile_image_url',
field=models.URLField(blank=True, null=True),
),
migrations.AddField(
model_name='person',
name='slug',
field=django_extensions.db.fields.AutoSlugField(populate_from=('given_name', 'family_name'), blank=True, editable=False),
),
migrations.AddField(
model_name='person',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, verbose_name='UUID', editable=False),
),
migrations.AlterUniqueTogether(
name='person',
unique_together=set([('partner', 'uuid')]),
),
migrations.AddField(
model_name='position',
name='person',
field=models.OneToOneField(to='course_metadata.Person'),
),
migrations.RemoveField(
model_name='person',
name='email',
),
migrations.RemoveField(
model_name='person',
name='expertises',
),
migrations.RemoveField(
model_name='person',
name='key',
),
migrations.RemoveField(
model_name='person',
name='major_works',
),
migrations.RemoveField(
model_name='person',
name='name',
),
migrations.RemoveField(
model_name='person',
name='organizations',
),
migrations.RemoveField(
model_name='person',
name='profile_image',
),
migrations.RemoveField(
model_name='person',
name='title',
),
migrations.RemoveField(
model_name='person',
name='username',
),
migrations.DeleteModel(
name='Expertise',
),
migrations.DeleteModel(
name='MajorWork',
),
]
...@@ -141,16 +141,6 @@ class SyllabusItem(AbstractValueModel): ...@@ -141,16 +141,6 @@ class SyllabusItem(AbstractValueModel):
parent = models.ForeignKey('self', blank=True, null=True, related_name='children') parent = models.ForeignKey('self', blank=True, null=True, related_name='children')
class Expertise(AbstractNamedModel):
""" Expertise model. """
pass
class MajorWork(AbstractNamedModel):
""" MajorWork model. """
pass
class Organization(TimeStampedModel): class Organization(TimeStampedModel):
""" Organization model. """ """ Organization model. """
partner = models.ForeignKey(Partner, null=True, blank=False) partner = models.ForeignKey(Partner, null=True, blank=False)
...@@ -178,24 +168,51 @@ class Organization(TimeStampedModel): ...@@ -178,24 +168,51 @@ class Organization(TimeStampedModel):
class Person(TimeStampedModel): class Person(TimeStampedModel):
""" Person model. """ """ Person model. """
key = models.CharField(max_length=255, unique=True) uuid = models.UUIDField(blank=False, null=False, default=uuid4, editable=False, verbose_name=_('UUID'))
name = models.CharField(max_length=255, null=True, blank=True) partner = models.ForeignKey(Partner, null=True, blank=False)
title = models.CharField(max_length=255, null=True, blank=True) given_name = models.CharField(max_length=255)
family_name = models.CharField(max_length=255, null=True, blank=True)
bio = models.TextField(null=True, blank=True) bio = models.TextField(null=True, blank=True)
profile_image = models.ForeignKey(Image, null=True, blank=True) profile_image_url = models.URLField(null=True, blank=True)
organizations = models.ManyToManyField(Organization, blank=True) slug = AutoSlugField(populate_from=('given_name', 'family_name'), editable=True)
email = models.EmailField(max_length=255, null=True, blank=True)
username = models.CharField(max_length=255, null=True, blank=True)
expertises = SortedManyToManyField(Expertise, blank=True, related_name='person_expertise')
major_works = SortedManyToManyField(MajorWork, blank=True, related_name='person_works')
history = HistoricalRecords() history = HistoricalRecords()
class Meta:
unique_together = (
('partner', 'uuid'),
)
verbose_name_plural = _('People')
def __str__(self): def __str__(self):
return '{key}: {name}'.format(key=self.key, name=self.name) return self.full_name
class Meta(object): @property
verbose_name_plural = 'People' def full_name(self):
return ' '.join((self.given_name, self.family_name,))
class Position(TimeStampedModel):
""" Position model.
This model represent's a `Person`'s role at an organization.
"""
person = models.OneToOneField(Person)
title = models.CharField(max_length=255)
organization = models.ForeignKey(Organization, null=True, blank=True)
organization_override = models.CharField(max_length=255, null=True, blank=True)
def __str__(self):
return '{title} at {organization}'.format(title=self.title, organization=self.organization_name)
@property
def organization_name(self):
name = self.organization_override
if self.organization and not name:
name = self.organization.name
return name
class Course(TimeStampedModel): class Course(TimeStampedModel):
......
from datetime import datetime from datetime import datetime
from uuid import uuid4
import factory import factory
from factory.fuzzy import ( from factory.fuzzy import FuzzyText, FuzzyChoice, FuzzyDateTime, FuzzyInteger, FuzzyDecimal
FuzzyText, FuzzyChoice, FuzzyDateTime, FuzzyInteger, FuzzyDecimal
)
from pytz import UTC from pytz import UTC
from course_discovery.apps.core.models import Currency
from course_discovery.apps.core.tests.factories import PartnerFactory from course_discovery.apps.core.tests.factories import PartnerFactory
from course_discovery.apps.core.tests.utils import FuzzyURL from course_discovery.apps.core.tests.utils import FuzzyURL
from course_discovery.apps.course_metadata.models import ( from course_discovery.apps.course_metadata.models import * # pylint: disable=wildcard-import
Course, CourseRun, Organization, Person, Image, Video, Subject, Seat, Prerequisite, LevelType, Program,
AbstractSocialNetworkModel, CourseRunSocialNetwork, PersonSocialNetwork, ProgramType, SeatType,
)
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
...@@ -68,7 +61,7 @@ class SeatFactory(factory.DjangoModelFactory): ...@@ -68,7 +61,7 @@ class SeatFactory(factory.DjangoModelFactory):
type = FuzzyChoice([name for name, __ in Seat.SEAT_TYPE_CHOICES]) type = FuzzyChoice([name for name, __ in Seat.SEAT_TYPE_CHOICES])
price = FuzzyDecimal(0.0, 650.0) price = FuzzyDecimal(0.0, 650.0)
currency = factory.Iterator(Currency.objects.all()) currency = factory.Iterator(Currency.objects.all())
upgrade_deadline = FuzzyDateTime(datetime(2014, 1, 1, tzinfo=UTC)) upgrade_deadline = FuzzyDateTime(datetime.datetime(2014, 1, 1, tzinfo=UTC))
class Meta: class Meta:
model = Seat model = Seat
...@@ -106,11 +99,11 @@ class CourseRunFactory(factory.DjangoModelFactory): ...@@ -106,11 +99,11 @@ class CourseRunFactory(factory.DjangoModelFactory):
short_description_override = None short_description_override = None
full_description_override = None full_description_override = None
language = factory.Iterator(LanguageTag.objects.all()) language = factory.Iterator(LanguageTag.objects.all())
start = FuzzyDateTime(datetime(2014, 1, 1, tzinfo=UTC)) start = FuzzyDateTime(datetime.datetime(2014, 1, 1, tzinfo=UTC))
end = FuzzyDateTime(datetime(2014, 1, 1, tzinfo=UTC)).end_dt end = FuzzyDateTime(datetime.datetime(2014, 1, 1, tzinfo=UTC)).end_dt
enrollment_start = FuzzyDateTime(datetime(2014, 1, 1, tzinfo=UTC)) enrollment_start = FuzzyDateTime(datetime.datetime(2014, 1, 1, tzinfo=UTC))
enrollment_end = FuzzyDateTime(datetime(2014, 1, 1, tzinfo=UTC)).end_dt enrollment_end = FuzzyDateTime(datetime.datetime(2014, 1, 1, tzinfo=UTC)).end_dt
announcement = FuzzyDateTime(datetime(2014, 1, 1, tzinfo=UTC)) announcement = FuzzyDateTime(datetime.datetime(2014, 1, 1, tzinfo=UTC))
image = factory.SubFactory(ImageFactory) image = factory.SubFactory(ImageFactory)
video = factory.SubFactory(VideoFactory) video = factory.SubFactory(VideoFactory)
min_effort = FuzzyInteger(1, 10) min_effort = FuzzyInteger(1, 10)
...@@ -146,16 +139,26 @@ class OrganizationFactory(factory.DjangoModelFactory): ...@@ -146,16 +139,26 @@ class OrganizationFactory(factory.DjangoModelFactory):
class PersonFactory(factory.DjangoModelFactory): class PersonFactory(factory.DjangoModelFactory):
key = FuzzyText(prefix='Person.fake/') uuid = factory.LazyFunction(uuid4)
name = FuzzyText() partner = factory.SubFactory(PartnerFactory)
title = FuzzyText() given_name = factory.Faker('first_name')
family_name = factory.Faker('last_name')
bio = FuzzyText() bio = FuzzyText()
profile_image = factory.SubFactory(ImageFactory) profile_image_url = FuzzyURL()
class Meta: class Meta:
model = Person model = Person
class PositionFactory(factory.DjangoModelFactory):
person = factory.SubFactory(PersonFactory)
title = FuzzyText()
organization = factory.SubFactory(OrganizationFactory)
class Meta:
model = Position
class ProgramTypeFactory(factory.django.DjangoModelFactory): class ProgramTypeFactory(factory.django.DjangoModelFactory):
class Meta(object): class Meta(object):
model = ProgramType model = ProgramType
......
...@@ -202,10 +202,38 @@ class OrganizationTests(TestCase): ...@@ -202,10 +202,38 @@ class OrganizationTests(TestCase):
class PersonTests(TestCase): class PersonTests(TestCase):
""" Tests for the `Person` model. """ """ Tests for the `Person` model. """
def setUp(self):
super(PersonTests, self).setUp()
self.person = factories.PersonFactory()
def test_full_name(self):
""" Verify the property returns the person's full name. """
expected = self.person.given_name + ' ' + self.person.family_name
self.assertEqual(self.person.full_name, expected)
def test_str(self): def test_str(self):
""" Verify casting an instance to a string returns a string containing the key and name. """ """ Verify casting an instance to a string returns the person's full name. """
person = factories.PersonFactory() self.assertEqual(str(self.person), self.person.full_name)
self.assertEqual(str(person), '{key}: {name}'.format(key=person.key, name=person.name))
class PositionTests(TestCase):
""" Tests for the `Position` model. """
def setUp(self):
super(PositionTests, self).setUp()
self.position = factories.PositionFactory()
def test_organization_name(self):
""" Verify the property returns the name of the related Organization or the overridden value. """
self.assertEqual(self.position.organization_name, self.position.organization.name)
self.position.organization_override = 'ACME'
self.assertEqual(self.position.organization_name, self.position.organization_override)
def test_str(self):
""" Verify casting an instance to a string returns the title and organization. """
expected = self.position.title + ' at ' + self.position.organization_name
self.assertEqual(str(self.position), expected)
class AbstractNamedModelTests(TestCase): class AbstractNamedModelTests(TestCase):
...@@ -378,6 +406,7 @@ class CourseSocialNetworkTests(TestCase): ...@@ -378,6 +406,7 @@ class CourseSocialNetworkTests(TestCase):
class SeatTypeTests(TestCase): class SeatTypeTests(TestCase):
""" Tests of the SeatType model. """ """ Tests of the SeatType model. """
def test_str(self): def test_str(self):
seat_type = factories.SeatTypeFactory() seat_type = factories.SeatTypeFactory()
self.assertEqual(str(seat_type), seat_type.name) self.assertEqual(str(seat_type), seat_type.name)
...@@ -385,6 +414,7 @@ class SeatTypeTests(TestCase): ...@@ -385,6 +414,7 @@ class SeatTypeTests(TestCase):
class ProgramTypeTests(TestCase): class ProgramTypeTests(TestCase):
""" Tests of the ProgramType model. """ """ Tests of the ProgramType model. """
def test_str(self): def test_str(self):
program_type = factories.ProgramTypeFactory() program_type = factories.ProgramTypeFactory()
self.assertEqual(str(program_type), program_type.name) self.assertEqual(str(program_type), program_type.name)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment