Commit 2dd8cf68 by Renzo Lucioni

Marketable programs must be active

Programs have a status field that must be respected when filtering marketable programs. The LMS consumes the set of marketable MicroMasters. We don't want to leak unpublished programs.
parent 70ea8c41
...@@ -20,6 +20,7 @@ from course_discovery.apps.catalogs.tests.factories import CatalogFactory ...@@ -20,6 +20,7 @@ from course_discovery.apps.catalogs.tests.factories import CatalogFactory
from course_discovery.apps.core.models import User from course_discovery.apps.core.models import User
from course_discovery.apps.core.tests.factories import UserFactory from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.core.tests.helpers import make_image_file from course_discovery.apps.core.tests.helpers import make_image_file
from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus
from course_discovery.apps.course_metadata.models import CourseRun, Program from course_discovery.apps.course_metadata.models import CourseRun, Program
from course_discovery.apps.course_metadata.tests.factories import ( from course_discovery.apps.course_metadata.tests.factories import (
CourseFactory, CourseRunFactory, SubjectFactory, PrerequisiteFactory, ImageFactory, VideoFactory, CourseFactory, CourseRunFactory, SubjectFactory, PrerequisiteFactory, ImageFactory, VideoFactory,
...@@ -715,7 +716,7 @@ class CourseRunSearchSerializerTests(TestCase): ...@@ -715,7 +716,7 @@ class CourseRunSearchSerializerTests(TestCase):
'type': course_run.type, 'type': course_run.type,
'level_type': course_run.level_type.name, 'level_type': course_run.level_type.name,
'availability': course_run.availability, 'availability': course_run.availability,
'published': course_run.status == CourseRun.Status.Published, 'published': course_run.status == CourseRunStatus.Published,
'partner': course_run.course.partner.short_code, 'partner': course_run.course.partner.short_code,
'program_types': course_run.program_types, 'program_types': course_run.program_types,
'authoring_organization_uuids': get_uuids(course_run.authoring_organizations.all()), 'authoring_organization_uuids': get_uuids(course_run.authoring_organizations.all()),
...@@ -749,7 +750,7 @@ class ProgramSearchSerializerTests(TestCase): ...@@ -749,7 +750,7 @@ class ProgramSearchSerializerTests(TestCase):
'content_type': 'program', 'content_type': 'program',
'card_image_url': program.card_image_url, 'card_image_url': program.card_image_url,
'status': program.status, 'status': program.status,
'published': program.status == Program.Status.Active, 'published': program.status == ProgramStatus.Active,
'partner': program.partner.short_code, 'partner': program.partner.short_code,
'authoring_organization_uuids': get_uuids(program.authoring_organizations.all()), 'authoring_organization_uuids': get_uuids(program.authoring_organizations.all()),
'subject_uuids': get_uuids([course.subjects for course in program.courses.all()]), 'subject_uuids': get_uuids([course.subjects for course in program.courses.all()]),
......
import ddt
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from rest_framework.test import APITestCase, APIRequestFactory from rest_framework.test import APITestCase, APIRequestFactory
from course_discovery.apps.api.serializers import ProgramSerializer from course_discovery.apps.api.serializers import ProgramSerializer
from course_discovery.apps.core.tests.factories import USER_PASSWORD, UserFactory from course_discovery.apps.core.tests.factories import USER_PASSWORD, UserFactory
from course_discovery.apps.course_metadata.choices import ProgramStatus
from course_discovery.apps.course_metadata.models import Program from course_discovery.apps.course_metadata.models import Program
from course_discovery.apps.course_metadata.tests.factories import ProgramFactory from course_discovery.apps.course_metadata.tests.factories import ProgramFactory
@ddt.ddt
class ProgramViewSetTests(APITestCase): class ProgramViewSetTests(APITestCase):
list_path = reverse('api:v1:program-list') list_path = reverse('api:v1:program-list')
...@@ -83,11 +86,18 @@ class ProgramViewSetTests(APITestCase): ...@@ -83,11 +86,18 @@ class ProgramViewSetTests(APITestCase):
self.assert_list_results(url, expected) self.assert_list_results(url, expected)
def test_filter_by_marketable(self): @ddt.data(
(ProgramStatus.Unpublished, False),
(ProgramStatus.Active, True),
)
@ddt.unpack
def test_filter_by_marketable(self, status, is_marketable):
""" Verify the endpoint filters programs to those that are marketable. """ """ Verify the endpoint filters programs to those that are marketable. """
url = self.list_path + '?marketable=1' url = self.list_path + '?marketable=1'
ProgramFactory(marketing_slug='') ProgramFactory(marketing_slug='')
expected = ProgramFactory.create_batch(3) programs = ProgramFactory.create_batch(3, status=status)
expected.reverse() programs.reverse()
expected = programs if is_marketable else []
self.assertEqual(list(Program.objects.marketable()), expected) self.assertEqual(list(Program.objects.marketable()), expected)
self.assert_list_results(url, expected) self.assert_list_results(url, expected)
...@@ -11,6 +11,7 @@ from rest_framework.test import APITestCase ...@@ -11,6 +11,7 @@ from rest_framework.test import APITestCase
from course_discovery.apps.api.serializers import CourseRunSearchSerializer, ProgramSearchSerializer from course_discovery.apps.api.serializers import CourseRunSearchSerializer, ProgramSearchSerializer
from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWORD, PartnerFactory from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWORD, PartnerFactory
from course_discovery.apps.core.tests.mixins import ElasticsearchTestMixin from course_discovery.apps.core.tests.mixins import ElasticsearchTestMixin
from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus
from course_discovery.apps.course_metadata.models import CourseRun, Program from course_discovery.apps.course_metadata.models import CourseRun, Program
from course_discovery.apps.course_metadata.tests.factories import CourseRunFactory, ProgramFactory from course_discovery.apps.course_metadata.tests.factories import CourseRunFactory, ProgramFactory
...@@ -82,7 +83,7 @@ class CourseRunSearchViewSetTests(DefaultPartnerMixin, SerializationMixin, Login ...@@ -82,7 +83,7 @@ class CourseRunSearchViewSetTests(DefaultPartnerMixin, SerializationMixin, Login
# Generate data that should be indexed and returned by the query # Generate data that should be indexed and returned by the query
course_run = CourseRunFactory(course__partner=self.partner, course__title='Software Testing', course_run = CourseRunFactory(course__partner=self.partner, course__title='Software Testing',
status=CourseRun.Status.Published) status=CourseRunStatus.Published)
response = self.get_search_response('software', faceted=faceted) response = self.get_search_response('software', faceted=faceted)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -119,13 +120,13 @@ class CourseRunSearchViewSetTests(DefaultPartnerMixin, SerializationMixin, Login ...@@ -119,13 +120,13 @@ class CourseRunSearchViewSetTests(DefaultPartnerMixin, SerializationMixin, Login
""" Verify the endpoint returns availability facets with the results. """ """ Verify the endpoint returns availability facets with the results. """
now = datetime.datetime.utcnow() now = datetime.datetime.utcnow()
archived = CourseRunFactory(course__partner=self.partner, start=now - datetime.timedelta(weeks=2), archived = CourseRunFactory(course__partner=self.partner, start=now - datetime.timedelta(weeks=2),
end=now - datetime.timedelta(weeks=1), status=CourseRun.Status.Published) end=now - datetime.timedelta(weeks=1), status=CourseRunStatus.Published)
current = CourseRunFactory(course__partner=self.partner, start=now - datetime.timedelta(weeks=2), current = CourseRunFactory(course__partner=self.partner, start=now - datetime.timedelta(weeks=2),
end=now + datetime.timedelta(weeks=1), status=CourseRun.Status.Published) end=now + datetime.timedelta(weeks=1), status=CourseRunStatus.Published)
starting_soon = CourseRunFactory(course__partner=self.partner, start=now + datetime.timedelta(days=10), starting_soon = CourseRunFactory(course__partner=self.partner, start=now + datetime.timedelta(days=10),
end=now + datetime.timedelta(days=90), status=CourseRun.Status.Published) end=now + datetime.timedelta(days=90), status=CourseRunStatus.Published)
upcoming = CourseRunFactory(course__partner=self.partner, start=now + datetime.timedelta(days=61), upcoming = CourseRunFactory(course__partner=self.partner, start=now + datetime.timedelta(days=61),
end=now + datetime.timedelta(days=90), status=CourseRun.Status.Published) end=now + datetime.timedelta(days=90), status=CourseRunStatus.Published)
response = self.get_search_response(faceted=True) response = self.get_search_response(faceted=True)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -184,11 +185,11 @@ class AggregateSearchViewSet(DefaultPartnerMixin, SerializationMixin, LoginMixin ...@@ -184,11 +185,11 @@ class AggregateSearchViewSet(DefaultPartnerMixin, SerializationMixin, LoginMixin
def test_results_only_include_published_objects(self): def test_results_only_include_published_objects(self):
""" Verify the search results only include items with status set to 'Published'. """ """ Verify the search results only include items with status set to 'Published'. """
# These items should NOT be in the results # These items should NOT be in the results
CourseRunFactory(course__partner=self.partner, status=CourseRun.Status.Unpublished) CourseRunFactory(course__partner=self.partner, status=CourseRunStatus.Unpublished)
ProgramFactory(partner=self.partner, status=Program.Status.Unpublished) ProgramFactory(partner=self.partner, status=ProgramStatus.Unpublished)
course_run = CourseRunFactory(course__partner=self.partner, status=CourseRun.Status.Published) course_run = CourseRunFactory(course__partner=self.partner, status=CourseRunStatus.Published)
program = ProgramFactory(partner=self.partner, status=Program.Status.Active) program = ProgramFactory(partner=self.partner, status=ProgramStatus.Active)
response = self.get_search_response() response = self.get_search_response()
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -199,13 +200,13 @@ class AggregateSearchViewSet(DefaultPartnerMixin, SerializationMixin, LoginMixin ...@@ -199,13 +200,13 @@ class AggregateSearchViewSet(DefaultPartnerMixin, SerializationMixin, LoginMixin
def test_results_filtered_by_default_partner(self): def test_results_filtered_by_default_partner(self):
""" Verify the search results only include items related to the default partner if no partner is """ Verify the search results only include items related to the default partner if no partner is
specified on the request. If a partner is included, the data should be filtered to the requested partner. """ specified on the request. If a partner is included, the data should be filtered to the requested partner. """
course_run = CourseRunFactory(course__partner=self.partner, status=CourseRun.Status.Published) course_run = CourseRunFactory(course__partner=self.partner, status=CourseRunStatus.Published)
program = ProgramFactory(partner=self.partner, status=Program.Status.Active) program = ProgramFactory(partner=self.partner, status=ProgramStatus.Active)
# This data should NOT be in the results # This data should NOT be in the results
other_partner = PartnerFactory() other_partner = PartnerFactory()
other_course_run = CourseRunFactory(course__partner=other_partner, status=CourseRun.Status.Published) other_course_run = CourseRunFactory(course__partner=other_partner, status=CourseRunStatus.Published)
other_program = ProgramFactory(partner=other_partner, status=Program.Status.Active) other_program = ProgramFactory(partner=other_partner, status=ProgramStatus.Active)
self.assertNotEqual(other_program.partner.short_code, self.partner.short_code) self.assertNotEqual(other_program.partner.short_code, self.partner.short_code)
self.assertNotEqual(other_course_run.course.partner.short_code, self.partner.short_code) self.assertNotEqual(other_course_run.course.partner.short_code, self.partner.short_code)
......
...@@ -402,7 +402,8 @@ class ProgramViewSet(viewsets.ReadOnlyModelViewSet): ...@@ -402,7 +402,8 @@ class ProgramViewSet(viewsets.ReadOnlyModelViewSet):
paramType: query paramType: query
multiple: false multiple: false
- name: marketable - name: marketable
description: Retrieve marketable programs. A program is considered marketable if it has a marketing slug. description: Retrieve marketable programs. A program is considered marketable if it is active
and has a marketing slug.
required: false required: false
type: integer type: integer
paramType: query paramType: query
......
from django.utils.translation import ugettext_lazy as _
from djchoices import DjangoChoices, ChoiceItem
class CourseRunStatus(DjangoChoices):
Published = ChoiceItem('published', _('Published'))
Unpublished = ChoiceItem('unpublished', _('Unpublished'))
class CourseRunPacing(DjangoChoices):
# Translators: Instructor-paced refers to course runs that operate on a schedule set by the instructor,
# similar to a normal university course.
Instructor = ChoiceItem('instructor_paced', _('Instructor-paced'))
# Translators: Self-paced refers to course runs that operate on the student's schedule.
Self = ChoiceItem('self_paced', _('Self-paced'))
class ProgramStatus(DjangoChoices):
Unpublished = ChoiceItem('unpublished', _('Unpublished'))
Active = ChoiceItem('active', _('Active'))
Retired = ChoiceItem('retired', _('Retired'))
Deleted = ChoiceItem('deleted', _('Deleted'))
...@@ -9,6 +9,7 @@ from opaque_keys.edx.keys import CourseKey ...@@ -9,6 +9,7 @@ from opaque_keys.edx.keys import CourseKey
import requests import requests
from course_discovery.apps.core.models import Currency from course_discovery.apps.core.models import Currency
from course_discovery.apps.course_metadata.choices import CourseRunStatus, CourseRunPacing
from course_discovery.apps.course_metadata.data_loaders import AbstractDataLoader from course_discovery.apps.course_metadata.data_loaders import AbstractDataLoader
from course_discovery.apps.course_metadata.models import ( from course_discovery.apps.course_metadata.models import (
Video, Organization, Seat, CourseRun, Program, Course, ProgramType, Video, Organization, Seat, CourseRun, Program, Course, ProgramType,
...@@ -141,7 +142,7 @@ class CoursesApiDataLoader(AbstractDataLoader): ...@@ -141,7 +142,7 @@ class CoursesApiDataLoader(AbstractDataLoader):
'title_override': body['name'], 'title_override': body['name'],
'short_description_override': body['short_description'], 'short_description_override': body['short_description'],
'video': self.get_courserun_video(body), 'video': self.get_courserun_video(body),
'status': CourseRun.Status.Published, 'status': CourseRunStatus.Published,
'pacing_type': self.get_pacing_type(body), 'pacing_type': self.get_pacing_type(body),
}) })
...@@ -157,9 +158,9 @@ class CoursesApiDataLoader(AbstractDataLoader): ...@@ -157,9 +158,9 @@ class CoursesApiDataLoader(AbstractDataLoader):
pacing = pacing.lower() pacing = pacing.lower()
if pacing == 'instructor': if pacing == 'instructor':
return CourseRun.Pacing.Instructor return CourseRunPacing.Instructor
elif pacing == 'self': elif pacing == 'self':
return CourseRun.Pacing.Self return CourseRunPacing.Self
else: else:
return None return None
......
...@@ -11,6 +11,7 @@ from django.db.models import Q ...@@ -11,6 +11,7 @@ from django.db.models import Q
from django.utils.functional import cached_property from django.utils.functional import cached_property
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from course_discovery.apps.course_metadata.choices import CourseRunStatus, CourseRunPacing
from course_discovery.apps.course_metadata.data_loaders import AbstractDataLoader from course_discovery.apps.course_metadata.data_loaders import AbstractDataLoader
from course_discovery.apps.course_metadata.models import ( from course_discovery.apps.course_metadata.models import (
Course, Organization, Person, Subject, Program, Position, LevelType, CourseRun Course, Organization, Person, Subject, Program, Position, LevelType, CourseRun
...@@ -382,7 +383,7 @@ class CourseMarketingSiteDataLoader(AbstractMarketingSiteDataLoader): ...@@ -382,7 +383,7 @@ class CourseMarketingSiteDataLoader(AbstractMarketingSiteDataLoader):
# If the course already exists update the fields only if the course_run we got from drupal is published. # If the course already exists update the fields only if the course_run we got from drupal is published.
# People often put temp data into required drupal fields for unpublished courses. We don't want to overwrite # People often put temp data into required drupal fields for unpublished courses. We don't want to overwrite
# the course info with this data, so we only update course info from published sources. # the course info with this data, so we only update course info from published sources.
published = self.get_course_run_status(data) == CourseRun.Status.Published published = self.get_course_run_status(data) == CourseRunStatus.Published
if not created and published: if not created and published:
for attr, value in defaults.items(): for attr, value in defaults.items():
setattr(course, attr, value) setattr(course, attr, value)
...@@ -403,7 +404,7 @@ class CourseMarketingSiteDataLoader(AbstractMarketingSiteDataLoader): ...@@ -403,7 +404,7 @@ class CourseMarketingSiteDataLoader(AbstractMarketingSiteDataLoader):
return description return description
def get_course_run_status(self, data): def get_course_run_status(self, data):
return CourseRun.Status.Published if bool(int(data['status'])) else CourseRun.Status.Unpublished return CourseRunStatus.Published if bool(int(data['status'])) else CourseRunStatus.Unpublished
def get_level_type(self, name): def get_level_type(self, name):
level_type = None level_type = None
...@@ -420,7 +421,7 @@ class CourseMarketingSiteDataLoader(AbstractMarketingSiteDataLoader): ...@@ -420,7 +421,7 @@ class CourseMarketingSiteDataLoader(AbstractMarketingSiteDataLoader):
def get_pacing_type(self, data): def get_pacing_type(self, data):
self_paced = data.get('field_course_self_paced', False) self_paced = data.get('field_course_self_paced', False)
return CourseRun.Pacing.Self if self_paced else CourseRun.Pacing.Instructor return CourseRunPacing.Self if self_paced else CourseRunPacing.Instructor
def create_course_run(self, course, data): def create_course_run(self, course, data):
uuid = data['uuid'] uuid = data['uuid']
......
...@@ -8,6 +8,7 @@ from django.test import TestCase ...@@ -8,6 +8,7 @@ from django.test import TestCase
from pytz import UTC from pytz import UTC
from course_discovery.apps.core.tests.utils import mock_api_callback, mock_jpeg_callback from course_discovery.apps.core.tests.utils import mock_api_callback, mock_jpeg_callback
from course_discovery.apps.course_metadata.choices import CourseRunStatus, CourseRunPacing
from course_discovery.apps.course_metadata.data_loaders.api import ( from course_discovery.apps.course_metadata.data_loaders.api import (
OrganizationsApiDataLoader, CoursesApiDataLoader, EcommerceApiDataLoader, AbstractDataLoader, ProgramsApiDataLoader OrganizationsApiDataLoader, CoursesApiDataLoader, EcommerceApiDataLoader, AbstractDataLoader, ProgramsApiDataLoader
) )
...@@ -157,7 +158,7 @@ class CoursesApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCas ...@@ -157,7 +158,7 @@ class CoursesApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCas
'title_override': body['name'], 'title_override': body['name'],
'short_description_override': self.loader.clean_string(body['short_description']), 'short_description_override': self.loader.clean_string(body['short_description']),
'video': self.loader.get_courserun_video(body), 'video': self.loader.get_courserun_video(body),
'status': CourseRun.Status.Published, 'status': CourseRunStatus.Published,
'pacing_type': self.loader.get_pacing_type(body), 'pacing_type': self.loader.get_pacing_type(body),
}) })
...@@ -215,10 +216,10 @@ class CoursesApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCas ...@@ -215,10 +216,10 @@ class CoursesApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCas
('', None), ('', None),
('foo', None), ('foo', None),
(None, None), (None, None),
('instructor', CourseRun.Pacing.Instructor), ('instructor', CourseRunPacing.Instructor),
('Instructor', CourseRun.Pacing.Instructor), ('Instructor', CourseRunPacing.Instructor),
('self', CourseRun.Pacing.Self), ('self', CourseRunPacing.Self),
('Self', CourseRun.Pacing.Self), ('Self', CourseRunPacing.Self),
) )
def test_get_pacing_type(self, pacing, expected_pacing_type): def test_get_pacing_type(self, pacing, expected_pacing_type):
""" Verify the method returns a pacing type corresponding to the API response's pacing field. """ """ Verify the method returns a pacing type corresponding to the API response's pacing field. """
......
...@@ -11,14 +11,14 @@ import responses ...@@ -11,14 +11,14 @@ import responses
from django.test import TestCase from django.test import TestCase
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from course_discovery.apps.course_metadata.choices import CourseRunStatus, CourseRunPacing
from course_discovery.apps.course_metadata.data_loaders.marketing_site import ( from course_discovery.apps.course_metadata.data_loaders.marketing_site import (
XSeriesMarketingSiteDataLoader, SubjectMarketingSiteDataLoader, SchoolMarketingSiteDataLoader, XSeriesMarketingSiteDataLoader, SubjectMarketingSiteDataLoader, SchoolMarketingSiteDataLoader,
SponsorMarketingSiteDataLoader, PersonMarketingSiteDataLoader, CourseMarketingSiteDataLoader SponsorMarketingSiteDataLoader, PersonMarketingSiteDataLoader, CourseMarketingSiteDataLoader
) )
from course_discovery.apps.course_metadata.data_loaders.tests import JSON, mock_data from course_discovery.apps.course_metadata.data_loaders.tests import JSON, mock_data
from course_discovery.apps.course_metadata.data_loaders.tests.mixins import DataLoaderTestMixin from course_discovery.apps.course_metadata.data_loaders.tests.mixins import DataLoaderTestMixin
from course_discovery.apps.course_metadata.models import Organization, Subject, Program, Video, Person, Course, \ from course_discovery.apps.course_metadata.models import Organization, Subject, Program, Video, Person, Course
CourseRun
from course_discovery.apps.course_metadata.tests import factories from course_discovery.apps.course_metadata.tests import factories
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
...@@ -364,8 +364,8 @@ class CourseMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMixi ...@@ -364,8 +364,8 @@ class CourseMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMixi
@ddt.unpack @ddt.unpack
@ddt.data( @ddt.data(
('0', CourseRun.Status.Unpublished), ('0', CourseRunStatus.Unpublished),
('1', CourseRun.Status.Published), ('1', CourseRunStatus.Published),
) )
def test_get_course_run_status(self, marketing_site_status, expected): def test_get_course_run_status(self, marketing_site_status, expected):
data = {'status': marketing_site_status} data = {'status': marketing_site_status}
...@@ -394,10 +394,10 @@ class CourseMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMixi ...@@ -394,10 +394,10 @@ class CourseMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMixi
@ddt.unpack @ddt.unpack
@ddt.data( @ddt.data(
(True, CourseRun.Pacing.Self), (True, CourseRunPacing.Self),
(False, CourseRun.Pacing.Instructor), (False, CourseRunPacing.Instructor),
(None, CourseRun.Pacing.Instructor), (None, CourseRunPacing.Instructor),
('', CourseRun.Pacing.Instructor), ('', CourseRunPacing.Instructor),
) )
def test_get_pacing_type(self, data_value, expected_pacing_type): def test_get_pacing_type(self, data_value, expected_pacing_type):
data = {'field_course_self_paced': data_value} data = {'field_course_self_paced': data_value}
......
...@@ -4,6 +4,7 @@ from django.core.exceptions import ValidationError ...@@ -4,6 +4,7 @@ from django.core.exceptions import ValidationError
from django.forms.utils import ErrorList from django.forms.utils import ErrorList
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from course_discovery.apps.course_metadata.choices import ProgramStatus
from course_discovery.apps.course_metadata.models import Program, CourseRun from course_discovery.apps.course_metadata.models import Program, CourseRun
...@@ -46,7 +47,7 @@ class ProgramAdminForm(forms.ModelForm): ...@@ -46,7 +47,7 @@ class ProgramAdminForm(forms.ModelForm):
def clean(self): def clean(self):
status = self.cleaned_data.get('status') status = self.cleaned_data.get('status')
banner_image = self.cleaned_data.get('banner_image') banner_image = self.cleaned_data.get('banner_image')
if status == Program.Status.Active and not banner_image: if status == ProgramStatus.Active and not banner_image:
raise ValidationError(_('Status cannot be change to active without banner image.')) raise ValidationError(_('Status cannot be change to active without banner image.'))
return self.cleaned_data return self.cleaned_data
......
...@@ -10,7 +10,6 @@ from django.db.models.query_utils import Q ...@@ -10,7 +10,6 @@ from django.db.models.query_utils import Q
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django_extensions.db.fields import AutoSlugField from django_extensions.db.fields import AutoSlugField
from django_extensions.db.models import TimeStampedModel from django_extensions.db.models import TimeStampedModel
from djchoices import DjangoChoices, ChoiceItem
from haystack import connections from haystack import connections
from haystack.query import SearchQuerySet from haystack.query import SearchQuerySet
from simple_history.models import HistoricalRecords from simple_history.models import HistoricalRecords
...@@ -20,6 +19,7 @@ from taggit.managers import TaggableManager ...@@ -20,6 +19,7 @@ from taggit.managers import TaggableManager
import waffle import waffle
from course_discovery.apps.core.models import Currency, Partner from course_discovery.apps.core.models import Currency, Partner
from course_discovery.apps.course_metadata.choices import CourseRunStatus, CourseRunPacing, ProgramStatus
from course_discovery.apps.course_metadata.publishers import MarketingSitePublisher from course_discovery.apps.course_metadata.publishers import MarketingSitePublisher
from course_discovery.apps.course_metadata.query import CourseQuerySet, CourseRunQuerySet, ProgramQuerySet from course_discovery.apps.course_metadata.query import CourseQuerySet, CourseRunQuerySet, ProgramQuerySet
from course_discovery.apps.course_metadata.utils import UploadToFieldNamePath from course_discovery.apps.course_metadata.utils import UploadToFieldNamePath
...@@ -325,22 +325,11 @@ class Course(TimeStampedModel): ...@@ -325,22 +325,11 @@ class Course(TimeStampedModel):
class CourseRun(TimeStampedModel): class CourseRun(TimeStampedModel):
""" CourseRun model. """ """ CourseRun model. """
class Status(DjangoChoices):
Published = ChoiceItem('published', _('Published'))
Unpublished = ChoiceItem('unpublished', _('Unpublished'))
class Pacing(DjangoChoices):
# Translators: Instructor-paced refers to course runs that operate on a schedule set by the instructor,
# similar to a normal university course.
Instructor = ChoiceItem('instructor_paced', _('Instructor-paced'))
# Translators: Self-paced refers to course runs that operate on the student's schedule.
Self = ChoiceItem('self_paced', _('Self-paced'))
uuid = models.UUIDField(default=uuid4, editable=False, verbose_name=_('UUID')) uuid = models.UUIDField(default=uuid4, editable=False, verbose_name=_('UUID'))
course = models.ForeignKey(Course, related_name='course_runs') course = models.ForeignKey(Course, related_name='course_runs')
key = models.CharField(max_length=255, unique=True) key = models.CharField(max_length=255, unique=True)
status = models.CharField(max_length=255, null=False, blank=False, db_index=True, choices=Status.choices, status = models.CharField(max_length=255, null=False, blank=False, db_index=True, choices=CourseRunStatus.choices,
validators=[Status.validator]) validators=[CourseRunStatus.validator])
title_override = models.CharField( title_override = models.CharField(
max_length=255, default=None, null=True, blank=True, max_length=255, default=None, null=True, blank=True,
help_text=_( help_text=_(
...@@ -369,8 +358,8 @@ class CourseRun(TimeStampedModel): ...@@ -369,8 +358,8 @@ class CourseRun(TimeStampedModel):
help_text=_('Estimated maximum number of hours per week needed to complete a course run.')) help_text=_('Estimated maximum number of hours per week needed to complete a course run.'))
language = models.ForeignKey(LanguageTag, null=True, blank=True) language = models.ForeignKey(LanguageTag, null=True, blank=True)
transcript_languages = models.ManyToManyField(LanguageTag, blank=True, related_name='transcript_courses') transcript_languages = models.ManyToManyField(LanguageTag, blank=True, related_name='transcript_courses')
pacing_type = models.CharField(max_length=255, db_index=True, null=True, blank=True, choices=Pacing.choices, pacing_type = models.CharField(max_length=255, db_index=True, null=True, blank=True,
validators=[Pacing.validator]) choices=CourseRunPacing.choices, validators=[CourseRunPacing.validator])
syllabus = models.ForeignKey(SyllabusItem, default=None, null=True, blank=True) syllabus = models.ForeignKey(SyllabusItem, default=None, null=True, blank=True)
card_image_url = models.URLField(null=True, blank=True) card_image_url = models.URLField(null=True, blank=True)
video = models.ForeignKey(Video, default=None, null=True, blank=True) video = models.ForeignKey(Video, default=None, null=True, blank=True)
...@@ -588,12 +577,6 @@ class ProgramType(TimeStampedModel): ...@@ -588,12 +577,6 @@ class ProgramType(TimeStampedModel):
class Program(TimeStampedModel): class Program(TimeStampedModel):
class Status(DjangoChoices):
Unpublished = ChoiceItem('unpublished', _('Unpublished'))
Active = ChoiceItem('active', _('Active'))
Retired = ChoiceItem('retired', _('Retired'))
Deleted = ChoiceItem('deleted', _('Deleted'))
uuid = models.UUIDField(blank=True, default=uuid4, editable=False, unique=True, verbose_name=_('UUID')) uuid = models.UUIDField(blank=True, default=uuid4, editable=False, unique=True, verbose_name=_('UUID'))
title = models.CharField( title = models.CharField(
help_text=_('The user-facing display title for this Program.'), max_length=255, unique=True) help_text=_('The user-facing display title for this Program.'), max_length=255, unique=True)
...@@ -602,7 +585,7 @@ class Program(TimeStampedModel): ...@@ -602,7 +585,7 @@ class Program(TimeStampedModel):
type = models.ForeignKey(ProgramType, null=True, blank=True) type = models.ForeignKey(ProgramType, null=True, blank=True)
status = models.CharField( status = models.CharField(
help_text=_('The lifecycle status of this Program.'), max_length=24, null=False, blank=False, db_index=True, help_text=_('The lifecycle status of this Program.'), max_length=24, null=False, blank=False, db_index=True,
choices=Status.choices, validators=[Status.validator] choices=ProgramStatus.choices, validators=[ProgramStatus.validator]
) )
marketing_slug = models.CharField( marketing_slug = models.CharField(
help_text=_('Slug used to generate links to the marketing site'), blank=True, max_length=255, db_index=True) help_text=_('Slug used to generate links to the marketing site'), blank=True, max_length=255, db_index=True)
...@@ -721,7 +704,7 @@ class Program(TimeStampedModel): ...@@ -721,7 +704,7 @@ class Program(TimeStampedModel):
@property @property
def is_active(self): def is_active(self):
return self.status == self.Status.Active return self.status == ProgramStatus.Active
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if waffle.switch_is_active('publish_program_to_marketing_site') and \ if waffle.switch_is_active('publish_program_to_marketing_site') and \
......
...@@ -4,6 +4,8 @@ import pytz ...@@ -4,6 +4,8 @@ import pytz
from django.db import models from django.db import models
from django.db.models.query_utils import Q from django.db.models.query_utils import Q
from course_discovery.apps.course_metadata.choices import ProgramStatus
class CourseQuerySet(models.QuerySet): class CourseQuerySet(models.QuerySet):
def active(self): def active(self):
...@@ -54,10 +56,16 @@ class ProgramQuerySet(models.QuerySet): ...@@ -54,10 +56,16 @@ class ProgramQuerySet(models.QuerySet):
def marketable(self): def marketable(self):
""" Returns Programs that can be marketed to learners. """ Returns Programs that can be marketed to learners.
A Program is considered marketable if it has a defined marketing slug. A Program is considered marketable if it is active and has a defined marketing slug.
Returns: Returns:
QuerySet QuerySet
""" """
return self.exclude(marketing_slug__isnull=True).exclude(marketing_slug='') return self.filter(
status=ProgramStatus.Active
).exclude(
marketing_slug__isnull=True
).exclude(
marketing_slug=''
)
...@@ -3,6 +3,7 @@ import json ...@@ -3,6 +3,7 @@ import json
from haystack import indexes from haystack import indexes
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus
from course_discovery.apps.course_metadata.models import Course, CourseRun, Program from course_discovery.apps.course_metadata.models import Course, CourseRun, Program
...@@ -125,7 +126,7 @@ class CourseRunIndex(BaseCourseIndex, indexes.Indexable): ...@@ -125,7 +126,7 @@ class CourseRunIndex(BaseCourseIndex, indexes.Indexable):
return obj.course.partner.short_code return obj.course.partner.short_code
def prepare_published(self, obj): def prepare_published(self, obj):
return obj.status == CourseRun.Status.Published return obj.status == CourseRunStatus.Published
def _prepare_language(self, language): def _prepare_language(self, language):
if language: if language:
...@@ -187,7 +188,7 @@ class ProgramIndex(BaseIndex, indexes.Indexable, OrganizationsMixin): ...@@ -187,7 +188,7 @@ class ProgramIndex(BaseIndex, indexes.Indexable, OrganizationsMixin):
published = indexes.BooleanField(null=False, faceted=True) published = indexes.BooleanField(null=False, faceted=True)
def prepare_published(self, obj): def prepare_published(self, obj):
return obj.status == Program.Status.Active return obj.status == ProgramStatus.Active
def prepare_organizations(self, obj): def prepare_organizations(self, obj):
return self.prepare_authoring_organizations(obj) + self.prepare_credit_backing_organizations(obj) return self.prepare_authoring_organizations(obj) + self.prepare_credit_backing_organizations(obj)
......
...@@ -6,6 +6,7 @@ from pytz import UTC ...@@ -6,6 +6,7 @@ from pytz import UTC
from course_discovery.apps.core.tests.factories import PartnerFactory from course_discovery.apps.core.tests.factories import PartnerFactory
from course_discovery.apps.core.tests.utils import FuzzyURL from course_discovery.apps.core.tests.utils import FuzzyURL
from course_discovery.apps.course_metadata.choices import CourseRunStatus, CourseRunPacing, ProgramStatus
from course_discovery.apps.course_metadata.models import * # pylint: disable=wildcard-import from course_discovery.apps.course_metadata.models import * # pylint: disable=wildcard-import
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
...@@ -106,7 +107,7 @@ class CourseFactory(factory.DjangoModelFactory): ...@@ -106,7 +107,7 @@ class CourseFactory(factory.DjangoModelFactory):
class CourseRunFactory(factory.DjangoModelFactory): class CourseRunFactory(factory.DjangoModelFactory):
status = CourseRun.Status.Published status = CourseRunStatus.Published
uuid = factory.LazyFunction(uuid4) uuid = factory.LazyFunction(uuid4)
key = FuzzyText(prefix='course-run-id/', suffix='/fake') key = FuzzyText(prefix='course-run-id/', suffix='/fake')
course = factory.SubFactory(CourseFactory) course = factory.SubFactory(CourseFactory)
...@@ -123,7 +124,7 @@ class CourseRunFactory(factory.DjangoModelFactory): ...@@ -123,7 +124,7 @@ class CourseRunFactory(factory.DjangoModelFactory):
video = factory.SubFactory(VideoFactory) video = factory.SubFactory(VideoFactory)
min_effort = FuzzyInteger(1, 10) min_effort = FuzzyInteger(1, 10)
max_effort = FuzzyInteger(10, 20) max_effort = FuzzyInteger(10, 20)
pacing_type = FuzzyChoice([name for name, __ in CourseRun.Pacing.choices]) pacing_type = FuzzyChoice([name for name, __ in CourseRunPacing.choices])
slug = FuzzyText() slug = FuzzyText()
@factory.post_generation @factory.post_generation
...@@ -240,7 +241,7 @@ class ProgramFactory(factory.django.DjangoModelFactory): ...@@ -240,7 +241,7 @@ class ProgramFactory(factory.django.DjangoModelFactory):
uuid = factory.LazyFunction(uuid4) uuid = factory.LazyFunction(uuid4)
subtitle = 'test-subtitle' subtitle = 'test-subtitle'
type = factory.SubFactory(ProgramTypeFactory) type = factory.SubFactory(ProgramTypeFactory)
status = Program.Status.Unpublished status = ProgramStatus.Unpublished
marketing_slug = factory.Sequence(lambda n: 'test-slug-{}'.format(n)) # pylint: disable=unnecessary-lambda marketing_slug = factory.Sequence(lambda n: 'test-slug-{}'.format(n)) # pylint: disable=unnecessary-lambda
banner_image_url = FuzzyText(prefix='https://example.com/program/banner') banner_image_url = FuzzyText(prefix='https://example.com/program/banner')
card_image_url = FuzzyText(prefix='https://example.com/program/card') card_image_url = FuzzyText(prefix='https://example.com/program/card')
......
import ddt import ddt
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from course_discovery.apps.course_metadata.forms import ProgramAdminForm
from course_discovery.apps.course_metadata.models import Program
from course_discovery.apps.course_metadata.choices import ProgramStatus
from course_discovery.apps.course_metadata.forms import ProgramAdminForm
from course_discovery.apps.course_metadata.tests import factories from course_discovery.apps.course_metadata.tests import factories
from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWORD from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWORD
from course_discovery.apps.core.tests.helpers import make_image_file from course_discovery.apps.core.tests.helpers import make_image_file
...@@ -109,7 +109,7 @@ class AdminTests(TestCase): ...@@ -109,7 +109,7 @@ class AdminTests(TestCase):
def test_program_without_image_and_active_status(self): def test_program_without_image_and_active_status(self):
""" Verify that new program cannot be added without `image` and active status together.""" """ Verify that new program cannot be added without `image` and active status together."""
data = self._post_data(Program.Status.Active) data = self._post_data(ProgramStatus.Active)
form = ProgramAdminForm(data, {'banner_image': ''}) form = ProgramAdminForm(data, {'banner_image': ''})
self.assertFalse(form.is_valid()) self.assertFalse(form.is_valid())
self.assertEqual(form.errors['__all__'], ['Status cannot be change to active without banner image.']) self.assertEqual(form.errors['__all__'], ['Status cannot be change to active without banner image.'])
...@@ -117,9 +117,9 @@ class AdminTests(TestCase): ...@@ -117,9 +117,9 @@ class AdminTests(TestCase):
form.save() form.save()
@ddt.data( @ddt.data(
Program.Status.Deleted, ProgramStatus.Deleted,
Program.Status.Retired, ProgramStatus.Retired,
Program.Status.Unpublished ProgramStatus.Unpublished
) )
def test_program_without_image_and_non_active_status(self, status): def test_program_without_image_and_non_active_status(self, status):
""" Verify that new program can be added without `image` and non-active """ Verify that new program can be added without `image` and non-active
...@@ -129,10 +129,10 @@ class AdminTests(TestCase): ...@@ -129,10 +129,10 @@ class AdminTests(TestCase):
self.valid_post_form(data, {'banner_image': ''}) self.valid_post_form(data, {'banner_image': ''})
@ddt.data( @ddt.data(
Program.Status.Deleted, ProgramStatus.Deleted,
Program.Status.Retired, ProgramStatus.Retired,
Program.Status.Unpublished, ProgramStatus.Unpublished,
Program.Status.Active ProgramStatus.Active
) )
def test_program_with_image(self, status): def test_program_with_image(self, status):
""" Verify that new program can be added with `image` and any status.""" """ Verify that new program can be added with `image` and any status."""
...@@ -157,7 +157,7 @@ class AdminTests(TestCase): ...@@ -157,7 +157,7 @@ class AdminTests(TestCase):
def test_new_program_without_courses(self): def test_new_program_without_courses(self):
""" Verify that new program can be added without `courses`.""" """ Verify that new program can be added without `courses`."""
data = self._post_data(Program.Status.Unpublished) data = self._post_data(ProgramStatus.Unpublished)
data['courses'] = [] data['courses'] = []
form = ProgramAdminForm(data) form = ProgramAdminForm(data)
self.assertTrue(form.is_valid()) self.assertTrue(form.is_valid())
......
...@@ -14,10 +14,10 @@ import responses ...@@ -14,10 +14,10 @@ import responses
from course_discovery.apps.core.models import Currency from course_discovery.apps.core.models import Currency
from course_discovery.apps.core.tests.helpers import make_image_file from course_discovery.apps.core.tests.helpers import make_image_file
from course_discovery.apps.core.utils import SearchQuerySetWrapper from course_discovery.apps.core.utils import SearchQuerySetWrapper
from course_discovery.apps.course_metadata.choices import ProgramStatus
from course_discovery.apps.course_metadata.models import ( from course_discovery.apps.course_metadata.models import (
AbstractMediaModel, AbstractNamedModel, AbstractValueModel, AbstractMediaModel, AbstractNamedModel, AbstractValueModel,
CorporateEndorsement, Program, Course, CourseRun, Endorsement, CorporateEndorsement, Course, CourseRun, Endorsement, FAQ, SeatType, ProgramType,
FAQ, SeatType, ProgramType,
) )
from course_discovery.apps.course_metadata.tests import factories, toggle_switch from course_discovery.apps.course_metadata.tests import factories, toggle_switch
from course_discovery.apps.course_metadata.tests.factories import CourseRunFactory, ImageFactory from course_discovery.apps.course_metadata.tests.factories import CourseRunFactory, ImageFactory
...@@ -386,10 +386,10 @@ class ProgramTests(MarketingSitePublisherTestMixin): ...@@ -386,10 +386,10 @@ class ProgramTests(MarketingSitePublisherTestMixin):
program = self.create_program_with_seats() program = self.create_program_with_seats()
self.assertEqual(program.seat_types, set(['credit', 'verified'])) self.assertEqual(program.seat_types, set(['credit', 'verified']))
@ddt.data(Program.Status.choices) @ddt.data(ProgramStatus.choices)
def test_is_active(self, status): def test_is_active(self, status):
self.program.status = status self.program.status = status
self.assertEqual(self.program.is_active, status == Program.Status.Active) self.assertEqual(self.program.is_active, status == ProgramStatus.Active)
@responses.activate @responses.activate
def test_save_without_publish(self): def test_save_without_publish(self):
......
import responses import responses
from course_discovery.apps.course_metadata.choices import ProgramStatus
from course_discovery.apps.course_metadata.publishers import ( from course_discovery.apps.course_metadata.publishers import (
MarketingSiteAPIClient, MarketingSiteAPIClient,
MarketingSitePublisher, MarketingSitePublisher,
...@@ -10,7 +11,7 @@ from course_discovery.apps.course_metadata.tests.mixins import ( ...@@ -10,7 +11,7 @@ from course_discovery.apps.course_metadata.tests.mixins import (
MarketingSiteAPIClientTestMixin, MarketingSiteAPIClientTestMixin,
MarketingSitePublisherTestMixin, MarketingSitePublisherTestMixin,
) )
from course_discovery.apps.course_metadata.models import Program, ProgramType from course_discovery.apps.course_metadata.models import ProgramType
class MarketingSiteAPIClientTests(MarketingSiteAPIClientTestMixin): class MarketingSiteAPIClientTests(MarketingSiteAPIClientTestMixin):
...@@ -115,7 +116,7 @@ class MarketingSitePublisherTests(MarketingSitePublisherTestMixin): ...@@ -115,7 +116,7 @@ class MarketingSitePublisherTests(MarketingSitePublisherTestMixin):
'author': { 'author': {
'id': self.user_id, 'id': self.user_id,
}, },
'status': 1 if self.program.status == Program.Status.Active else 0 'status': 1 if self.program.status == ProgramStatus.Active else 0
} }
self.assertDictEqual(publish_data, expected) self.assertDictEqual(publish_data, expected)
......
...@@ -4,6 +4,7 @@ import ddt ...@@ -4,6 +4,7 @@ import ddt
import pytz import pytz
from django.test import TestCase from django.test import TestCase
from course_discovery.apps.course_metadata.choices import ProgramStatus
from course_discovery.apps.course_metadata.models import Course, CourseRun, Program from course_discovery.apps.course_metadata.models import Course, CourseRun, Program
from course_discovery.apps.course_metadata.tests.factories import CourseRunFactory, ProgramFactory from course_discovery.apps.course_metadata.tests.factories import CourseRunFactory, ProgramFactory
...@@ -72,11 +73,18 @@ class CourseRunQuerySetTests(TestCase): ...@@ -72,11 +73,18 @@ class CourseRunQuerySetTests(TestCase):
self.assertEqual(CourseRun.objects.marketable().count(), 0) self.assertEqual(CourseRun.objects.marketable().count(), 0)
@ddt.ddt
class ProgramQuerySetTests(TestCase): class ProgramQuerySetTests(TestCase):
def test_marketable(self): @ddt.data(
""" Verify the method filters Programs to those with marketing slugs. """ (ProgramStatus.Unpublished, False),
program = ProgramFactory() (ProgramStatus.Active, True),
self.assertEqual(list(Program.objects.marketable()), [program]) )
@ddt.unpack
def test_marketable(self, status, is_marketable):
""" Verify the method filters Programs to those which are active and have marketing slugs. """
program = ProgramFactory(status=status)
expected = [program] if is_marketable else []
self.assertEqual(list(Program.objects.marketable()), expected)
def test_marketable_exclusions(self): def test_marketable_exclusions(self):
""" Verify the method excludes Programs without a marketing slug. """ """ Verify the method excludes Programs without a marketing slug. """
......
...@@ -12,8 +12,8 @@ from simple_history.models import HistoricalRecords ...@@ -12,8 +12,8 @@ from simple_history.models import HistoricalRecords
from sortedm2m.fields import SortedManyToManyField from sortedm2m.fields import SortedManyToManyField
from course_discovery.apps.core.models import User, Currency from course_discovery.apps.core.models import User, Currency
from course_discovery.apps.course_metadata.choices import CourseRunPacing
from course_discovery.apps.course_metadata.models import LevelType, Subject, Person, Organization from course_discovery.apps.course_metadata.models import LevelType, Subject, Person, Organization
from course_discovery.apps.course_metadata.models import CourseRun as CourseMetadataCourseRun
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -152,8 +152,8 @@ class CourseRun(TimeStampedModel, ChangedByMixin): ...@@ -152,8 +152,8 @@ class CourseRun(TimeStampedModel, ChangedByMixin):
enrollment_end = models.DateTimeField(null=True, blank=True) enrollment_end = models.DateTimeField(null=True, blank=True)
certificate_generation = models.DateTimeField(null=True, blank=True) certificate_generation = models.DateTimeField(null=True, blank=True)
pacing_type = models.CharField( pacing_type = models.CharField(
max_length=255, db_index=True, null=True, blank=True, choices=CourseMetadataCourseRun.Pacing.choices, max_length=255, db_index=True, null=True, blank=True, choices=CourseRunPacing.choices,
validators=[CourseMetadataCourseRun.Pacing.validator] validators=[CourseRunPacing.validator]
) )
staff = SortedManyToManyField(Person, blank=True, related_name='publisher_course_runs_staffed') staff = SortedManyToManyField(Person, blank=True, related_name='publisher_course_runs_staffed')
min_effort = models.PositiveSmallIntegerField( min_effort = models.PositiveSmallIntegerField(
......
...@@ -6,8 +6,8 @@ from factory.fuzzy import FuzzyText, FuzzyChoice, FuzzyDecimal, FuzzyDateTime, F ...@@ -6,8 +6,8 @@ from factory.fuzzy import FuzzyText, FuzzyChoice, FuzzyDecimal, FuzzyDateTime, F
from pytz import UTC from pytz import UTC
from course_discovery.apps.core.models import Currency from course_discovery.apps.core.models import Currency
from course_discovery.apps.course_metadata.choices import CourseRunPacing
from course_discovery.apps.course_metadata.tests import factories from course_discovery.apps.course_metadata.tests import factories
from course_discovery.apps.course_metadata.models import CourseRun as CourseMetadataCourseRun
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.models import Course, CourseRun, Seat, State from course_discovery.apps.publisher.models import Course, CourseRun, Seat, State
...@@ -48,7 +48,7 @@ class CourseRunFactory(factory.DjangoModelFactory): ...@@ -48,7 +48,7 @@ class CourseRunFactory(factory.DjangoModelFactory):
min_effort = FuzzyInteger(1, 10) min_effort = FuzzyInteger(1, 10)
max_effort = FuzzyInteger(10, 20) max_effort = FuzzyInteger(10, 20)
language = factory.Iterator(LanguageTag.objects.all()) language = factory.Iterator(LanguageTag.objects.all())
pacing_type = FuzzyChoice([name for name, __ in CourseMetadataCourseRun.Pacing.choices]) pacing_type = FuzzyChoice([name for name, __ in CourseRunPacing.choices])
length = FuzzyInteger(1, 10) length = FuzzyInteger(1, 10)
seo_review = "test-seo-review" seo_review = "test-seo-review"
keywords = "Test1, Test2, Test3" keywords = "Test1, Test2, Test3"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment