Skip to content

Commit e03f8a7

Browse files
authored
feat: add CRUD operations for taxi form (#4385)
1 parent 374836e commit e03f8a7

File tree

5 files changed

+214
-19
lines changed

5 files changed

+214
-19
lines changed

course_discovery/apps/api/serializers.py

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,13 @@ class Meta:
688688
fields = ('title', 'description', 'keywords')
689689

690690

691+
class TaxiFormSerializer(BaseModelSerializer):
692+
"""Serializer for the ``TaxiForm`` model."""
693+
class Meta:
694+
model = TaxiForm
695+
fields = ('form_id', 'grouping', 'title', 'subtitle', 'post_submit_url')
696+
697+
691698
class AdditionalMetadataSerializer(BaseModelSerializer):
692699
"""Serializer for the ``AdditionalMetadata`` model."""
693700

@@ -698,20 +705,32 @@ class AdditionalMetadataSerializer(BaseModelSerializer):
698705
end_date = serializers.DateTimeField()
699706
registration_deadline = serializers.DateTimeField(allow_null=True)
700707
variant_id = serializers.UUIDField(allow_null=True)
708+
taxi_form = TaxiFormSerializer(required=False, allow_null=True)
701709

702710
@classmethod
703711
def prefetch_queryset(cls):
704-
return AdditionalMetadata.objects.select_related('facts', 'certificate_info', 'product_meta')
712+
return AdditionalMetadata.objects.select_related('facts', 'certificate_info', 'product_meta', 'taxi_form')
705713

706714
class Meta:
707715
model = AdditionalMetadata
708716
fields = (
709717
'external_identifier', 'external_url', 'lead_capture_form_url',
710718
'facts', 'certificate_info', 'organic_url', 'start_date', 'end_date',
711719
'registration_deadline', 'variant_id', 'course_term_override', 'product_status',
712-
'product_meta', 'external_course_marketing_type', 'display_on_org_page',
720+
'product_meta', 'external_course_marketing_type', 'display_on_org_page', 'taxi_form',
713721
)
714722

723+
def update_taxi_form(self, instance, taxi_form):
724+
if instance.taxi_form:
725+
if not taxi_form:
726+
instance.taxi_form = None
727+
else:
728+
TaxiForm.objects.filter(
729+
id=instance.taxi_form.id
730+
).update(**taxi_form)
731+
elif taxi_form:
732+
instance.taxi_form = TaxiForm.objects.create(**taxi_form)
733+
715734
def _update_product_meta(self, instance, product_meta):
716735
if instance.product_meta:
717736
ProductMeta.objects.filter(
@@ -721,22 +740,32 @@ def _update_product_meta(self, instance, product_meta):
721740
**product_meta)
722741

723742
def update(self, instance, validated_data):
724-
# Handle writing nested fields separately
725-
if 'product_meta' in validated_data:
743+
"""
744+
Update the AdditionalMetadata instance with the validated data along with handling
745+
of the nested fields separately.
746+
"""
747+
def handle_product_meta(instance, validated_data):
726748
# Handle product metadata only for 2U Executive Education courses else just pop
727-
product_meta_data = validated_data.pop(
728-
'product_meta')
729-
if instance:
730-
self._update_product_meta(
731-
instance, product_meta_data)
732-
return super().update(instance, validated_data)
749+
if 'product_meta' in validated_data:
750+
product_meta_data = validated_data.pop('product_meta')
733751

752+
if instance:
753+
self._update_product_meta(instance, product_meta_data)
734754

735-
class TaxiFormSerializer(BaseModelSerializer):
736-
"""Serializer for the ``TaxiForm`` model."""
737-
class Meta:
738-
model = TaxiForm
739-
fields = ('form_id', 'grouping', 'title', 'subtitle', 'post_submit_url')
755+
return validated_data
756+
757+
def handle_taxi_form(instance, validated_data):
758+
if 'taxi_form' in validated_data:
759+
taxi_form_data = validated_data.pop('taxi_form')
760+
761+
if instance:
762+
self.update_taxi_form(instance, taxi_form_data)
763+
764+
return validated_data
765+
766+
validated_data = handle_product_meta(instance, validated_data)
767+
validated_data = handle_taxi_form(instance, validated_data)
768+
return super().update(instance, validated_data)
740769

741770

742771
class DegreeAdditionalMetadataSerializer(BaseModelSerializer):
@@ -1488,12 +1517,24 @@ def update_product_meta(self, instance, product_meta_data):
14881517

14891518
return instance.product_meta, changed
14901519

1520+
def update_taxi_form(self, instance, taxi_form):
1521+
changed = False
1522+
if instance.taxi_form:
1523+
_, changed = update_instance(instance.taxi_form, taxi_form, True)
1524+
elif taxi_form:
1525+
instance.taxi_form = TaxiForm.objects.create(**taxi_form)
1526+
instance.save()
1527+
changed = True
1528+
1529+
return instance.taxi_form, changed
1530+
14911531
def update_additional_metadata(self, instance, additional_metadata):
14921532

14931533
changed = False
14941534
facts = additional_metadata.pop('facts', None)
14951535
certificate_info = additional_metadata.pop('certificate_info', None)
14961536
product_meta = additional_metadata.pop('product_meta', None)
1537+
taxi_form = additional_metadata.pop('taxi_form', None)
14971538

14981539
if instance.additional_metadata:
14991540
_, additional_metadata_changed = update_instance(instance.additional_metadata, additional_metadata, True)
@@ -1513,6 +1554,10 @@ def update_additional_metadata(self, instance, additional_metadata):
15131554
_, certification_info_changed = self.update_certificate_info(instance.additional_metadata, certificate_info)
15141555
changed = changed or certification_info_changed
15151556

1557+
if taxi_form:
1558+
_, taxi_form_changed = self.update_taxi_form(instance.additional_metadata, taxi_form)
1559+
changed = changed or taxi_form_changed
1560+
15161561
return changed
15171562
# save() will be called by main update()
15181563

course_discovery/apps/api/tests/test_serializers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2059,6 +2059,7 @@ def test_data(self):
20592059
'product_status': additional_metadata.product_status,
20602060
'external_course_marketing_type': additional_metadata.external_course_marketing_type,
20612061
'display_on_org_page': additional_metadata.display_on_org_page,
2062+
'taxi_form': TaxiFormSerializer(additional_metadata.taxi_form).data,
20622063
}
20632064
assert serializer.data == expected
20642065

