diff --git a/frontends/api/src/generated/v1/api.ts b/frontends/api/src/generated/v1/api.ts index b230201908..0b518da320 100644 --- a/frontends/api/src/generated/v1/api.ts +++ b/frontends/api/src/generated/v1/api.ts @@ -806,6 +806,12 @@ export interface CourseResource { * @memberof CourseResource */ continuing_ed_credits?: string | null + /** + * + * @type {string} + * @memberof CourseResource + */ + location?: string } /** @@ -1020,6 +1026,12 @@ export interface CourseResourceRequest { * @memberof CourseResourceRequest */ continuing_ed_credits?: string | null + /** + * + * @type {string} + * @memberof CourseResourceRequest + */ + location?: string } /** @@ -1582,6 +1594,12 @@ export interface LearningPathResource { * @memberof LearningPathResource */ continuing_ed_credits?: string | null + /** + * + * @type {string} + * @memberof LearningPathResource + */ + location?: string } /** @@ -1680,6 +1698,12 @@ export interface LearningPathResourceRequest { * @memberof LearningPathResourceRequest */ continuing_ed_credits?: string | null + /** + * + * @type {string} + * @memberof LearningPathResourceRequest + */ + location?: string } /** @@ -2336,6 +2360,12 @@ export interface LearningResourceRun { * @memberof LearningResourceRun */ availability?: AvailabilityEnum | null + /** + * + * @type {string} + * @memberof LearningResourceRun + */ + location?: string } /** @@ -2491,6 +2521,12 @@ export interface LearningResourceRunRequest { * @memberof LearningResourceRunRequest */ availability?: AvailabilityEnum | null + /** + * + * @type {string} + * @memberof LearningResourceRunRequest + */ + location?: string } /** @@ -3517,6 +3553,12 @@ export interface PatchedLearningPathResourceRequest { * @memberof PatchedLearningPathResourceRequest */ continuing_ed_credits?: string | null + /** + * + * @type {string} + * @memberof PatchedLearningPathResourceRequest + */ + location?: string } /** @@ -4315,6 +4357,12 @@ export interface PodcastEpisodeResource { * @memberof PodcastEpisodeResource */ continuing_ed_credits?: string | null + /** + * + * @type {string} + * @memberof PodcastEpisodeResource + */ + location?: string } /** @@ -4413,6 +4461,12 @@ export interface PodcastEpisodeResourceRequest { * @memberof PodcastEpisodeResourceRequest */ continuing_ed_credits?: string | null + /** + * + * @type {string} + * @memberof PodcastEpisodeResourceRequest + */ + location?: string } /** @@ -4691,6 +4745,12 @@ export interface PodcastResource { * @memberof PodcastResource */ continuing_ed_credits?: string | null + /** + * + * @type {string} + * @memberof PodcastResource + */ + location?: string } /** @@ -4789,6 +4849,12 @@ export interface PodcastResourceRequest { * @memberof PodcastResourceRequest */ continuing_ed_credits?: string | null + /** + * + * @type {string} + * @memberof PodcastResourceRequest + */ + location?: string } /** @@ -5287,6 +5353,12 @@ export interface ProgramResource { * @memberof ProgramResource */ continuing_ed_credits?: string | null + /** + * + * @type {string} + * @memberof ProgramResource + */ + location?: string } /** @@ -5385,6 +5457,12 @@ export interface ProgramResourceRequest { * @memberof ProgramResourceRequest */ continuing_ed_credits?: string | null + /** + * + * @type {string} + * @memberof ProgramResourceRequest + */ + location?: string } /** @@ -6142,6 +6220,12 @@ export interface VideoPlaylistResource { * @memberof VideoPlaylistResource */ continuing_ed_credits?: string | null + /** + * + * @type {string} + * @memberof VideoPlaylistResource + */ + location?: string } /** @@ -6240,6 +6324,12 @@ export interface VideoPlaylistResourceRequest { * @memberof VideoPlaylistResourceRequest */ continuing_ed_credits?: string | null + /** + * + * @type {string} + * @memberof VideoPlaylistResourceRequest + */ + location?: string } /** @@ -6506,6 +6596,12 @@ export interface VideoResource { * @memberof VideoResource */ continuing_ed_credits?: string | null + /** + * + * @type {string} + * @memberof VideoResource + */ + location?: string } /** @@ -6604,6 +6700,12 @@ export interface VideoResourceRequest { * @memberof VideoResourceRequest */ continuing_ed_credits?: string | null + /** + * + * @type {string} + * @memberof VideoResourceRequest + */ + location?: string } /** diff --git a/learning_resources/etl/constants.py b/learning_resources/etl/constants.py index fd1e7fcc6a..339e83c291 100644 --- a/learning_resources/etl/constants.py +++ b/learning_resources/etl/constants.py @@ -116,3 +116,4 @@ class ResourceNextRunConfig: next_start_date: datetime = None prices: list[Decimal] = field(default_factory=list) availability: str = None + location: str = None diff --git a/learning_resources/etl/loaders.py b/learning_resources/etl/loaders.py index be472cce26..238ef8b40b 100644 --- a/learning_resources/etl/loaders.py +++ b/learning_resources/etl/loaders.py @@ -138,17 +138,20 @@ def load_run_dependent_values( next_upcoming_run or resource.runs.filter(published=True).order_by("-start_date").first() ) - resource.availability = best_run.availability if best_run else resource.availability - resource.prices = ( - best_run.prices - if resource.certification and best_run and best_run.prices - else [] - ) + if best_run: + resource.availability = best_run.availability + resource.prices = ( + best_run.prices + if resource.certification and best_run and best_run.prices + else [] + ) + resource.location = best_run.location resource.save() return ResourceNextRunConfig( next_start_date=resource.next_start_date, prices=resource.prices, availability=resource.availability, + location=resource.location, ) diff --git a/learning_resources/etl/loaders_test.py b/learning_resources/etl/loaders_test.py index 6a57a47261..1aabe6e9e9 100644 --- a/learning_resources/etl/loaders_test.py +++ b/learning_resources/etl/loaders_test.py @@ -1463,6 +1463,7 @@ def test_load_run_dependent_values(certification): availability=Availability.dated.name, prices=[Decimal("0.00"), Decimal("20.00")], start_date=closest_date, + location="Portland, ME", ) LearningResourceRunFactory.create( learning_resource=course, @@ -1470,11 +1471,13 @@ def test_load_run_dependent_values(certification): availability=Availability.dated.name, prices=[Decimal("0.00"), Decimal("50.00")], start_date=furthest_date, + location="Portland, OR", ) result = load_run_dependent_values(course) assert result.next_start_date == course.next_start_date == closest_date assert result.prices == course.prices == ([] if not certification else run.prices) assert result.availability == course.availability == Availability.dated.name + assert result.location == course.location == run.location @pytest.mark.parametrize( diff --git a/learning_resources/etl/sloan.py b/learning_resources/etl/sloan.py index a58b4e311e..deb90008ce 100644 --- a/learning_resources/etl/sloan.py +++ b/learning_resources/etl/sloan.py @@ -170,6 +170,21 @@ def parse_format(run_data: dict) -> str: return default_format() +def parse_location(run_data: dict) -> str: + """ + Parse location from run data + + Args: + run_data (list): the run data + + Returns: + str: the location + """ + if not run_data or run_data["Delivery"] == "Online": + return "" + return run_data["Location"] or "" + + def extract(): """ Extract Sloan Executive Education data @@ -232,6 +247,7 @@ def transform_run(run_data, course_data): "instructors": [{"full_name": name.strip()} for name in faculty_names], "pace": [parse_pace(run_data)], "format": parse_format(run_data), + "location": parse_location(run_data), } diff --git a/learning_resources/etl/sloan_test.py b/learning_resources/etl/sloan_test.py index 85c472d15e..b22a09ef7d 100644 --- a/learning_resources/etl/sloan_test.py +++ b/learning_resources/etl/sloan_test.py @@ -17,6 +17,7 @@ parse_datetime, parse_format, parse_image, + parse_location, parse_pace, transform_course, transform_delivery, @@ -130,6 +131,7 @@ def test_transform_run( "instructors": [{"full_name": name.strip()} for name in faculty_names], "pace": [Pace.instructor_paced.name], "format": [Format.synchronous.name], + "location": run_data["Location"], } @@ -280,3 +282,22 @@ def test_parse_format(delivery, run_format, expected_format): } assert parse_format(run_data) == expected_format assert parse_format(None) == [Format.asynchronous.name] + + +@pytest.mark.parametrize( + ("delivery", "location", "result"), + [ + ("Online", "Online", ""), + ("In Person", "Cambridge, MA", "Cambridge, MA"), + ("Blended", "Boston, MA", "Boston, MA"), + ("Online", None, ""), + ], +) +def test_parse_location(delivery, location, result): + """Test that the location is parsed correctly""" + run_data = { + "Delivery": delivery, + "Location": location, + } + assert parse_location(run_data) == result + assert parse_location(None) == "" diff --git a/learning_resources/migrations/0070_learningresource_location.py b/learning_resources/migrations/0070_learningresource_location.py new file mode 100644 index 0000000000..3d6ba2cec0 --- /dev/null +++ b/learning_resources/migrations/0070_learningresource_location.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.16 on 2024-09-24 15:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("learning_resources", "0069_learningresource_ocw_topics"), + ] + + operations = [ + migrations.AddField( + model_name="learningresource", + name="location", + field=models.CharField(blank=True, max_length=256), + ), + migrations.AddField( + model_name="learningresourcerun", + name="location", + field=models.CharField(blank=True, max_length=256), + ), + ] diff --git a/learning_resources/models.py b/learning_resources/models.py index e3c3bf3d75..b5f11e2981 100644 --- a/learning_resources/models.py +++ b/learning_resources/models.py @@ -8,7 +8,7 @@ from django.contrib.auth.models import User from django.contrib.postgres.fields import ArrayField from django.db import models -from django.db.models import Count, JSONField, OuterRef, Prefetch, Q +from django.db.models import CharField, Count, JSONField, OuterRef, Prefetch, Q from django.db.models.functions import Lower from django.utils import timezone @@ -455,6 +455,7 @@ class LearningResource(TimestampedModel): ), default=default_format, ) + location = models.CharField(max_length=256, blank=True) @property def audience(self) -> str | None: @@ -606,6 +607,7 @@ class LearningResourceRun(TimestampedModel): ), default=default_format, ) + location = CharField(max_length=256, blank=True) def __str__(self): return f"LearningResourceRun platform={self.learning_resource.platform} run_id={self.run_id}" # noqa: E501 diff --git a/learning_resources/serializers_test.py b/learning_resources/serializers_test.py index eb7a43e0b9..960c0bc622 100644 --- a/learning_resources/serializers_test.py +++ b/learning_resources/serializers_test.py @@ -273,6 +273,7 @@ def test_learning_resource_serializer( # noqa: PLR0913 "pace": [ {"code": lr_pace, "name": Pace[lr_pace].value} for lr_pace in resource.pace ], + "location": resource.location, "next_start_date": resource.next_start_date, "availability": resource.availability, "completeness": 1.0, diff --git a/learning_resources_search/constants.py b/learning_resources_search/constants.py index 31b9b8fa8a..d64ac12ada 100644 --- a/learning_resources_search/constants.py +++ b/learning_resources_search/constants.py @@ -285,6 +285,7 @@ class FilterConfig: }, }, "prices": {"type": "scaled_float", "scaling_factor": 100}, + "location": {"type": "keyword"}, }, }, "next_start_date": {"type": "date"}, @@ -293,6 +294,7 @@ class FilterConfig: "completeness": {"type": "float"}, "license_cc": {"type": "boolean"}, "continuing_ed_credits": {"type": "float"}, + "location": {"type": "keyword"}, } diff --git a/openapi/specs/v1.yaml b/openapi/specs/v1.yaml index 3ea879b09b..e0a8571a55 100644 --- a/openapi/specs/v1.yaml +++ b/openapi/specs/v1.yaml @@ -8095,6 +8095,9 @@ components: format: decimal pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ nullable: true + location: + type: string + maxLength: 256 required: - certification - certification_type @@ -8186,6 +8189,9 @@ components: format: decimal pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ nullable: true + location: + type: string + maxLength: 256 required: - readable_id - title @@ -8584,6 +8590,9 @@ components: format: decimal pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ nullable: true + location: + type: string + maxLength: 256 required: - certification - certification_type @@ -8672,6 +8681,9 @@ components: format: decimal pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ nullable: true + location: + type: string + maxLength: 256 required: - title LearningPathResourceResourceTypeEnum: @@ -9204,6 +9216,9 @@ components: oneOf: - $ref: '#/components/schemas/AvailabilityEnum' - $ref: '#/components/schemas/NullEnum' + location: + type: string + maxLength: 256 required: - delivery - format @@ -9315,6 +9330,9 @@ components: oneOf: - $ref: '#/components/schemas/AvailabilityEnum' - $ref: '#/components/schemas/NullEnum' + location: + type: string + maxLength: 256 required: - level - run_id @@ -10057,6 +10075,9 @@ components: format: decimal pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ nullable: true + location: + type: string + maxLength: 256 PatchedLearningResourceRelationshipRequest: type: object description: CRUD serializer for LearningResourceRelationship @@ -10688,6 +10709,9 @@ components: format: decimal pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ nullable: true + location: + type: string + maxLength: 256 required: - certification - certification_type @@ -10779,6 +10803,9 @@ components: format: decimal pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ nullable: true + location: + type: string + maxLength: 256 required: - readable_id - title @@ -11018,6 +11045,9 @@ components: format: decimal pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ nullable: true + location: + type: string + maxLength: 256 required: - certification - certification_type @@ -11109,6 +11139,9 @@ components: format: decimal pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ nullable: true + location: + type: string + maxLength: 256 required: - readable_id - title @@ -11478,6 +11511,9 @@ components: format: decimal pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ nullable: true + location: + type: string + maxLength: 256 required: - certification - certification_type @@ -11569,6 +11605,9 @@ components: format: decimal pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ nullable: true + location: + type: string + maxLength: 256 required: - readable_id - title @@ -12080,6 +12119,9 @@ components: format: decimal pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ nullable: true + location: + type: string + maxLength: 256 required: - certification - certification_type @@ -12171,6 +12213,9 @@ components: format: decimal pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ nullable: true + location: + type: string + maxLength: 256 required: - readable_id - title @@ -12396,6 +12441,9 @@ components: format: decimal pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ nullable: true + location: + type: string + maxLength: 256 required: - certification - certification_type @@ -12487,6 +12535,9 @@ components: format: decimal pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ nullable: true + location: + type: string + maxLength: 256 required: - readable_id - title