Commit f0547b5a by Waheed Ahmed Committed by GitHub

Merge pull request #382 from edx/waheed/ecom-5175-help-text-course-run

Added new course run form.
parents 2cfb7f9a 834b7900
...@@ -5,6 +5,7 @@ from django.contrib.auth.models import Group ...@@ -5,6 +5,7 @@ from django.contrib.auth.models import Group
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from course_discovery.apps.course_metadata.choices import CourseRunPacing
from course_discovery.apps.course_metadata.models import Person from course_discovery.apps.course_metadata.models import Person
from course_discovery.apps.publisher.models import Course, CourseRun, Seat, User from course_discovery.apps.publisher.models import Course, CourseRun, Seat, User
...@@ -49,8 +50,8 @@ class CustomCourseForm(CourseForm): ...@@ -49,8 +50,8 @@ class CustomCourseForm(CourseForm):
""" Course Form. """ """ Course Form. """
institution = forms.ModelChoiceField(queryset=Group.objects.all(), required=True) institution = forms.ModelChoiceField(queryset=Group.objects.all(), required=True)
title = forms.CharField(label='Course Title', required=True, max_length=255) title = forms.CharField(label=_('Course Title'), required=True)
number = forms.CharField(label='Course Number', required=True, max_length=255) number = forms.CharField(label=_('Course Number'), required=True)
team_admin = forms.ModelChoiceField(queryset=User.objects.filter(is_staff=True), required=True) team_admin = forms.ModelChoiceField(queryset=User.objects.filter(is_staff=True), required=True)
class Meta(CourseForm.Meta): class Meta(CourseForm.Meta):
...@@ -63,6 +64,28 @@ class CustomCourseForm(CourseForm): ...@@ -63,6 +64,28 @@ class CustomCourseForm(CourseForm):
) )
class UpdateCourseForm(BaseCourseForm):
""" Course form to update specific fields for already created course. """
number = forms.CharField(label=_('Course Number'), required=True)
team_admin = forms.ModelChoiceField(queryset=User.objects.filter(is_staff=True), required=True)
class Meta:
model = Course
fields = ('number', 'team_admin',)
def save(self, commit=True, changed_by=None): # pylint: disable=arguments-differ
course = super(UpdateCourseForm, self).save(commit=False)
if changed_by:
course.changed_by = changed_by
if commit:
course.save()
return course
class CourseRunForm(BaseCourseForm): class CourseRunForm(BaseCourseForm):
""" Course Run Form. """ """ Course Run Form. """
...@@ -78,7 +101,8 @@ class CustomCourseRunForm(CourseRunForm): ...@@ -78,7 +101,8 @@ class CustomCourseRunForm(CourseRunForm):
contacted_partner_manager = forms.BooleanField( contacted_partner_manager = forms.BooleanField(
widget=forms.RadioSelect(choices=((1, _("Yes")), (0, _("No")))), initial=0, required=False widget=forms.RadioSelect(choices=((1, _("Yes")), (0, _("No")))), initial=0, required=False
) )
start = forms.DateTimeField(required=True) start = forms.DateTimeField(label=_('Course start date'), required=True)
end = forms.DateTimeField(label=_('Course end date'), required=False)
staff = forms.ModelMultipleChoiceField( staff = forms.ModelMultipleChoiceField(
queryset=Person.objects.all(), widget=forms.SelectMultiple, required=False queryset=Person.objects.all(), widget=forms.SelectMultiple, required=False
) )
...@@ -86,14 +110,36 @@ class CustomCourseRunForm(CourseRunForm): ...@@ -86,14 +110,36 @@ class CustomCourseRunForm(CourseRunForm):
widget=forms.RadioSelect( widget=forms.RadioSelect(
choices=((1, _("Yes")), (0, _("No")))), initial=0, required=False choices=((1, _("Yes")), (0, _("No")))), initial=0, required=False
) )
is_self_paced = forms.BooleanField(label=_('Yes, course will be Self-Paced'), required=False)
class Meta(CourseRunForm.Meta): class Meta(CourseRunForm.Meta):
fields = ( fields = (
'length', 'transcript_languages', 'language', 'min_effort', 'max_effort', 'length', 'transcript_languages', 'language', 'min_effort', 'max_effort',
'contacted_partner_manager', 'target_content', 'pacing_type', 'contacted_partner_manager', 'target_content', 'pacing_type',
'video_language', 'staff', 'start', 'end', 'video_language', 'staff', 'start', 'end', 'is_self_paced',
) )
def clean(self):
super(CustomCourseRunForm, self).clean()
self.cleaned_data['pacing_type'] = CourseRunPacing.Self if self.cleaned_data['is_self_paced']\
else CourseRunPacing.Instructor
return self.cleaned_data
def save(self, commit=True, course=None, changed_by=None): # pylint: disable=arguments-differ
course_run = super(CustomCourseRunForm, self).save(commit=False)
if course:
course_run.course = course
if changed_by:
course_run.changed_by = changed_by
if commit:
course_run.save()
return course_run
class SeatForm(BaseCourseForm): class SeatForm(BaseCourseForm):
""" Course Seat Form. """ """ Course Seat Form. """
...@@ -103,7 +149,7 @@ class SeatForm(BaseCourseForm): ...@@ -103,7 +149,7 @@ class SeatForm(BaseCourseForm):
fields = '__all__' fields = '__all__'
exclude = ('currency', 'changed_by',) exclude = ('currency', 'changed_by',)
def save(self, commit=True): def save(self, commit=True, course_run=None, changed_by=None): # pylint: disable=arguments-differ
seat = super(SeatForm, self).save(commit=False) seat = super(SeatForm, self).save(commit=False)
if seat.type in [Seat.HONOR, Seat.AUDIT]: if seat.type in [Seat.HONOR, Seat.AUDIT]:
seat.price = 0.00 seat.price = 0.00
...@@ -118,6 +164,12 @@ class SeatForm(BaseCourseForm): ...@@ -118,6 +164,12 @@ class SeatForm(BaseCourseForm):
seat.credit_provider = '' seat.credit_provider = ''
seat.credit_hours = None seat.credit_hours = None
if course_run:
seat.course_run = course_run
if changed_by:
seat.changed_by = changed_by
if commit: if commit:
seat.save() seat.save()
......
# pylint: disable=no-member # pylint: disable=no-member
import json import json
from datetime import datetime
import ddt import ddt
import factory
from mock import patch from mock import patch
from django.db import IntegrityError from django.db import IntegrityError
...@@ -12,6 +14,7 @@ from django.core.urlresolvers import reverse ...@@ -12,6 +14,7 @@ from django.core.urlresolvers import reverse
from django.forms import model_to_dict from django.forms import model_to_dict
from django.test import TestCase from django.test import TestCase
from guardian.shortcuts import assign_perm from guardian.shortcuts import assign_perm
from testfixtures import LogCapture
from course_discovery.apps.core.models import User from course_discovery.apps.core.models import User
from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWORD from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWORD
...@@ -21,7 +24,7 @@ from course_discovery.apps.publisher.models import Course, CourseRun, Seat, Stat ...@@ -21,7 +24,7 @@ from course_discovery.apps.publisher.models import Course, CourseRun, Seat, Stat
from course_discovery.apps.publisher.tests import factories, JSON_CONTENT_TYPE from course_discovery.apps.publisher.tests import factories, JSON_CONTENT_TYPE
from course_discovery.apps.publisher.tests.utils import create_non_staff_user_and_login from course_discovery.apps.publisher.tests.utils import create_non_staff_user_and_login
from course_discovery.apps.publisher.utils import is_email_notification_enabled from course_discovery.apps.publisher.utils import is_email_notification_enabled
from course_discovery.apps.publisher.views import CourseRunDetailView from course_discovery.apps.publisher.views import CourseRunDetailView, logger as publisher_views_logger
from course_discovery.apps.publisher.wrappers import CourseRunWrapper from course_discovery.apps.publisher.wrappers import CourseRunWrapper
from course_discovery.apps.publisher_comments.tests.factories import CommentFactory from course_discovery.apps.publisher_comments.tests.factories import CommentFactory
...@@ -43,7 +46,7 @@ class CreateUpdateCourseViewTests(TestCase): ...@@ -43,7 +46,7 @@ class CreateUpdateCourseViewTests(TestCase):
self.site = Site.objects.get(pk=settings.SITE_ID) self.site = Site.objects.get(pk=settings.SITE_ID)
self.client.login(username=self.user.username, password=USER_PASSWORD) self.client.login(username=self.user.username, password=USER_PASSWORD)
self.group_2 = factories.GroupFactory() self.group_2 = factories.GroupFactory()
self.start_date_time = '2050-07-08 05:59:53' self.start_date_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def test_course_form_without_login(self): def test_course_form_without_login(self):
""" Verify that user can't access new course form page when not logged in. """ """ Verify that user can't access new course form page when not logged in. """
...@@ -299,8 +302,13 @@ class CreateUpdateCourseRunViewTests(TestCase): ...@@ -299,8 +302,13 @@ class CreateUpdateCourseRunViewTests(TestCase):
def setUp(self): def setUp(self):
super(CreateUpdateCourseRunViewTests, self).setUp() super(CreateUpdateCourseRunViewTests, self).setUp()
self.user = UserFactory(is_staff=True, is_superuser=True)
self.course = factories.CourseFactory(team_admin=self.user)
self.course_run = factories.CourseRunFactory() self.course_run = factories.CourseRunFactory()
self.course_run_dict = model_to_dict(self.course_run) self.course_run_dict = model_to_dict(self.course_run)
self.course_run_dict.update(
{'number': self.course.number, 'team_admin': self.course.team_admin.id, 'is_self_paced': True}
)
self._pop_valuse_from_dict( self._pop_valuse_from_dict(
self.course_run_dict, self.course_run_dict,
[ [
...@@ -308,7 +316,7 @@ class CreateUpdateCourseRunViewTests(TestCase): ...@@ -308,7 +316,7 @@ class CreateUpdateCourseRunViewTests(TestCase):
'priority', 'certificate_generation', 'video_language' 'priority', 'certificate_generation', 'video_language'
] ]
) )
self.user = UserFactory(is_staff=True, is_superuser=True) self.course_run_dict['start'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.site = Site.objects.get(pk=settings.SITE_ID) self.site = Site.objects.get(pk=settings.SITE_ID)
self.client.login(username=self.user.username, password=USER_PASSWORD) self.client.login(username=self.user.username, password=USER_PASSWORD)
...@@ -316,47 +324,113 @@ class CreateUpdateCourseRunViewTests(TestCase): ...@@ -316,47 +324,113 @@ class CreateUpdateCourseRunViewTests(TestCase):
for key in key_list: for key in key_list:
data_dict.pop(key) data_dict.pop(key)
def test_courserun_form_with_login(self):
""" Verify that user can access new course run form page when logged in. """
response = self.client.get(
reverse('publisher:publisher_course_runs_new', kwargs={'parent_course_id': self.course.id})
)
self.assertEqual(response.status_code, 200)
def test_courserun_form_without_login(self): def test_courserun_form_without_login(self):
""" Verify that user can't access new course run form page when not logged in. """ """ Verify that user can't access new course run form page when not logged in. """
self.client.logout() self.client.logout()
response = self.client.get(reverse('publisher:publisher_course_runs_new')) response = self.client.get(
reverse('publisher:publisher_course_runs_new', kwargs={'parent_course_id': self.course.id})
)
self.assertRedirects( self.assertRedirects(
response, response,
expected_url='{url}?next={next}'.format( expected_url='{url}?next={next}'.format(
url=reverse('login'), url=reverse('login'),
next=reverse('publisher:publisher_course_runs_new') next=reverse('publisher:publisher_course_runs_new', kwargs={'parent_course_id': self.course.id})
), ),
status_code=302, status_code=302,
target_status_code=302 target_status_code=302
) )
def test_create_course_run(self): def test_create_course_run_and_seat_with_errors(self):
""" Verify that we can create a new course run. """ """ Verify that without providing required data course run and seat
lms_course_id = 'course-v1:testX+AS12131+2016_q4' cannot be created.
self.course_run_dict['lms_course_id'] = lms_course_id """
self.course_run_dict['start'] = '2050-07-08 05:59:53' response = self.client.post(
response = self.client.post(reverse('publisher:publisher_course_runs_new'), self.course_run_dict) reverse('publisher:publisher_course_runs_new', kwargs={'parent_course_id': self.course.id}),
self.course_run_dict
)
self.assertEqual(response.status_code, 400)
post_data = model_to_dict(self.course)
post_data.update(self.course_run_dict)
post_data.update(factory.build(dict, FACTORY_CLASS=factories.SeatFactory))
self._pop_valuse_from_dict(post_data, ['id', 'upgrade_deadline', 'image', 'team_admin'])
response = self.client.post(
reverse('publisher:publisher_course_runs_new', kwargs={'parent_course_id': self.course.id}),
post_data
)
self.assertEqual(response.status_code, 400)
with patch('django.forms.models.BaseModelForm.is_valid') as mocked_is_valid:
mocked_is_valid.return_value = True
with LogCapture(publisher_views_logger.name) as log_capture:
response = self.client.post(
reverse('publisher:publisher_course_runs_new', kwargs={'parent_course_id': self.course.id}),
post_data
)
self.assertEqual(response.status_code, 400)
log_capture.check(
(
publisher_views_logger.name,
'ERROR',
'Unable to create course run and seat for course [{}].'.format(self.course.id)
)
)
def test_create_course_run_and_seat(self):
""" Verify that we can create a new course run with seat. """
updated_course_number = '{number}.2'.format(number=self.course.number)
new_price = 450
post_data = self.course_run_dict
seat = factories.SeatFactory(course_run=self.course_run, type=Seat.HONOR, price=0)
post_data.update(**model_to_dict(seat))
post_data.update(
{
'number': updated_course_number,
'type': Seat.VERIFIED,
'price': new_price
}
)
self._pop_valuse_from_dict(post_data, ['id', 'course', 'course_run'])
response = self.client.post(
reverse('publisher:publisher_course_runs_new', kwargs={'parent_course_id': self.course.id}),
post_data
)
course_run = CourseRun.objects.get(course=self.course_run.course, lms_course_id=lms_course_id) new_seat = Seat.objects.get(type=post_data['type'], price=post_data['price'])
self.assertRedirects( self.assertRedirects(
response, response,
expected_url=reverse('publisher:publisher_course_runs_edit', kwargs={'pk': course_run.id}), expected_url=reverse('publisher:publisher_course_run_detail', kwargs={'pk': new_seat.course_run.id}),
status_code=302, status_code=302,
target_status_code=200 target_status_code=200
) )
self.assertEqual(course_run.lms_course_id, lms_course_id) # Verify that new seat and new course run are unique
self.assertNotEqual(new_seat.type, seat.type)
self.assertEqual(new_seat.type, Seat.VERIFIED)
self.assertNotEqual(new_seat.price, seat.price)
self.assertEqual(new_seat.price, new_price)
self.assertNotEqual(new_seat.course_run, self.course_run)
response = self.client.get(reverse('publisher:publisher_course_runs_new')) self.course = new_seat.course_run.course
self.assertNotContains(response, 'Add new comment') # Verify that number is updated for parent course
self.assertNotContains(response, 'Total Comments') self.assertEqual(self.course.number, updated_course_number)
def test_update_course_run_with_staff(self): def test_update_course_run_with_staff(self):
""" Verify that staff user can update an existing course run. """ """ Verify that staff user can update an existing course run. """
updated_lms_course_id = 'course-v1:testX+AS121+2018_q1' updated_lms_course_id = 'course-v1:testX+AS121+2018_q1'
self.course_run_dict['lms_course_id'] = updated_lms_course_id self.course_run_dict['lms_course_id'] = updated_lms_course_id
self.course_run_dict['start'] = '2050-07-08 05:59:53'
self.assertNotEqual(self.course_run.lms_course_id, updated_lms_course_id) self.assertNotEqual(self.course_run.lms_course_id, updated_lms_course_id)
self.assertNotEqual(self.course_run.changed_by, self.user) self.assertNotEqual(self.course_run.changed_by, self.user)
...@@ -390,7 +464,6 @@ class CreateUpdateCourseRunViewTests(TestCase): ...@@ -390,7 +464,6 @@ class CreateUpdateCourseRunViewTests(TestCase):
updated_lms_course_id = 'course-v1:testX+AS121+2018_q1' updated_lms_course_id = 'course-v1:testX+AS121+2018_q1'
self.course_run_dict['lms_course_id'] = updated_lms_course_id self.course_run_dict['lms_course_id'] = updated_lms_course_id
self.course_run_dict['start'] = '2050-07-08 05:59:53'
self.assertNotEqual(self.course_run.lms_course_id, updated_lms_course_id) self.assertNotEqual(self.course_run.lms_course_id, updated_lms_course_id)
response = self.client.get( response = self.client.get(
...@@ -414,7 +487,6 @@ class CreateUpdateCourseRunViewTests(TestCase): ...@@ -414,7 +487,6 @@ class CreateUpdateCourseRunViewTests(TestCase):
non_staff_user, group = create_non_staff_user_and_login(self) non_staff_user, group = create_non_staff_user_and_login(self)
updated_lms_course_id = 'course-v1:testX+AS121+2018_q1' updated_lms_course_id = 'course-v1:testX+AS121+2018_q1'
self.course_run_dict['start'] = '2050-07-08 05:59:53'
self.course_run_dict['lms_course_id'] = updated_lms_course_id self.course_run_dict['lms_course_id'] = updated_lms_course_id
self.assertNotEqual(self.course_run.lms_course_id, updated_lms_course_id) self.assertNotEqual(self.course_run.lms_course_id, updated_lms_course_id)
......
...@@ -10,15 +10,19 @@ urlpatterns = [ ...@@ -10,15 +10,19 @@ urlpatterns = [
url(r'^courses/new$', views.CreateCourseView.as_view(), name='publisher_courses_new'), url(r'^courses/new$', views.CreateCourseView.as_view(), name='publisher_courses_new'),
url(r'^courses/(?P<pk>\d+)/view/$', views.ReadOnlyView.as_view(), name='publisher_courses_readonly'), url(r'^courses/(?P<pk>\d+)/view/$', views.ReadOnlyView.as_view(), name='publisher_courses_readonly'),
url(r'^courses/(?P<pk>\d+)/edit/$', views.UpdateCourseView.as_view(), name='publisher_courses_edit'), url(r'^courses/(?P<pk>\d+)/edit/$', views.UpdateCourseView.as_view(), name='publisher_courses_edit'),
url(
r'^courses/(?P<parent_course_id>\d+)/course_runs/new/$',
views.CreateCourseRunView.as_view(),
name='publisher_course_runs_new'
),
url(r'^course_runs/(?P<pk>\d+)/$', views.CourseRunDetailView.as_view(), name='publisher_course_run_detail'), url(r'^course_runs/(?P<pk>\d+)/$', views.CourseRunDetailView.as_view(), name='publisher_course_run_detail'),
url(r'^course_runs/new$', views.CreateCourseRunView.as_view(), name='publisher_course_runs_new'),
url(r'^course_runs/(?P<pk>\d+)/edit/$', views.UpdateCourseRunView.as_view(), name='publisher_course_runs_edit'), url(r'^course_runs/(?P<pk>\d+)/edit/$', views.UpdateCourseRunView.as_view(), name='publisher_course_runs_edit'),
url( url(
r'^course_runs/(?P<course_run_id>\d+)/change_state/$', r'^course_runs/(?P<course_run_id>\d+)/change_state/$',
views.ChangeStateView.as_view(), views.ChangeStateView.as_view(),
name='publisher_change_state' name='publisher_change_state'
), ),
url(r'^seats/new$', views.CreateSeatView.as_view(), name='publisher_seats_new'), url(r'^seats/new/$', views.CreateSeatView.as_view(), name='publisher_seats_new'),
url(r'^seats/(?P<pk>\d+)/edit/$', views.UpdateSeatView.as_view(), name='publisher_seats_edit'), url(r'^seats/(?P<pk>\d+)/edit/$', views.UpdateSeatView.as_view(), name='publisher_seats_edit'),
url( url(
r'^user/toggle/email_settings/$', r'^user/toggle/email_settings/$',
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
Course publisher views. Course publisher views.
""" """
import json import json
import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from django.contrib import messages from django.contrib import messages
...@@ -11,23 +12,23 @@ from django.db import transaction ...@@ -11,23 +12,23 @@ from django.db import transaction
from django.http import HttpResponseRedirect, HttpResponseForbidden, JsonResponse from django.http import HttpResponseRedirect, HttpResponseForbidden, JsonResponse
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import View from django.views.generic import View, CreateView, UpdateView, DetailView, ListView
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, UpdateView
from django.views.generic.list import ListView
from django_fsm import TransitionNotAllowed from django_fsm import TransitionNotAllowed
from guardian.shortcuts import get_objects_for_user from guardian.shortcuts import get_objects_for_user
from rest_framework.generics import UpdateAPIView from rest_framework.generics import UpdateAPIView
from course_discovery.apps.publisher.forms import ( from course_discovery.apps.publisher.forms import (
CourseForm, CourseRunForm, SeatForm, CustomCourseForm, CustomCourseRunForm, CustomSeatForm CourseForm, CourseRunForm, SeatForm, CustomCourseForm, CustomCourseRunForm, CustomSeatForm,
) UpdateCourseForm)
from course_discovery.apps.publisher import mixins from course_discovery.apps.publisher import mixins
from course_discovery.apps.publisher.models import Course, CourseRun, Seat, State, UserAttributes from course_discovery.apps.publisher.models import Course, CourseRun, Seat, State, UserAttributes
from course_discovery.apps.publisher.serializers import UpdateCourseKeySerializer from course_discovery.apps.publisher.serializers import UpdateCourseKeySerializer
from course_discovery.apps.publisher.wrappers import CourseRunWrapper from course_discovery.apps.publisher.wrappers import CourseRunWrapper
logger = logging.getLogger(__name__)
SEATS_HIDDEN_FIELDS = ['price', 'currency', 'upgrade_deadline', 'credit_provider', 'credit_hours'] SEATS_HIDDEN_FIELDS = ['price', 'currency', 'upgrade_deadline', 'credit_provider', 'credit_hours']
...@@ -183,15 +184,74 @@ class ReadOnlyView(mixins.LoginRequiredMixin, mixins.ViewPermissionMixin, Detail ...@@ -183,15 +184,74 @@ class ReadOnlyView(mixins.LoginRequiredMixin, mixins.ViewPermissionMixin, Detail
return context return context
class CreateCourseRunView(mixins.LoginRequiredMixin, mixins.FormValidMixin, CreateView): class CreateCourseRunView(mixins.LoginRequiredMixin, CreateView):
""" Create Course Run View.""" """ Create Course Run View."""
model = CourseRun model = CourseRun
form_class = CourseRunForm course_form = UpdateCourseForm
template_name = 'publisher/course_run_form.html' run_form = CustomCourseRunForm
success_url = 'publisher:publisher_course_runs_edit' seat_form = CustomSeatForm
template_name = 'publisher/add_courserun_form.html'
success_url = 'publisher:publisher_course_run_detail'
parent_course = None
fields = ()
def get_success_url(self): def get_parent_course(self):
return reverse(self.success_url, kwargs={'pk': self.object.id}) if not self.parent_course:
self.parent_course = get_object_or_404(Course, pk=self.kwargs.get('parent_course_id'))
return self.parent_course
def get_context_data(self, **kwargs):
parent_course = self.get_parent_course()
course_form = self.course_form(instance=parent_course)
context = {
'parent_course': parent_course,
'course_form': course_form,
'run_form': self.run_form,
'seat_form': self.seat_form,
'is_team_admin_hidden': parent_course.team_admin and 'team_admin' not in course_form.errors
}
return context
def post(self, request, *args, **kwargs):
user = request.user
parent_course = self.get_parent_course()
course_form = self.course_form(request.POST, instance=self.get_parent_course())
run_form = self.run_form(request.POST)
seat_form = self.seat_form(request.POST)
if course_form.is_valid() and run_form.is_valid() and seat_form.is_valid():
try:
with transaction.atomic():
course = course_form.save(changed_by=user)
course_run = run_form.save(course=course, changed_by=user)
seat_form.save(course_run=course_run, changed_by=user)
# pylint: disable=no-member
success_msg = _('Course run created successfully for course "{course_title}".').format(
course_title=course.title
)
messages.success(request, success_msg)
return HttpResponseRedirect(reverse(self.success_url, kwargs={'pk': course_run.id}))
except Exception as error: # pylint: disable=broad-except
# pylint: disable=no-member
error_msg = _('There was an error saving course run, {error}').format(error=error)
messages.error(request, error_msg)
logger.exception('Unable to create course run and seat for course [%s].', parent_course.id)
else:
messages.error(request, _('Please fill all required fields.'))
context = self.get_context_data()
context.update(
{
'course_form': course_form,
'run_form': run_form,
'seat_form': seat_form,
'is_team_admin_hidden': parent_course.team_admin and 'team_admin' not in course_form.errors
}
)
return render(request, self.template_name, context, status=400)
class UpdateCourseRunView(mixins.LoginRequiredMixin, mixins.ViewPermissionMixin, mixins.FormValidMixin, UpdateView): class UpdateCourseRunView(mixins.LoginRequiredMixin, mixins.ViewPermissionMixin, mixins.FormValidMixin, UpdateView):
......
...@@ -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-11-17 23:20+0500\n" "POT-Creation-Date: 2016-11-18 17:35+0000\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"
...@@ -17,7 +17,6 @@ msgstr "" ...@@ -17,7 +17,6 @@ msgstr ""
"Language: \n" "Language: \n"
#: apps/api/filters.py #: apps/api/filters.py
#, python-brace-format
msgid "No user with the username [{username}] exists." msgid "No user with the username [{username}] exists."
msgstr "" msgstr ""
...@@ -111,7 +110,6 @@ msgstr "" ...@@ -111,7 +110,6 @@ msgstr ""
#. Translators: 'period_choices' is a list of possible values, like ('second', #. Translators: 'period_choices' is a list of possible values, like ('second',
#. 'minute', 'hour') #. 'minute', 'hour')
#: apps/core/forms.py #: apps/core/forms.py
#, python-brace-format
msgid "period must be one of {period_choices}." msgid "period must be one of {period_choices}."
msgstr "" msgstr ""
...@@ -410,7 +408,6 @@ msgid "JSON string containing an elasticsearch function score config." ...@@ -410,7 +408,6 @@ msgid "JSON string containing an elasticsearch function score config."
msgstr "" msgstr ""
#: apps/publisher/emails.py #: apps/publisher/emails.py
#, python-brace-format
msgid "Course Run {title}-{pacing_type}-{start} state has been changed." msgid "Course Run {title}-{pacing_type}-{start} state has been changed."
msgstr "" msgstr ""
...@@ -418,6 +415,16 @@ msgstr "" ...@@ -418,6 +415,16 @@ msgstr ""
msgid "Studio instance created" msgid "Studio instance created"
msgstr "" msgstr ""
#: apps/publisher/forms.py templates/publisher/add_course_form.html
#: templates/publisher/add_courserun_form.html
msgid "Course Title"
msgstr ""
#: apps/publisher/forms.py templates/publisher/add_course_form.html
#: templates/publisher/dashboard/_studio_requests.html
msgid "Course Number"
msgstr ""
#: apps/publisher/forms.py #: apps/publisher/forms.py
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""
...@@ -427,6 +434,18 @@ msgid "No" ...@@ -427,6 +434,18 @@ msgid "No"
msgstr "" msgstr ""
#: apps/publisher/forms.py #: apps/publisher/forms.py
msgid "Course start date"
msgstr ""
#: apps/publisher/forms.py
msgid "Course end date"
msgstr ""
#: apps/publisher/forms.py
msgid "Yes, course will be Self-Paced"
msgstr ""
#: apps/publisher/forms.py
msgid "Only honor/audit seats can be without price." msgid "Only honor/audit seats can be without price."
msgstr "" msgstr ""
...@@ -546,7 +565,18 @@ msgid "Please fill all required field." ...@@ -546,7 +565,18 @@ msgid "Please fill all required field."
msgstr "" msgstr ""
#: apps/publisher/views.py #: apps/publisher/views.py
#, python-brace-format msgid "Course run created successfully for course \"{course_title}\"."
msgstr ""
#: apps/publisher/views.py
msgid "There was an error saving course run, {error}"
msgstr ""
#: apps/publisher/views.py
msgid "Please fill all required fields."
msgstr ""
#: apps/publisher/views.py
msgid "Content moved to `{state}` successfully." msgid "Content moved to `{state}` successfully."
msgstr "" msgstr ""
...@@ -571,12 +601,10 @@ msgid "Publish" ...@@ -571,12 +601,10 @@ msgid "Publish"
msgstr "" msgstr ""
#: apps/publisher_comments/emails.py #: apps/publisher_comments/emails.py
#, python-brace-format
msgid "New comment added in course run: {title}-{pacing_type}-{start}" msgid "New comment added in course run: {title}-{pacing_type}-{start}"
msgstr "" msgstr ""
#: apps/publisher_comments/emails.py #: apps/publisher_comments/emails.py
#, python-brace-format
msgid "New comment added in Course: {title}" msgid "New comment added in Course: {title}"
msgstr "" msgstr ""
...@@ -706,34 +734,37 @@ msgid "" ...@@ -706,34 +734,37 @@ msgid ""
msgstr "" msgstr ""
#: templates/publisher/add_course_form.html #: templates/publisher/add_course_form.html
#: templates/publisher/add_courserun_form.html
msgid "required" msgid "required"
msgstr "" msgstr ""
#: templates/publisher/add_course_form.html #: templates/publisher/add_course_form.html
msgid "Course Title" #: templates/publisher/add_courserun_form.html
msgstr ""
#: templates/publisher/add_course_form.html
msgid "Best Practices" msgid "Best Practices"
msgstr "" msgstr ""
#: templates/publisher/add_course_form.html #: templates/publisher/add_course_form.html
#: templates/publisher/add_courserun_form.html
msgid "Example" msgid "Example"
msgstr "" msgstr ""
#: templates/publisher/add_course_form.html #: templates/publisher/add_course_form.html
#: templates/publisher/add_courserun_form.html
msgid "Concise 70 characters maximum; < 50 chars. recommended." msgid "Concise 70 characters maximum; < 50 chars. recommended."
msgstr "" msgstr ""
#: templates/publisher/add_course_form.html #: templates/publisher/add_course_form.html
#: templates/publisher/add_courserun_form.html
msgid "Descriptive - clearly indicates what the course is about." msgid "Descriptive - clearly indicates what the course is about."
msgstr "" msgstr ""
#: templates/publisher/add_course_form.html #: templates/publisher/add_course_form.html
#: templates/publisher/add_courserun_form.html
msgid "SEO-optimized and targeted to a global audience." msgid "SEO-optimized and targeted to a global audience."
msgstr "" msgstr ""
#: templates/publisher/add_course_form.html #: templates/publisher/add_course_form.html
#: templates/publisher/add_courserun_form.html
msgid "" msgid ""
"If the course falls in a sequence, our titling convention is: \"Course " "If the course falls in a sequence, our titling convention is: \"Course "
"Title: Subtitle\"" "Title: Subtitle\""
...@@ -744,14 +775,17 @@ msgid "English Grammar and Essay Writing Sequence Courses:" ...@@ -744,14 +775,17 @@ msgid "English Grammar and Essay Writing Sequence Courses:"
msgstr "" msgstr ""
#: templates/publisher/add_course_form.html #: templates/publisher/add_course_form.html
#: templates/publisher/add_courserun_form.html
msgid "Introduction to Statistics" msgid "Introduction to Statistics"
msgstr "" msgstr ""
#: templates/publisher/add_course_form.html #: templates/publisher/add_course_form.html
#: templates/publisher/add_courserun_form.html
msgid "Statistics: Inference" msgid "Statistics: Inference"
msgstr "" msgstr ""
#: templates/publisher/add_course_form.html #: templates/publisher/add_course_form.html
#: templates/publisher/add_courserun_form.html
msgid "Statistics: Probability" msgid "Statistics: Probability"
msgstr "" msgstr ""
...@@ -786,16 +820,12 @@ msgid "Pacing Type" ...@@ -786,16 +820,12 @@ msgid "Pacing Type"
msgstr "" msgstr ""
#: templates/publisher/add_course_form.html #: templates/publisher/add_course_form.html
#: templates/publisher/add_courserun_form.html
msgid "" msgid ""
"Will your course be open to students at the same time as it is announced?" "Will your course be open to students at the same time as it is announced?"
msgstr "" msgstr ""
#: templates/publisher/add_course_form.html #: templates/publisher/add_course_form.html
#: templates/publisher/dashboard/_studio_requests.html
msgid "Course Number"
msgstr ""
#: templates/publisher/add_course_form.html
msgid "" msgid ""
"Courses split into several modules can be denoted by adding .1, .2, etc. at " "Courses split into several modules can be denoted by adding .1, .2, etc. at "
"the end of the course number before the “x”" "the end of the course number before the “x”"
...@@ -808,6 +838,7 @@ msgid "" ...@@ -808,6 +838,7 @@ msgid ""
msgstr "" msgstr ""
#: templates/publisher/add_course_form.html #: templates/publisher/add_course_form.html
#: templates/publisher/add_courserun_form.html
msgid "About page information" msgid "About page information"
msgstr "" msgstr ""
...@@ -819,6 +850,7 @@ msgid "End Date" ...@@ -819,6 +850,7 @@ msgid "End Date"
msgstr "" msgstr ""
#: templates/publisher/add_course_form.html #: templates/publisher/add_course_form.html
#: templates/publisher/add_courserun_form.html
msgid "" msgid ""
"The date when this self-paced course run will end, replaced by an updated " "The date when this self-paced course run will end, replaced by an updated "
"version of the course" "version of the course"
...@@ -1044,9 +1076,109 @@ msgid "" ...@@ -1044,9 +1076,109 @@ msgid ""
msgstr "" msgstr ""
#: templates/publisher/add_course_form.html #: templates/publisher/add_course_form.html
#: templates/publisher/add_courserun_form.html
msgid "Save Draft" msgid "Save Draft"
msgstr "" msgstr ""
#: templates/publisher/add_courserun_form.html
msgid "New Course Run"
msgstr ""
#: templates/publisher/add_courserun_form.html
#, python-format
msgid ""
"\n"
" The fields below will only affect this new course run. If you feel the need edit information from the parent course then you may do so on the %(link_start)s%(course_url)s%(link_middle)sparent course for this run%(link_end)s.\n"
" "
msgstr ""
#: templates/publisher/add_courserun_form.html
msgid "Studio instance information"
msgstr ""
#: templates/publisher/add_courserun_form.html
msgid ""
"\n"
" The information in this section is required to create a course studio instance. You must fill all required information but are welcome to come back and enter the rest of the information when you are ready to announce the course, or you can do it all at once if you are ready to do so.\n"
" "
msgstr ""
#: templates/publisher/add_courserun_form.html
msgid "COURSE TITLE"
msgstr ""
#: templates/publisher/add_courserun_form.html
msgid "English Grammar and Essay Writing"
msgstr ""
#: templates/publisher/add_courserun_form.html
msgid "Sequence Courses:"
msgstr ""
#: templates/publisher/add_courserun_form.html
msgid "Institution Course Admin"
msgstr ""
#: templates/publisher/add_courserun_form.html
msgid "Change"
msgstr ""
#: templates/publisher/add_courserun_form.html
msgid "COURSE START DATE"
msgstr ""
#: templates/publisher/add_courserun_form.html
msgid ""
"\n"
" Start on a weekday (preferably Tuesday, Wednesday, or Thursday) and avoid major U.S. holidays\n"
" for best access to edX staff.\n"
" "
msgstr ""
#: templates/publisher/add_courserun_form.html
msgid ""
"\n"
" Approximate dates are acceptable; If you are unable to give an exact date, please identify a\n"
" month in which the course will be offered.\n"
" "
msgstr ""
#: templates/publisher/add_courserun_form.html
msgid "COURSE NUMBER"
msgstr ""
#: templates/publisher/add_courserun_form.html
msgid ""
"\n"
" Courses split into several modules can be denoted by adding .1, .2, etc. at the end of the\n"
" course number before the “x”\n"
" No special html characters, accents, spaces, dashes, or underscores\n"
" 10 character limit\n"
" "
msgstr ""
#: templates/publisher/add_courserun_form.html
msgid "Example:"
msgstr ""
#: templates/publisher/add_courserun_form.html
msgid "etc."
msgstr ""
#: templates/publisher/add_courserun_form.html
msgid "COURSE END DATE"
msgstr ""
#: templates/publisher/add_courserun_form.html
msgid "CERTIFICATE TYPE AND PRICE"
msgstr ""
#: templates/publisher/add_courserun_form.html
msgid ""
"If Verified or Professional Ed, indicate certificate price in US dollars "
"(minimum of $49)"
msgstr ""
#: templates/publisher/course_form.html #: templates/publisher/course_form.html
msgid "Add Course Run" msgid "Add Course Run"
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-11-17 23:20+0500\n" "POT-Creation-Date: 2016-11-18 17:35+0000\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-11-17 23:20+0500\n" "POT-Creation-Date: 2016-11-18 17:35+0000\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"
......
$(".administration-nav .tab-container > button").click(function(event) {
event.preventDefault();
$(this).addClass("selected");
$(this).siblings().removeClass("selected");
var tab = $(this).data("tab");
$(".tab-content").not(tab).css("display", "none");
$(tab).fadeIn();
});
$(document).ready(function(){ $(document).ready(function(){
$(".administration-nav .tab-container > button").click(function(event) {
event.preventDefault();
$(this).addClass("selected");
$(this).siblings().removeClass("selected");
var tab = $(this).data("tab");
$(".tab-content").not(tab).css("display", "none");
$(tab).fadeIn();
});
$('ul.tabs .course-tabs').click(function(){ $('ul.tabs .course-tabs').click(function(){
var tab_id = $(this).attr('data-tab'); var tab_id = $(this).attr('data-tab'),
$('ul.tabs .course-tabs').removeClass('active'); $tabContent = $("#"+tab_id);
$('.content').removeClass('active'); $(this).parent().find('.course-tabs').removeClass('active');
$tabContent.parent().find('.content').removeClass('active');
$(this).addClass('active'); $(this).addClass('active');
$("#"+tab_id).addClass('active'); $tabContent.addClass('active');
}) });
$("#change-admin").click(function (e) {
e.preventDefault();
$(".field-admin-name").hide();
$("#field-team-admin").show();
});
}); });
...@@ -47,6 +47,7 @@ $light-gray: rgba(204, 204, 204, 1); ...@@ -47,6 +47,7 @@ $light-gray: rgba(204, 204, 204, 1);
height: auto; height: auto;
position: absolute; position: absolute;
width: 340px; width: 340px;
z-index: 10000;
&.is-shown { &.is-shown {
display: block; display: block;
...@@ -266,6 +267,7 @@ $light-gray: rgba(204, 204, 204, 1); ...@@ -266,6 +267,7 @@ $light-gray: rgba(204, 204, 204, 1);
} }
.tabs { .tabs {
@include margin-left(0);
width: 100%; width: 100%;
display: inline-block; display: inline-block;
} }
...@@ -444,24 +446,27 @@ select { ...@@ -444,24 +446,27 @@ select {
border-bottom: 1px solid rgb(51, 51, 51); border-bottom: 1px solid rgb(51, 51, 51);
padding-bottom: 10px; padding-bottom: 10px;
margin-bottom: 20px; margin-bottom: 20px;
font-weight: bold; margin-top: 20px;
}
.field-label {
font-weight: bold;
} }
.course-tabs{ .course-tabs{
list-style: none; list-style: none;
@include float(left); @include float(left);
@include margin-right(10px); @include margin-right(10px);
padding: 10px; padding: 10px;
border-bottom: 2px solid #ccc; border-bottom: 3px solid #ccc;
color: #ccc; color: #ccc;
text-align: center; text-align: center;
&:hover{ &:hover{
color: #337ab7; color: #337ab7;
border-bottom: 2px solid #337ab7; border-bottom: 3px solid #337ab7;
} }
} }
.course-tabs.active{ .course-tabs.active{
border-bottom: 2px solid #337ab7; border-bottom: 3px solid #337ab7;
color: #337ab7; color: #337ab7;
} }
.content{ .content{
...@@ -477,6 +482,12 @@ select { ...@@ -477,6 +482,12 @@ select {
display: inline-block; display: inline-block;
} }
#number-example {
p:first-of-type{
margin-bottom: 0;
}
}
.info-item { .info-item {
margin-bottom: 15px; margin-bottom: 15px;
...@@ -621,3 +632,11 @@ select { ...@@ -621,3 +632,11 @@ select {
margin: 0; margin: 0;
} }
} }
.margin-top20 {
margin-top: 20px;
}
.hidden {
display: none;
}
{% if messages %}
<div class="alert-messages">
{% for message in messages %}
<div class="alert alert-{{ message.tags }}" role="alert" aria-labelledby="alert-title-{{ message.tags }}" tabindex="-1">
<div class="alert-message-with-action">
<p class="alert-copy">
{{ message }}
</p>
</div>
</div>
{% endfor %}
</div>
{% endif %}
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<div class="layout-full layout"> <div class="layout-full layout publisher-layout">
<div class="publisher-container"> <div class="publisher-container">
<div class="course-information"> <div class="course-information">
<h4 class="hd-4">{% trans "Course Form" %}</h4> <h4 class="hd-4">{% trans "Course Form" %}</h4>
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
</div> </div>
<div class="comment-container"> <div class="comment-container">
{% if object.id %} {% if object.id %}
<a href="{% url 'publisher:publisher_course_runs_new' %}" class="btn btn-neutral btn-add"> <a href="{% url 'publisher:publisher_course_runs_new' object.id %}" class="btn btn-neutral btn-add">
{% trans "Add Course Run" %} {% trans "Add Course Run" %}
</a> </a>
{% endif %} {% endif %}
......
...@@ -23,19 +23,8 @@ ...@@ -23,19 +23,8 @@
<span class="course-name">{{ object.title }}</span> <span class="course-name">{{ object.title }}</span>
</h2> </h2>
</div> </div>
<div class="alert-messages">
{% if messages %} {% include 'alert_messages.html' %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}" role="alert" aria-labelledby="alert-title-{{ message.tags }}" tabindex="-1">
<div class="alert-message-with-action">
<p class="alert-copy">
{{ message }}
</p>
</div>
</div>
{% endfor %}
{% endif %}
</div>
<div class="status-information"> <div class="status-information">
<div class="info-item"> <div class="info-item">
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<div class="layout-full layout"> <div class="layout-full layout publisher-layout">
<div class="publisher-container"> <div class="publisher-container">
<div class="course-information"> <div class="course-information">
<h4 class="hd-4">{% trans "Course Run Form" %}</h4> <h4 class="hd-4">{% trans "Course Run Form" %}</h4>
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<div class="layout-full layout"> <div class="layout-full layout publisher-layout">
<div class="publisher-container"> <div class="publisher-container">
<div class="course-information"> <div class="course-information">
<h4 class="hd-4">{% trans "Seat Form" %}</h4> <h4 class="hd-4">{% trans "Seat Form" %}</h4>
......
...@@ -5,19 +5,7 @@ ...@@ -5,19 +5,7 @@
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<div class="alert-messages"> {% include 'alert_messages.html' %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}" role="alert" aria-labelledby="alert-title-{{ message.tags }}" tabindex="-1">
<div class="alert-message-with-action">
<p class="alert-copy">
{{ message }}
</p>
</div>
</div>
{% endfor %}
{% endif %}
</div>
<div class="layout-full publisher-layout layout"> <div class="layout-full publisher-layout layout">
<h2 class="layout-title">{% trans "Base information" %}</h2> <h2 class="layout-title">{% trans "Base information" %}</h2>
......
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