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.
"""
from django.contrib.auth.models import Group
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):
......@@ -40,6 +43,24 @@ class CourseForm(BaseCourseForm):
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):
""" Course Run Form. """
......@@ -49,6 +70,30 @@ class CourseRunForm(BaseCourseForm):
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):
""" Course Seat Form. """
......@@ -76,3 +121,20 @@ class SeatForm(BaseCourseForm):
seat.save()
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
from django.utils.translation import ugettext_lazy as _
from django_extensions.db.models import TimeStampedModel
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 sortedm2m.fields import SortedManyToManyField
from stdimage.models import StdImageField
......@@ -84,18 +84,18 @@ class Course(TimeStampedModel, ChangedByMixin):
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'))
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(
Organization, blank=True, related_name='publisher_courses', verbose_name=_('Partner Name')
)
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)
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)
verification_deadline = models.DateTimeField(
null=True,
......@@ -147,6 +147,15 @@ class Course(TimeStampedModel, ChangedByMixin):
for group in user.groups.all():
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):
""" Publisher CourseRun model. It contains fields related to the course run intake form."""
......@@ -185,7 +194,10 @@ class CourseRun(TimeStampedModel, ChangedByMixin):
max_effort = models.PositiveSmallIntegerField(
null=True, blank=True,
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(
LanguageTag, blank=True, related_name='publisher_transcript_course_runs'
)
......
......@@ -7,6 +7,7 @@ from factory.fuzzy import FuzzyText, FuzzyChoice, FuzzyDecimal, FuzzyDateTime, F
from pytz import UTC
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.tests import factories
from course_discovery.apps.ietf_language_tags.models import LanguageTag
......@@ -34,6 +35,8 @@ class CourseFactory(factory.DjangoModelFactory):
secondary_subject = factory.SubFactory(factories.SubjectFactory)
tertiary_subject = factory.SubFactory(factories.SubjectFactory)
team_admin = factory.SubFactory(UserFactory)
class Meta:
model = Course
......
# pylint: disable=no-member
import ddt
from mock import patch
from django.db import IntegrityError
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse
......@@ -24,12 +26,18 @@ class CreateUpdateCourseViewTests(TestCase):
def setUp(self):
super(CreateUpdateCourseViewTests, self).setUp()
self.course = factories.CourseFactory()
self.group = factories.GroupFactory()
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.site = Site.objects.get(pk=settings.SITE_ID)
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):
""" Verify that user can't access new course form page when not logged in. """
......@@ -46,38 +54,48 @@ class CreateUpdateCourseViewTests(TestCase):
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(
{'number': 'course_1', 'image': '', 'team_admin': False},
{'number': 'course_2', 'image': make_image_file('test_banner.jpg'), 'team_admin': False},
{'number': 'course_3', 'image': make_image_file('test_banner1.jpg'), 'team_admin': True}
{'number': 'course_1', 'image': ''},
{'number': 'course_2', 'image': make_image_file('test_banner.jpg')},
{'number': 'course_3', 'image': make_image_file('test_banner1.jpg')}
)
def test_create_course(self, data):
""" Verify that new course can be created with different data sets. """
course_dict = model_to_dict(self.course)
course_dict.pop('verification_deadline')
course_dict.update(**data)
def test_create_course_and_course_run_and_seat(self, data):
""" Verify that new course, course run and seat can be created
with different data sets.
"""
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']:
course_dict['team_admin'] = self.user.id
else:
course_dict['team_admin'] = ''
if data['image']:
self._assert_image(course)
course_number = course_dict['number']
response = self.client.post(reverse('publisher:publisher_courses_new'), course_dict)
self._assert_test_data(response, course, self.seat.type, self.seat.price)
course = Course.objects.get(number=course_number)
self.assertRedirects(
response,
expected_url=reverse('publisher:publisher_courses_edit', kwargs={'pk': course.id}),
status_code=302,
target_status_code=200
)
def test_create_with_fail_transaction(self):
""" Verify that in case of any error transactions roll back and no object
created in db.
"""
self._assert_records(1)
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.assertTrue(self.user.has_perm(Course.VIEW_PERMISSION, course))
response = self.client.get(reverse('publisher:publisher_courses_new'))
self.assertNotContains(response, 'Add new comment')
self.assertNotContains(response, 'Total Comments')
self.assertEqual(response.status_code, 400)
self._assert_records(1)
def test_update_course_with_staff(self):
""" Verify that staff user can update an existing course. """
......@@ -183,6 +201,88 @@ class CreateUpdateCourseViewTests(TestCase):
self.assertContains(response, 'Add new 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):
""" Tests for the publisher `CreateCourseRunView` and `UpdateCourseRunView`. """
......@@ -194,7 +294,7 @@ class CreateUpdateCourseRunViewTests(TestCase):
self._pop_valuse_from_dict(
self.course_run_dict,
[
'start', 'end', 'enrollment_start', 'enrollment_end',
'end', 'enrollment_start', 'enrollment_end',
'priority', 'certificate_generation', 'video_language'
]
)
......@@ -225,6 +325,7 @@ class CreateUpdateCourseRunViewTests(TestCase):
""" Verify that we can create a new course run. """
lms_course_id = 'course-v1:testX+AS12131+2016_q4'
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)
course_run = CourseRun.objects.get(course=self.course_run.course, lms_course_id=lms_course_id)
......@@ -245,6 +346,8 @@ class CreateUpdateCourseRunViewTests(TestCase):
""" Verify that staff user can update an existing course run. """
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['start'] = '2050-07-08 05:59:53'
self.assertNotEqual(self.course_run.lms_course_id, updated_lms_course_id)
self.assertNotEqual(self.course_run.changed_by, self.user)
response = self.client.post(
......@@ -277,6 +380,7 @@ class CreateUpdateCourseRunViewTests(TestCase):
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['start'] = '2050-07-08 05:59:53'
self.assertNotEqual(self.course_run.lms_course_id, updated_lms_course_id)
response = self.client.get(
......@@ -300,6 +404,7 @@ class CreateUpdateCourseRunViewTests(TestCase):
non_staff_user, group = create_non_staff_user_and_login(self)
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.assertNotEqual(self.course_run.lms_course_id, updated_lms_course_id)
......
......@@ -7,6 +7,7 @@ from course_discovery.apps.publisher import views
urlpatterns = [
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'^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'),
......
......@@ -2,8 +2,11 @@
Course publisher views.
"""
from django.contrib import messages
from django.contrib.auth.models import Group
from django.core.urlresolvers import reverse
from django.db import transaction
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.views.generic import View
from django.views.generic.detail import DetailView
......@@ -12,7 +15,9 @@ from django.views.generic.list import ListView
from django_fsm import TransitionNotAllowed
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.models import Course, CourseRun, Seat, State
from course_discovery.apps.publisher.wrappers import CourseRunWrapper
......@@ -59,16 +64,72 @@ class CourseRunDetailView(mixins.LoginRequiredMixin, mixins.ViewPermissionMixin,
# pylint: disable=attribute-defined-outside-init
class CreateCourseView(mixins.LoginRequiredMixin, mixins.FormValidMixin, CreateView):
class CreateCourseView(mixins.LoginRequiredMixin, CreateView):
""" Create Course View."""
model = Course
form_class = CourseForm
template_name = 'publisher/course_form.html'
success_url = 'publisher:publisher_courses_edit'
assign_user_groups = True
course_form = CustomCourseForm
run_form = CustomCourseRunForm
seat_form = CustomSeatForm
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):
return reverse(self.success_url, kwargs={'pk': self.object.id})
messages.success(
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):
......@@ -88,6 +149,17 @@ class UpdateCourseView(mixins.LoginRequiredMixin, mixins.ViewPermissionMixin, mi
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):
""" Create Course Run View."""
model = CourseRun
......
......@@ -7,6 +7,18 @@ $(".administration-nav .tab-container > button").click(function(event) {
$(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) {
setTimeout(function(){
$('.alert-messages').html('');
......
......@@ -321,10 +321,97 @@ $light-gray: rgba(204, 204, 204, 1);
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 {
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);
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 {
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