Skip to content

Commit

Permalink
Merge pull request #857 from openedx/iahmad/ENT-9101
Browse files Browse the repository at this point in the history
feat: Updated video catalog models and utils
  • Loading branch information
irfanuddinahmad authored Jun 27, 2024
2 parents ad94880 + 01d4d53 commit 50968c3
Show file tree
Hide file tree
Showing 17 changed files with 241 additions and 41 deletions.
3 changes: 1 addition & 2 deletions enterprise_catalog/apps/api/v1/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1230,6 +1230,7 @@ def _get_expected_json_metadata(self, content_metadata, is_learner_portal_enable
"""
content_type = content_metadata.content_type
json_metadata = content_metadata.json_metadata.copy()
enrollment_url = '{}/enterprise/{}/{}/{}/enroll/?catalog={}&utm_medium=enterprise&utm_source={}'

json_metadata['content_last_modified'] = content_metadata.modified.isoformat()[:-6] + 'Z'
if content_metadata.is_exec_ed_2u_course and is_learner_portal_enabled:
Expand All @@ -1244,8 +1245,6 @@ def _get_expected_json_metadata(self, content_metadata, is_learner_portal_enable
enrollment_url = enterprise_proxy_login_url(self.enterprise_slug, next_url=exec_ed_enrollment_url)
elif is_learner_portal_enabled and content_type in (COURSE, COURSE_RUN):
enrollment_url = '{}/{}/course/{}?{}utm_medium=enterprise&utm_source={}'
else:
enrollment_url = '{}/enterprise/{}/{}/{}/enroll/?catalog={}&utm_medium=enterprise&utm_source={}'
marketing_url = '{}?utm_medium=enterprise&utm_source={}'
xapi_activity_id = '{}/xapi/activities/{}/{}'

Expand Down
4 changes: 4 additions & 0 deletions enterprise_catalog/apps/api_client/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
ENTERPRISE_CUSTOMER_ENDPOINT = urljoin(ENTERPRISE_API_URL, 'enterprise-customer/')
ENTERPRISE_CUSTOMER_CACHE_KEY_TPL = 'customer:{uuid}'
STUDIO_API_COURSE_VIDEOS_ENDPOINT = urljoin(settings.STUDIO_BASE_URL, '/api/contentstore/v1/videos/{course_run_key}')
STUDIO_API_VIDEOS_LOCATION_ENDPOINT = urljoin(
settings.STUDIO_BASE_URL,
'/api/contentstore/v1/videos/{course_run_key}/{edx_video_id}/usage'
)

# Ecommerce API Client Constants
COUPONS_OVERVIEW_ENDPOINT = urljoin(
Expand Down
22 changes: 21 additions & 1 deletion enterprise_catalog/apps/api_client/studio.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from .base_oauth import BaseOAuthClient
from .constants import STUDIO_API_COURSE_VIDEOS_ENDPOINT
from .constants import (
STUDIO_API_COURSE_VIDEOS_ENDPOINT,
STUDIO_API_VIDEOS_LOCATION_ENDPOINT,
)


class StudioApiClient(BaseOAuthClient):
Expand All @@ -20,3 +23,20 @@ def get_course_videos(self, course_run_key):
return self.client.get(
STUDIO_API_COURSE_VIDEOS_ENDPOINT.format(course_run_key=course_run_key),
).json()

def get_video_usage_locations(self, course_run_key, edx_video_id):
"""
Retrieve course video locations for the given course run and edx video id.
Arguments:
course_run_key (str): The course run key for which to retrieve video metadata.
edx_video_id (str): The edx video id for which to retrieve video metadata.
Returns:
(list): List of course video locations.
"""
locations = self.client.get(
STUDIO_API_VIDEOS_LOCATION_ENDPOINT.format(course_run_key=course_run_key, edx_video_id=edx_video_id),
).json().get('usage_locations', [])

return ([location['url'] for location in locations]) if locations else []
21 changes: 21 additions & 0 deletions enterprise_catalog/apps/video_catalog/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
Admin for video catalog models.
"""
from django.contrib import admin
from import_export import resources
from import_export.admin import ImportExportModelAdmin
from simple_history.admin import SimpleHistoryAdmin

from enterprise_catalog.apps.video_catalog.models import (
Video,
VideoShortlist,
VideoTranscriptSummary,
)

Expand All @@ -26,3 +29,21 @@ class VideoAdmin(SimpleHistoryAdmin):
"""
list_display = ('edx_video_id', 'client_video_id', 'created', 'modified', )
search_fields = ('edx_video_id', 'client_video_id', )


class VideoShortlistResource(resources.ModelResource):

class Meta:
model = VideoShortlist
import_id_fields = ('video_usage_key',)
fields = ('video_usage_key',)


@admin.register(VideoShortlist)
class VideoShortlistAdmin(ImportExportModelAdmin):
"""
Django admin for VideoShortlist.
"""
resource_classes = [VideoShortlistResource]
list_display = ('video_usage_key',)
search_fields = ('video_usage_key',)
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 4.2.13 on 2024-06-26 10:19

from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):

dependencies = [
('video_catalog', '0002_alter_video_parent_content_metadata'),
]

operations = [
migrations.CreateModel(
name='VideoShortlist',
fields=[
('video_usage_key', models.CharField(help_text='Video Xblock Usage Key', max_length=255, primary_key=True, serialize=False)),
],
options={
'verbose_name': 'Shortlisted Video',
'verbose_name_plural': 'Shortlisted Videos',
},
),
migrations.AddField(
model_name='historicalvideo',
name='video_usage_key',
field=models.CharField(default=django.utils.timezone.now, help_text='Video Xblock Usage Key', max_length=255),
preserve_default=False,
),
migrations.AddField(
model_name='video',
name='video_usage_key',
field=models.CharField(default=django.utils.timezone.now, help_text='Video Xblock Usage Key', max_length=255),
preserve_default=False,
),
]
28 changes: 28 additions & 0 deletions enterprise_catalog/apps/video_catalog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class Video(TimeStampedModel):
"""
edx_video_id = models.CharField(primary_key=True, max_length=255, help_text=_('EdX video id'))
client_video_id = models.CharField(max_length=255, help_text=_('Client video id'))
video_usage_key = models.CharField(max_length=255, help_text=_('Video Xblock Usage Key'))
parent_content_metadata = models.ForeignKey(
ContentMetadata,
related_name='videos',
Expand Down Expand Up @@ -128,3 +129,30 @@ def __str__(self):
video_id=str(self.video)
)
)