course_discovery/apps/api/v1/tests/test_views/test_courses.py

Lines changed: 151 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from course_discovery.apps.course_metadata.models import (
2727
AbstractLocationRestrictionModel, AdditionalMetadata, CertificateInfo, Course, CourseEditor, CourseEntitlement,
2828
CourseLocationRestriction, CourseRun, CourseRunType, CourseType, Fact, GeoLocation, ProductMeta, ProductValue,
29-
RestrictedCourseRun, Seat, Source
29+
RestrictedCourseRun, Seat, Source, TaxiForm
3030
)
3131
from course_discovery.apps.course_metadata.signals import (
3232
additional_metadata_facts_changed, connect_course_data_modified_timestamp_signal_handlers,
@@ -1306,6 +1306,7 @@ def test_update_with_additional_metadata_if_type_2U(self, type_slug):
13061306
'product_meta': None,
13071307
'external_course_marketing_type': None,
13081308
'display_on_org_page': True,
1309+
'taxi_form': None,
13091310
}
13101311
url = reverse('api:v1:course-detail', kwargs={'key': course.uuid})
13111312
course_data = {
@@ -1418,6 +1419,7 @@ def test_update_facts_with_additional_metadata(self): # pylint: disable=too-man
14181419
'product_meta': None,
14191420
'external_course_marketing_type': None,
14201421
'display_on_org_page': True,
1422+
'taxi_form': None,
14211423
}
14221424
additional_metadata_1 = {
14231425
**additional_metadata,
@@ -1583,6 +1585,7 @@ def test_update_product_meta_with_additional_metadata(self):
15831585
'product_meta': product_meta,
15841586
'external_course_marketing_type': None,
15851587
'display_on_org_page': True,
1588+
'taxi_form': None,
15861589
}
15871590

15881591
url = reverse('api:v1:course-detail', kwargs={'key': course.uuid})
@@ -1596,7 +1599,6 @@ def test_update_product_meta_with_additional_metadata(self):
15961599
assert ProductMeta.objects.count() == 2
15971600

15981601
course = Course.everything.get(uuid=course.uuid, draft=True)
1599-
16001602
self.assertDictEqual(self.serialize_course(course)['additional_metadata'], additional_metadata)
16011603
self.assertDictEqual(self.serialize_course(course)['additional_metadata']['product_meta'], product_meta)
16021604
assert previous_data_modified_timestamp < course.data_modified_timestamp
@@ -1609,7 +1611,7 @@ def test_update_product_meta_with_additional_metadata(self):
16091611
response = self.client.patch(
16101612
url, {'additional_metadata': {'product_meta': product_meta}}, format='json'
16111613
)
1612-
additional_metadata['product_meta'] = product_meta
1614+
assert additional_metadata['product_meta'] == product_meta
16131615
assert response.status_code == 200
16141616
course = Course.everything.get(uuid=course.uuid, draft=True)
16151617

