Commit 0dca3588 by Awais Qureshi Committed by GitHub

Merge pull request #362 from edx/awais786/ECOM-5496

Awais786/ecom 5496
parents 057bca2d 5b361188
""" """
Course publisher forms. Course publisher forms.
""" """
from django.contrib.auth.models import Group
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _
from course_discovery.apps.publisher.models import Course, CourseRun, Seat from course_discovery.apps.course_metadata.models import Person
from course_discovery.apps.publisher.models import Course, CourseRun, Seat, User
class BaseCourseForm(forms.ModelForm): class BaseCourseForm(forms.ModelForm):
...@@ -40,6 +43,24 @@ class CourseForm(BaseCourseForm): ...@@ -40,6 +43,24 @@ class CourseForm(BaseCourseForm):
exclude = ('changed_by',) exclude = ('changed_by',)
class CustomCourseForm(CourseForm):
""" Course Form. """
institution = forms.ModelChoiceField(queryset=Group.objects.all(), required=True)
title = forms.CharField(label='Course Title', required=True, max_length=255)
number = forms.CharField(label='Course Number', required=True, max_length=255)
team_admin = forms.ModelChoiceField(queryset=User.objects.filter(is_staff=True), required=True)
class Meta(CourseForm.Meta):
model = Course
fields = (
'title', 'number', 'short_description', 'full_description',
'expected_learnings', 'level_type', 'primary_subject', 'secondary_subject',
'tertiary_subject', 'prerequisites', 'level_type', 'image', 'team_admin',
'level_type', 'institution',
)
class CourseRunForm(BaseCourseForm): class CourseRunForm(BaseCourseForm):
""" Course Run Form. """ """ Course Run Form. """
...@@ -49,6 +70,30 @@ class CourseRunForm(BaseCourseForm): ...@@ -49,6 +70,30 @@ class CourseRunForm(BaseCourseForm):
exclude = ('state', 'changed_by',) exclude = ('state', 'changed_by',)
class CustomCourseRunForm(CourseRunForm):
""" Course Run Form. """
contacted_partner_manager = forms.BooleanField(
widget=forms.RadioSelect(choices=((1, _("Yes")), (0, _("No")))), initial=0, required=False
)
start = forms.DateTimeField(required=True)
staff = forms.ModelMultipleChoiceField(
queryset=Person.objects.all(), widget=forms.SelectMultiple, required=False
)
target_content = forms.BooleanField(
widget=forms.RadioSelect(
choices=((1, _("Yes")), (0, _("No")))), initial=0, required=False
)
class Meta(CourseRunForm.Meta):
fields = (
'keywords', 'start', 'end', 'length',
'transcript_languages', 'language', 'min_effort', 'max_effort', 'keywords',
'contacted_partner_manager', 'target_content', 'pacing_type', 'is_seo_review',
'video_language', 'staff',
)
class SeatForm(BaseCourseForm): class SeatForm(BaseCourseForm):
""" Course Seat Form. """ """ Course Seat Form. """
...@@ -76,3 +121,20 @@ class SeatForm(BaseCourseForm): ...@@ -76,3 +121,20 @@ class SeatForm(BaseCourseForm):
seat.save() seat.save()
return seat return seat
def clean(self):
price = self.cleaned_data.get('price')
seat_type = self.cleaned_data.get('type')
if seat_type in [Seat.PROFESSIONAL, Seat.NO_ID_PROFESSIONAL, Seat.VERIFIED, Seat.CREDIT] \
and not price:
self.add_error('price', _('Only honor/audit seats can be without price.'))
return self.cleaned_data
class CustomSeatForm(SeatForm):
""" Course Seat Form. """
class Meta(SeatForm.Meta):
fields = ('price', 'type')
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('publisher', '0009_auto_20160929_1927'),
]
operations = [
migrations.AlterField(
model_name='course',
name='expected_learnings',
field=models.TextField(default=None, null=True, blank=True, verbose_name='Expected Learnings'),
),
migrations.AlterField(
model_name='course',
name='full_description',
field=models.TextField(default=None, null=True, blank=True, verbose_name='Full Description'),
),
migrations.AlterField(
model_name='course',
name='level_type',
field=models.ForeignKey(related_name='publisher_courses', default=None, to='course_metadata.LevelType', blank=True, verbose_name='Level Type', null=True),
),
migrations.AlterField(
model_name='course',
name='prerequisites',
field=models.TextField(default=None, null=True, blank=True, verbose_name='Prerequisites'),
),
migrations.AlterField(
model_name='course',
name='short_description',
field=models.CharField(default=None, max_length=255, null=True, blank=True, verbose_name='Brief Description'),
),
migrations.AlterField(
model_name='courserun',
name='language',
field=models.ForeignKey(related_name='publisher_course_runs', to='ietf_language_tags.LanguageTag', verbose_name='Content Language', blank=True, null=True),
),
migrations.AlterField(
model_name='historicalcourse',
name='expected_learnings',
field=models.TextField(default=None, null=True, blank=True, verbose_name='Expected Learnings'),
),
migrations.AlterField(
model_name='historicalcourse',
name='full_description',
field=models.TextField(default=None, null=True, blank=True, verbose_name='Full Description'),
),
migrations.AlterField(
model_name='historicalcourse',
name='prerequisites',
field=models.TextField(default=None, null=True, blank=True, verbose_name='Prerequisites'),
),
migrations.AlterField(
model_name='historicalcourse',
name='short_description',
field=models.CharField(default=None, max_length=255, null=True, blank=True, verbose_name='Brief Description'),
),
]
...@@ -7,7 +7,7 @@ from django.dispatch import receiver ...@@ -7,7 +7,7 @@ from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django_extensions.db.models import TimeStampedModel from django_extensions.db.models import TimeStampedModel
from django_fsm import FSMField, transition from django_fsm import FSMField, transition
from guardian.shortcuts import assign_perm from guardian.shortcuts import assign_perm, get_groups_with_perms
from simple_history.models import HistoricalRecords from simple_history.models import HistoricalRecords
from sortedm2m.fields import SortedManyToManyField from sortedm2m.fields import SortedManyToManyField
from stdimage.models import StdImageField from stdimage.models import StdImageField
...@@ -84,18 +84,18 @@ class Course(TimeStampedModel, ChangedByMixin): ...@@ -84,18 +84,18 @@ class Course(TimeStampedModel, ChangedByMixin):
title = models.CharField(max_length=255, default=None, null=True, blank=True, verbose_name=_('Course title')) title = models.CharField(max_length=255, default=None, null=True, blank=True, verbose_name=_('Course title'))
number = models.CharField(max_length=50, null=True, blank=True, verbose_name=_('Course number')) number = models.CharField(max_length=50, null=True, blank=True, verbose_name=_('Course number'))
short_description = models.CharField( short_description = models.CharField(
max_length=255, default=None, null=True, blank=True, verbose_name=_('Course subtitle') max_length=255, default=None, null=True, blank=True, verbose_name=_('Brief Description')
) )
full_description = models.TextField(default=None, null=True, blank=True, verbose_name=_('About this course')) full_description = models.TextField(default=None, null=True, blank=True, verbose_name=_('Full Description'))
organizations = models.ManyToManyField( organizations = models.ManyToManyField(
Organization, blank=True, related_name='publisher_courses', verbose_name=_('Partner Name') Organization, blank=True, related_name='publisher_courses', verbose_name=_('Partner Name')
) )
level_type = models.ForeignKey( level_type = models.ForeignKey(
LevelType, default=None, null=True, blank=True, related_name='publisher_courses', verbose_name=_('Course level') LevelType, default=None, null=True, blank=True, related_name='publisher_courses', verbose_name=_('Level Type')
) )
expected_learnings = models.TextField(default=None, null=True, blank=True, verbose_name=_("What you'll learn")) expected_learnings = models.TextField(default=None, null=True, blank=True, verbose_name=_("Expected Learnings"))
syllabus = models.TextField(default=None, null=True, blank=True) syllabus = models.TextField(default=None, null=True, blank=True)
prerequisites = models.TextField(default=None, null=True, blank=True) prerequisites = models.TextField(default=None, null=True, blank=True, verbose_name=_('Prerequisites'))
learner_testimonial = models.CharField(max_length=50, null=True, blank=True) learner_testimonial = models.CharField(max_length=50, null=True, blank=True)
verification_deadline = models.DateTimeField( verification_deadline = models.DateTimeField(
null=True, null=True,
...@@ -147,6 +147,15 @@ class Course(TimeStampedModel, ChangedByMixin): ...@@ -147,6 +147,15 @@ class Course(TimeStampedModel, ChangedByMixin):
for group in user.groups.all(): for group in user.groups.all():
assign_perm(self.VIEW_PERMISSION, group, self) assign_perm(self.VIEW_PERMISSION, group, self)
def assign_permission_by_group(self, institution):
assign_perm(self.VIEW_PERMISSION, institution, self)
@property
def get_group_institution(self):
""" Returns the Group object with for the given course object. """
available_groups = get_groups_with_perms(self)
return available_groups[0] if available_groups else None
class CourseRun(TimeStampedModel, ChangedByMixin): class CourseRun(TimeStampedModel, ChangedByMixin):
""" Publisher CourseRun model. It contains fields related to the course run intake form.""" """ Publisher CourseRun model. It contains fields related to the course run intake form."""
...@@ -185,7 +194,10 @@ class CourseRun(TimeStampedModel, ChangedByMixin): ...@@ -185,7 +194,10 @@ class CourseRun(TimeStampedModel, ChangedByMixin):
max_effort = models.PositiveSmallIntegerField( max_effort = models.PositiveSmallIntegerField(
null=True, blank=True, null=True, blank=True,
help_text=_('Estimated maximum number of hours per week needed to complete a course run.')) help_text=_('Estimated maximum number of hours per week needed to complete a course run.'))
language = models.ForeignKey(LanguageTag, null=True, blank=True, related_name='publisher_course_runs') language = models.ForeignKey(
LanguageTag, null=True, blank=True,
related_name='publisher_course_runs', verbose_name=_('Content Language')
)
transcript_languages = models.ManyToManyField( transcript_languages = models.ManyToManyField(
LanguageTag, blank=True, related_name='publisher_transcript_course_runs' LanguageTag, blank=True, related_name='publisher_transcript_course_runs'
) )
......
...@@ -7,6 +7,7 @@ from factory.fuzzy import FuzzyText, FuzzyChoice, FuzzyDecimal, FuzzyDateTime, F ...@@ -7,6 +7,7 @@ from factory.fuzzy import FuzzyText, FuzzyChoice, FuzzyDecimal, FuzzyDateTime, F
from pytz import UTC from pytz import UTC
from course_discovery.apps.core.models import Currency from course_discovery.apps.core.models import Currency
from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.course_metadata.choices import CourseRunPacing from course_discovery.apps.course_metadata.choices import CourseRunPacing
from course_discovery.apps.course_metadata.tests import factories from course_discovery.apps.course_metadata.tests import factories
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
...@@ -34,6 +35,8 @@ class CourseFactory(factory.DjangoModelFactory): ...@@ -34,6 +35,8 @@ class CourseFactory(factory.DjangoModelFactory):
secondary_subject = factory.SubFactory(factories.SubjectFactory) secondary_subject = factory.SubFactory(factories.SubjectFactory)
tertiary_subject = factory.SubFactory(factories.SubjectFactory) tertiary_subject = factory.SubFactory(factories.SubjectFactory)
team_admin = factory.SubFactory(UserFactory)
class Meta: class Meta:
model = Course model = Course
......
# pylint: disable=no-member # pylint: disable=no-member
import ddt import ddt
from mock import patch from mock import patch
from django.db import IntegrityError
from django.conf import settings from django.conf import settings
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -24,12 +26,18 @@ class CreateUpdateCourseViewTests(TestCase): ...@@ -24,12 +26,18 @@ class CreateUpdateCourseViewTests(TestCase):
def setUp(self): def setUp(self):
super(CreateUpdateCourseViewTests, self).setUp() super(CreateUpdateCourseViewTests, self).setUp()
self.course = factories.CourseFactory()
self.group = factories.GroupFactory()
self.user = UserFactory(is_staff=True, is_superuser=True) self.user = UserFactory(is_staff=True, is_superuser=True)
self.group = factories.GroupFactory()
self.course = factories.CourseFactory(team_admin=self.user)
self.course_run = factories.CourseRunFactory(course=self.course)
self.seat = factories.SeatFactory(course_run=self.course_run, type=Seat.VERIFIED, price=2)
self.user.groups.add(self.group) self.user.groups.add(self.group)
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.start_date_time = '2050-07-08 05:59:53'
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. """
...@@ -46,38 +54,48 @@ class CreateUpdateCourseViewTests(TestCase): ...@@ -46,38 +54,48 @@ class CreateUpdateCourseViewTests(TestCase):
target_status_code=302 target_status_code=302
) )
def test_create_course_and_course_run_and_seat_with_errors(self):
""" Verify that without providing required data course and other
objects cannot be created.
"""
course_dict = model_to_dict(self.course)
course_dict['number'] = 'test course'
course_dict['image'] = ''
response = self.client.post(reverse('publisher:publisher_courses_new'), course_dict)
self.assertEqual(response.status_code, 400)
@ddt.data( @ddt.data(
{'number': 'course_1', 'image': '', 'team_admin': False}, {'number': 'course_1', 'image': ''},
{'number': 'course_2', 'image': make_image_file('test_banner.jpg'), 'team_admin': False}, {'number': 'course_2', 'image': make_image_file('test_banner.jpg')},
{'number': 'course_3', 'image': make_image_file('test_banner1.jpg'), 'team_admin': True} {'number': 'course_3', 'image': make_image_file('test_banner1.jpg')}
) )
def test_create_course(self, data): def test_create_course_and_course_run_and_seat(self, data):
""" Verify that new course can be created with different data sets. """ """ Verify that new course, course run and seat can be created
course_dict = model_to_dict(self.course) with different data sets.
course_dict.pop('verification_deadline') """
course_dict.update(**data) self._assert_records(1)
course_dict = self._post_data(data, self.course, self.course_run, self.seat)
response = self.client.post(reverse('publisher:publisher_courses_new'), course_dict, files=data['image'])
course = Course.objects.get(number=data['number'])
if data['team_admin']: if data['image']:
course_dict['team_admin'] = self.user.id self._assert_image(course)
else:
course_dict['team_admin'] = ''
course_number = course_dict['number'] self._assert_test_data(response, course, self.seat.type, self.seat.price)
response = self.client.post(reverse('publisher:publisher_courses_new'), course_dict)
course = Course.objects.get(number=course_number) def test_create_with_fail_transaction(self):
self.assertRedirects( """ Verify that in case of any error transactions roll back and no object
response, created in db.
expected_url=reverse('publisher:publisher_courses_edit', kwargs={'pk': course.id}), """
status_code=302, self._assert_records(1)
target_status_code=200 data = {'number': 'course_2', 'image': make_image_file('test_banner.jpg')}
) course_dict = self._post_data(data, self.course, self.course_run, self.seat)
with patch.object(Course, "assign_permission_by_group") as mock_method:
mock_method.side_effect = IntegrityError
response = self.client.post(reverse('publisher:publisher_courses_new'), course_dict, files=data['image'])
self.assertEqual(course.number, course_number) self.assertEqual(response.status_code, 400)
self.assertTrue(self.user.has_perm(Course.VIEW_PERMISSION, course)) self._assert_records(1)
response = self.client.get(reverse('publisher:publisher_courses_new'))
self.assertNotContains(response, 'Add new comment')
self.assertNotContains(response, 'Total Comments')
def test_update_course_with_staff(self): def test_update_course_with_staff(self):
""" Verify that staff user can update an existing course. """ """ Verify that staff user can update an existing course. """
...@@ -183,6 +201,88 @@ class CreateUpdateCourseViewTests(TestCase): ...@@ -183,6 +201,88 @@ class CreateUpdateCourseViewTests(TestCase):
self.assertContains(response, 'Add new comment') self.assertContains(response, 'Add new comment')
self.assertContains(response, comment.comment) self.assertContains(response, comment.comment)
@ddt.data(Seat.VERIFIED, Seat.PROFESSIONAL, Seat.NO_ID_PROFESSIONAL, Seat.CREDIT)
def test_create_course_without_price_with_error(self, seat_type):
""" Verify that if seat type is not honor/audit then price should be given.
Otherwise it will throw error.
"""
self._assert_records(1)
data = {'number': 'course_1', 'image': ''}
course_dict = self._post_data(data, self.course, self.course_run, self.seat)
course_dict['price'] = 0
course_dict['type'] = seat_type
response = self.client.post(reverse('publisher:publisher_courses_new'), course_dict, files=data['image'])
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.context['seat_form'].errors['price'][0], 'Only honor/audit seats can be without price.'
)
self._assert_records(1)
@ddt.data(Seat.AUDIT, Seat.HONOR)
def test_create_course_without_price_with_success(self, seat_type):
""" Verify that if seat type is honor/audit then price is not required. """
self._assert_records(1)
data = {'number': 'course_1', 'image': ''}
course_dict = self._post_data(data, self.course, self.course_run, self.seat)
course_dict['price'] = 0
course_dict['type'] = seat_type
response = self.client.post(reverse('publisher:publisher_courses_new'), course_dict, files=data['image'])
course = Course.objects.get(number=data['number'])
self._assert_test_data(response, course, seat_type, 0)
def _post_data(self, data, course, course_run, seat):
course_dict = model_to_dict(course)
course_dict.update(**data)
if course_run:
course_dict.update(**model_to_dict(course_run))
course_dict.pop('video_language')
course_dict.pop('end')
course_dict.pop('priority')
course_dict['start'] = self.start_date_time
course_dict['institution'] = self.group.id
if seat:
course_dict.update(**model_to_dict(seat))
course_dict.pop('verification_deadline')
return course_dict
def _assert_image(self, course):
image_url_prefix = '{}media/publisher/courses/images'.format(settings.MEDIA_URL)
self.assertIn(image_url_prefix, course.image.url)
for size_key in course.image.field.variations:
# Get different sizes specs from the model field
# Then get the file path from the available files
sized_file = getattr(course.image, size_key, None)
self.assertIsNotNone(sized_file)
self.assertIn(image_url_prefix, sized_file.url)
def _assert_records(self, count):
# DRY method to count records in db.
self.assertEqual(Course.objects.all().count(), count)
self.assertEqual(CourseRun.objects.all().count(), count)
self.assertEqual(Seat.objects.all().count(), count)
def _assert_test_data(self, response, course, expected_type, expected_price):
# DRY method to assert response and data.
self.assertRedirects(
response,
expected_url=reverse('publisher:publisher_courses_readonly', kwargs={'pk': course.id}),
status_code=302,
target_status_code=200
)
self.assertEqual(course.get_group_institution, self.group)
self.assertEqual(course.team_admin, self.user)
self.assertTrue(self.user.has_perm(Course.VIEW_PERMISSION, course))
course_run = course.publisher_course_runs.all()[0]
self.assertEqual(self.course_run.language, course_run.language)
self.assertEqual(course_run.start.strftime("%Y-%m-%d %H:%M:%S"), self.start_date_time)
seat = course_run.seats.all()[0]
self.assertEqual(seat.type, expected_type)
self.assertEqual(seat.price, expected_price)
self._assert_records(2)
response = self.client.get(reverse('publisher:publisher_courses_readonly', kwargs={'pk': course.id}))
self.assertEqual(response.status_code, 200)
class CreateUpdateCourseRunViewTests(TestCase): class CreateUpdateCourseRunViewTests(TestCase):
""" Tests for the publisher `CreateCourseRunView` and `UpdateCourseRunView`. """ """ Tests for the publisher `CreateCourseRunView` and `UpdateCourseRunView`. """
...@@ -194,7 +294,7 @@ class CreateUpdateCourseRunViewTests(TestCase): ...@@ -194,7 +294,7 @@ class CreateUpdateCourseRunViewTests(TestCase):
self._pop_valuse_from_dict( self._pop_valuse_from_dict(
self.course_run_dict, self.course_run_dict,
[ [
'start', 'end', 'enrollment_start', 'enrollment_end', 'end', 'enrollment_start', 'enrollment_end',
'priority', 'certificate_generation', 'video_language' 'priority', 'certificate_generation', 'video_language'
] ]
) )
...@@ -225,6 +325,7 @@ class CreateUpdateCourseRunViewTests(TestCase): ...@@ -225,6 +325,7 @@ class CreateUpdateCourseRunViewTests(TestCase):
""" Verify that we can create a new course run. """ """ Verify that we can create a new course run. """
lms_course_id = 'course-v1:testX+AS12131+2016_q4' lms_course_id = 'course-v1:testX+AS12131+2016_q4'
self.course_run_dict['lms_course_id'] = lms_course_id 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(reverse('publisher:publisher_course_runs_new'), self.course_run_dict) response = self.client.post(reverse('publisher:publisher_course_runs_new'), self.course_run_dict)
course_run = CourseRun.objects.get(course=self.course_run.course, lms_course_id=lms_course_id) course_run = CourseRun.objects.get(course=self.course_run.course, lms_course_id=lms_course_id)
...@@ -245,6 +346,8 @@ class CreateUpdateCourseRunViewTests(TestCase): ...@@ -245,6 +346,8 @@ class CreateUpdateCourseRunViewTests(TestCase):
""" 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)
response = self.client.post( response = self.client.post(
...@@ -277,6 +380,7 @@ class CreateUpdateCourseRunViewTests(TestCase): ...@@ -277,6 +380,7 @@ 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(
...@@ -300,6 +404,7 @@ class CreateUpdateCourseRunViewTests(TestCase): ...@@ -300,6 +404,7 @@ 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)
......
...@@ -7,6 +7,7 @@ from course_discovery.apps.publisher import views ...@@ -7,6 +7,7 @@ from course_discovery.apps.publisher import views
urlpatterns = [ 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+)/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'^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/$', views.CourseRunListView.as_view(), name='publisher_course_runs'), url(r'^course_runs/$', views.CourseRunListView.as_view(), name='publisher_course_runs'),
......
...@@ -2,8 +2,11 @@ ...@@ -2,8 +2,11 @@
Course publisher views. Course publisher views.
""" """
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.models import Group
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import transaction
from django.http import HttpResponseRedirect, HttpResponseForbidden from django.http import HttpResponseRedirect, HttpResponseForbidden
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
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
...@@ -12,7 +15,9 @@ from django.views.generic.list import ListView ...@@ -12,7 +15,9 @@ 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 course_discovery.apps.publisher.forms import CourseForm, CourseRunForm, SeatForm from course_discovery.apps.publisher.forms import (
CourseForm, CourseRunForm, SeatForm, CustomCourseForm, CustomCourseRunForm, CustomSeatForm
)
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 from course_discovery.apps.publisher.models import Course, CourseRun, Seat, State
from course_discovery.apps.publisher.wrappers import CourseRunWrapper from course_discovery.apps.publisher.wrappers import CourseRunWrapper
...@@ -59,16 +64,72 @@ class CourseRunDetailView(mixins.LoginRequiredMixin, mixins.ViewPermissionMixin, ...@@ -59,16 +64,72 @@ class CourseRunDetailView(mixins.LoginRequiredMixin, mixins.ViewPermissionMixin,
# pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
class CreateCourseView(mixins.LoginRequiredMixin, mixins.FormValidMixin, CreateView): class CreateCourseView(mixins.LoginRequiredMixin, CreateView):
""" Create Course View.""" """ Create Course View."""
model = Course model = Course
form_class = CourseForm course_form = CustomCourseForm
template_name = 'publisher/course_form.html' run_form = CustomCourseRunForm
success_url = 'publisher:publisher_courses_edit' seat_form = CustomSeatForm
assign_user_groups = True template_name = 'publisher/add_course_form.html'
success_url = 'publisher:publisher_courses_readonly'
def get_success_url(self, course_id): # pylint: disable=arguments-differ
return reverse(self.success_url, kwargs={'pk': course_id})
def get_context_data(self):
return {
'course_form': self.course_form,
'run_form': self.run_form,
'seat_form': self.seat_form
}
def get(self, request, *args, **kwargs):
return render(request, self.template_name, self.get_context_data())
def post(self, request, *args, **kwargs):
ctx = self.get_context_data()
course_form = self.course_form(request.POST, request.FILES)
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():
seat = seat_form.save(commit=False)
run_course = run_form.save(commit=False)
course = course_form.save(commit=False)
course.changed_by = self.request.user
course.save()
run_course.course = course
run_course.changed_by = self.request.user
run_course.save()
# commit false does not save m2m object.
run_form.save_m2m()
seat.course_run = run_course
seat.changed_by = self.request.user
seat.save()
institution = get_object_or_404(Group, pk=course_form.data['institution'])
# assign guardian permission.
course.assign_permission_by_group(institution)
def get_success_url(self): messages.success(
return reverse(self.success_url, kwargs={'pk': self.object.id}) request, _('Course created successfully.')
)
return HttpResponseRedirect(self.get_success_url(course.id))
except Exception as e: # pylint: disable=broad-except
messages.error(request, str(e))
messages.error(request, _('Please fill all required field.'))
ctx.update(
{
'course_form': course_form,
'run_form': run_form,
'seat_form': seat_form
}
)
return render(request, self.template_name, ctx, status=400)
class UpdateCourseView(mixins.LoginRequiredMixin, mixins.ViewPermissionMixin, mixins.FormValidMixin, UpdateView): class UpdateCourseView(mixins.LoginRequiredMixin, mixins.ViewPermissionMixin, mixins.FormValidMixin, UpdateView):
...@@ -88,6 +149,17 @@ class UpdateCourseView(mixins.LoginRequiredMixin, mixins.ViewPermissionMixin, mi ...@@ -88,6 +149,17 @@ class UpdateCourseView(mixins.LoginRequiredMixin, mixins.ViewPermissionMixin, mi
return context return context
class ReadOnlyView(mixins.LoginRequiredMixin, mixins.ViewPermissionMixin, DetailView):
""" Course Run Detail View."""
model = Course
template_name = 'publisher/view_course_form.html'
def get_context_data(self, **kwargs):
context = super(ReadOnlyView, self).get_context_data(**kwargs)
context['comment_object'] = self
return context
class CreateCourseRunView(mixins.LoginRequiredMixin, mixins.FormValidMixin, CreateView): class CreateCourseRunView(mixins.LoginRequiredMixin, mixins.FormValidMixin, CreateView):
""" Create Course Run View.""" """ Create Course Run View."""
model = CourseRun model = CourseRun
......
...@@ -7,6 +7,18 @@ $(".administration-nav .tab-container > button").click(function(event) { ...@@ -7,6 +7,18 @@ $(".administration-nav .tab-container > button").click(function(event) {
$(tab).fadeIn(); $(tab).fadeIn();
}); });
$(document).ready(function(){
$('ul.tabs .course-tabs').click(function(){
var tab_id = $(this).attr('data-tab');
$('ul.tabs .course-tabs').removeClass('active');
$('.content').removeClass('active');
$(this).addClass('active');
$("#"+tab_id).addClass('active');
})
});
function alertTimeout(wait) { function alertTimeout(wait) {
setTimeout(function(){ setTimeout(function(){
$('.alert-messages').html(''); $('.alert-messages').html('');
......
...@@ -321,10 +321,97 @@ $light-gray: rgba(204, 204, 204, 1); ...@@ -321,10 +321,97 @@ $light-gray: rgba(204, 204, 204, 1);
margin-bottom: 20px; margin-bottom: 20px;
} }
.publisher-layout {
background: rgb(242, 242, 242);
padding: 15px;
margin-bottom: 20px;
.layout-title {
font-size: 24px;
font-weight: bold;
}
.course-form {
margin: 0;
box-shadow: none;
}
}
input,
select {
max-width: 100%;
}
.required {
color: red;
font-size: 12px;
}
.checkbox-inline {
ul {
list-style: none;
margin-left: 0;
margin-bottom: 15px;
li {
display: inline-block;
@include margin-right(10px);
&:last-child {
margin-right: 0;
}
}
}
}
.course-information { .course-information {
margin-bottom: 30px; margin-bottom: 30px;
input, select {
margin-bottom: 15px;
}
.hd-4 {
margin-bottom: 25px;
padding-bottom: 10px;
border-bottom: 1px solid rgb(51, 51, 51);
}
.field-title {
border-bottom: 1px solid rgb(51, 51, 51);
padding-bottom: 10px;
margin-bottom: 20px;
font-weight: bold;
}
.course-tabs{
list-style: none;
@include float(left); @include float(left);
width: 50%; @include margin-right(10px);
padding: 10px;
border-bottom: 2px solid #ccc;
color: #ccc;
text-align: center;
&:hover{
color: #337ab7;
border-bottom: 2px solid #337ab7;
}
}
.course-tabs.active{
border-bottom: 2px solid #337ab7;
color: #337ab7;
}
.content{
display: none;
p{
margin: 0;
}
p:first-of-type{
margin-bottom: 20px;
}
}
.content.active{
display: inline-block;
}
.info-item { .info-item {
margin-bottom: 15px; margin-bottom: 15px;
......
{% extends 'base.html' %}
{% load i18n %}
{% load staticfiles %}
{% block title %}
{% trans "Course Form" %}
{% endblock title %}
{% block extra_js %}
<script src="{% static 'js/publisher/publisher.js' %}"></script>
{% endblock %}
{% block content %}
<div>
<h1 class="hd-1 emphasized">New Course</h1>
<div class="alert-messages">
{% 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 %}
<form class="form" method="post" action="" enctype="multipart/form-data">
{% csrf_token %}
<div class="layout-full publisher-layout layout">
<h2 class="layout-title">{% trans "Base information" %}</h2>
<div class="card course-form">
<div class="course-information">
<fieldset class="form-group grid-container grid-manual">
<div class="field-title">{% trans "INSTITUTION INFORMATION" %}</div>
<div class="row">
<div class="col col-6 help-text">
{% trans "Please choose the school that will be providing the course. Once chosen then you can select an administrator for the studio shell." %}
</div>
<div class="col col-6">
<label class="field-label">{{ course_form.institution.label_tag }}
<span class="required">* {% trans "required" %}</span>
</label>
{{ course_form.institution}}
<label class="field-label">
{{ course_form.team_admin.label_tag }}
<span class="required">* {% trans "required" %}</span>
</label>
{{ course_form.team_admin }}
</div>
</div>
<div class="field-title">{% trans "Course Title" %}</div>
<div class="row">
<div class="col col-6 help-text">
<div class="row">
<ul class="tabs">
<li class="course-tabs active" data-tab="tab-practices">
{% trans "Best Practices" %}
</li>
<li class="course-tabs" data-tab="tab-example">
{% trans "Example" %}
</li>
</ul>
</div>
<div id="tab-practices" class="content active">
<p>{% trans "Concise 70 characters maximum; < 50 chars. recommended." %}</p>
<p>{% trans "Descriptive - clearly indicates what the course is about." %}</p>
<p>{% trans "SEO-optimized and targeted to a global audience." %}</p>
<p>{% trans 'If the course falls in a sequence, our titling convention is: "Course Title: Subtitle"' %}</p>
</div>
<div id="tab-example" class="content">
{% trans "English Grammar and Essay Writing Sequence Courses:" %}
<ul>
<li>{% trans "Introduction to Statistics" %}</li>
<li>{% trans "Statistics: Inference" %}</li>
<li>{% trans "Statistics: Probability" %}</li>
</ul>
</div>
</div>
<div class="col col-6">
<label class="field-label ">{{ course_form.title.label }}
<span class="required">* {% trans "required" %}</span>
</label>
{{ course_form.title }}
<label class="field-label ">{{ run_form.contacted_partner_manager.label_tag }}</label>
<div class="checkbox-inline">{{ run_form.contacted_partner_manager}}</div>
<label class="field-label">{% trans "Priority content" %}</label>
<div class="checkbox-inline">{{ run_form.target_content}}</div>
</div>
</div>
<div class="field-title">{% trans "Start Date" %}</div>
<div class="row">
<div class="col col-6 help-text">
{% trans "Start on a weekday (preferably Tuesday, Wednesday, or Thursday) and avoid major U.S. holidays for best access to edX staff." %}
{% trans "Approximate dates are acceptable; If you are unable to give an exact date, please identify a month in which the course will be offered." %}
</div>
<div class="col col-6">
<label class="field-label ">{{ run_form.start.label_tag }}
<span class="required">* {% trans "required" %}</span>
</label>
{{ run_form.start }}
</div>
</div>
<div class="field-title">{% trans "Pacing Type" %}</div>
<div class="row">
<div class="col col-6 help-text">
{% trans "Will your course be open to students at the same time as it is announced?" %}
</div>
<div class="col col-6">
<label class="field-label ">{{ run_form.pacing_type.label_tag }}</label>
{{ run_form.pacing_type }}
</div>
</div>
<div class="field-title">{% trans "Course Number" %}</div>
<div class="row">
<div class="col col-6 help-text">
{% trans 'Courses split into several modules can be denoted by adding .1, .2, etc. at the end of the course number before the “x”' %}
{% trans 'No special html characters, accents, spaces, dashes, or underscores 10 character limit' %}
</div>
<div class="col col-6">
<label class="field-label ">{{ course_form.number.label_tag }}
<span class="required">* {% trans "required" %}</span>
</label>
{{ course_form.number }}
</div>
</div>
</fieldset>
</div>
</div>
</div>
<div class="layout-full publisher-layout layout">
<h2 class="layout-title">{% trans "About page information" %}</h2>
<div class="card course-form">
<div class="course-information">
<fieldset class="form-group grid-container grid-manual">
<div class="field-title">{% trans "End Date" %}</div>
<div class="row">
<div class="col col-6 help-text">
{% trans "The date when this self-paced course run will end, replaced by an updated version of the course" %}
</div>
<div class="col col-6">
<label class="field-label ">{{ run_form.end.label_tag }}</label>
{{ run_form.end }}
</div>
</div>
<div class="field-title">{% trans "Seat Type" %}</div>
<div class="row">
<div class="col col-6 help-text">
{% trans "The date when this self-paced course run will end, replaced by an updated version of the course" %}
</div>
<div class="col col-6">
<div class="row">
<div class="col col-6">
<label class="field-label ">{{ seat_form.type.label_tag }}
<span class="required">* {% trans "required" %}</span>
</label>
{{ seat_form.type}}
</div>
<div class="col col-6">
<label class="field-label ">{{ seat_form.price.label_tag }}</label>
{{ seat_form.price}}
</div>
</div>
{% if seat_form.price.errors %}
<div class="field-message has-error">
<span class="field-message-content">
{{ seat_form.price.errors|escape }}
</span>
</div>
{% endif %}
</div>
</div>
<div class="field-title">{% trans "Brief Description" %}</div>
<div class="row">
<div class="col col-6 help-text">
<ul>
<li>{% trans "Reads as a tag line - a short, engaging description for students browsing course listings" %}
</li>
<li>{% trans "Conveys why someone should take the course" %}</li>
<li>{% trans "SEO optimized and targeted to a global audience" %}</li>
</ul>
</div>
<div class="col col-6">
<label class="field-label ">{{ course_form.short_description.label_tag }}</label>
{{ course_form.short_description }}
</div>
</div>
<div class="field-title">{% trans "FULL DESCRIPTION" %}</div>
<div class="row">
<div class="col col-6 help-text">
{% trans "Summarized description of course content" %}
<ul>
<li>{% trans "Describe why a learner should take this course" %}</li>
<li>{% trans "SEO optimized and targeted to a global audience" %}</li>
<li>{% trans "Text should be easily scannable, using bullet points to highlight instead of long, dense text paragraphs" %}
</li>
<li>
{% trans "Note: the first 4-5 lines will be visible to the learner immediately upon clicking the page;" %}
{% trans 'additional text will be hidden yet available via "See More" clickable text under the first 4-5 lines' %}
</li>
</ul>
</div>
<div class="col col-6">
<label class="field-label ">{{ course_form.full_description.label_tag }}</label>
{{ course_form.full_description }}
</div>
</div>
<div class="field-title">{% trans "EXPECTED LEARNINGS" %}</div>
<div class="row">
<div class="col col-6 help-text">
<ul>
<li>{% trans 'Answer to the question: "What will you learn from this course?"' %}</li>
<li>{% trans "bulleted items, approximately 4-10 words per bullet" %}</li>
</ul>
</div>
<div class="col col-6">
<label class="field-label ">{{ course_form.expected_learnings.label_tag }}</label>
{{ course_form.expected_learnings }}
</div>
</div>
<div class="field-title">{% trans "COURSE STAFF" %}</div>
<div class="row">
<div class="col col-6 help-text">
<ul>
<li>{% trans "If there is more than one instructor, please indicate the order in which the instructors should be listed" %}
</li>
<li>{% trans "Limited to the primary instructors a learner will encounter in videos" %}</li>
</ul>
</div>
<div class="col col-6">
{{ run_form.staff }}
</div>
</div>
<div class="field-title">{% trans "KEYWORDS" %}</div>
<div class="row">
<div class="col col-6 help-text">
{% trans "Some instructions here???" %}
</div>
<div class="col col-6">
<label class="field-label ">{{ run_form.keywords.label_tag }}</label>
{{ run_form.keywords}}
<label class="field-label ">{{ run_form.is_seo_review.label_tag }}</label>
{{ run_form.is_seo_review}}
</div>
</div>
<div class="field-title">{% trans "SUBJECT FIELD" %}</div>
<div class="row">
<div class="col col-6 help-text">
{% trans "Only one primary subject will appear on the About Page; please select one primary subject and a maximum of two additional subject areas for search." %}
</div>
<div class="col col-6">
<label class="field-label ">{{ course_form.primary_subject.label_tag }}</label>
{{ course_form.primary_subject }}
</div>
</div>
<div class="row">
<div class="col col-6 help-text">&nbsp;
</div>
<div class="col col-6">
<label class="field-label ">{{ course_form.secondary_subject.label_tag }}</label>
{{ course_form.secondary_subject }}
</div>
</div>
<div class="row">
<div class="col col-6 help-text">&nbsp;
</div>
<div class="col col-6">
<label class="field-label ">{{ course_form.tertiary_subject.label_tag }}</label>
{{ course_form.tertiary_subject }}
</div>
</div>
<div class="field-title">{% trans "COURSE IMAGE" %}</div>
<div class="row">
<div class="col col-6 help-text">
{% trans "Select an eye-catching, colorful image that captures the content and essence of your course" %}
<ul>
<li>{% trans "Do not include text or headlines" %}</li>
<li>{% trans "Choose an image that you have permission to use." %}
{% trans "This can be a stock photo (try Flickr creative commons, " %}
{% trans "Stock Vault, Stock XCHNG, iStock Photo) or an image custom designed for your course" %}
</li>
<li>{% trans "Sequenced courses should each have a unique image" %}</li>
<li>{% trans "Size: 2120 x 1192 pixels" %}</li>
</ul>
</div>
<div class="col col-6">
<label class="field-label ">{{ course_form.image.label_tag }}</label>
{{ course_form.image }}
</div>
</div>
<div class="field-title">{% trans "PREREQUISITES" %}</div>
<div class="row">
<div class="col col-6 help-text">
<ul>
<li>{% trans "List concepts and level (basic, advanced, undergraduate, graduate) students should be familiar with" %}
</li>
<li>{% trans 'If there are no prerequisites, please list "None."' %}</li>
<li>{% trans "200 character limit, including spaces" %}</li>
</ul>
</div>
<div class="col col-6">
<label class="field-label ">{{ course_form.prerequisites.label_tag }}</label>
{{ course_form.prerequisites }}
</div>
</div>
<div class="field-title">{% trans "ESTIMATED EFFORT" %}</div>
<div class="row">
<div class="col col-6">
<ul>
<li>{% trans "Number of hours per week the learner should expect to spend on the course to be successful" %}
</li>
<li>{% trans "Should be realistic, and can be a range" %}</li>
</ul>
</div>
<div class="col col-6">
<div class="row">
<div class="col col-6">
<label class="field-label ">{% trans "Min Effort" %}</label>
{{ run_form.min_effort }}
</div>
<div class="col col-6">
<label class="field-label ">{% trans "Max Effort" %}</label>
{{ run_form.min_effort }}
</div>
</div>
</div>
</div>
<div class="field-title">{% trans "LANGUAGE(S)" %}</div>
<div class="row">
<div class="col col-6 help-text">
<ul>
<li>
{% trans "Course content (navigation and course content excluding videos)" %}
</li>
<li>
{% trans "Videos (language spoken in course videos)" %}
</li>
<li>
{% trans "Video transcript (video caption language)" %}
</li>
</ul>
</div>
<div class="col col-6">
<label class="field-label ">{{ run_form.language.label_tag }}</label>
{{ run_form.language}}
<label class="field-label ">{{ run_form.transcript_languages.label_tag }}</label>
{{ run_form.transcript_languages}}
<label class="field-label ">{{ run_form.video_language.label_tag }}</label>
{{ run_form.video_language}}
</div>
</div>
<div class="field-title">{% trans "LENGTH" %}</div>
<div class="row">
<div class="col col-6 help-text">
<ul>
<li>{% trans "Length of course, in number of weeks" %}</li>
<li>{% trans "If the time between start/end dates is not exact, ex: 8.5 weeks, " %}
{% trans "indicate whether the course should be listed as 8 weeks or 9 weeks." %}
</li>
</ul>
</div>
<div class="col col-6">
<label class="field-label ">{{ run_form.length.label_tag }}</label>
{{ run_form.length}}
</div>
</div>
<div class="field-title">{% trans "LEVEL TYPE" %}</div>
<div class="row">
<div class="col col-6 help-text">
<ul>
<li>{% trans "Introductory - No prerequisites; an individual with some to all of a secondary school degree could complete" %}
</li>
<li>
{% trans "Intermediate - Basic prerequisites; a secondary school degree likely required to be successful as well as some university" %}
</li>
<li>
{% trans "Advanced - Significant number of prerequisites required; course geared to 3rd or 4th year university student or a masters degree student" %}
</li>
</ul>
</div>
<div class="col col-6 help-text">
<label class="field-label ">{{ course_form.level_type.label_tag }}</label>
{{ course_form.level_type }}
</div>
</div>
</fieldset>
</div>
</div>
</div>
<div class="layout-full publisher-layout layout">
<div class="card course-form">
<div class="course-information">
<fieldset class="form-group grid-container grid-manual">
<button class="btn-brand btn-base" type="submit">{% trans "Save Draft" %}</button>
</fieldset>
</div>
</div>
</div>
</form>
</div>
</div>
{% endblock content %}
{% extends 'base.html' %}
{% load i18n %}
{% block title %}
{% trans "Course Form" %}
{% endblock title %}
{% block content %}
<div class="alert-messages">
{% 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">
<h2 class="layout-title">{% trans "Base information" %}</h2>
<div class="card course-form">
<div class="course-information">
<h4 class="hd-4">{% trans "Course Form" %}</h4>
<fieldset class="form-group grid-container grid-manual">
<div class="field-title">{% trans "INSTITUTION INFORMATION" %}</div>
<div class="row">
<div class="col col-6">
{{ object.get_group_institution }}
</div>
</div>
</fieldset>
</div>
</div>
</div>
<div class="layout-full layout">
<h2 class="layout-title">{% trans "Course information" %}</h2>
<div class="card course-form">
<div class="course-information">
<fieldset class="form-group">
<div class="field-row">
<div class="field-col">
<label class="field-label " for="title">{{ course_form.title.label }}</label>
{{ object.title }}
</div>
</div>
</fieldset>
</div>
</div>
</div>
{% endblock content %}
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