Commit 4acf759a by Renzo Lucioni

Publish course run status changes to the marketing site

Generalizes program publication code so that it can be applied to any model, including course runs.

LEARNER-419
parent aabf4fcd
...@@ -4,10 +4,17 @@ from django.http import HttpResponseRedirect ...@@ -4,10 +4,17 @@ from django.http import HttpResponseRedirect
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from course_discovery.apps.course_metadata.exceptions import (
MarketingSiteAPIClientException, MarketingSitePublisherException
)
from course_discovery.apps.course_metadata.forms import CourseAdminForm, ProgramAdminForm from course_discovery.apps.course_metadata.forms import CourseAdminForm, ProgramAdminForm
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.course_metadata.publishers import ProgramPublisherException
from course_discovery.apps.course_metadata.utils import MarketingSiteAPIClientException
PUBLICATION_FAILURE_MSG_TPL = _(
'An error occurred while publishing the {model} to the marketing site. '
'Please try again. If the error persists, please contact the Engineering Team.'
)
class ProgramEligibilityFilter(admin.SimpleListFilter): class ProgramEligibilityFilter(admin.SimpleListFilter):
...@@ -101,6 +108,24 @@ class CourseRunAdmin(admin.ModelAdmin): ...@@ -101,6 +108,24 @@ class CourseRunAdmin(admin.ModelAdmin):
ordering = ('key',) ordering = ('key',)
readonly_fields = ('uuid',) readonly_fields = ('uuid',)
search_fields = ('uuid', 'key', 'title_override', 'course__title', 'slug',) search_fields = ('uuid', 'key', 'title_override', 'course__title', 'slug',)
save_error = False
def response_change(self, request, obj):
if self.save_error:
return self.response_post_save_change(request, obj)
return super().response_change(request, obj)
def save_model(self, request, obj, form, change):
try:
super().save_model(request, obj, form, change)
except (MarketingSitePublisherException, MarketingSiteAPIClientException):
self.save_error = True
logger.exception('An error occurred while publishing course run [%s] to the marketing site.', obj.key)
msg = PUBLICATION_FAILURE_MSG_TPL.format(model='course run') # pylint: disable=no-member
messages.add_message(request, messages.ERROR, msg)
@admin.register(Program) @admin.register(Program)
...@@ -158,15 +183,14 @@ class ProgramAdmin(admin.ModelAdmin): ...@@ -158,15 +183,14 @@ class ProgramAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
try: try:
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
self.save_error = False except (MarketingSitePublisherException, MarketingSiteAPIClientException):
except (ProgramPublisherException, MarketingSiteAPIClientException):
# TODO Redirect the user back to the form so that he/she can try again.
logger.exception('An error occurred while publishing the program [%s] to the marketing site.', obj.uuid)
msg = _('An error occurred while publishing the program to the marketing site. Please try again. '
'If the error persists, please contact the Engineering Team.')
messages.add_message(request, messages.ERROR, msg)
self.save_error = True self.save_error = True
logger.exception('An error occurred while publishing program [%s] to the marketing site.', obj.uuid)
msg = PUBLICATION_FAILURE_MSG_TPL.format(model='program') # pylint: disable=no-member
messages.add_message(request, messages.ERROR, msg)
class Media: class Media:
js = ('bower_components/jquery-ui/ui/minified/jquery-ui.min.js', js = ('bower_components/jquery-ui/ui/minified/jquery-ui.min.js',
'js/sortable_select.js') 'js/sortable_select.js')
......
class MarketingSiteAPIClientException(Exception): class MarketingSiteAPIClientException(Exception):
""" The exception thrown from MarketingSiteAPIClient """
pass pass
class ProgramPublisherException(Exception): class MarketingSitePublisherException(Exception):
""" The exception thrown during the program publishing process to marketing site """ pass
def __init__(self, message):
super(ProgramPublisherException, self).__init__(message) class AliasCreateError(MarketingSitePublisherException):
suffix = 'The program data has not been saved. Please check your marketing site configuration' pass
self.message = '{exception_msg} {suffix}'.format(exception_msg=message, suffix=suffix)
class AliasDeleteError(MarketingSitePublisherException):
pass
class FormRetrievalError(MarketingSitePublisherException):
pass
class NodeCreateError(MarketingSitePublisherException):
pass
class NodeDeleteError(MarketingSitePublisherException):
pass
class NodeEditError(MarketingSitePublisherException):
pass
class NodeLookupError(MarketingSitePublisherException):
pass
class PersonToMarketingException(Exception): class PersonToMarketingException(Exception):
......
...@@ -94,8 +94,8 @@ class CourseRunSelectionForm(forms.ModelForm): ...@@ -94,8 +94,8 @@ class CourseRunSelectionForm(forms.ModelForm):
) )
query_set = [course.pk for course in instance.courses.all()] query_set = [course.pk for course in instance.courses.all()]
self.fields["excluded_course_runs"].widget = forms.widgets.CheckboxSelectMultiple() self.fields['excluded_course_runs'].widget = forms.widgets.CheckboxSelectMultiple()
self.fields["excluded_course_runs"].help_text = "" self.fields['excluded_course_runs'].help_text = ''
self.fields['excluded_course_runs'].queryset = CourseRun.objects.filter( self.fields['excluded_course_runs'].queryset = CourseRun.objects.filter(
course__id__in=query_set course__id__in=query_set
) )
......
from django.db import migrations
SWITCH = 'publish_course_runs_to_marketing_site'
def create_switch(apps, schema_editor):
"""Create the publish_course_runs_to_marketing_site switch."""
Switch = apps.get_model('waffle', 'Switch')
Switch.objects.get_or_create(name=SWITCH, defaults={'active': False})
def delete_switch(apps, schema_editor):
"""Delete the publish_course_runs_to_marketing_site switch."""
Switch = apps.get_model('waffle', 'Switch')
Switch.objects.filter(name=SWITCH).delete()
class Migration(migrations.Migration):
dependencies = [
('course_metadata', '0051_program_one_click_purchase_enabled'),
('waffle', '0001_initial'),
]
operations = [
migrations.RunPython(create_switch, reverse_code=delete_switch),
]
...@@ -23,7 +23,10 @@ from taggit_autosuggest.managers import TaggableManager ...@@ -23,7 +23,10 @@ from taggit_autosuggest.managers import TaggableManager
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 CourseRunPacing, CourseRunStatus, ProgramStatus, ReportingType from course_discovery.apps.course_metadata.choices import CourseRunPacing, CourseRunStatus, ProgramStatus, ReportingType
from course_discovery.apps.course_metadata.publishers import MarketingSitePublisher from course_discovery.apps.course_metadata.publishers import (
CourseRunMarketingSitePublisher,
ProgramMarketingSitePublisher
)
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, clean_query, custom_render_variations from course_discovery.apps.course_metadata.utils import UploadToFieldNamePath, clean_query, custom_render_variations
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
...@@ -337,7 +340,6 @@ class Course(TimeStampedModel): ...@@ -337,7 +340,6 @@ class Course(TimeStampedModel):
class CourseRun(TimeStampedModel): class CourseRun(TimeStampedModel):
""" CourseRun model. """ """ CourseRun model. """
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)
...@@ -595,6 +597,22 @@ class CourseRun(TimeStampedModel): ...@@ -595,6 +597,22 @@ class CourseRun(TimeStampedModel):
def __str__(self): def __str__(self):
return '{key}: {title}'.format(key=self.key, title=self.title) return '{key}: {title}'.format(key=self.key, title=self.title)
def save(self, *args, **kwargs):
is_publishable = (
self.course.partner.has_marketing_site and
waffle.switch_is_active('publish_course_runs_to_marketing_site')
)
if is_publishable:
publisher = CourseRunMarketingSitePublisher(self.course.partner)
previous_obj = CourseRun.objects.get(id=self.id) if self.id else None
with transaction.atomic():
super(CourseRun, self).save(*args, **kwargs)
publisher.publish_obj(self, previous_obj=previous_obj)
else:
super(CourseRun, self).save(*args, **kwargs)
class SeatType(TimeStampedModel): class SeatType(TimeStampedModel):
name = models.CharField(max_length=64, unique=True) name = models.CharField(max_length=64, unique=True)
...@@ -973,19 +991,18 @@ class Program(TimeStampedModel): ...@@ -973,19 +991,18 @@ class Program(TimeStampedModel):
return self.status == ProgramStatus.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 self.partner.has_marketing_site: is_publishable = (
# Before save, get from database the existing data if exists self.partner.has_marketing_site and
existing_program = None waffle.switch_is_active('publish_program_to_marketing_site')
if self.id: )
existing_program = Program.objects.get(id=self.id)
# Pass existing data to the publisher so it can decide whether we should publish if is_publishable:
publisher = MarketingSitePublisher(existing_program) publisher = ProgramMarketingSitePublisher(self.partner)
previous_obj = Program.objects.get(id=self.id) if self.id else None
with transaction.atomic(): with transaction.atomic():
super(Program, self).save(*args, **kwargs) super(Program, self).save(*args, **kwargs)
# Once save complete, we need to update the marketing site publisher.publish_obj(self, previous_obj=previous_obj)
# So the marketing page for this program is automatically updated
publisher.publish_program(self)
else: else:
super(Program, self).save(*args, **kwargs) super(Program, self).save(*args, **kwargs)
......
...@@ -3,12 +3,16 @@ from django.db.models.signals import pre_delete ...@@ -3,12 +3,16 @@ from django.db.models.signals import pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from course_discovery.apps.course_metadata.models import Program from course_discovery.apps.course_metadata.models import Program
from course_discovery.apps.course_metadata.publishers import MarketingSitePublisher from course_discovery.apps.course_metadata.publishers import ProgramMarketingSitePublisher
@receiver(pre_delete, sender=Program) @receiver(pre_delete, sender=Program)
def delete_program(sender, instance, **kwargs): # pylint: disable=unused-argument def delete_program(sender, instance, **kwargs): # pylint: disable=unused-argument
if waffle.switch_is_active('publish_program_to_marketing_site') and \ is_publishable = (
instance.partner.has_marketing_site: instance.partner.has_marketing_site and
publisher = MarketingSitePublisher() waffle.switch_is_active('publish_program_to_marketing_site')
publisher.delete_program(instance) )
if is_publishable:
publisher = ProgramMarketingSitePublisher(instance.partner)
publisher.delete_obj(instance)
import json import json
import random
import urllib import urllib
import responses import responses
...@@ -77,49 +78,46 @@ class MarketingSiteAPIClientTestMixin(TestCase): ...@@ -77,49 +78,46 @@ class MarketingSiteAPIClientTestMixin(TestCase):
class MarketingSitePublisherTestMixin(MarketingSiteAPIClientTestMixin): class MarketingSitePublisherTestMixin(MarketingSiteAPIClientTestMixin):
""" """
The mixing to help mock the responses for marketing site publisher Mixin for mocking Drupal responses when testing marketing site publishers.
""" """
def setUp(self): def setUp(self):
super(MarketingSitePublisherTestMixin, self).setUp() super(MarketingSitePublisherTestMixin, self).setUp()
self.node_id = FuzzyText().fuzz() self.node_id = str(random.randint(1, 1000))
def mock_api_client(self, status): def mock_api_client(self, status=200):
self.mock_login_response(status) self.mock_login_response(status)
self.mock_csrf_token_response(status) self.mock_csrf_token_response(status)
self.mock_user_id_response(status) self.mock_user_id_response(status)
def mock_node_retrieval(self, program_uuid, exists=True): def mock_node_retrieval(self, node_lookup_field, node_lookup_value, exists=True, status=200):
data = {} url = '{root}/node.json?{node_lookup_field}={node_lookup_value}'.format(
status = 200 root=self.api_root,
node_lookup_field=node_lookup_field,
node_lookup_value=node_lookup_value,
)
data = { data = {
'list': [] 'list': [{'nid': self.node_id}] if exists else []
} }
if exists:
data['list'] = [{
'nid': self.node_id
}]
responses.add( responses.add(
responses.GET, responses.GET,
'{root}/node.json?field_uuid={uuid}'.format(root=self.api_root, uuid=str(program_uuid)), url,
body=json.dumps(data), body=json.dumps(data),
content_type='application/json', content_type='application/json',
status=status, status=status,
match_querystring=True match_querystring=True
) )
def mock_add_alias(self, status=200): def mock_add_alias(self, alias=None, status=200):
node_url = 'node/{node_id}'.format(node_id=self.node_id) node_url = 'node/{node_id}'.format(node_id=self.node_id)
alias = '{program_type}/{slug}'.format(
program_type=self.program.type.name.lower(),
slug=self.program.marketing_slug
)
data = { data = {
'source': node_url, 'source': node_url,
'alias': alias, 'alias': alias,
'form_id': 'path_admin_form', 'form_id': 'path_admin_form',
'op': 'Save' 'op': 'Save'
} }
responses.add( responses.add(
responses.POST, responses.POST,
'{root}/admin/config/search/path/add'.format(root=self.api_root), '{root}/admin/config/search/path/add'.format(root=self.api_root),
...@@ -129,13 +127,14 @@ class MarketingSitePublisherTestMixin(MarketingSiteAPIClientTestMixin): ...@@ -129,13 +127,14 @@ class MarketingSitePublisherTestMixin(MarketingSiteAPIClientTestMixin):
def mock_delete_alias(self, status=200): def mock_delete_alias(self, status=200):
data = { data = {
"confirm": 1, 'confirm': 1,
"form_id": "path_admin_delete_confirm", 'form_id': 'path_admin_delete_confirm',
"op": "Confirm" 'op': 'Confirm'
} }
responses.add( responses.add(
responses.POST, responses.POST,
'{root}/foo'.format(root=self.api_root), '{root}/admin/config/search/path/delete/foo'.format(root=self.api_root),
body=urllib.parse.urlencode(data), body=urllib.parse.urlencode(data),
status=status status=status
) )
...@@ -154,12 +153,15 @@ class MarketingSitePublisherTestMixin(MarketingSiteAPIClientTestMixin): ...@@ -154,12 +153,15 @@ class MarketingSitePublisherTestMixin(MarketingSiteAPIClientTestMixin):
responses.GET, responses.GET,
'{root}/admin/config/search/path/list/{alias}'.format(root=self.api_root, alias=alias), '{root}/admin/config/search/path/list/{alias}'.format(root=self.api_root, alias=alias),
status=status, status=status,
body='<li class="delete last"><a href="/admin/config/search/path/delete/bar"></a></li>' body='<li class="delete last"><a href="/admin/config/search/path/delete/foo"></a></li>'
) )
def mock_node_create(self, response_data, status):
responses.add( responses.add(
responses.POST, responses.POST,
'{root}/admin/config/search/path/delete/bar'.format(root=self.api_root), '{root}/node.json'.format(root=self.api_root),
body=json.dumps(response_data),
content_type='application/json',
status=status status=status
) )
...@@ -172,15 +174,6 @@ class MarketingSitePublisherTestMixin(MarketingSiteAPIClientTestMixin): ...@@ -172,15 +174,6 @@ class MarketingSitePublisherTestMixin(MarketingSiteAPIClientTestMixin):
status=status status=status
) )
def mock_node_create(self, response_data, status):
responses.add(
responses.POST,
'{root}/node.json'.format(root=self.api_root),
body=json.dumps(response_data),
content_type='application/json',
status=status
)
def mock_node_delete(self, status): def mock_node_delete(self, status):
responses.add( responses.add(
responses.DELETE, responses.DELETE,
......
...@@ -4,13 +4,11 @@ from decimal import Decimal ...@@ -4,13 +4,11 @@ from decimal import Decimal
import ddt import ddt
import mock import mock
import responses
from dateutil.parser import parse from dateutil.parser import parse
from django.conf import settings from django.conf import settings
from django.db import IntegrityError from django.db import IntegrityError
from django.db.models.functions import Lower from django.db.models.functions import Lower
from django.test import TestCase from django.test import TestCase
from factory.fuzzy import FuzzyText
from freezegun import freeze_time from freezegun import freeze_time
from course_discovery.apps.core.models import Currency from course_discovery.apps.core.models import Currency
...@@ -20,12 +18,13 @@ from course_discovery.apps.core.utils import SearchQuerySetWrapper ...@@ -20,12 +18,13 @@ from course_discovery.apps.core.utils import SearchQuerySetWrapper
from course_discovery.apps.course_metadata.choices import ProgramStatus 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 (
FAQ, AbstractMediaModel, AbstractNamedModel, AbstractValueModel, CorporateEndorsement, Course, CourseRun, FAQ, AbstractMediaModel, AbstractNamedModel, AbstractValueModel, CorporateEndorsement, Course, CourseRun,
Endorsement, ProgramType, Seat, SeatType Endorsement, Seat, SeatType
)
from course_discovery.apps.course_metadata.publishers import (
CourseRunMarketingSitePublisher, ProgramMarketingSitePublisher
) )
from course_discovery.apps.course_metadata.publishers import MarketingSitePublisher
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
from course_discovery.apps.course_metadata.tests.mixins import MarketingSitePublisherTestMixin
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
...@@ -257,7 +256,7 @@ class CourseRunTests(TestCase): ...@@ -257,7 +256,7 @@ class CourseRunTests(TestCase):
@ddt.unpack @ddt.unpack
def test_get_paid_seat_enrollment_end(self, seat_config, course_end, course_enrollment_end, expected_result): def test_get_paid_seat_enrollment_end(self, seat_config, course_end, course_enrollment_end, expected_result):
""" """
Verify that paid_seat_enrollment_end returns the latest possible date for which an unerolled user may Verify that paid_seat_enrollment_end returns the latest possible date for which an unenrolled user may
enroll and purchase an upgrade for the CourseRun or None if date unknown or paid Seats are not available. enroll and purchase an upgrade for the CourseRun or None if date unknown or paid Seats are not available.
""" """
end = parse(course_end) if course_end else None end = parse(course_end) if course_end else None
...@@ -270,6 +269,43 @@ class CourseRunTests(TestCase): ...@@ -270,6 +269,43 @@ class CourseRunTests(TestCase):
expected_result = parse(expected_result) if expected_result else None expected_result = parse(expected_result) if expected_result else None
self.assertEqual(course_run.get_paid_seat_enrollment_end(), expected_result) self.assertEqual(course_run.get_paid_seat_enrollment_end(), expected_result)
def test_publication_disabled(self):
"""
Verify that the publisher is not initialized when publication is disabled.
"""
toggle_switch('publish_course_runs_to_marketing_site', active=False)
with mock.patch.object(CourseRunMarketingSitePublisher, '__init__') as mock_init:
self.course_run.save()
self.course_run.delete()
assert mock_init.call_count == 0
toggle_switch('publish_course_runs_to_marketing_site')
self.course_run.course.partner.marketing_site_url_root = ''
self.course_run.course.partner.save()
with mock.patch.object(CourseRunMarketingSitePublisher, '__init__') as mock_init:
self.course_run.save()
self.course_run.delete()
assert mock_init.call_count == 0
def test_publication_enabled(self):
"""
Verify that the publisher is called when publication is enabled.
"""
toggle_switch('publish_course_runs_to_marketing_site')
with mock.patch.object(CourseRunMarketingSitePublisher, 'publish_obj', return_value=None) as mock_publish_obj:
self.course_run.save()
assert mock_publish_obj.called
with mock.patch.object(CourseRunMarketingSitePublisher, 'delete_obj', return_value=None) as mock_delete_obj:
self.course_run.delete()
# We don't want to delete course run nodes when CourseRuns are deleted.
assert not mock_delete_obj.called
class OrganizationTests(TestCase): class OrganizationTests(TestCase):
""" Tests for the `Organization` model. """ """ Tests for the `Organization` model. """
...@@ -390,7 +426,7 @@ class AbstractValueModelTests(TestCase): ...@@ -390,7 +426,7 @@ class AbstractValueModelTests(TestCase):
@ddt.ddt @ddt.ddt
class ProgramTests(MarketingSitePublisherTestMixin): class ProgramTests(TestCase):
"""Tests of the Program model.""" """Tests of the Program model."""
def setUp(self): def setUp(self):
...@@ -737,77 +773,41 @@ class ProgramTests(MarketingSitePublisherTestMixin): ...@@ -737,77 +773,41 @@ class ProgramTests(MarketingSitePublisherTestMixin):
self.program.status = status self.program.status = status
self.assertEqual(self.program.is_active, status == ProgramStatus.Active) self.assertEqual(self.program.is_active, status == ProgramStatus.Active)
@responses.activate def test_publication_disabled(self):
def test_save_without_publish(self):
self.program.title = FuzzyText().fuzz()
self.program.save()
self.assert_responses_call_count(0)
@responses.activate
def test_delete_without_publish(self):
self.program.delete()
self.assert_responses_call_count(0)
@responses.activate
def test_save_and_publish_success(self):
self.program.partner.marketing_site_url_root = self.api_root
self.program.partner.marketing_site_api_username = self.username
self.program.partner.marketing_site_api_password = self.password
self.program.type = ProgramType.objects.get(name='MicroMasters')
self.mock_api_client(200)
self.mock_node_retrieval(self.program.uuid)
self.mock_node_edit(200)
toggle_switch('publish_program_to_marketing_site', True)
self.program.title = FuzzyText().fuzz()
self.mock_add_alias()
self.mock_delete_alias()
with mock.patch.object(MarketingSitePublisher, '_get_headers', return_value={}):
with mock.patch.object(MarketingSitePublisher, '_get_form_build_id_and_form_token', return_value={}):
with mock.patch.object(MarketingSitePublisher, '_get_delete_alias_url', return_value='/foo'):
self.program.save()
self.assert_responses_call_count(8)
@responses.activate
def test_xseries_program_save(self):
""" """
Make sure if the Program instance is of type XSeries, we do not publish to Marketing Site Verify that the publisher is not initialized when publication is disabled.
""" """
self.program.partner.marketing_site_url_root = self.api_root toggle_switch('publish_program_to_marketing_site', active=False)
self.program.partner.marketing_site_api_username = self.username
self.program.partner.marketing_site_api_password = self.password
self.program.type = ProgramType.objects.get(name='XSeries')
toggle_switch('publish_program_to_marketing_site', True)
self.program.title = FuzzyText().fuzz()
self.program.save()
self.assert_responses_call_count(0)
@responses.activate with mock.patch.object(ProgramMarketingSitePublisher, '__init__') as mock_init:
def test_save_and_no_marketing_site(self): self.program.save()
self.program.partner.marketing_site_url_root = None self.program.delete()
toggle_switch('publish_program_to_marketing_site', True)
self.program.title = FuzzyText().fuzz() assert mock_init.call_count == 0
self.program.save()
self.assert_responses_call_count(0) toggle_switch('publish_program_to_marketing_site')
self.program.partner.marketing_site_url_root = ''
@responses.activate self.program.partner.save()
def test_delete_and_publish_success(self):
self.program.partner.marketing_site_url_root = self.api_root with mock.patch.object(ProgramMarketingSitePublisher, '__init__') as mock_init:
self.program.partner.marketing_site_api_username = self.username self.program.save()
self.program.partner.marketing_site_api_password = self.password self.program.delete()
self.program.type = ProgramType.objects.get(name='MicroMasters')
self.mock_api_client(200) assert mock_init.call_count == 0
self.mock_node_retrieval(self.program.uuid)
self.mock_node_delete(204) def test_publication_enabled(self):
toggle_switch('publish_program_to_marketing_site', True) """
self.program.delete() Verify that the publisher is called when publication is enabled.
self.assert_responses_call_count(5) """
toggle_switch('publish_program_to_marketing_site')
@responses.activate
def test_delete_and_no_marketing_site(self): with mock.patch.object(ProgramMarketingSitePublisher, 'publish_obj', return_value=None) as mock_publish_obj:
self.program.partner.marketing_site_url_root = None self.program.save()
toggle_switch('publish_program_to_marketing_site', True) assert mock_publish_obj.called
self.program.delete()
self.assert_responses_call_count(0) with mock.patch.object(ProgramMarketingSitePublisher, 'delete_obj', return_value=None) as mock_delete_obj:
self.program.delete()
assert mock_delete_obj.called
class PersonSocialNetworkTests(TestCase): class PersonSocialNetworkTests(TestCase):
......
...@@ -3,8 +3,8 @@ import responses ...@@ -3,8 +3,8 @@ import responses
from course_discovery.apps.core.tests.factories import PartnerFactory from course_discovery.apps.core.tests.factories import PartnerFactory
from course_discovery.apps.course_metadata.exceptions import PersonToMarketingException from course_discovery.apps.course_metadata.exceptions import PersonToMarketingException
from course_discovery.apps.course_metadata.people import MarketingSitePeople from course_discovery.apps.course_metadata.people import MarketingSitePeople
from course_discovery.apps.course_metadata.publishers import MarketingSiteAPIClient
from course_discovery.apps.course_metadata.tests.mixins import MarketingSitePublisherTestMixin from course_discovery.apps.course_metadata.tests.mixins import MarketingSitePublisherTestMixin
from course_discovery.apps.course_metadata.utils import MarketingSiteAPIClient
class MarketingSitePublisherTests(MarketingSitePublisherTestMixin): class MarketingSitePublisherTests(MarketingSitePublisherTestMixin):
...@@ -71,6 +71,6 @@ class MarketingSitePublisherTests(MarketingSitePublisherTestMixin): ...@@ -71,6 +71,6 @@ class MarketingSitePublisherTests(MarketingSitePublisherTestMixin):
@responses.activate @responses.activate
def test_delete_program(self): def test_delete_program(self):
self.mock_api_client(200) self.mock_api_client(200)
self.mock_node_delete(204) self.mock_node_delete(200)
people = MarketingSitePeople() people = MarketingSitePeople()
people.delete_person(self.partner, self.node_id) people.delete_person(self.partner, self.node_id)
# pylint: disable=no-member
from unittest.mock import patch
from django.test import TestCase
from course_discovery.apps.course_metadata.models import ProgramType
from course_discovery.apps.course_metadata.tests import factories, toggle_switch
MARKETING_SITE_PUBLISHERS_MODULE = 'course_discovery.apps.course_metadata.publishers.MarketingSitePublisher'
@patch(MARKETING_SITE_PUBLISHERS_MODULE + '.delete_program')
class SignalsTest(TestCase):
def setUp(self):
super(SignalsTest, self).setUp()
self.program = factories.ProgramFactory(type=ProgramType.objects.get(name='MicroMasters'))
def test_delete_program_signal_no_publish(self, delete_program_mock):
toggle_switch('publish_program_to_marketing_site', False)
self.program.delete()
self.assertFalse(delete_program_mock.called)
def test_delete_program_signal_with_publish(self, delete_program_mock):
toggle_switch('publish_program_to_marketing_site', True)
self.program.delete()
delete_program_mock.assert_called_once_with(self.program)
...@@ -28,14 +28,14 @@ class CourseRunSelectionAdmin(UpdateView): ...@@ -28,14 +28,14 @@ class CourseRunSelectionAdmin(UpdateView):
context = super(CourseRunSelectionAdmin, self).get_context_data(**kwargs) context = super(CourseRunSelectionAdmin, self).get_context_data(**kwargs)
context.update({ context.update({
'program_id': self.object.id, 'program_id': self.object.id,
'title': _('Change program excluded course runs') 'title': _('Update excluded course runs')
}) })
return context return context
raise Http404 raise Http404
def form_valid(self, form): def form_valid(self, form):
self.object = form.save() self.object = form.save()
message = 'The program course runs was changed successfully.' message = _('The program was changed successfully.')
messages.add_message(self.request, messages.SUCCESS, message) messages.add_message(self.request, messages.SUCCESS, message)
return HttpResponseRedirect(self.get_success_url()) return HttpResponseRedirect(self.get_success_url())
......
...@@ -7,7 +7,7 @@ msgid "" ...@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-04-20 16:51+0500\n" "POT-Creation-Date: 2017-04-20 11:56-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
...@@ -201,6 +201,13 @@ msgid "Partners" ...@@ -201,6 +201,13 @@ msgid "Partners"
msgstr "" msgstr ""
#: apps/course_metadata/admin.py #: apps/course_metadata/admin.py
#, python-brace-format
msgid ""
"An error occurred while publishing the {model} to the marketing site. Please"
" try again. If the error persists, please contact the Engineering Team."
msgstr ""
#: apps/course_metadata/admin.py
msgid "eligible for one-click purchase" msgid "eligible for one-click purchase"
msgstr "" msgstr ""
...@@ -217,12 +224,6 @@ msgstr "" ...@@ -217,12 +224,6 @@ msgstr ""
msgid "Included course runs" msgid "Included course runs"
msgstr "" msgstr ""
#: apps/course_metadata/admin.py
msgid ""
"An error occurred while publishing the program to the marketing site. Please"
" try again. If the error persists, please contact the Engineering Team."
msgstr ""
#: apps/course_metadata/choices.py apps/publisher/choices.py #: apps/course_metadata/choices.py apps/publisher/choices.py
msgid "Published" msgid "Published"
msgstr "" msgstr ""
...@@ -440,7 +441,11 @@ msgid "Allow courses in this program to be purchased in a single transaction" ...@@ -440,7 +441,11 @@ msgid "Allow courses in this program to be purchased in a single transaction"
msgstr "" msgstr ""
#: apps/course_metadata/views.py #: apps/course_metadata/views.py
msgid "Change program excluded course runs" msgid "Update excluded course runs"
msgstr ""
#: apps/course_metadata/views.py
msgid "The program was changed successfully."
msgstr "" msgstr ""
#: apps/publisher/api/serializers.py #: apps/publisher/api/serializers.py
......
...@@ -7,7 +7,7 @@ msgid "" ...@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-04-20 16:51+0500\n" "POT-Creation-Date: 2017-04-20 11:56-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
......
...@@ -7,7 +7,7 @@ msgid "" ...@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-04-20 16:51+0500\n" "POT-Creation-Date: 2017-04-20 11:56-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
...@@ -242,6 +242,21 @@ msgid "Partners" ...@@ -242,6 +242,21 @@ msgid "Partners"
msgstr "Pärtnérs Ⱡ'σяєм ιρѕυм ∂#" msgstr "Pärtnérs Ⱡ'σяєм ιρѕυм ∂#"
#: apps/course_metadata/admin.py #: apps/course_metadata/admin.py
#, python-brace-format
msgid ""
"An error occurred while publishing the {model} to the marketing site. Please"
" try again. If the error persists, please contact the Engineering Team."
msgstr ""
"Àn érrör öççürréd whïlé püßlïshïng thé {model} tö thé märkétïng sïté. Pléäsé"
" trý ägäïn. Ìf thé érrör pérsïsts, pléäsé çöntäçt thé Éngïnéérïng Téäm. "
"Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ "
"тємρσя ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαgηα αłιqυα. υт єηιм α∂ мιηιм νєηιαм,"
" qυιѕ ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ ηιѕι υт αłιqυιρ єχ єα ¢σммσ∂σ "
"¢σηѕєqυαт. ∂υιѕ αυтє ιяυяє ∂σłσя ιη яєρяєнєη∂єяιт ιη νσłυρтαтє νєłιт єѕѕє "
"¢ιłłυм ∂σłσяє єυ ƒυgιαт ηυłłα ραяιαтυя. єχ¢єρтєυя ѕιηт σ¢¢αє¢αт ¢υρι∂αтαт "
"ηση ρяσι∂єηт, ѕυηт ιη ¢υłρα qυι σƒƒι¢ια ∂єѕєяυηт мσłłιт αηιм ι∂ єѕ#"
#: apps/course_metadata/admin.py
msgid "eligible for one-click purchase" msgid "eligible for one-click purchase"
msgstr "élïgïßlé för öné-çlïçk pürçhäsé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#" msgstr "élïgïßlé för öné-çlïçk pürçhäsé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#"
...@@ -258,20 +273,6 @@ msgstr "Nö Ⱡ'σя#" ...@@ -258,20 +273,6 @@ msgstr "Nö Ⱡ'σя#"
msgid "Included course runs" msgid "Included course runs"
msgstr "Ìnçlüdéd çöürsé rüns Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" msgstr "Ìnçlüdéd çöürsé rüns Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
#: apps/course_metadata/admin.py
msgid ""
"An error occurred while publishing the program to the marketing site. Please"
" try again. If the error persists, please contact the Engineering Team."
msgstr ""
"Àn érrör öççürréd whïlé püßlïshïng thé prögräm tö thé märkétïng sïté. Pléäsé"
" trý ägäïn. Ìf thé érrör pérsïsts, pléäsé çöntäçt thé Éngïnéérïng Téäm. "
"Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ "
"тємρσя ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαgηα αłιqυα. υт єηιм α∂ мιηιм νєηιαм,"
" qυιѕ ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ ηιѕι υт αłιqυιρ єχ єα ¢σммσ∂σ "
"¢σηѕєqυαт. ∂υιѕ αυтє ιяυяє ∂σłσя ιη яєρяєнєη∂єяιт ιη νσłυρтαтє νєłιт єѕѕє "
"¢ιłłυм ∂σłσяє єυ ƒυgιαт ηυłłα ραяιαтυя. єχ¢єρтєυя ѕιηт σ¢¢αє¢αт ¢υρι∂αтαт "
"ηση ρяσι∂єηт, ѕυηт ιη ¢υłρα qυι σƒƒι¢ια ∂єѕєяυηт мσłłιт αηιм #"
#: apps/course_metadata/choices.py apps/publisher/choices.py #: apps/course_metadata/choices.py apps/publisher/choices.py
msgid "Published" msgid "Published"
msgstr "Püßlïshéd Ⱡ'σяєм ιρѕυм ∂σł#" msgstr "Püßlïshéd Ⱡ'σяєм ιρѕυм ∂σł#"
...@@ -551,9 +552,14 @@ msgstr "" ...@@ -551,9 +552,14 @@ msgstr ""
" ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #" " ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #"
#: apps/course_metadata/views.py #: apps/course_metadata/views.py
msgid "Change program excluded course runs" msgid "Update excluded course runs"
msgstr "Ûpdäté éxçlüdéd çöürsé rüns Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#"
#: apps/course_metadata/views.py
msgid "The program was changed successfully."
msgstr "" msgstr ""
"Çhängé prögräm éxçlüdéd çöürsé rüns Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєт#" "Thé prögräm wäs çhängéd süççéssfüllý. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, "
"¢σηѕє¢тєтυ#"
#: apps/publisher/api/serializers.py #: apps/publisher/api/serializers.py
#, python-brace-format #, python-brace-format
......
...@@ -7,7 +7,7 @@ msgid "" ...@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-04-20 16:51+0500\n" "POT-Creation-Date: 2017-04-20 11:56-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
......
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