Skip to content

Commit

Permalink
feat: add bootcamps url restructuring method to command (#4075)
Browse files Browse the repository at this point in the history
  • Loading branch information
AfaqShuaib09 authored Sep 8, 2023
1 parent f18c8b0 commit 8e73eff
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 5 deletions.
6 changes: 5 additions & 1 deletion course_discovery/apps/course_metadata/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
REFRESH_COURSE_SKILLS_URL_NAME = 'refresh_course_skills'
REFRESH_PROGRAM_SKILLS_URL_NAME = 'refresh_program_skills'
COURSE_UUID_REGEX = r'[0-9a-f-]+'
SUBDIRECTORY_SLUG_FORMAT_REGEX = r'learn\/[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_]+$|executive-education\/[a-zA-Z0-9-_]+$'
SUBDIRECTORY_SLUG_FORMAT_REGEX = (
r'learn\/[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_]+$|'
r'executive-education\/[a-zA-Z0-9-_]+$|'
r'boot-camps\/[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_]+$'
)
SLUG_FORMAT_REGEX = r'[a-zA-Z0-9-_]+$'

DEFAULT_SLUG_FORMAT_ERROR_MSG = 'Enter a valid “slug” consisting of letters, numbers, underscores or hyphens.'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
class Command(BaseCommand):
help = """
It will update course url slugs to the format 'learn/<primary_subject>/<organization_name>-<course_title>' for
open courses and 'executive-education/<organization_name>-<course_title>' for executive education courses
open courses, 'executive-education/<organization_name>-<course_title>' for executive education courses, and
'boot-camps/<primary-subject>/<organization_name>-<course_title>' for bootcamps
"""

def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -55,7 +56,7 @@ def add_arguments(self, parser):
'--course_type',
help='Course Type to update slug',
type=str,
choices=[CourseType.EXECUTIVE_EDUCATION_2U, 'open-course'],
choices=[CourseType.BOOTCAMP_2U, CourseType.EXECUTIVE_EDUCATION_2U, 'open-course'],
default='open-course',
)
parser.add_argument(
Expand Down Expand Up @@ -186,6 +187,8 @@ def _get_courses(self):
courses = Course.everything.filter(product_source__slug=self.product_source, draft=True)
if self.course_type == CourseType.EXECUTIVE_EDUCATION_2U:
return courses.filter(type__slug=CourseType.EXECUTIVE_EDUCATION_2U)
elif self.course_type == CourseType.BOOTCAMP_2U:
return courses.filter(type__slug=CourseType.BOOTCAMP_2U)
# Return Open Courses only
return courses.exclude(type__slug__in=[CourseType.EXECUTIVE_EDUCATION_2U, CourseType.BOOTCAMP_2U])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,12 @@ def setUp(self):
self.exec_ed_course1 = CourseFactory(
draft=True, product_source=self.external_product_source, partner=partner, type=ee_type_2u
)
self.exec_ed_course1.authoring_organizations.add(self.organization)
self.bootcamp_course_1 = CourseFactory(
draft=True, product_source=self.product_source, partner=partner, type=bootcamp_type
draft=True, product_source=self.external_product_source, partner=partner, type=bootcamp_type
)
self.exec_ed_course1.authoring_organizations.add(self.organization)
self.bootcamp_course_1.authoring_organizations.add(self.organization)
self.bootcamp_course_1.subjects.add(self.subject)

def test_migrate_course_slug_success_flow(self):
with LogCapture(LOGGER_PATH) as log_capture:
Expand Down Expand Up @@ -286,6 +288,39 @@ def test_migrate_course_slug_success_flow__executive_education(self):

assert self.exec_ed_course1.active_url_slug == f"executive-education/test-organization-{slugify(self.exec_ed_course1.title)}" # pylint: disable=line-too-long

def test_migrate_course_slug_success_flow__bootcamps(self):
"""
It will verify that command is generating and saving correct slugs for bootcamp courses
"""

with LogCapture(LOGGER_PATH) as log_capture:
current_slug_course1 = self.bootcamp_course_1.active_url_slug

with override_waffle_switch(IS_SUBDIRECTORY_SLUG_FORMAT_ENABLED, active=True):
call_command(
'migrate_course_slugs',
'--course_uuids', self.bootcamp_course_1.uuid,
'--course_type', CourseType.BOOTCAMP_2U,
'--product_source', self.external_product_source.slug
)
log_capture.check_present(
(
LOGGER_PATH,
'INFO',
f"Updating slug for course with uuid {self.bootcamp_course_1.uuid} and title "
f"{self.bootcamp_course_1.title}, current slug is '{current_slug_course1}'"
),
(
LOGGER_PATH,
'INFO',
f"course_uuid,old_slug,new_slug,error\n"
f"{self.bootcamp_course_1.uuid},{current_slug_course1},"
f"{self.bootcamp_course_1.active_url_slug},None\n"
)
)

assert self.bootcamp_course_1.active_url_slug == f"boot-camps/{self.subject.slug}/test-organization-{slugify(self.bootcamp_course_1.title)}" # pylint: disable=line-too-long

def test_migrate_course_slug_success_flow__edx_bootcamps(self):
"""
It will verify that command is generating and saving correct slugs for executive education courses
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.20 on 2023-09-01 13:26

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('course_metadata', '0331_auto_20230810_0748'),
]

operations = [
migrations.AlterField(
model_name='migratecourseslugconfiguration',
name='course_type',
field=models.CharField(choices=[('open-course', 'Open Courses'), ('executive-education-2u', '2U Executive Education Courses'), ('bootcamp-2u', 'Bootcamps')], default='open-course', max_length=255),
),
]
1 change: 1 addition & 0 deletions course_discovery/apps/course_metadata/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4219,6 +4219,7 @@ class MigrateCourseSlugConfiguration(ConfigurationModel):
COURSE_TYPE_CHOICES = (
(OPEN_COURSE, _('Open Courses')),
(CourseType.EXECUTIVE_EDUCATION_2U, _('2U Executive Education Courses')),
(CourseType.BOOTCAMP_2U, _('Bootcamps'))
)
# Timeout set to 0 so that the model does not read from cached config in case the config entry is deleted.
cache_timeout = 0
Expand Down
52 changes: 52 additions & 0 deletions course_discovery/apps/course_metadata/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1082,6 +1082,34 @@ def test_get_slug_for_exec_ed_course(self):
assert error is None
assert slug == f"executive-education/{organization.name}-{slugify(course.title)}"

def test_get_slug_for_bootcamp_course__raise_error_for_bootcamp_with_no_authoring_org(self):
"""
It will verify that slug aren't generated for bootcamp courses with no authoring org and error is raised
"""
bootcamp_type = CourseTypeFactory(slug=CourseType.BOOTCAMP_2U)
course = CourseFactory(title='test-bootcamp', type=bootcamp_type)
slug, error = utils.get_slug_for_course(course)
assert slug is None
assert error == f"Course with uuid {course.uuid} and title {course.title} does not have any authoring " \
f"organizations"

def test_get_slug_for_bootcamp_course(self):
"""
It will verify that slug are generated correctly for bootcamp courses
"""
bootcamp_type = CourseTypeFactory(slug=CourseType.BOOTCAMP_2U)
course = CourseFactory(title='test-bootcamp', type=bootcamp_type)
org = OrganizationFactory(name='test-organization')
course.authoring_organizations.add(org)

slug, error = utils.get_slug_for_course(course)
subject = SubjectFactory(name='business')
course.subjects.add(subject)
slug, error = utils.get_slug_for_course(course)

assert error is None
assert slug == f"boot-camps/{subject.slug}/{org.name}-{slugify(course.title)}"

def test_get_slug_for_course__with_no_url_slug(self):
course = CourseFactory(title='test-title')
subject = SubjectFactory(name='business')
Expand Down Expand Up @@ -1170,6 +1198,30 @@ def test_get_slug_for_exec_ed_course__with_existing_url_slug(self):
assert error is None
assert slug == f"executive-education/{organization.name}-{slugify(course2.title)}-3"

def test_get_slug_for_bootcamp_course__with_existing_url_slug(self):
"""
Test for bootcamp course with existing subdirectory url slug
"""
bootcamp_type = CourseTypeFactory(slug=CourseType.BOOTCAMP_2U)
partner = PartnerFactory()
subject = SubjectFactory(name='business')
org = OrganizationFactory(name='test-organization')

# Create and test multiple courses with the same title, subject, and organization
for i in range(1, 3):
course = CourseFactory(title='test-title', type=bootcamp_type, partner=partner)
course.authoring_organizations.add(org)
course.subjects.add(subject)
course.save()
CourseUrlSlug.objects.filter(course=course).delete()
slug, error = utils.get_slug_for_course(course)
assert error is None
if i == 1:
assert slug == f"boot-camps/{subject.slug}/{org.name}-{slugify(course.title)}"
else:
assert slug == f"boot-camps/{subject.slug}/{org.name}-{slugify(course.title)}-{i}"
course.set_active_url_slug(slug)

def test_get_existing_slug_count(self):
course1 = CourseFactory(title='test-title')
slug = 'learn/business/test-organization-test-title'
Expand Down
30 changes: 30 additions & 0 deletions course_discovery/apps/course_metadata/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,9 @@ def get_slug_for_course(course):
organization_slug = slugify(organizations[0].name.replace('\'', ''))

def create_slug_for_exec_ed():
"""
Method to create subdirectory url slug for executive education courses
"""
course_slug = slugify(course.title)
slug = f"executive-education/{organization_slug}-{course_slug}"
if is_existing_slug(slug, course):
Expand All @@ -1022,7 +1025,31 @@ def create_slug_for_exec_ed():
slug = f"executive-education/{organization_slug}-{course_slug}"
return slug, error

def create_slug_for_bootcamps():
"""
Method to create subdirectory url slug for bootcamps
"""
course_slug = slugify(course.title)
course_subjects = course.subjects.all()
if not course_subjects:
error = f"Bootcamp with uuid {course.uuid} and title {course.title} does not have any subject"
logger.info(error)
return None, error
primary_subject_slug = course_subjects[0].slug

slug = f'boot-camps/{primary_subject_slug}/{organization_slug}-{course_slug}'
if is_existing_slug(slug, course):
logger.info(
f"Bootcamp Slug '{slug}' already exists in DB, recreating slug by adding a number in course_title"
)
course_slug = f"{slugify(course.title)}-{get_existing_slug_count(slug) + 1}"
slug = f"boot-camps/{primary_subject_slug}/{organization_slug}-{course_slug}"
return slug, None

def create_slug_for_ocm():
"""
Method to create subdirectory url slug for OCM courses
"""
course_subjects = course.subjects.all()
if not course_subjects:
error = f"Course with uuid {course.uuid} and title {course.title} does not have any subject"
Expand All @@ -1042,6 +1069,9 @@ def create_slug_for_ocm():
slug = f"learn/{primary_subject_slug}/{organization_slug}-{course_slug}"
return slug, None

if course.type.slug == CourseType.BOOTCAMP_2U:
return create_slug_for_bootcamps()

if course.type.slug == CourseType.EXECUTIVE_EDUCATION_2U:
return create_slug_for_exec_ed()

Expand Down

0 comments on commit 8e73eff

Please sign in to comment.