Commit 4ac7f58f by Renzo Lucioni Committed by GitHub

Merge pull request #409 from edx/clintonb/program-admin-quick-fix

Fixed bug on program admin page
parents df21d5db 8b2b0a2b
from django.contrib import admin, messages from django.contrib import admin, messages
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect 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.forms import ProgramAdminForm from course_discovery.apps.course_metadata.forms import 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
...@@ -85,38 +87,47 @@ class ProgramAdmin(admin.ModelAdmin): ...@@ -85,38 +87,47 @@ class ProgramAdmin(admin.ModelAdmin):
'authoring_organizations', 'credit_backing_organizations' 'authoring_organizations', 'credit_backing_organizations'
) )
fields += filter_horizontal fields += filter_horizontal
save_error = None save_error = False
def custom_course_runs_display(self, obj): def custom_course_runs_display(self, obj):
return ", ".join([str(run) for run in obj.course_runs]) return mark_safe('<br>'.join([str(run) for run in obj.course_runs]))
custom_course_runs_display.short_description = "Included course runs" custom_course_runs_display.short_description = _('Included course runs')
def _redirect_course_run_update_page(self, obj):
""" Returns a response redirect to a page where the user can update the
course runs for the program being edited.
Returns:
HttpResponseRedirect
"""
return HttpResponseRedirect(reverse('admin_metadata:update_course_runs', kwargs={'pk': obj.pk}))
def response_add(self, request, obj, post_url_continue=None): def response_add(self, request, obj, post_url_continue=None):
if self.save_error: if self.save_error:
return self.response_post_save_add(request, obj) return self.response_post_save_add(request, obj)
else: else:
return HttpResponseRedirect(reverse('admin_metadata:update_course_runs', kwargs={'pk': obj.pk})) return self._redirect_course_run_update_page(obj)
def response_change(self, request, obj): def response_change(self, request, obj):
if self.save_error: if self.save_error:
return self.response_post_save_change(request, obj) return self.response_post_save_change(request, obj)
else: else:
if any(status in request.POST for status in ['_continue', '_save']): if any(status in request.POST for status in ['_continue', '_save']):
return HttpResponseRedirect(reverse('admin_metadata:update_course_runs', kwargs={'pk': obj.pk})) return self._redirect_course_run_update_page(obj)
else: else:
return HttpResponseRedirect(reverse('admin:course_metadata_program_add')) return HttpResponseRedirect(reverse('admin:course_metadata_program_add'))
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
try: try:
# courses are ordered by django id, but form.cleaned_data is ordered correctly super().save_model(request, obj, form, change)
obj.courses = form.cleaned_data.get('courses')
obj.authoring_organizations = form.cleaned_data.get('authoring_organizations')
obj.credit_backing_organizations = form.cleaned_data.get('credit_backing_organizations')
obj.save()
self.save_error = False self.save_error = False
except ProgramPublisherException as ex: except ProgramPublisherException:
messages.add_message(request, messages.ERROR, ex.message) # 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
class Media: class Media:
......
from dal import widgets from dal import autocomplete
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.forms.utils import ErrorList from django.forms.utils import ErrorList
...@@ -8,32 +8,30 @@ from course_discovery.apps.course_metadata.choices import ProgramStatus ...@@ -8,32 +8,30 @@ from course_discovery.apps.course_metadata.choices import ProgramStatus
from course_discovery.apps.course_metadata.models import Program, CourseRun from course_discovery.apps.course_metadata.models import Program, CourseRun
class HackDjangoAutocompleteMixin(object): def filter_choices_to_render_with_order_preserved(self, selected_choices):
# It seems to me there is an issue with the select 2 widget in django autocomplete. """
# When the widget loads selected choices it loads them in order of django id, not the order Preserves ordering of selected_choices when creating the choices queryset.
# they are stored in in the database. This workaround works, but not sure what approach
# would be less hacky. Perhaps opening a PR to the django autocomplete repo if this is
# fact an issue?
class QuerySetSelectMixin2(widgets.WidgetMixin): See https://codybonney.com/creating-a-queryset-from-a-list-while-preserving-order-using-django.
def filter_choices_to_render(self, selected_choices): django-autocomplete's definition of this method on QuerySetSelectMixin loads selected choices in
# preserve ordering of selected_choices in queryset order of primary key instead of the order in which the choices are actually stored.
# https://codybonney.com/creating-a-queryset-from-a-list-while-preserving-order-using-django/ """
clauses = ' '.join(['WHEN id={} THEN {}'.format(pk, i) for i, pk in enumerate(selected_choices)]) clauses = ' '.join(['WHEN id={} THEN {}'.format(pk, i) for i, pk in enumerate(selected_choices)])
ordering = 'CASE {} END'.format(clauses) ordering = 'CASE {} END'.format(clauses)
self.choices.queryset = self.choices.queryset.filter( self.choices.queryset = self.choices.queryset.filter(
pk__in=[c for c in selected_choices if c] pk__in=[c for c in selected_choices if c]
).extra(select={'ordering': ordering}, order_by=('ordering',)) ).extra(select={'ordering': ordering}, order_by=('ordering',))
widgets.QuerySetSelectMixin = QuerySetSelectMixin2
class ProgramAdminForm(forms.ModelForm):
class ProgramAdminForm(HackDjangoAutocompleteMixin, forms.ModelForm):
class Meta: class Meta:
model = Program model = Program
fields = '__all__' fields = '__all__'
from dal import autocomplete
# Monkey patch filter_choices_to_render with our own definition which preserves ordering.
autocomplete.ModelSelect2Multiple.filter_choices_to_render = filter_choices_to_render_with_order_preserved
widgets = { widgets = {
'courses': autocomplete.ModelSelect2Multiple( 'courses': autocomplete.ModelSelect2Multiple(
url='admin_metadata:course-autocomplete', url='admin_metadata:course-autocomplete',
......
...@@ -4,19 +4,25 @@ import ddt ...@@ -4,19 +4,25 @@ import ddt
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase, LiveServerTestCase from django.test import TestCase, LiveServerTestCase
from selenium import webdriver from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import Select
from selenium.webdriver.support.wait import WebDriverWait
from course_discovery.apps.core.models import Partner
from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWORD
from course_discovery.apps.core.tests.helpers import make_image_file
from course_discovery.apps.course_metadata.choices import ProgramStatus from course_discovery.apps.course_metadata.choices import ProgramStatus
from course_discovery.apps.course_metadata.forms import ProgramAdminForm from course_discovery.apps.course_metadata.forms import ProgramAdminForm
from course_discovery.apps.course_metadata.models import Program, ProgramType
from course_discovery.apps.course_metadata.tests import factories from course_discovery.apps.course_metadata.tests import factories
from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWORD
from course_discovery.apps.core.tests.helpers import make_image_file
# pylint: disable=no-member # pylint: disable=no-member
@ddt.ddt @ddt.ddt
class AdminTests(TestCase): class AdminTests(TestCase):
""" Tests Admin page.""" """ Tests Admin page."""
def setUp(self): def setUp(self):
super(AdminTests, self).setUp() super(AdminTests, self).setUp()
self.user = UserFactory(is_staff=True, is_superuser=True) self.user = UserFactory(is_staff=True, is_superuser=True)
...@@ -185,71 +191,144 @@ class AdminTests(TestCase): ...@@ -185,71 +191,144 @@ class AdminTests(TestCase):
class ProgramAdminFunctionalTests(LiveServerTestCase): class ProgramAdminFunctionalTests(LiveServerTestCase):
""" Functional Tests for Admin page.""" """ Functional Tests for Admin page."""
create_view_name = 'admin:course_metadata_program_add'
edit_view_name = 'admin:course_metadata_program_change'
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.browser = webdriver.Firefox()
cls.browser.implicitly_wait(10)
cls.browser.set_window_size(1024, 768)
@classmethod
def tearDownClass(cls):
cls.browser.quit()
super().tearDownClass()
@classmethod
def _build_url(cls, path):
""" Returns a URL for the live test server. """
return cls.live_server_url + path
@classmethod
def _wait_for_page_load(cls, body_class):
""" Wait for the page to load. """
WebDriverWait(cls.browser, 2).until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'body.' + body_class))
)
def setUp(self): def setUp(self):
super(ProgramAdminFunctionalTests, self).setUp() super().setUp()
self.user = UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=self.user.username, password=USER_PASSWORD)
self.course_runs = factories.CourseRunFactory.create_batch(2) self.course_runs = factories.CourseRunFactory.create_batch(2)
self.courses = [course_run.course for course_run in self.course_runs] self.courses = [course_run.course for course_run in self.course_runs]
self.excluded_course_run = factories.CourseRunFactory(course=self.courses[0]) self.excluded_course_run = factories.CourseRunFactory(course=self.courses[0])
self.program = factories.ProgramFactory( self.program = factories.ProgramFactory(
courses=self.courses, excluded_course_runs=[self.excluded_course_run] courses=self.courses, excluded_course_runs=[self.excluded_course_run], status=ProgramStatus.Unpublished
) )
self.browser = webdriver.Firefox()
self.user = UserFactory(is_staff=True, is_superuser=True)
# Get Page self._login()
domain = self.live_server_url
url = reverse('admin:course_metadata_program_change', args=(self.program.id,)) def _login(self):
self.browser.get(domain + url) """ Log into Django admin. """
self.browser.get(self._build_url(reverse('admin:login')))
# Login self.browser.find_element_by_id('id_username').send_keys(self.user.username)
username = self.browser.find_element_by_id('id_username') self.browser.find_element_by_id('id_password').send_keys(USER_PASSWORD)
password = self.browser.find_element_by_id('id_password')
username.send_keys(self.user.username)
password.send_keys(USER_PASSWORD)
self.browser.find_element_by_css_selector('input[type=submit]').click() self.browser.find_element_by_css_selector('input[type=submit]').click()
self._wait_for_page_load('dashboard')
def _wait_for_add_edit_page_to_load(self):
self._wait_for_page_load('change-form')
def _wait_for_excluded_course_runs_page_to_load(self):
self._wait_for_page_load('change-program-excluded-course-runs-form')
def _navigate_to_edit_page(self):
url = self._build_url(reverse(self.edit_view_name, args=(self.program.id,)))
self.browser.get(url)
self._wait_for_add_edit_page_to_load()
def _select_option(self, select_id, option_value):
select = Select(self.browser.find_element_by_id(select_id))
select.select_by_value(option_value)
def _submit_program_form(self):
self.browser.find_element_by_css_selector('input[type=submit][name=_save]').click()
self._wait_for_excluded_course_runs_page_to_load()
def assert_form_fields_present(self):
""" Asserts the correct fields are rendered on the form. """
# Check the model fields
actual = []
for element in self.browser.find_elements_by_class_name('form-row'):
actual += [_class for _class in element.get_attribute('class').split(' ') if _class.startswith('field-')]
expected = [
'field-title', 'field-subtitle', 'field-status', 'field-type', 'field-partner', 'field-banner_image',
'field-banner_image_url', 'field-card_image_url', 'field-marketing_slug', 'field-overview',
'field-credit_redemption_overview', 'field-video', 'field-weeks_to_complete',
'field-min_hours_effort_per_week', 'field-max_hours_effort_per_week', 'field-courses',
'field-order_courses_by_start_date', 'field-custom_course_runs_display', 'field-excluded_course_runs',
'field-authoring_organizations', 'field-credit_backing_organizations', 'field-job_outlook_items',
'field-expected_learning_items',
]
self.assertEqual(actual, expected)
# Check the inline fields
expected = ['Program_faq-group', 'Program_individual_endorsements-group',
'Program_corporate_endorsements-group']
actual = [element.get_attribute('id') for element in self.browser.find_elements_by_class_name('inline-group')]
self.assertEqual(actual, expected)
def test_program_creation(self):
url = self._build_url(reverse(self.create_view_name))
self.browser.get(url)
self._wait_for_add_edit_page_to_load()
self.assert_form_fields_present()
program = factories.ProgramFactory.build(
partner=Partner.objects.first(),
status=ProgramStatus.Unpublished,
type=ProgramType.objects.first()
)
self.browser.find_element_by_id('id_title').send_keys(program.title)
self.browser.find_element_by_id('id_subtitle').send_keys(program.subtitle)
self._select_option('id_status', program.status)
self._select_option('id_type', str(program.type.id))
self._select_option('id_partner', str(program.partner.id))
self._submit_program_form()
actual = Program.objects.latest()
self.assertEqual(actual.title, program.title)
self.assertEqual(actual.subtitle, program.subtitle)
self.assertEqual(actual.status, program.status)
self.assertEqual(actual.type, program.type)
self.assertEqual(actual.partner, program.partner)
def test_program_update(self):
self._navigate_to_edit_page()
self.assert_form_fields_present()
title = 'Test Program'
subtitle = 'This is a test.'
# Update the program
data = (
('title', title),
('subtitle', subtitle),
)
for field, value in data:
element = self.browser.find_element_by_id('id_' + field)
element.clear()
element.send_keys(value)
self._submit_program_form()
# This window size is close to the window size when running on travis # Verify the program was updated
self.browser.set_window_size(548, 768) self.program = Program.objects.get(pk=self.program.pk)
self.assertEqual(self.program.title, title)
def tearDown(self): self.assertEqual(self.program.subtitle, subtitle)
super(ProgramAdminFunctionalTests, self).tearDown()
self.browser.quit()
def test_all_fields(self):
# Make sure that all expected fields are present
classes = [css_class for field in self.browser.find_elements_by_class_name('form-row')
for css_class in field.get_attribute('class').split(' ')
if css_class.startswith('field-') or css_class.startswith('dynamic-')]
expected_classes = ['field-title', 'field-subtitle', 'field-status', 'field-type',
'field-partner', 'field-banner_image', 'field-banner_image_url',
'field-card_image_url', 'field-marketing_slug', 'field-overview',
'field-credit_redemption_overview', 'field-video',
'field-weeks_to_complete', 'field-min_hours_effort_per_week',
'field-max_hours_effort_per_week', 'field-courses',
'field-order_courses_by_start_date', 'field-custom_course_runs_display',
'field-excluded_course_runs', 'field-authoring_organizations',
'field-credit_backing_organizations', 'field-job_outlook_items',
'field-expected_learning_items', 'dynamic-Program_faq',
'dynamic-Program_individual_endorsements',
'dynamic-Program_corporate_endorsements']
self.assertEqual(classes, expected_classes)
def test_sortable_select_drag_and_drop(self):
# Get order of select elements
hidden_options_text = [el.text for el in
self.browser.find_elements_by_css_selector('.field-courses option')]
first_select_element = self.browser.find_element_by_css_selector('.field-courses .select2-selection__choice')
# Drag and drop
first_select_element.click()
ActionChains(self.browser).drag_and_drop_by_offset(first_select_element, 500, 0).perform()
# Simulate expected drag and drop
hidden_options_text = [hidden_options_text[1], hidden_options_text[0]]
# Get actual results of drag and drop
new_hidden_options_text = [el.text for el in
self.browser.find_elements_by_css_selector('.field-courses option')]
self.assertEqual(hidden_options_text, new_hidden_options_text)
from django.contrib import messages from django.contrib import messages
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect, Http404 from django.http import HttpResponseRedirect, Http404
from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView, UpdateView from django.views.generic import TemplateView, UpdateView
from course_discovery.apps.course_metadata.forms import CourseRunSelectionForm from course_discovery.apps.course_metadata.forms import CourseRunSelectionForm
...@@ -25,7 +26,10 @@ class CourseRunSelectionAdmin(UpdateView): ...@@ -25,7 +26,10 @@ class CourseRunSelectionAdmin(UpdateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
if self.request.user.is_authenticated() and self.request.user.is_staff: if self.request.user.is_authenticated() and self.request.user.is_staff:
context = super(CourseRunSelectionAdmin, self).get_context_data(**kwargs) context = super(CourseRunSelectionAdmin, self).get_context_data(**kwargs)
context['program_id'] = self.object.id context.update({
'program_id': self.object.id,
'title': _('Change program excluded course runs')
})
return context return context
raise Http404 raise Http404
......
...@@ -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: 2016-10-28 14:40+0500\n" "POT-Creation-Date: 2016-10-31 19:34-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"
...@@ -196,6 +196,16 @@ msgstr "" ...@@ -196,6 +196,16 @@ msgstr ""
msgid "Partners" msgid "Partners"
msgstr "" msgstr ""
#: apps/course_metadata/admin.py
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/models.py #: apps/course_metadata/choices.py apps/publisher/models.py
msgid "Published" msgid "Published"
msgstr "" msgstr ""
...@@ -391,6 +401,10 @@ msgstr "" ...@@ -391,6 +401,10 @@ msgstr ""
msgid "The description of credit redemption for courses in program" msgid "The description of credit redemption for courses in program"
msgstr "" msgstr ""
#: apps/course_metadata/views.py
msgid "Change program excluded course runs"
msgstr ""
#: apps/edx_haystack_extensions/models.py #: apps/edx_haystack_extensions/models.py
msgid "Function Score" msgid "Function Score"
msgstr "" msgstr ""
...@@ -620,7 +634,7 @@ msgid "" ...@@ -620,7 +634,7 @@ msgid ""
"trademarks or trademarks of %(link_start)sedX Inc.%(link_end)s" "trademarks or trademarks of %(link_start)sedX Inc.%(link_end)s"
msgstr "" msgstr ""
#: templates/footer.html templates/footer.html.py #: templates/footer.html
msgid "Powered by Open edX" msgid "Powered by Open edX"
msgstr "" msgstr ""
...@@ -658,10 +672,6 @@ msgid "Sign Out" ...@@ -658,10 +672,6 @@ msgid "Sign Out"
msgstr "" msgstr ""
#: templates/metadata/admin/course_run.html #: templates/metadata/admin/course_run.html
msgid "CourseRun Selection Form"
msgstr ""
#: templates/metadata/admin/course_run.html
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
......
...@@ -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: 2016-10-28 14:40+0500\n" "POT-Creation-Date: 2016-10-31 19:34-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: 2016-10-28 14:40+0500\n" "POT-Creation-Date: 2016-10-31 19:34-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"
...@@ -237,6 +237,24 @@ msgstr "Pärtnér Ⱡ'σяєм ιρѕυм #" ...@@ -237,6 +237,24 @@ msgstr "Pärtnér Ⱡ'σяєм ιρѕυм #"
msgid "Partners" msgid "Partners"
msgstr "Pärtnérs Ⱡ'σяєм ιρѕυм ∂#" msgstr "Pärtnérs Ⱡ'σяєм ιρѕυм ∂#"
#: apps/course_metadata/admin.py
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/models.py #: apps/course_metadata/choices.py apps/publisher/models.py
msgid "Published" msgid "Published"
msgstr "Püßlïshéd Ⱡ'σяєм ιρѕυм ∂σł#" msgstr "Püßlïshéd Ⱡ'σяєм ιρѕυм ∂σł#"
...@@ -484,6 +502,11 @@ msgstr "" ...@@ -484,6 +502,11 @@ msgstr ""
"Thé désçrïptïön öf çrédït rédémptïön för çöürsés ïn prögräm Ⱡ'σяєм ιρѕυм " "Thé désçrïptïön öf çrédït rédémptïön för çöürsés ïn prögräm Ⱡ'σяєм ιρѕυм "
"∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#" "∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#"
#: apps/course_metadata/views.py
msgid "Change program excluded course runs"
msgstr ""
"Çhängé prögräm éxçlüdéd çöürsé rüns Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєт#"
#: apps/edx_haystack_extensions/models.py #: apps/edx_haystack_extensions/models.py
msgid "Function Score" msgid "Function Score"
msgstr "Fünçtïön Sçöré Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#" msgstr "Fünçtïön Sçöré Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#"
...@@ -741,7 +764,7 @@ msgstr "" ...@@ -741,7 +764,7 @@ msgstr ""
"trädémärks ör trädémärks öf %(link_start)sédX Ìnç.%(link_end)s Ⱡ'σяєм ιρѕυм " "trädémärks ör trädémärks öf %(link_start)sédX Ìnç.%(link_end)s Ⱡ'σяєм ιρѕυм "
"∂σł#" "∂σł#"
#: templates/footer.html templates/footer.html.py #: templates/footer.html
msgid "Powered by Open edX" msgid "Powered by Open edX"
msgstr "Pöwéréd ßý Öpén édX Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#" msgstr "Pöwéréd ßý Öpén édX Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#"
...@@ -779,10 +802,6 @@ msgid "Sign Out" ...@@ -779,10 +802,6 @@ msgid "Sign Out"
msgstr "Sïgn Öüt Ⱡ'σяєм ιρѕυм ∂#" msgstr "Sïgn Öüt Ⱡ'σяєм ιρѕυм ∂#"
#: templates/metadata/admin/course_run.html #: templates/metadata/admin/course_run.html
msgid "CourseRun Selection Form"
msgstr "ÇöürséRün Séléçtïön Förm Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢ση#"
#: templates/metadata/admin/course_run.html
msgid "Cancel" msgid "Cancel"
msgstr "Çänçél Ⱡ'σяєм ιρѕυ#" msgstr "Çänçél Ⱡ'σяєм ιρѕυ#"
......
...@@ -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: 2016-10-28 14:40+0500\n" "POT-Creation-Date: 2016-10-31 19:34-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"
......
{% extends 'admin/base_site.html' %} {% extends 'admin/base_site.html' %}
{% load i18n %} {% load i18n %}
{% block title %}
{% trans "CourseRun Selection Form" %}
{% endblock title %}
{% block content %} {% block bodyclass %}
change-program-excluded-course-runs-form
{% endblock %}
{% block content %}
<form class="form" method="post" action=""> <form class="form" method="post" action="">
{% csrf_token %} {% csrf_token %}
<fieldset class="module aligned "> <fieldset class="module aligned ">
...@@ -30,5 +30,4 @@ ...@@ -30,5 +30,4 @@
</div> </div>
{% endblock %} {% endblock %}
</form> </form>
{% endblock %} {% endblock %}
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