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
from django.utils.safestring import mark_safe
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.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):
......@@ -101,6 +108,24 @@ class CourseRunAdmin(admin.ModelAdmin):
ordering = ('key',)
readonly_fields = ('uuid',)
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)
......@@ -158,15 +183,14 @@ class ProgramAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
try:
super().save_model(request, obj, form, change)
self.save_error = False
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)
except (MarketingSitePublisherException, MarketingSiteAPIClientException):
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:
js = ('bower_components/jquery-ui/ui/minified/jquery-ui.min.js',
'js/sortable_select.js')
......
class MarketingSiteAPIClientException(Exception):
""" The exception thrown from MarketingSiteAPIClient """
pass
class ProgramPublisherException(Exception):
""" The exception thrown during the program publishing process to marketing site """
class MarketingSitePublisherException(Exception):
pass
def __init__(self, message):
super(ProgramPublisherException, self).__init__(message)
suffix = 'The program data has not been saved. Please check your marketing site configuration'
self.message = '{exception_msg} {suffix}'.format(exception_msg=message, suffix=suffix)
class AliasCreateError(MarketingSitePublisherException):
pass
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):
......
......@@ -94,8 +94,8 @@ class CourseRunSelectionForm(forms.ModelForm):
)
query_set = [course.pk for course in instance.courses.all()]
self.fields["excluded_course_runs"].widget = forms.widgets.CheckboxSelectMultiple()
self.fields["excluded_course_runs"].help_text = ""
self.fields['excluded_course_runs'].widget = forms.widgets.CheckboxSelectMultiple()
self.fields['excluded_course_runs'].help_text = ''
self.fields['excluded_course_runs'].queryset = CourseRun.objects.filter(
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
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.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.utils import UploadToFieldNamePath, clean_query, custom_render_variations
from course_discovery.apps.ietf_language_tags.models import LanguageTag
......@@ -337,7 +340,6 @@ class Course(TimeStampedModel):
class CourseRun(TimeStampedModel):
""" CourseRun model. """
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)
......@@ -595,6 +597,22 @@ class CourseRun(TimeStampedModel):
def __str__(self):
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):
name = models.CharField(max_length=64, unique=True)
......@@ -973,19 +991,18 @@ class Program(TimeStampedModel):
return self.status == ProgramStatus.Active
def save(self, *args, **kwargs):
if waffle.switch_is_active('publish_program_to_marketing_site') and self.partner.has_marketing_site:
# Before save, get from database the existing data if exists
existing_program = None
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
publisher = MarketingSitePublisher(existing_program)
is_publishable = (
self.partner.has_marketing_site and
waffle.switch_is_active('publish_program_to_marketing_site')
)
if is_publishable:
publisher = ProgramMarketingSitePublisher(self.partner)
previous_obj = Program.objects.get(id=self.id) if self.id else None
with transaction.atomic():
super(Program, self).save(*args, **kwargs)
# Once save complete, we need to update the marketing site
# So the marketing page for this program is automatically updated
publisher.publish_program(self)
publisher.publish_obj(self, previous_obj=previous_obj)
else:
super(Program, self).save(*args, **kwargs)
......
......@@ -3,12 +3,16 @@ from django.db.models.signals import pre_delete
from django.dispatch import receiver
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)
def delete_program(sender, instance, **kwargs): # pylint: disable=unused-argument
if waffle.switch_is_active('publish_program_to_marketing_site') and \
instance.partner.has_marketing_site:
publisher = MarketingSitePublisher()
publisher.delete_program(instance)
is_publishable = (
instance.partner.has_marketing_site and
waffle.switch_is_active('publish_program_to_marketing_site')
)
if is_publishable:
publisher = ProgramMarketingSitePublisher(instance.partner)
publisher.delete_obj(instance)
import json
import random
import urllib
import responses
......@@ -77,49 +78,46 @@ class MarketingSiteAPIClientTestMixin(TestCase):
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):
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_csrf_token_response(status)
self.mock_user_id_response(status)
def mock_node_retrieval(self, program_uuid, exists=True):
data = {}
status = 200
def mock_node_retrieval(self, node_lookup_field, node_lookup_value, exists=True, status=200):
url = '{root}/node.json?{node_lookup_field}={node_lookup_value}'.format(
root=self.api_root,
node_lookup_field=node_lookup_field,
node_lookup_value=node_lookup_value,
)
data = {
'list': []
'list': [{'nid': self.node_id}] if exists else []
}
if exists:
data['list'] = [{
'nid': self.node_id
}]
responses.add(
responses.GET,
'{root}/node.json?field_uuid={uuid}'.format(root=self.api_root, uuid=str(program_uuid)),
url,
body=json.dumps(data),
content_type='application/json',
status=status,
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)
alias = '{program_type}/{slug}'.format(
program_type=self.program.type.name.lower(),
slug=self.program.marketing_slug
)
data = {
'source': node_url,
'alias': alias,
'form_id': 'path_admin_form',
'op': 'Save'
}
responses.add(
responses.POST,
'{root}/admin/config/search/path/add'.format(root=self.api_root),
......@@ -129,13 +127,14 @@ class MarketingSitePublisherTestMixin(MarketingSiteAPIClientTestMixin):
def mock_delete_alias(self, status=200):
data = {
"confirm": 1,
"form_id": "path_admin_delete_confirm",
"op": "Confirm"
'confirm': 1,
'form_id': 'path_admin_delete_confirm',
'op': 'Confirm'
}
responses.add(
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),
status=status
)
......@@ -154,12 +153,15 @@ class MarketingSitePublisherTestMixin(MarketingSiteAPIClientTestMixin):
responses.GET,
'{root}/admin/config/search/path/list/{alias}'.format(root=self.api_root, alias=alias),
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.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
)
......@@ -172,15 +174,6 @@ class MarketingSitePublisherTestMixin(MarketingSiteAPIClientTestMixin):
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):
responses.add(
responses.DELETE,
......
......@@ -4,13 +4,11 @@ from decimal import Decimal
import ddt
import mock
import responses
from dateutil.parser import parse
from django.conf import settings
from django.db import IntegrityError
from django.db.models.functions import Lower
from django.test import TestCase
from factory.fuzzy import FuzzyText
from freezegun import freeze_time
from course_discovery.apps.core.models import Currency
......@@ -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.models import (
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.factories import CourseRunFactory, ImageFactory
from course_discovery.apps.course_metadata.tests.mixins import MarketingSitePublisherTestMixin
from course_discovery.apps.ietf_language_tags.models import LanguageTag
......@@ -257,7 +256,7 @@ class CourseRunTests(TestCase):
@ddt.unpack
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.
"""
end = parse(course_end) if course_end else None
......@@ -270,6 +269,43 @@ class CourseRunTests(TestCase):
expected_result = parse(expected_result) if expected_result else None
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):
""" Tests for the `Organization` model. """
......@@ -390,7 +426,7 @@ class AbstractValueModelTests(TestCase):
@ddt.ddt
class ProgramTests(MarketingSitePublisherTestMixin):
class ProgramTests(TestCase):
"""Tests of the Program model."""
def setUp(self):
......@@ -737,77 +773,41 @@ class ProgramTests(MarketingSitePublisherTestMixin):
self.program.status = status
self.assertEqual(self.program.is_active, status == ProgramStatus.Active)
@responses.activate
def test_save_without_publish(self):
self.program.title = FuzzyText().fuzz()
self.program.save()
self.assert_responses_call_count(0)
def test_publication_disabled(self):
"""
Verify that the publisher is not initialized when publication is disabled.
"""
toggle_switch('publish_program_to_marketing_site', active=False)
@responses.activate
def test_delete_without_publish(self):
with mock.patch.object(ProgramMarketingSitePublisher, '__init__') as mock_init:
self.program.save()
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'):
assert mock_init.call_count == 0
toggle_switch('publish_program_to_marketing_site')
self.program.partner.marketing_site_url_root = ''
self.program.partner.save()
with mock.patch.object(ProgramMarketingSitePublisher, '__init__') as mock_init:
self.program.save()
self.assert_responses_call_count(8)
self.program.delete()
@responses.activate
def test_xseries_program_save(self):
assert mock_init.call_count == 0
def test_publication_enabled(self):
"""
Make sure if the Program instance is of type XSeries, we do not publish to Marketing Site
Verify that the publisher is called when publication is enabled.
"""
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='XSeries')
toggle_switch('publish_program_to_marketing_site', True)
self.program.title = FuzzyText().fuzz()
self.program.save()
self.assert_responses_call_count(0)
toggle_switch('publish_program_to_marketing_site')
@responses.activate
def test_save_and_no_marketing_site(self):
self.program.partner.marketing_site_url_root = None
toggle_switch('publish_program_to_marketing_site', True)
self.program.title = FuzzyText().fuzz()
with mock.patch.object(ProgramMarketingSitePublisher, 'publish_obj', return_value=None) as mock_publish_obj:
self.program.save()
self.assert_responses_call_count(0)
@responses.activate
def test_delete_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_delete(204)
toggle_switch('publish_program_to_marketing_site', True)
self.program.delete()
self.assert_responses_call_count(5)
assert mock_publish_obj.called
@responses.activate
def test_delete_and_no_marketing_site(self):
self.program.partner.marketing_site_url_root = None
toggle_switch('publish_program_to_marketing_site', True)
with mock.patch.object(ProgramMarketingSitePublisher, 'delete_obj', return_value=None) as mock_delete_obj:
self.program.delete()
self.assert_responses_call_count(0)
assert mock_delete_obj.called
class PersonSocialNetworkTests(TestCase):
......
......@@ -3,8 +3,8 @@ import responses
from course_discovery.apps.core.tests.factories import PartnerFactory
from course_discovery.apps.course_metadata.exceptions import PersonToMarketingException
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.utils import MarketingSiteAPIClient
class MarketingSitePublisherTests(MarketingSitePublisherTestMixin):
......@@ -71,6 +71,6 @@ class MarketingSitePublisherTests(MarketingSitePublisherTestMixin):
@responses.activate
def test_delete_program(self):
self.mock_api_client(200)
self.mock_node_delete(204)
self.mock_node_delete(200)
people = MarketingSitePeople()
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):
context = super(CourseRunSelectionAdmin, self).get_context_data(**kwargs)
context.update({
'program_id': self.object.id,
'title': _('Change program excluded course runs')
'title': _('Update excluded course runs')
})
return context
raise Http404
def form_valid(self, form):
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)
return HttpResponseRedirect(self.get_success_url())
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -201,6 +201,13 @@ msgid "Partners"
msgstr ""
#: 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"
msgstr ""
......@@ -217,12 +224,6 @@ msgstr ""
msgid "Included course runs"
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
msgid "Published"
msgstr ""
......@@ -440,7 +441,11 @@ msgid "Allow courses in this program to be purchased in a single transaction"
msgstr ""
#: 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 ""
#: apps/publisher/api/serializers.py
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -242,6 +242,21 @@ msgid "Partners"
msgstr "Pärtnérs Ⱡ'σяєм ιρѕυм ∂#"
#: 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"
msgstr "élïgïßlé för öné-çlïçk pürçhäsé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#"
......@@ -258,20 +273,6 @@ msgstr "Nö Ⱡ'σя#"
msgid "Included course runs"
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
msgid "Published"
msgstr "Püßlïshéd Ⱡ'σяєм ιρѕυм ∂σł#"
......@@ -551,9 +552,14 @@ msgstr ""
" ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #"
#: 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 ""
"Ç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
#, python-brace-format
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\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