Skip to content

Commit

Permalink
feat: skill validation configuration to disable validation for xblock…
Browse files Browse the repository at this point in the history
… skills
  • Loading branch information
muhammad-ammar committed Mar 21, 2024
1 parent 036041f commit d0b6283
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 4 deletions.
9 changes: 8 additions & 1 deletion course_discovery/apps/taxonomy_support/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
"""
from django.contrib import admin

from course_discovery.apps.taxonomy_support.models import CourseRecommendation, UpdateCourseRecommendationsConfig
from course_discovery.apps.taxonomy_support.models import (
CourseRecommendation, SkillValidationConfiguration, UpdateCourseRecommendationsConfig
)


@admin.register(CourseRecommendation)
Expand All @@ -20,3 +22,8 @@ class CourseRecommendationAdmin(admin.ModelAdmin):
class UpdateCourseRecommendationsConfigAdmin(admin.ModelAdmin):
fields = ('uuids', 'num_past_days', 'all_courses')
list_display = ('id',)


@admin.register(SkillValidationConfiguration)
class SkillValidationConfigurationAdmin(admin.ModelAdmin):
autocomplete_fields = ['course', 'organization']
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated by Django 4.2.9 on 2024-03-21 07:24

from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields


class Migration(migrations.Migration):

dependencies = [
('course_metadata', '0340_auto_20231211_1032'),
('taxonomy_support', '0003_updatecourserecommendationsconfig_num_past_days'),
]

operations = [
migrations.CreateModel(
name='SkillValidationConfiguration',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('course', models.ForeignKey(blank=True, help_text='The course, for which skill validation is disabled.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='course_metadata.course')),
('organization', models.ForeignKey(blank=True, help_text='The organization, for which skill validation is disabled.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='course_metadata.organization')),
],
options={
'verbose_name': 'Skill Validation Configuration',
'verbose_name_plural': 'Skill Validation Configurations',
},
),
migrations.AddConstraint(
model_name='skillvalidationconfiguration',
constraint=models.CheckConstraint(check=models.Q(models.Q(('course__isnull', False), ('organization__isnull', True)), models.Q(('course__isnull', True), ('organization__isnull', False)), _connector='OR'), name='either_course_or_org', violation_error_message='Select either course or organization.'),
),
migrations.AddConstraint(
model_name='skillvalidationconfiguration',
constraint=models.UniqueConstraint(fields=('course',), name='unique_course'),
),
migrations.AddConstraint(
model_name='skillvalidationconfiguration',
constraint=models.UniqueConstraint(fields=('organization',), name='unique_organization'),
),
]
113 changes: 112 additions & 1 deletion course_discovery/apps/taxonomy_support/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@
Models for taxonomy related tables, these tables are placed here as opposed to inside `taxonomy-connector`
because they have direct dependency with models from course discovery.
"""
import logging

from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from model_utils.models import TimeStampedModel
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from solo.models import SingletonModel

from course_discovery.apps.course_metadata.models import Course
from course_discovery.apps.course_metadata.models import Course, CourseRun, Organization

LOGGER = logging.getLogger(__name__)


class CourseRecommendation(TimeStampedModel):
Expand Down Expand Up @@ -60,3 +67,107 @@ class UpdateCourseRecommendationsConfig(SingletonModel):

def __str__(self):
return 'Configuration for the update_course_recommendations management command'


class SkillValidationConfiguration(TimeStampedModel):
"""
Model to store the configuration for disabling skill validation for a course or organization.
"""

course = models.ForeignKey(
Course,
null=True,
blank=True,
related_name='+',
on_delete=models.CASCADE,
help_text=_('The course, for which skill validation is disabled.'),
)
organization = models.ForeignKey(
Organization,
null=True,
blank=True,
related_name='+',
on_delete=models.CASCADE,
help_text=_('The organization, for which skill validation is disabled.'),
)

def __str__(self):
"""
Create a human-readable string representation of the object.
"""
message = ''

if self.course:
message = f'Skill validation disabled for course: {self.course.key}'
elif self.organization:
message = f'Skill validation disabled for organization: {self.organization.key}'

return message

class Meta:
"""
Meta configuration for SkillValidationConfiguration model.
"""
constraints = [
models.CheckConstraint(
check=(
Q(course__isnull=False) &
Q(organization__isnull=True)
) | (
Q(course__isnull=True) &
Q(organization__isnull=False)
),
name='either_course_or_org',
violation_error_message='Select either course or organization.'
),
models.UniqueConstraint(fields=['course'], name="unique_course"),
models.UniqueConstraint(fields=['organization'], name="unique_organization")
]

verbose_name = 'Skill Validation Configuration'
verbose_name_plural = 'Skill Validation Configurations'

@staticmethod
def is_valid_course_run_key(course_run_key):
"""
Check if the given course run key is in valid format.
Arguments:
course_run_key (str): Course run key
"""
try:
return True, CourseKey.from_string(course_run_key)
except InvalidKeyError:
LOGGER.error('[TAXONOMY_SKILL_VALIDATION_CONFIGURATION] Invalid course_run key: [%s]', course_run_key)

return False, None

@classmethod
def is_disabled(cls, course_run_key) -> bool:
"""
Check if skill validation is disabled for the given course run key.
Arguments:
course_run_key (str): Course run key
Returns:
bool: True if skill validation is disabled for the given course run key.
"""
is_valid_course_run_key, course_run_key = cls.is_valid_course_run_key(course_run_key)
if not is_valid_course_run_key:
return False