@@ -1632,6 +1634,152 @@ def test_update_product_meta_with_additional_metadata(self):
16321634
pre_save.disconnect(data_modified_timestamp_update, ProductMeta)
16331635
m2m_changed.disconnect(product_meta_taggable_changed, ProductMeta.keywords.through)
16341636

1637+
@responses.activate
1638+
def test_update_taxi_form_with_additional_metadata(self):
1639+
""" Verify that the taxi_form is updated when additional_metadata is updated. """
1640+
pre_save.connect(data_modified_timestamp_update, TaxiForm)
1641+
current = datetime.datetime.now(pytz.UTC)
1642+
EE_type_2U = CourseTypeFactory(slug=CourseType.EXECUTIVE_EDUCATION_2U)
1643+
course = CourseFactory(additional_metadata=None, type=EE_type_2U)
1644+
previous_data_modified_timestamp = course.data_modified_timestamp
1645+
1646+
taxi_form = {
1647+
"form_id": "12344",
1648+
"grouping": "test-grouping",
1649+
"title": course.title,
1650+
"subtitle": "hey you",
1651+
"post_submit_url": "http://www.edx.org",
1652+
}
1653+
1654+
additional_metadata = {
1655+
'external_url': 'https://example.com/456',
1656+
'external_identifier': '456',
1657+
'lead_capture_form_url': 'https://example.com/lead-capture',
1658+
'organic_url': 'https://example.com/organic',
1659+
'facts': [{'heading': 'Fact1 heading', 'blurb': '<p>Fact1 blurb</p>'}],
1660+
'certificate_info': {
1661+
'heading': 'Certificate heading',
1662+
'blurb': '<p>Certificate blurb</p>',
1663+
},
1664+
'start_date': serialize_datetime(current),
1665+
'end_date': serialize_datetime(current + datetime.timedelta(days=10)),
1666+
'registration_deadline': serialize_datetime(current),
1667+
'variant_id': str(uuid4()),
1668+
'course_term_override': 'Example Program',
1669+
'product_status': 'published',
1670+
'product_meta': None,
1671+
'external_course_marketing_type': None,
1672+
'display_on_org_page': True,
1673+
'taxi_form': taxi_form,
1674+
}
1675+
1676+
url = reverse('api:v1:course-detail', kwargs={'key': course.uuid})
1677+
response = self.client.patch(url, {'additional_metadata': additional_metadata}, format='json')
1678+
assert response.status_code == 200
1679+
1680+
assert TaxiForm.objects.get(form_id=taxi_form['form_id']) is not None
1681+
1682+
course = Course.everything.get(uuid=course.uuid, draft=True)
1683+
self.assertDictEqual(self.serialize_course(course)['additional_metadata'], additional_metadata)
1684+
self.assertDictEqual(self.serialize_course(course)['additional_metadata']['taxi_form'], taxi_form)
1685+
assert previous_data_modified_timestamp < course.data_modified_timestamp
1686+
1687+
previous_data_modified_timestamp = course.data_modified_timestamp
1688+
taxi_form['title'] = 'New title'
1689+
response = self.client.patch(
1690+
url, {'additional_metadata': {'taxi_form': taxi_form}}, format='json'
1691+
)
1692+
additional_metadata['taxi_form'] = taxi_form
1693+
1694+
assert response.status_code == 200
1695+
course = Course.everything.get(uuid=course.uuid, draft=True)
1696+
1697+
self.assertDictEqual(self.serialize_course(course)['additional_metadata'], additional_metadata)
1698+
self.assertDictEqual(self.serialize_course(course)['additional_metadata']['taxi_form'], taxi_form)
1699+
course.refresh_from_db(fields=('data_modified_timestamp',))
1700+
1701+
# If there is no taxi form change, the timestamp won't be updated.
1702+
previous_data_modified_timestamp = course.data_modified_timestamp
1703+
response = self.client.patch(
1704+
url, {'additional_metadata': {'taxi_form': taxi_form}}, format='json'
1705+
)
1706+
additional_metadata['taxi_form'] = taxi_form
1707+
assert response.status_code == 200
1708+
course = Course.everything.get(uuid=course.uuid, draft=True)
1709+
assert previous_data_modified_timestamp == course.data_modified_timestamp
1710+
1711+
pre_save.disconnect(data_modified_timestamp_update, TaxiForm)
1712+
1713+
@responses.activate
1714+
def test_update_additional_metadata__taxi_form_removal(self):
1715+
""" Verify that the taxi_form removed when additional_metadata is updated. """
1716+
pre_save.connect(data_modified_timestamp_update, TaxiForm)
1717+
current = datetime.datetime.now(pytz.UTC)
1718+
EE_type_2U = CourseTypeFactory(slug=CourseType.EXECUTIVE_EDUCATION_2U)
1719+
course = CourseFactory(additional_metadata=None, type=EE_type_2U)
1720+
previous_data_modified_timestamp = course.data_modified_timestamp
1721+
1722+
taxi_form = {
1723+
"form_id": "12344",
1724+
"grouping": "test-grouping",
1725+
"title": course.title,
1726+
"subtitle": "hey you",
1727+
"post_submit_url": "http://www.edx.org",
1728+
}
1729+
1730+
additional_metadata = {
1731+
'external_url': 'https://example.com/456',
1732+
'external_identifier': '456',
1733+
'lead_capture_form_url': 'https://example.com/lead-capture',
1734+
'organic_url': 'https://example.com/organic',
1735+
'facts': [{'heading': 'Fact1 heading', 'blurb': '<p>Fact1 blurb</p>'}],
1736+
'certificate_info': {
1737+
'heading': 'Certificate heading',
1738+
'blurb': '<p>Certificate blurb</p>',
1739+
},
1740+
'start_date': serialize_datetime(current),
1741+
'end_date': serialize_datetime(current + datetime.timedelta(days=10)),
1742+
'registration_deadline': serialize_datetime(current),
1743+
'variant_id': str(uuid4()),
1744+
'course_term_override': 'Example Program',
1745+
'product_status': 'published',
1746+
'product_meta': None,
1747+
'external_course_marketing_type': None,
1748+
'display_on_org_page': True,
1749+
'taxi_form': taxi_form,
1750+
}
1751+
1752+
url = reverse('api:v1:course-detail', kwargs={'key': course.uuid})
1753+
response = self.client.patch(url, {'additional_metadata': additional_metadata}, format='json')
1754+
assert response.status_code == 200
1755+
assert TaxiForm.objects.get(form_id=taxi_form['form_id']) is not None
1756+
1757+
course = Course.everything.get(uuid=course.uuid, draft=True)
1758+
self.assertDictEqual(self.serialize_course(course)['additional_metadata'], additional_metadata)
1759+
self.assertDictEqual(self.serialize_course(course)['additional_metadata']['taxi_form'], taxi_form)
1760+
assert previous_data_modified_timestamp < course.data_modified_timestamp
1761+
1762+
previous_data_modified_timestamp = course.data_modified_timestamp
1763+
taxi_form = {
1764+
"form_id": "",
1765+
"grouping": "",
1766+
"title": "",
1767+
"subtitle": "",
1768+
"post_submit_url": "",
1769+
}
1770+
additional_metadata['taxi_form'] = taxi_form
1771+
1772+
response = self.client.patch(
1773+
url, {'additional_metadata': {'taxi_form': taxi_form}}, format='json'
1774+
)
1775+
1776+
assert response.status_code == 200
1777+
course = Course.everything.get(uuid=course.uuid, draft=True)
1778+
1779+
self.assertDictEqual(self.serialize_course(course)['additional_metadata'], additional_metadata)
1780+
assert self.serialize_course(course)['additional_metadata']['taxi_form'] == taxi_form
1781+
pre_save.disconnect(data_modified_timestamp_update, TaxiForm)
1782+
16351783
@responses.activate
16361784
def test_update_success_with_course_type_verified(self):
16371785
verified_mode = SeatTypeFactory.verified()

course_discovery/apps/course_metadata/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,7 @@ def _get_course_keys(additional_metadata_object):
577577
class AdditionalMetadataAdmin(admin.ModelAdmin):
578578
list_display = (
579579
'id', 'external_identifier', 'external_url', 'courses', 'facts_list', 'external_course_marketing_type',
580+
'taxi_form',
580581
)
581582
search_fields = ('external_identifier', 'external_url')
582583
list_filter = ('product_status', )

course_discovery/apps/course_metadata/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -971,7 +971,7 @@ class AdditionalMetadata(ManageHistoryMixin, TimeStampedModel):
971971
def has_changed(self):
972972
if not self.pk:
973973
return False
974-
external_keys = [self.product_meta,]
974+
external_keys = [self.product_meta, self.taxi_form]
975975
return self.has_model_changed(external_keys=external_keys)
976976

977977
def update_product_data_modified_timestamp(self, bypass_has_changed=False):

0 commit comments

Comments
 (0)