class VideoShortlist(models.Model):
"""
Stores the shortlisted videos for microlearning index and search.
Example video_usage_key
block-v1:UnivX+QMB1+2T2017+type@video+block@0accf77cf6634c93b0f095f65fed41a1
.. no_pii:
"""
video_usage_key = models.CharField(primary_key=True, max_length=255, help_text=_('Video Xblock Usage Key'))

class Meta:
verbose_name = _("Shortlisted Video")
verbose_name_plural = _("Shortlisted Videos")
app_label = 'video_catalog'

def __str__(self):
"""
Return human-readable string representation.
"""
return (
"<VideoShortlist for '{usage_key}'>".format(
usage_key=str(self.video_usage_key)
)
)
47 changes: 28 additions & 19 deletions enterprise_catalog/apps/video_catalog/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,64 @@

import requests
from django.conf import settings
from opaque_keys.edx.keys import UsageKey
from rest_framework.exceptions import ValidationError

from enterprise_catalog.apps.ai_curation.openai_client import chat_completions
from enterprise_catalog.apps.api_client.studio import StudioApiClient
from enterprise_catalog.apps.catalog.constants import COURSE, COURSE_RUN
from enterprise_catalog.apps.catalog.constants import COURSE_RUN
from enterprise_catalog.apps.catalog.models import ContentMetadata
from enterprise_catalog.apps.video_catalog.models import Video
from enterprise_catalog.apps.video_catalog.models import Video, VideoShortlist


logger = logging.getLogger(__name__)


def fetch_course_video_metadata(course_run_key):
def fetch_course_video_metadata(course_run_key, video_usage_key):
"""
Fetch and store video metadata from the Studio service.
Arguments:
course_run_key (str): The course run key for which to fetch video metadata.
video_usage_key (str): The video usage key for which to fetch video metadata.
Raises:
(DoesNotExist): If the course run key does not exist in the ContentMetadata model.
"""
client = StudioApiClient()
video_metadata = client.get_course_videos(course_run_key)
for video_data in video_metadata.get('previous_uploads', []):
Video.objects.update_or_create(
edx_video_id=video_data['edx_video_id'],
defaults={
'client_video_id': video_data['client_video_id'],
'json_metadata': video_data,
'parent_content_metadata': ContentMetadata.objects.get(
content_key=course_run_key, content_type=COURSE_RUN
video_usage_locations = client.get_video_usage_locations(course_run_key, video_data['edx_video_id'])
for location in video_usage_locations:
if video_usage_key in location:
Video.objects.update_or_create(
edx_video_id=video_data['edx_video_id'],
video_usage_key=video_usage_key,
defaults={
'client_video_id': video_data['client_video_id'],
'json_metadata': video_data,
'parent_content_metadata': ContentMetadata.objects.get(
content_key=course_run_key, content_type=COURSE_RUN
)
}
)
}
)


def fetch_videos(course_keys):
def fetch_videos():
"""
Fetch and store video metadata for multiple course run keys.
Arguments:
course_keys (list): List of course run keys for which to fetch video metadata.
"""
# Import is placed here to avoid circular imports
courses = ContentMetadata.objects.filter(content_key__in=course_keys, content_type=COURSE)
for course in courses:
course_run = course.get_child_records(course).first()
if course_run:
fetch_course_video_metadata(course_run.content_key)
shortlisted_videos = VideoShortlist.objects.all()
for video in shortlisted_videos:
try:
video_usage_key = UsageKey.from_string(video.video_usage_key)
except ValueError:
raise ValidationError('Invalid usage key') # lint-amnesty, pylint: disable=raise-missing-from
course_run_key = str(video_usage_key.context_key)
fetch_course_video_metadata(course_run_key, video.video_usage_key)


def get_transcript_summary(transcript: str, max_length: int = 260) -> str:
Expand Down
1 change: 1 addition & 0 deletions enterprise_catalog/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
'waffle',
'release_util',
'rules.apps.AutodiscoverRulesConfig',
'import_export',
)