course_run_org = course_run_key.org
if cls.objects.filter(organization__key=course_run_org).exists():
return True

try:
course_run = CourseRun.objects.select_related('course').get(key=course_run_key)
course_key = course_run.course.key
except CourseRun.DoesNotExist:
return False

if cls.objects.filter(course__key=course_key).exists():
return True

return False
13 changes: 13 additions & 0 deletions course_discovery/apps/taxonomy_support/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
aggregate_contentful_data, fetch_and_transform_bootcamp_contentful_data, fetch_and_transform_degree_contentful_data
)
from course_discovery.apps.course_metadata.models import Course, CourseRun, Program
from course_discovery.apps.taxonomy_support.models import SkillValidationConfiguration


class DiscoveryCourseMetadataProvider(CourseMetadataProvider):
Expand Down Expand Up @@ -72,6 +73,18 @@ def get_all_courses(): # lint-amnesty, pylint: disable=arguments-differ
),
}

def skill_validation_disabled(self, course_run_key) -> bool:
"""
Get whether skill validation is disabled for a course.
Arguments:
course_run_key(str): A course run key.
Returns:
bool: `True` if skill validation is disabled for the course run, `False` otherwise.
"""
return SkillValidationConfiguration.is_disabled(course_run_key)


class DiscoveryCourseRunMetadataProvider(CourseRunMetadataProvider):
"""
Expand Down
13 changes: 13 additions & 0 deletions course_discovery/apps/taxonomy_support/tests/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""
Model factory classes for testing.
"""

import factory

from course_discovery.apps.taxonomy_support.models import SkillValidationConfiguration


class SkillValidationConfigurationFactory(factory.django.DjangoModelFactory):

class Meta:
model = SkillValidationConfiguration
68 changes: 66 additions & 2 deletions course_discovery/apps/taxonomy_support/tests/test_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,25 @@
from unittest import mock
from urllib.parse import urljoin

import pytest
from django.conf import settings
from django.test import TestCase
from taxonomy.providers import CourseRunContent
from taxonomy.providers.utils import get_course_run_metadata_provider, get_xblock_metadata_provider
from taxonomy.providers.utils import (
get_course_metadata_provider, get_course_run_metadata_provider, get_xblock_metadata_provider
)
from taxonomy.validators import (
CourseMetadataProviderValidator, CourseRunMetadataProviderValidator, ProgramMetadataProviderValidator,
XBlockMetadataProviderValidator
)
from taxonomy_support.tests.factories import SkillValidationConfigurationFactory

from course_discovery.apps.core.tests.factories import PartnerFactory
from course_discovery.apps.core.tests.mixins import LMSAPIClientMixin
from course_discovery.apps.course_metadata.choices import CourseRunStatus
from course_discovery.apps.course_metadata.tests.factories import CourseFactory, CourseRunFactory, ProgramFactory
from course_discovery.apps.course_metadata.tests.factories import (
CourseFactory, CourseRunFactory, OrganizationFactory, ProgramFactory
)


class TaxonomyIntegrationTests(TestCase, LMSAPIClientMixin):
Expand Down Expand Up @@ -138,3 +144,61 @@ def test_get_video_xblocks_content(self):
provider = get_xblock_metadata_provider()
xblocks = provider.get_xblocks(block_ids)
assert 'Should not be included in tagging content.' not in xblocks[0].content


@pytest.mark.django_db
class DiscoveryCourseMetadataProviderTests(TestCase):
"""
Tests for `DiscoveryCourseMetadataProvider`.
"""
def setUp(self):
super().setUp()

self.course_with_skill_validation_disabled = CourseFactory()
self.course_with_skill_validation_enabled = CourseFactory()

self.courserun_with_skill_validation_disabled = CourseRunFactory(
course=self.course_with_skill_validation_disabled
)
self.courserun_with_skill_validation_enabled = CourseRunFactory(
course=self.course_with_skill_validation_enabled
)

organization = OrganizationFactory(key='MAx')
self.course_with_org_skill_validation_disabled = CourseFactory(authoring_organizations=[organization])
self.courserun_with_org_skill_validation_disabled_1 = CourseRunFactory(
key='course-v1:MAx+4084+1T2025',
authoring_organizations=[organization]
)
self.courserun_with_org_skill_validation_disabled_2 = CourseRunFactory(
key='course-v1:MAx+3985+1T2025',
authoring_organizations=[organization]
)

# create a config that will disable skill validation for a course
SkillValidationConfigurationFactory(
course=self.course_with_skill_validation_disabled
)
# create a config that will disable skill validation for an organization
SkillValidationConfigurationFactory(
organization=organization
)

def test_skill_validation_disabled(self):
"""
Test that `skill_validation_disabled` work as expected.
"""
assert get_course_metadata_provider().skill_validation_disabled(
self.courserun_with_skill_validation_disabled.key
) is True
assert get_course_metadata_provider().skill_validation_disabled(
self.course_with_skill_validation_enabled.key
) is False

# skill validation should be disabled for all courses runs of an orgranization
assert get_course_metadata_provider().skill_validation_disabled(
self.courserun_with_org_skill_validation_disabled_1.key
) is True
assert get_course_metadata_provider().skill_validation_disabled(
self.courserun_with_org_skill_validation_disabled_2.key
) is True

0 comments on commit d0b6283

Please sign in to comment.