Commit 8b2b0a2b by Clinton Blackburn Committed by Renzo Lucioni

Fixed bug on program admin page

The previous override of save_model() was largely unnecessary and broke because it attempts to set m2m fields on unsaved models. The override now relies on the super method (which performs the correct behavior) and simply does error handling.

Our use of Select2 has also been cleaned up to make our monkey patching clearer.

ECOM-6238
parent 963a20c2
...@@ -87,38 +87,47 @@ class ProgramAdmin(admin.ModelAdmin): ...@@ -87,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 mark_safe('<br>'.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 autocomplete, 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,31 +8,30 @@ from course_discovery.apps.course_metadata.choices import ProgramStatus ...@@ -8,31 +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):
# preserve ordering of selected_choices in queryset django-autocomplete's definition of this method on QuerySetSelectMixin loads selected choices in
# https://codybonney.com/creating-a-queryset-from-a-list-while-preserving-order-using-django/ order of primary key instead of the order in which the choices are actually stored.
"""
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(HackDjangoAutocompleteMixin, forms.ModelForm): class ProgramAdminForm(forms.ModelForm):
class Meta: class Meta:
model = Program model = Program
fields = '__all__' fields = '__all__'
# 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',
......
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-31 11:33-0400\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"
...@@ -200,6 +200,12 @@ msgstr "" ...@@ -200,6 +200,12 @@ 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/models.py #: apps/course_metadata/choices.py apps/publisher/models.py
msgid "Published" msgid "Published"
msgstr "" msgstr ""
...@@ -395,6 +401,10 @@ msgstr "" ...@@ -395,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 ""
...@@ -662,10 +672,6 @@ msgid "Sign Out" ...@@ -662,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-31 11:33-0400\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-31 11:33-0400\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"
...@@ -241,6 +241,20 @@ msgstr "Pärtnérs Ⱡ'σяєм ιρѕυм ∂#" ...@@ -241,6 +241,20 @@ msgstr "Pärtnérs Ⱡ'σяєм ιρѕυм ∂#"
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/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 Ⱡ'σяєм ιρѕυм ∂σł#"
...@@ -488,6 +502,11 @@ msgstr "" ...@@ -488,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é Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#"
...@@ -783,10 +802,6 @@ msgid "Sign Out" ...@@ -783,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-31 11:33-0400\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