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
from course_discovery.apps.core.models import User
from course_discovery.apps.core.tests.factories import UserFactory
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.tests.factories import (
CourseFactory, CourseRunFactory, SubjectFactory, PrerequisiteFactory, ImageFactory, VideoFactory,
......@@ -715,7 +716,7 @@ class CourseRunSearchSerializerTests(TestCase):
'type': course_run.type,
'level_type': course_run.level_type.name,
'availability': course_run.availability,
'published': course_run.status == CourseRun.Status.Published,
'published': course_run.status == CourseRunStatus.Published,
'partner': course_run.course.partner.short_code,
'program_types': course_run.program_types,
'authoring_organization_uuids': get_uuids(course_run.authoring_organizations.all()),
......@@ -749,7 +750,7 @@ class ProgramSearchSerializerTests(TestCase):
'content_type': 'program',
'card_image_url': program.card_image_url,
'status': program.status,
'published': program.status == Program.Status.Active,
'published': program.status == ProgramStatus.Active,
'partner': program.partner.short_code,
'authoring_organization_uuids': get_uuids(program.authoring_organizations.all()),
'subject_uuids': get_uuids([course.subjects for course in program.courses.all()]),
......
import ddt
from django.core.urlresolvers import reverse
from rest_framework.test import APITestCase, APIRequestFactory
from course_discovery.apps.api.serializers import ProgramSerializer
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.tests.factories import ProgramFactory
@ddt.ddt
class ProgramViewSetTests(APITestCase):
list_path = reverse('api:v1:program-list')
......@@ -83,11 +86,18 @@ class ProgramViewSetTests(APITestCase):
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. """
url = self.list_path + '?marketable=1'
ProgramFactory(marketing_slug='')
expected = ProgramFactory.create_batch(3)
expected.reverse()
programs = ProgramFactory.create_batch(3, status=status)
programs.reverse()
expected = programs if is_marketable else []
self.assertEqual(list(Program.objects.marketable()), expected)
self.assert_list_results(url, expected)
......@@ -11,6 +11,7 @@ from rest_framework.test import APITestCase
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.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.tests.factories import CourseRunFactory, ProgramFactory
......@@ -82,7 +83,7 @@ class CourseRunSearchViewSetTests(DefaultPartnerMixin, SerializationMixin, Login
# Generate data that should be indexed and returned by the query
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)
self.assertEqual(response.status_code, 200)
......@@ -119,13 +120,13 @@ class CourseRunSearchViewSetTests(DefaultPartnerMixin, SerializationMixin, Login
""" Verify the endpoint returns availability facets with the results. """
now = datetime.datetime.utcnow()
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),
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),
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),
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)
self.assertEqual(response.status_code, 200)
......@@ -184,11 +185,11 @@ class AggregateSearchViewSet(DefaultPartnerMixin, SerializationMixin, LoginMixin
def test_results_only_include_published_objects(self):
""" Verify the search results only include items with status set to 'Published'. """
# These items should NOT be in the results
CourseRunFactory(course__partner=self.partner, status=CourseRun.Status.Unpublished)
ProgramFactory(partner=self.partner, status=Program.Status.Unpublished)
CourseRunFactory(course__partner=self.partner, status=CourseRunStatus.Unpublished)
ProgramFactory(partner=self.partner, status=ProgramStatus.Unpublished)
course_run = CourseRunFactory(course__partner=self.partner, status=CourseRun.Status.Published)
program = ProgramFactory(partner=self.partner, status=Program.Status.Active)
course_run = CourseRunFactory(course__partner=self.partner, status=CourseRunStatus.Published)
program = ProgramFactory(partner=self.partner, status=ProgramStatus.Active)
response = self.get_search_response()
self.assertEqual(response.status_code, 200)
......@@ -199,13 +200,13 @@ class AggregateSearchViewSet(DefaultPartnerMixin, SerializationMixin, LoginMixin
def test_results_filtered_by_default_partner(self):
""" 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. """
course_run = CourseRunFactory(course__partner=self.partner, status=CourseRun.Status.Published)
program = ProgramFactory(partner=self.partner, status=Program.Status.Active)
course_run = CourseRunFactory(course__partner=self.partner, status=CourseRunStatus.Published)
program = ProgramFactory(partner=self.partner, status=ProgramStatus.Active)
# This data should NOT be in the results
other_partner = PartnerFactory()
other_course_run = CourseRunFactory(course__partner=other_partner, status=CourseRun.Status.Published)
other_program = ProgramFactory(partner=other_partner, status=Program.Status.Active)
other_course_run = CourseRunFactory(course__partner=other_partner, status=CourseRunStatus.Published)
other_program = ProgramFactory(partner=other_partner, status=ProgramStatus.Active)
self.assertNotEqual(other_program.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):
paramType: query
multiple: false
- 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
type: integer
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
import requests
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.models import (
Video, Organization, Seat, CourseRun, Program, Course, ProgramType,
......@@ -141,7 +142,7 @@ class CoursesApiDataLoader(AbstractDataLoader):
'title_override': body['name'],
'short_description_override': body['short_description'],
'video': self.get_courserun_video(body),
'status': CourseRun.Status.Published,
'status': CourseRunStatus.Published,
'pacing_type': self.get_pacing_type(body),
})
......@@ -157,9 +158,9 @@ class CoursesApiDataLoader(AbstractDataLoader):
pacing = pacing.lower()
if pacing == 'instructor':
return CourseRun.Pacing.Instructor
return CourseRunPacing.Instructor
elif pacing == 'self':
return CourseRun.Pacing.Self
return CourseRunPacing.Self
else:
return None
......
......@@ -11,6 +11,7 @@ from django.db.models import Q
from django.utils.functional import cached_property
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.models import (
Course, Organization, Person, Subject, Program, Position, LevelType, CourseRun
......@@ -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.
# 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.
published = self.get_course_run_status(data) == CourseRun.Status.Published
published = self.get_course_run_status(data) == CourseRunStatus.Published
if not created and published:
for attr, value in defaults.items():
setattr(course, attr, value)
......@@ -403,7 +404,7 @@ class CourseMarketingSiteDataLoader(AbstractMarketingSiteDataLoader):
return description
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):
level_type = None
......@@ -420,7 +421,7 @@ class CourseMarketingSiteDataLoader(AbstractMarketingSiteDataLoader):
def get_pacing_type(self, data):
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):
uuid = data['uuid']
......
......@@ -8,6 +8,7 @@ from django.test import TestCase
from pytz import UTC
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 (
OrganizationsApiDataLoader, CoursesApiDataLoader, EcommerceApiDataLoader, AbstractDataLoader, ProgramsApiDataLoader
)
......@@ -157,7 +158,7 @@ class CoursesApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCas
'title_override': body['name'],
'short_description_override': self.loader.clean_string(body['short_description']),
'video': self.loader.get_courserun_video(body),
'status': CourseRun.Status.Published,
'status': CourseRunStatus.Published,
'pacing_type': self.loader.get_pacing_type(body),
})
......@@ -215,10 +216,10 @@ class CoursesApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCas
('', None),
('foo', None),
(None, None),
('instructor', CourseRun.Pacing.Instructor),
('Instructor', CourseRun.Pacing.Instructor),
('self', CourseRun.Pacing.Self),
('Self', CourseRun.Pacing.Self),
('instructor', CourseRunPacing.Instructor),
('Instructor', CourseRunPacing.Instructor),
('self', CourseRunPacing.Self),
('Self', CourseRunPacing.Self),
)
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. """
......
......@@ -11,14 +11,14 @@ import responses
from django.test import TestCase
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 (
XSeriesMarketingSiteDataLoader, SubjectMarketingSiteDataLoader, SchoolMarketingSiteDataLoader,
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.mixins import DataLoaderTestMixin
from course_discovery.apps.course_metadata.models import Organization, Subject, Program, Video, Person, Course, \
CourseRun
from course_discovery.apps.course_metadata.models import Organization, Subject, Program, Video, Person, Course
from course_discovery.apps.course_metadata.tests import factories
from course_discovery.apps.ietf_language_tags.models import LanguageTag
......@@ -364,8 +364,8 @@ class CourseMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMixi
@ddt.unpack
@ddt.data(
('0', CourseRun.Status.Unpublished),
('1', CourseRun.Status.Published),
('0', CourseRunStatus.Unpublished),
('1', CourseRunStatus.Published),
)
def test_get_course_run_status(self, marketing_site_status, expected):
data = {'status': marketing_site_status}
......@@ -394,10 +394,10 @@ class CourseMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMixi
@ddt.unpack
@ddt.data(
(True, CourseRun.Pacing.Self),
(False, CourseRun.Pacing.Instructor),
(None, CourseRun.Pacing.Instructor),
('', CourseRun.Pacing.Instructor),
(True, CourseRunPacing.Self),
(False, CourseRunPacing.Instructor),
(None, CourseRunPacing.Instructor),
('', CourseRunPacing.Instructor),
)
def test_get_pacing_type(self, data_value, expected_pacing_type):
data = {'field_course_self_paced': data_value}
......
......@@ -4,6 +4,7 @@ from django.core.exceptions import ValidationError
from django.forms.utils import ErrorList
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
......@@ -46,7 +47,7 @@ class ProgramAdminForm(forms.ModelForm):
def clean(self):
status = self.cleaned_data.get('status')
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.'))
return self.cleaned_data
......
......@@ -10,7 +10,6 @@ from django.db.models.query_utils import Q
from django.utils.translation import ugettext_lazy as _
from django_extensions.db.fields import AutoSlugField
from django_extensions.db.models import TimeStampedModel
from djchoices import DjangoChoices, ChoiceItem
from haystack import connections
from haystack.query import SearchQuerySet
from simple_history.models import HistoricalRecords
......@@ -20,6 +19,7 @@ from taggit.managers import TaggableManager
import waffle
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.query import CourseQuerySet, CourseRunQuerySet, ProgramQuerySet
from course_discovery.apps.course_metadata.utils import UploadToFieldNamePath
......@@ -325,22 +325,11 @@ class Course(TimeStampedModel):
class CourseRun(TimeStampedModel):
""" 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'))
course = models.ForeignKey(Course, related_name='course_runs')
key = models.CharField(max_length=255, unique=True)
status = models.CharField(max_length=255, null=False, blank=False, db_index=True, choices=Status.choices,
validators=[Status.validator])
status = models.CharField(max_length=255, null=False, blank=False, db_index=True, choices=CourseRunStatus.choices,
validators=[CourseRunStatus.validator])
title_override = models.CharField(
max_length=255, default=None, null=True, blank=True,
help_text=_(
......@@ -369,8 +358,8 @@ class CourseRun(TimeStampedModel):
help_text=_('Estimated maximum number of hours per week needed to complete a course run.'))
language = models.ForeignKey(LanguageTag, null=True, blank=True)
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,
validators=[Pacing.validator])
pacing_type = models.CharField(max_length=255, db_index=True, null=True, blank=True,
choices=CourseRunPacing.choices, validators=[CourseRunPacing.validator])
syllabus = models.ForeignKey(SyllabusItem, default=None, null=True, blank=True)
card_image_url = models.URLField(null=True, blank=True)
video = models.ForeignKey(Video, default=None, null=True, blank=True)
......@@ -588,12 +577,6 @@ class ProgramType(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'))
title = models.CharField(
help_text=_('The user-facing display title for this Program.'), max_length=255, unique=True)
......@@ -602,7 +585,7 @@ class Program(TimeStampedModel):
type = models.ForeignKey(ProgramType, null=True, blank=True)
status = models.CharField(
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(
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):
@property
def is_active(self):
return self.status == self.Status.Active
return self.status == ProgramStatus.Active
def save(self, *args, **kwargs):
if waffle.switch_is_active('publish_program_to_marketing_site') and \
......
......@@ -4,6 +4,8 @@ import pytz
from django.db import models
from django.db.models.query_utils import Q
from course_discovery.apps.course_metadata.choices import ProgramStatus
class CourseQuerySet(models.QuerySet):
def active(self):
......@@ -54,10 +56,16 @@ class ProgramQuerySet(models.QuerySet):
def marketable(self):
""" 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:
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
from haystack import indexes
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
......@@ -125,7 +126,7 @@ class CourseRunIndex(BaseCourseIndex, indexes.Indexable):
return obj.course.partner.short_code
def prepare_published(self, obj):
return obj.status == CourseRun.Status.Published
return obj.status == CourseRunStatus.Published
def _prepare_language(self, language):
if language:
......@@ -187,7 +188,7 @@ class ProgramIndex(BaseIndex, indexes.Indexable, OrganizationsMixin):
published = indexes.BooleanField(null=False, faceted=True)
def prepare_published(self, obj):
return obj.status == Program.Status.Active
return obj.status == ProgramStatus.Active
def prepare_organizations(self, obj):
return self.prepare_authoring_organizations(obj) + self.prepare_credit_backing_organizations(obj)
......
......@@ -6,6 +6,7 @@ from pytz import UTC
from course_discovery.apps.core.tests.factories import PartnerFactory
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.ietf_language_tags.models import LanguageTag
......@@ -106,7 +107,7 @@ class CourseFactory(factory.DjangoModelFactory):
class CourseRunFactory(factory.DjangoModelFactory):
status = CourseRun.Status.Published
status = CourseRunStatus.Published
uuid = factory.LazyFunction(uuid4)
key = FuzzyText(prefix='course-run-id/', suffix='/fake')
course = factory.SubFactory(CourseFactory)
......@@ -123,7 +124,7 @@ class CourseRunFactory(factory.DjangoModelFactory):
video = factory.SubFactory(VideoFactory)
min_effort = FuzzyInteger(1, 10)
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()
@factory.post_generation
......@@ -240,7 +241,7 @@ class ProgramFactory(factory.django.DjangoModelFactory):
uuid = factory.LazyFunction(uuid4)
subtitle = 'test-subtitle'
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
banner_image_url = FuzzyText(prefix='https://example.com/program/banner')
card_image_url = FuzzyText(prefix='https://example.com/program/card')
......
import ddt
from django.core.urlresolvers import reverse
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.core.tests.factories import UserFactory, USER_PASSWORD
from course_discovery.apps.core.tests.helpers import make_image_file
......@@ -109,7 +109,7 @@ class AdminTests(TestCase):
def test_program_without_image_and_active_status(self):
""" 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': ''})
self.assertFalse(form.is_valid())
self.assertEqual(form.errors['__all__'], ['Status cannot be change to active without banner image.'])
......@@ -117,9 +117,9 @@ class AdminTests(TestCase):
form.save()
@ddt.data(
Program.Status.Deleted,
Program.Status.Retired,
Program.Status.Unpublished
ProgramStatus.Deleted,
ProgramStatus.Retired,
ProgramStatus.Unpublished
)
def test_program_without_image_and_non_active_status(self, status):
""" Verify that new program can be added without `image` and non-active
......@@ -129,10 +129,10 @@ class AdminTests(TestCase):
self.valid_post_form(data, {'banner_image': ''})
@ddt.data(
Program.Status.Deleted,
Program.Status.Retired,
Program.Status.Unpublished,
Program.Status.Active
ProgramStatus.Deleted,
ProgramStatus.Retired,
ProgramStatus.Unpublished,
ProgramStatus.Active
)
def test_program_with_image(self, status):
""" Verify that new program can be added with `image` and any status."""
......@@ -157,7 +157,7 @@ class AdminTests(TestCase):
def test_new_program_without_courses(self):
""" Verify that new program can be added without `courses`."""
data = self._post_data(Program.Status.Unpublished)
data = self._post_data(ProgramStatus.Unpublished)
data['courses'] = []
form = ProgramAdminForm(data)
self.assertTrue(form.is_valid())
......
......@@ -14,10 +14,10 @@ import responses
from course_discovery.apps.core.models import Currency
from course_discovery.apps.core.tests.helpers import make_image_file
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 (
AbstractMediaModel, AbstractNamedModel, AbstractValueModel,
CorporateEndorsement, Program, Course, CourseRun, Endorsement,
FAQ, SeatType, ProgramType,
CorporateEndorsement, Course, CourseRun, Endorsement, FAQ, SeatType, ProgramType,
)
from course_discovery.apps.course_metadata.tests import factories, toggle_switch
from course_discovery.apps.course_metadata.tests.factories import CourseRunFactory, ImageFactory
......@@ -386,10 +386,10 @@ class ProgramTests(MarketingSitePublisherTestMixin):
program = self.create_program_with_seats()
self.assertEqual(program.seat_types, set(['credit', 'verified']))
@ddt.data(Program.Status.choices)
@ddt.data(ProgramStatus.choices)
def test_is_active(self, 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
def test_save_without_publish(self):
......
import responses
from course_discovery.apps.course_metadata.choices import ProgramStatus
from course_discovery.apps.course_metadata.publishers import (
MarketingSiteAPIClient,
MarketingSitePublisher,
......@@ -10,7 +11,7 @@ from course_discovery.apps.course_metadata.tests.mixins import (
MarketingSiteAPIClientTestMixin,
MarketingSitePublisherTestMixin,
)
from course_discovery.apps.course_metadata.models import Program, ProgramType
from course_discovery.apps.course_metadata.models import ProgramType
class MarketingSiteAPIClientTests(MarketingSiteAPIClientTestMixin):
......@@ -115,7 +116,7 @@ class MarketingSitePublisherTests(MarketingSitePublisherTestMixin):
'author': {
'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)
......
......@@ -4,6 +4,7 @@ import ddt
import pytz
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.tests.factories import CourseRunFactory, ProgramFactory
......@@ -72,11 +73,18 @@ class CourseRunQuerySetTests(TestCase):
self.assertEqual(CourseRun.objects.marketable().count(), 0)
@ddt.ddt
class ProgramQuerySetTests(TestCase):
def test_marketable(self):
""" Verify the method filters Programs to those with marketing slugs. """
program = ProgramFactory()
self.assertEqual(list(Program.objects.marketable()), [program])
@ddt.data(
(ProgramStatus.Unpublished, False),
(ProgramStatus.Active, True),
)
@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):
""" Verify the method excludes Programs without a marketing slug. """
......
......@@ -12,8 +12,8 @@ from simple_history.models import HistoricalRecords
from sortedm2m.fields import SortedManyToManyField
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 CourseRun as CourseMetadataCourseRun
from course_discovery.apps.ietf_language_tags.models import LanguageTag
logger = logging.getLogger(__name__)
......@@ -152,8 +152,8 @@ class CourseRun(TimeStampedModel, ChangedByMixin):
enrollment_end = models.DateTimeField(null=True, blank=True)
certificate_generation = models.DateTimeField(null=True, blank=True)
pacing_type = models.CharField(
max_length=255, db_index=True, null=True, blank=True, choices=CourseMetadataCourseRun.Pacing.choices,
validators=[CourseMetadataCourseRun.Pacing.validator]
max_length=255, db_index=True, null=True, blank=True, choices=CourseRunPacing.choices,
validators=[CourseRunPacing.validator]
)
staff = SortedManyToManyField(Person, blank=True, related_name='publisher_course_runs_staffed')
min_effort = models.PositiveSmallIntegerField(
......
......@@ -6,8 +6,8 @@ from factory.fuzzy import FuzzyText, FuzzyChoice, FuzzyDecimal, FuzzyDateTime, F
from pytz import UTC
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.models import CourseRun as CourseMetadataCourseRun
from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.models import Course, CourseRun, Seat, State
......@@ -48,7 +48,7 @@ class CourseRunFactory(factory.DjangoModelFactory):
min_effort = FuzzyInteger(1, 10)
max_effort = FuzzyInteger(10, 20)
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)
seo_review = "test-seo-review"
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