PROJECT_APPS = (
Expand Down
1 change: 1 addition & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ django-config-models
django-cors-headers
django-crum
django-extensions
django-import-export
django-model-utils
django-simple-history
djangorestframework
Expand Down
9 changes: 8 additions & 1 deletion requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ defusedxml==0.7.1
# djangorestframework-xml
# python3-openid
# social-auth-core
diff-match-patch==20230430
# via django-import-export
distro==1.9.0
# via openai
django==4.2.13
Expand All @@ -83,6 +85,7 @@ django==4.2.13
# django-cors-headers
# django-crum
# django-extensions
# django-import-export
# django-log-request-id
# django-model-utils
# django-simple-history
Expand Down Expand Up @@ -116,6 +119,8 @@ django-crum==0.7.9
# edx-toggles
django-extensions==3.2.3
# via -r requirements/base.in
django-import-export==4.1.0
# via -r requirements/base.in
django-log-request-id==2.1.0
# via -r requirements/base.in
django-model-utils==4.5.1
Expand Down Expand Up @@ -244,7 +249,7 @@ pyjwt[crypto]==2.8.0
# social-auth-core
pymemcache==4.0.0
# via -r requirements/base.in
pymongo==4.7.3
pymongo==4.8.0
# via edx-opaque-keys
pynacl==1.5.0
# via edx-django-utils
Expand Down Expand Up @@ -325,6 +330,8 @@ stevedore==5.2.0
# code-annotations
# edx-django-utils
# edx-opaque-keys
tablib==3.5.0
# via django-import-export
text-unidecode==1.3
# via python-slugify
threadpoolctl==3.5.0
Expand Down
21 changes: 18 additions & 3 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ diff-cover==4.0.0
# via
# -c requirements/constraints.txt
# -r requirements/dev.in
diff-match-patch==20230430
# via
# -r requirements/quality.txt
# -r requirements/test.txt
# django-import-export
dill==0.3.8
# via
# -r requirements/quality.txt
Expand Down Expand Up @@ -189,6 +194,7 @@ django==4.2.13
# django-crum
# django-debug-toolbar
# django-extensions
# django-import-export
# django-log-request-id
# django-model-utils
# django-simple-history
Expand Down Expand Up @@ -238,6 +244,10 @@ django-extensions==3.2.3
# via
# -r requirements/quality.txt
# -r requirements/test.txt
django-import-export==4.1.0
# via
# -r requirements/quality.txt
# -r requirements/test.txt
django-log-request-id==2.1.0
# via
# -r requirements/quality.txt
Expand Down Expand Up @@ -340,7 +350,7 @@ edx-toggles==5.2.0
# -r requirements/test.txt
factory-boy==3.3.0
# via -r requirements/test.txt
faker==25.9.1
faker==26.0.0
# via
# -r requirements/test.txt
# factory-boy
Expand Down Expand Up @@ -559,7 +569,7 @@ pyjwt[crypto]==2.8.0
# edx-drf-extensions
# edx-rest-api-client
# social-auth-core
pylint==3.2.3
pylint==3.2.4
# via
# -r requirements/quality.txt
# -r requirements/test.txt
Expand Down Expand Up @@ -587,7 +597,7 @@ pymemcache==4.0.0
# via
# -r requirements/quality.txt
# -r requirements/test.txt
pymongo==4.7.3
pymongo==4.8.0
# via
# -r requirements/quality.txt
# -r requirements/test.txt
Expand Down Expand Up @@ -757,6 +767,11 @@ stevedore==5.2.0
# code-annotations
# edx-django-utils
# edx-opaque-keys
tablib==3.5.0
# via
# -r requirements/quality.txt
# -r requirements/test.txt
# django-import-export
text-unidecode==1.3
# via
# -r requirements/quality.txt
Expand Down
Loading

0 comments on commit 50968c3

Please sign in to comment.