Commit 09a629fd by Waheed Ahmed

Added send for review functionality for parent course.

ECOM-6047
parent b2bb4fe7
...@@ -119,8 +119,9 @@ class CourseStateSerializer(serializers.ModelSerializer): ...@@ -119,8 +119,9 @@ class CourseStateSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data): def update(self, instance, validated_data):
state = validated_data.get('name') state = validated_data.get('name')
request = self.context.get('request')
try: try:
instance.change_state(state=state) instance.change_state(state=state, user=request.user)
except TransitionNotAllowed: except TransitionNotAllowed:
# pylint: disable=no-member # pylint: disable=no-member
raise serializers.ValidationError( raise serializers.ValidationError(
......
...@@ -3,15 +3,15 @@ from django.test import RequestFactory, TestCase ...@@ -3,15 +3,15 @@ from django.test import RequestFactory, TestCase
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from course_discovery.apps.core.tests.factories import UserFactory from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.publisher.api.serializers import ( from course_discovery.apps.core.tests.helpers import make_image_file
CourseRevisionSerializer, CourseRunStateSerializer, CourseStateSerializer, CourseUserRoleSerializer, from course_discovery.apps.publisher.api.serializers import (CourseRevisionSerializer, CourseRunStateSerializer,
GroupUserSerializer, UpdateCourseKeySerializer CourseStateSerializer, CourseUserRoleSerializer,
) GroupUserSerializer, UpdateCourseKeySerializer)
from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole
from course_discovery.apps.publisher.models import CourseRunState, CourseState from course_discovery.apps.publisher.models import CourseRunState, CourseState
from course_discovery.apps.publisher.tests.factories import ( from course_discovery.apps.publisher.tests.factories import (CourseFactory, CourseRunFactory, CourseRunStateFactory,
CourseFactory, CourseRunFactory, CourseRunStateFactory, CourseStateFactory, CourseUserRoleFactory CourseStateFactory, CourseUserRoleFactory,
) OrganizationExtensionFactory)
class CourseUserRoleSerializerTests(TestCase): class CourseUserRoleSerializerTests(TestCase):
...@@ -138,24 +138,36 @@ class CourseStateSerializerTests(TestCase): ...@@ -138,24 +138,36 @@ class CourseStateSerializerTests(TestCase):
def setUp(self): def setUp(self):
super(CourseStateSerializerTests, self).setUp() super(CourseStateSerializerTests, self).setUp()
self.course_state = CourseStateFactory(name=CourseStateChoices.Draft) self.course_state = CourseStateFactory(name=CourseStateChoices.Draft)
self.request = RequestFactory()
self.user = UserFactory()
self.request.user = self.user
def test_update(self): def test_update(self):
""" """
Verify that we can update course workflow state with serializer. Verify that we can update course workflow state with serializer.
""" """
CourseUserRoleFactory(
course=self.course_state.course, role=PublisherUserRole.CourseTeam, user=self.user
)
course = self.course_state.course
course.image = make_image_file('test_banner.jpg')
course.save()
course.organizations.add(OrganizationExtensionFactory().organization)
self.assertNotEqual(self.course_state, CourseStateChoices.Review) self.assertNotEqual(self.course_state, CourseStateChoices.Review)
serializer = self.serializer_class(self.course_state) serializer = self.serializer_class(self.course_state, context={'request': self.request})
data = {'name': CourseStateChoices.Review} data = {'name': CourseStateChoices.Review}
serializer.update(self.course_state, data) serializer.update(self.course_state, data)
self.course_state = CourseState.objects.get(course=self.course_state.course) self.course_state = CourseState.objects.get(course=self.course_state.course)
self.assertEqual(self.course_state.name, CourseStateChoices.Review) self.assertEqual(self.course_state.name, CourseStateChoices.Review)
self.assertEqual(self.course_state.owner_role, PublisherUserRole.MarketingReviewer)
def test_update_with_error(self): def test_update_with_error(self):
""" """
Verify that serializer raises `ValidationError` with wrong transition. Verify that serializer raises `ValidationError` with wrong transition.
""" """
serializer = self.serializer_class(self.course_state) serializer = self.serializer_class(self.course_state, context={'request': self.request})
data = {'name': CourseStateChoices.Approved} data = {'name': CourseStateChoices.Approved}
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
......
...@@ -12,6 +12,7 @@ from guardian.shortcuts import assign_perm ...@@ -12,6 +12,7 @@ from guardian.shortcuts import assign_perm
from mock import patch from mock import patch
from course_discovery.apps.core.tests.factories import USER_PASSWORD, UserFactory from course_discovery.apps.core.tests.factories import USER_PASSWORD, UserFactory
from course_discovery.apps.core.tests.helpers import make_image_file
from course_discovery.apps.course_metadata.tests import toggle_switch from course_discovery.apps.course_metadata.tests import toggle_switch
from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole
from course_discovery.apps.publisher.constants import INTERNAL_USER_GROUP_NAME from course_discovery.apps.publisher.constants import INTERNAL_USER_GROUP_NAME
...@@ -383,15 +384,56 @@ class ChangeCourseStateViewTests(TestCase): ...@@ -383,15 +384,56 @@ class ChangeCourseStateViewTests(TestCase):
self.user = UserFactory() self.user = UserFactory()
self.user.groups.add(Group.objects.get(name=INTERNAL_USER_GROUP_NAME)) self.user.groups.add(Group.objects.get(name=INTERNAL_USER_GROUP_NAME))
course = self.course_state.course
course.image = make_image_file('test_banner.jpg')
course.save()
self.organization_extension = factories.OrganizationExtensionFactory()
course.organizations.add(self.organization_extension.organization)
self.change_state_url = reverse('publisher:api:change_course_state', kwargs={'pk': self.course_state.id}) self.change_state_url = reverse('publisher:api:change_course_state', kwargs={'pk': self.course_state.id})
self.client.login(username=self.user.username, password=USER_PASSWORD) self.client.login(username=self.user.username, password=USER_PASSWORD)
def test_change_course_state(self): def test_change_course_state(self):
""" """
Verify that publisher user can change course workflow state. Verify that marketing user can change course workflow state
and owner role changed to `CourseTeam`.
"""
self.assertNotEqual(self.course_state.name, CourseStateChoices.Review)
factories.CourseUserRoleFactory(
course=self.course_state.course, role=PublisherUserRole.MarketingReviewer, user=self.user
)
factories.CourseUserRoleFactory(
course=self.course_state.course, role=PublisherUserRole.CourseTeam, user=UserFactory()
)
response = self.client.patch(
self.change_state_url,
data=json.dumps({'name': CourseStateChoices.Review}),
content_type=JSON_CONTENT_TYPE
)
self.assertEqual(response.status_code, 200)
self.course_state = CourseState.objects.get(course=self.course_state.course)
self.assertEqual(self.course_state.name, CourseStateChoices.Review)
self.assertEqual(self.course_state.owner_role, PublisherUserRole.CourseTeam)
def test_change_course_state_with_course_team(self):
"""
Verify that course team admin can change course workflow state
and owner role changed to `MarketingReviewer`.
""" """
self.user.groups.remove(Group.objects.get(name=INTERNAL_USER_GROUP_NAME))
self.user.groups.add(self.organization_extension.group)
self.assertNotEqual(self.course_state.name, CourseStateChoices.Review) self.assertNotEqual(self.course_state.name, CourseStateChoices.Review)
factories.CourseUserRoleFactory(
course=self.course_state.course, role=PublisherUserRole.CourseTeam, user=self.user
)
response = self.client.patch( response = self.client.patch(
self.change_state_url, self.change_state_url,
...@@ -404,6 +446,7 @@ class ChangeCourseStateViewTests(TestCase): ...@@ -404,6 +446,7 @@ class ChangeCourseStateViewTests(TestCase):
self.course_state = CourseState.objects.get(course=self.course_state.course) self.course_state = CourseState.objects.get(course=self.course_state.course)
self.assertEqual(self.course_state.name, CourseStateChoices.Review) self.assertEqual(self.course_state.name, CourseStateChoices.Review)
self.assertEqual(self.course_state.owner_role, PublisherUserRole.MarketingReviewer)
def test_change_course_state_with_error(self): def test_change_course_state_with_error(self):
""" """
......
...@@ -494,12 +494,28 @@ class CourseState(TimeStampedModel, ChangedByMixin): ...@@ -494,12 +494,28 @@ class CourseState(TimeStampedModel, ChangedByMixin):
def __str__(self): def __str__(self):
return self.get_name_display() return self.get_name_display()
def can_send_for_review(self):
"""
Validate minimum required fields before sending for review.
"""
course = self.course
return all([
course.title, course.number, course.short_description, course.full_description,
course.organizations.first(), course.level_type, course.expected_learnings,
course.prerequisites, course.primary_subject, course.image, course.course_team_admin
])
@transition(field=name, source='*', target=CourseStateChoices.Draft) @transition(field=name, source='*', target=CourseStateChoices.Draft)
def draft(self): def draft(self):
# TODO: send email etc. # TODO: send email etc.
pass pass
@transition(field=name, source=CourseStateChoices.Draft, target=CourseStateChoices.Review) @transition(
field=name,
source=CourseStateChoices.Draft,
target=CourseStateChoices.Review,
conditions=[can_send_for_review]
)
def review(self): def review(self):
# TODO: send email etc. # TODO: send email etc.
pass pass
...@@ -509,10 +525,15 @@ class CourseState(TimeStampedModel, ChangedByMixin): ...@@ -509,10 +525,15 @@ class CourseState(TimeStampedModel, ChangedByMixin):
# TODO: send email etc. # TODO: send email etc.
pass pass
def change_state(self, state): def change_state(self, state, user):
if state == CourseStateChoices.Draft: if state == CourseStateChoices.Draft:
self.draft() self.draft()
elif state == CourseStateChoices.Review: elif state == CourseStateChoices.Review:
user_role = self.course.course_user_roles.get(user=user)
if user_role.role == PublisherUserRole.MarketingReviewer:
self.owner_role = PublisherUserRole.CourseTeam
elif user_role.role == PublisherUserRole.CourseTeam:
self.owner_role = PublisherUserRole.MarketingReviewer
self.review() self.review()
elif state == CourseStateChoices.Approved: elif state == CourseStateChoices.Approved:
self.approved() self.approved()
......
...@@ -7,6 +7,7 @@ from django_fsm import TransitionNotAllowed ...@@ -7,6 +7,7 @@ from django_fsm import TransitionNotAllowed
from guardian.shortcuts import assign_perm from guardian.shortcuts import assign_perm
from course_discovery.apps.core.tests.factories import UserFactory from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.core.tests.helpers import make_image_file
from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory
from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole
from course_discovery.apps.publisher.mixins import check_course_organization_permission from course_discovery.apps.publisher.mixins import check_course_organization_permission
...@@ -414,6 +415,19 @@ class CourseStateTests(TestCase): ...@@ -414,6 +415,19 @@ class CourseStateTests(TestCase):
def setUpClass(cls): def setUpClass(cls):
super(CourseStateTests, cls).setUpClass() super(CourseStateTests, cls).setUpClass()
cls.course_state = factories.CourseStateFactory(name=CourseStateChoices.Draft) cls.course_state = factories.CourseStateFactory(name=CourseStateChoices.Draft)
cls.user = UserFactory()
factories.CourseUserRoleFactory(
course=cls.course_state.course, role=PublisherUserRole.CourseTeam, user=cls.user
)
def setUp(self):
super(CourseStateTests, self).setUp()
self.course = self.course_state.course
self.course.image = make_image_file('test_banner.jpg')
self.course.save()
self.course.organizations.add(factories.OrganizationExtensionFactory().organization)
def test_str(self): def test_str(self):
""" """
...@@ -432,10 +446,31 @@ class CourseStateTests(TestCase): ...@@ -432,10 +446,31 @@ class CourseStateTests(TestCase):
""" """
self.assertNotEqual(self.course_state.name, state) self.assertNotEqual(self.course_state.name, state)
self.course_state.change_state(state=state) self.course_state.change_state(state=state, user=self.user)
self.assertEqual(self.course_state.name, state) self.assertEqual(self.course_state.name, state)
def test_review_with_condition_failed(self):
"""
Verify that user cannot change state to `Review` if `can_send_for_review` failed.
"""
self.course.image = None
self.assertEqual(self.course_state.name, CourseStateChoices.Draft)
with self.assertRaises(TransitionNotAllowed):
self.course_state.change_state(state=CourseStateChoices.Review, user=self.user)
def test_can_send_for_review(self):
"""
Verify `can_send_for_review` return False if minimum required fields are empty or None.
"""
self.assertTrue(self.course_state.can_send_for_review())
self.course.image = None
self.assertFalse(self.course_state.can_send_for_review())
@ddt.ddt @ddt.ddt
class CourseRunStateTests(TestCase): class CourseRunStateTests(TestCase):
......
...@@ -22,11 +22,10 @@ from course_discovery.apps.core.tests.helpers import make_image_file ...@@ -22,11 +22,10 @@ from course_discovery.apps.core.tests.helpers import make_image_file
from course_discovery.apps.course_metadata.tests import toggle_switch from course_discovery.apps.course_metadata.tests import toggle_switch
from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.choices import PublisherUserRole from course_discovery.apps.publisher.choices import CourseStateChoices, PublisherUserRole
from course_discovery.apps.publisher.constants import ( from course_discovery.apps.publisher.constants import (ADMIN_GROUP_NAME, INTERNAL_USER_GROUP_NAME,
ADMIN_GROUP_NAME, INTERNAL_USER_GROUP_NAME, PARTNER_COORDINATOR_GROUP_NAME, REVIEWER_GROUP_NAME PARTNER_COORDINATOR_GROUP_NAME, REVIEWER_GROUP_NAME)
) from course_discovery.apps.publisher.models import Course, CourseRun, CourseState, OrganizationExtension, Seat, State
from course_discovery.apps.publisher.models import Course, CourseRun, OrganizationExtension, Seat, State
from course_discovery.apps.publisher.tests import factories from course_discovery.apps.publisher.tests import factories
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 get_internal_users, is_email_notification_enabled from course_discovery.apps.publisher.utils import get_internal_users, is_email_notification_enabled
...@@ -1442,6 +1441,9 @@ class CourseDetailViewTests(TestCase): ...@@ -1442,6 +1441,9 @@ class CourseDetailViewTests(TestCase):
self.organization_extension = factories.OrganizationExtensionFactory() self.organization_extension = factories.OrganizationExtensionFactory()
self.course.organizations.add(self.organization_extension.organization) self.course.organizations.add(self.organization_extension.organization)
# Initialize workflow for Course.
self.course_state = factories.CourseStateFactory(course=self.course, owner_role=PublisherUserRole.CourseTeam)
self.detail_page_url = reverse('publisher:publisher_course_detail', args=[self.course.id]) self.detail_page_url = reverse('publisher:publisher_course_detail', args=[self.course.id])
def test_detail_page_without_permission(self): def test_detail_page_without_permission(self):
...@@ -1580,6 +1582,70 @@ class CourseDetailViewTests(TestCase): ...@@ -1580,6 +1582,70 @@ class CourseDetailViewTests(TestCase):
response = self.client.get(self.detail_page_url) response = self.client.get(self.detail_page_url)
self.assertContains(response, '<div id="comments-widget" class="comment-container hidden">') self.assertContains(response, '<div id="comments-widget" class="comment-container hidden">')
def test_course_approval_widget(self):
"""
Verify that user can see approval widget on course detail page.
"""
factories.CourseUserRoleFactory(
course=self.course, user=self.user, role=PublisherUserRole.CourseTeam
)
self.user.groups.add(self.organization_extension.group)
assign_perm(OrganizationExtension.VIEW_COURSE, self.organization_extension.group, self.organization_extension)
response = self.client.get(self.detail_page_url)
self.assertContains(response, 'APPROVALS')
self.assertContains(response, '0 day in ownership')
self.assertContains(response, 'Send for Review')
self.assertContains(response, self.user.full_name)
# Verify that `Send for Review` button is disabled
self.assertContains(response, self.get_expected_data(CourseStateChoices.Review, disabled=True))
# Enable `Send for Review` button by filling all required fields
self.course.image = make_image_file('test_banner1.jpg')
self.course.save()
response = self.client.get(self.detail_page_url)
# Verify that `Send for Review` button is enabled
self.assertContains(response, self.get_expected_data(CourseStateChoices.Review))
def test_course_approval_widget_with_reviewed(self):
"""
Verify that user can see approval widget on course detail page with `Reviewed`.
"""
factories.CourseUserRoleFactory(
course=self.course, user=self.user, role=PublisherUserRole.MarketingReviewer
)
self.course_state.owner_role = PublisherUserRole.MarketingReviewer
self.course_state.save()
new_user = UserFactory()
factories.CourseUserRoleFactory(
course=self.course, user=new_user, role=PublisherUserRole.CourseTeam
)
self.course.course_state.name = CourseStateChoices.Review
self.course.course_state.save()
self.user.groups.add(self.organization_extension.group)
assign_perm(OrganizationExtension.VIEW_COURSE, self.organization_extension.group, self.organization_extension)
response = self.client.get(self.detail_page_url)
# Verify that content is sent for review and user can see Reviewed button.
self.assertContains(response, 'Reviewed')
self.assertContains(response, '<span class="icon fa fa-check" aria-hidden="true">')
self.assertContains(response, 'Send for Review')
self.assertContains(response, self.get_expected_data(CourseStateChoices.Approved))
def get_expected_data(self, state_name, disabled=False):
expected = '<button class="{}" data-change-state-url="{}" data-state-name="{}"{} type="button">'.format(
'btn btn-neutral btn-change-state',
reverse('publisher:api:change_course_state', kwargs={'pk': self.course.course_state.id}),
state_name,
' disabled' if disabled else ''
)
return expected
class CourseEditViewTests(TestCase): class CourseEditViewTests(TestCase):
""" Tests for the course edit view. """ """ Tests for the course edit view. """
...@@ -1593,6 +1659,9 @@ class CourseEditViewTests(TestCase): ...@@ -1593,6 +1659,9 @@ class CourseEditViewTests(TestCase):
self.organization_extension = factories.OrganizationExtensionFactory() self.organization_extension = factories.OrganizationExtensionFactory()
self.course.organizations.add(self.organization_extension.organization) self.course.organizations.add(self.organization_extension.organization)
# Initialize workflow for Course.
CourseState.objects.create(course=self.course, owner_role=PublisherUserRole.CourseTeam)
self.course_team_role = factories.CourseUserRoleFactory(course=self.course, role=PublisherUserRole.CourseTeam) self.course_team_role = factories.CourseUserRoleFactory(course=self.course, role=PublisherUserRole.CourseTeam)
self.organization_extension.group.user_set.add(*(self.user, self.course_team_role.user)) self.organization_extension.group.user_set.add(*(self.user, self.course_team_role.user))
...@@ -1742,6 +1811,32 @@ class CourseEditViewTests(TestCase): ...@@ -1742,6 +1811,32 @@ class CourseEditViewTests(TestCase):
return post_data return post_data
def test_update_course_with_state(self):
"""
Verify that course state changed to `Draft` on updating.
"""
self.client.logout()
self.client.login(username=self.course_team_role.user.username, password=USER_PASSWORD)
self._assign_permissions(self.organization_extension)
self.course.course_state.name = CourseStateChoices.Review
self.course.course_state.save()
post_data = self._post_data(self.organization_extension)
post_data['team_admin'] = self.course_team_role.user.id
response = self.client.post(self.edit_page_url, data=post_data)
self.assertRedirects(
response,
expected_url=reverse('publisher:publisher_course_detail', kwargs={'pk': self.course.id}),
status_code=302,
target_status_code=200
)
course_state = CourseState.objects.get(id=self.course.course_state.id)
self.assertEqual(course_state.name, CourseStateChoices.Draft)
@ddt.ddt @ddt.ddt
class CourseRunEditViewTests(TestCase): class CourseRunEditViewTests(TestCase):
......
...@@ -12,6 +12,7 @@ from django.core.urlresolvers import reverse ...@@ -12,6 +12,7 @@ from django.core.urlresolvers import reverse
from django.db import transaction from django.db import transaction
from django.http import Http404, HttpResponseRedirect, JsonResponse from django.http import Http404, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import CreateView, DetailView, ListView, UpdateView, View from django.views.generic import CreateView, DetailView, ListView, UpdateView, View
from django_fsm import TransitionNotAllowed from django_fsm import TransitionNotAllowed
...@@ -19,14 +20,12 @@ from guardian.shortcuts import get_objects_for_user ...@@ -19,14 +20,12 @@ from guardian.shortcuts import get_objects_for_user
from course_discovery.apps.core.models import User from course_discovery.apps.core.models import User
from course_discovery.apps.publisher import emails, mixins from course_discovery.apps.publisher import emails, mixins
from course_discovery.apps.publisher.choices import PublisherUserRole from course_discovery.apps.publisher.choices import CourseStateChoices, PublisherUserRole
from course_discovery.apps.publisher.forms import CustomCourseForm, CustomCourseRunForm, CustomSeatForm, SeatForm from course_discovery.apps.publisher.forms import CustomCourseForm, CustomCourseRunForm, CustomSeatForm, SeatForm
from course_discovery.apps.publisher.models import ( from course_discovery.apps.publisher.models import (Course, CourseRun, CourseState, CourseUserRole,
Course, CourseRun, CourseUserRole, OrganizationExtension, Seat, State, UserAttributes OrganizationExtension, Seat, State, UserAttributes)
) from course_discovery.apps.publisher.utils import (get_internal_users, is_internal_user, is_partner_coordinator_user,
from course_discovery.apps.publisher.utils import ( is_publisher_admin, make_bread_crumbs)
get_internal_users, is_internal_user, is_partner_coordinator_user, is_publisher_admin, make_bread_crumbs
)
from course_discovery.apps.publisher.wrappers import CourseRunWrapper from course_discovery.apps.publisher.wrappers import CourseRunWrapper
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -42,6 +41,11 @@ ROLE_WIDGET_HEADINGS = { ...@@ -42,6 +41,11 @@ ROLE_WIDGET_HEADINGS = {
PublisherUserRole.CourseTeam: _('Course Team') PublisherUserRole.CourseTeam: _('Course Team')
} }
STATE_BUTTONS = {
CourseStateChoices.Draft: {'text': _('Send for Review'), 'value': CourseStateChoices.Review},
CourseStateChoices.Review: {'text': _('Reviewed'), 'value': CourseStateChoices.Approved}
}
class Dashboard(mixins.LoginRequiredMixin, ListView): class Dashboard(mixins.LoginRequiredMixin, ListView):
""" Create Course View.""" """ Create Course View."""
...@@ -200,9 +204,10 @@ class CreateCourseView(mixins.LoginRequiredMixin, mixins.PublisherUserRequiredMi ...@@ -200,9 +204,10 @@ class CreateCourseView(mixins.LoginRequiredMixin, mixins.PublisherUserRequiredMi
# pass selected organization to CustomCourseForm to populate related # pass selected organization to CustomCourseForm to populate related
# choices into institution admin field # choices into institution admin field
user = self.request.user
organization = self.request.POST.get('organization') organization = self.request.POST.get('organization')
course_form = self.course_form( course_form = self.course_form(
request.POST, request.FILES, user=self.request.user, organization=organization request.POST, request.FILES, user=user, organization=organization
) )
run_form = self.run_form(request.POST) run_form = self.run_form(request.POST)
seat_form = self.seat_form(request.POST) seat_form = self.seat_form(request.POST)
...@@ -215,13 +220,13 @@ class CreateCourseView(mixins.LoginRequiredMixin, mixins.PublisherUserRequiredMi ...@@ -215,13 +220,13 @@ class CreateCourseView(mixins.LoginRequiredMixin, mixins.PublisherUserRequiredMi
run_course = run_form.save(commit=False) run_course = run_form.save(commit=False)
course = course_form.save(commit=False) course = course_form.save(commit=False)
course.changed_by = self.request.user course.changed_by = user
course.save() course.save()
# commit false does not save m2m object. Keyword field is m2m. # commit false does not save m2m object. Keyword field is m2m.
course_form.save_m2m() course_form.save_m2m()
run_course.course = course run_course.course = course
run_course.changed_by = self.request.user run_course.changed_by = user
run_course.save() run_course.save()
# commit false does not save m2m object. # commit false does not save m2m object.
...@@ -229,7 +234,7 @@ class CreateCourseView(mixins.LoginRequiredMixin, mixins.PublisherUserRequiredMi ...@@ -229,7 +234,7 @@ class CreateCourseView(mixins.LoginRequiredMixin, mixins.PublisherUserRequiredMi
if seat: if seat:
seat.course_run = run_course seat.course_run = run_course
seat.changed_by = self.request.user seat.changed_by = user
seat.save() seat.save()
organization_extension = get_object_or_404( organization_extension = get_object_or_404(
...@@ -244,6 +249,9 @@ class CreateCourseView(mixins.LoginRequiredMixin, mixins.PublisherUserRequiredMi ...@@ -244,6 +249,9 @@ class CreateCourseView(mixins.LoginRequiredMixin, mixins.PublisherUserRequiredMi
CourseUserRole.add_course_roles(course=course, role=PublisherUserRole.CourseTeam, CourseUserRole.add_course_roles(course=course, role=PublisherUserRole.CourseTeam,
user=User.objects.get(id=course_form.data['team_admin'])) user=User.objects.get(id=course_form.data['team_admin']))
# Initialize workflow for Course.
CourseState.objects.create(course=course, owner_role=PublisherUserRole.CourseTeam)
# pylint: disable=no-member # pylint: disable=no-member
messages.success( messages.success(
request, _( request, _(
...@@ -319,8 +327,9 @@ class CourseEditView(mixins.PublisherPermissionMixin, UpdateView): ...@@ -319,8 +327,9 @@ class CourseEditView(mixins.PublisherPermissionMixin, UpdateView):
""" """
If the form is valid, update organization and team_admin. If the form is valid, update organization and team_admin.
""" """
user = self.request.user
self.object = form.save(commit=False) self.object = form.save(commit=False)
self.object.changed_by = self.request.user self.object.changed_by = user
self.object.save() self.object.save()
organization = form.cleaned_data['organization'] organization = form.cleaned_data['organization']
...@@ -338,6 +347,10 @@ class CourseEditView(mixins.PublisherPermissionMixin, UpdateView): ...@@ -338,6 +347,10 @@ class CourseEditView(mixins.PublisherPermissionMixin, UpdateView):
course_admin_role.user = team_admin course_admin_role.user = team_admin
course_admin_role.save() course_admin_role.save()
user_role = self.object.course_user_roles.get(user=user)
self.object.course_state.owner_role = user_role.role
self.object.course_state.change_state(state=CourseStateChoices.Draft, user=user)
messages.success(self.request, _('Course updated successfully.')) messages.success(self.request, _('Course updated successfully.'))
return HttpResponseRedirect(self.get_success_url()) return HttpResponseRedirect(self.get_success_url())
...@@ -351,8 +364,9 @@ class CourseDetailView(mixins.LoginRequiredMixin, mixins.PublisherPermissionMixi ...@@ -351,8 +364,9 @@ class CourseDetailView(mixins.LoginRequiredMixin, mixins.PublisherPermissionMixi
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(CourseDetailView, self).get_context_data(**kwargs) context = super(CourseDetailView, self).get_context_data(**kwargs)
user = self.request.user
context['can_edit'] = mixins.check_course_organization_permission( context['can_edit'] = mixins.check_course_organization_permission(
self.request.user, self.object, OrganizationExtension.EDIT_COURSE user, self.object, OrganizationExtension.EDIT_COURSE
) )
context['breadcrumbs'] = make_bread_crumbs( context['breadcrumbs'] = make_bread_crumbs(
...@@ -366,8 +380,38 @@ class CourseDetailView(mixins.LoginRequiredMixin, mixins.PublisherPermissionMixi ...@@ -366,8 +380,38 @@ class CourseDetailView(mixins.LoginRequiredMixin, mixins.PublisherPermissionMixi
context['publisher_hide_features_for_pilot'] = waffle.switch_is_active('publisher_hide_features_for_pilot') context['publisher_hide_features_for_pilot'] = waffle.switch_is_active('publisher_hide_features_for_pilot')
context['publisher_comment_widget_feature'] = waffle.switch_is_active('publisher_comment_widget_feature') context['publisher_comment_widget_feature'] = waffle.switch_is_active('publisher_comment_widget_feature')
context['role_widgets'] = self.get_role_widgets_data()
return context return context
def get_role_widgets_data(self):
""" Create role widgets list for course user roles. """
user = self.request.user
course_state = self.object.course_state
role_widgets = []
for course_role in self.object.course_user_roles.order_by('role'):
role_widget = {
'course_role': course_role,
'heading': ROLE_WIDGET_HEADINGS.get(course_role.role)
}
if course_state.owner_role == course_role.role:
role_widget['ownership'] = timezone.now() - course_state.modified
if user == course_role.user:
role_widget['state_button'] = STATE_BUTTONS.get(course_state.name)
if course_state.name == CourseStateChoices.Draft and not course_state.can_send_for_review():
role_widget['button_disabled'] = True
if course_role.role in [PublisherUserRole.CourseTeam, PublisherUserRole.MarketingReviewer]:
if course_state.owner_role != course_role.role and course_state.name == CourseStateChoices.Review:
role_widget['sent_for_review'] = course_state.modified
role_widgets.append(role_widget)
return role_widgets
class CreateCourseRunView(mixins.LoginRequiredMixin, CreateView): class CreateCourseRunView(mixins.LoginRequiredMixin, CreateView):
""" Create Course Run View.""" """ Create Course Run View."""
......
...@@ -7,14 +7,14 @@ msgid "" ...@@ -7,14 +7,14 @@ 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: 2017-02-10 14:57+0500\n" "POT-Creation-Date: 2017-02-13 14:02+0500\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"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: \n"
#: apps/api/filters.py #: apps/api/filters.py
#, python-brace-format #, python-brace-format
...@@ -736,6 +736,15 @@ msgid "PUBLISHER" ...@@ -736,6 +736,15 @@ msgid "PUBLISHER"
msgstr "" msgstr ""
#: apps/publisher/views.py #: apps/publisher/views.py
#: templates/publisher/course_detail/_approval_widget.html
msgid "Send for Review"
msgstr ""
#: apps/publisher/views.py
msgid "Reviewed"
msgstr ""
#: apps/publisher/views.py
msgid "" msgid ""
"You have successfully created a course. You can edit the course information " "You have successfully created a course. You can edit the course information "
"or enter information for the course About page at any time. An edX project " "or enter information for the course About page at any time. An edX project "
...@@ -1667,6 +1676,14 @@ msgstr "" ...@@ -1667,6 +1676,14 @@ msgstr ""
msgid "Course Level" msgid "Course Level"
msgstr "" msgstr ""
#: templates/publisher/course_detail/_approval_widget.html
msgid "APPROVALS"
msgstr ""
#: templates/publisher/course_detail/_approval_widget.html
msgid "day in ownership"
msgstr ""
#: templates/publisher/course_detail/_widgets.html #: templates/publisher/course_detail/_widgets.html
#: templates/publisher/course_run_detail/_approval_widget.html #: templates/publisher/course_run_detail/_approval_widget.html
msgid "EDIT" msgid "EDIT"
......
...@@ -7,14 +7,14 @@ msgid "" ...@@ -7,14 +7,14 @@ 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: 2017-02-10 14:57+0500\n" "POT-Creation-Date: 2017-02-13 14:02+0500\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"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: \n"
#: static/js/catalogs-change-form.js #: static/js/catalogs-change-form.js
msgid "Preview" msgid "Preview"
......
...@@ -7,14 +7,14 @@ msgid "" ...@@ -7,14 +7,14 @@ 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: 2017-02-10 14:57+0500\n" "POT-Creation-Date: 2017-02-13 14:02+0500\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"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: apps/api/filters.py #: apps/api/filters.py
...@@ -868,6 +868,15 @@ msgid "PUBLISHER" ...@@ -868,6 +868,15 @@ msgid "PUBLISHER"
msgstr "PÛBLÌSHÉR Ⱡ'σяєм ιρѕυм ∂σł#" msgstr "PÛBLÌSHÉR Ⱡ'σяєм ιρѕυм ∂σł#"
#: apps/publisher/views.py #: apps/publisher/views.py
#: templates/publisher/course_detail/_approval_widget.html
msgid "Send for Review"
msgstr "Sénd för Révïéw Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#"
#: apps/publisher/views.py
msgid "Reviewed"
msgstr "Révïéwéd Ⱡ'σяєм ιρѕυм ∂#"
#: apps/publisher/views.py
msgid "" msgid ""
"You have successfully created a course. You can edit the course information " "You have successfully created a course. You can edit the course information "
"or enter information for the course About page at any time. An edX project " "or enter information for the course About page at any time. An edX project "
...@@ -2008,6 +2017,14 @@ msgstr "Çöürsé Ìmägé Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#" ...@@ -2008,6 +2017,14 @@ msgstr "Çöürsé Ìmägé Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#"
msgid "Course Level" msgid "Course Level"
msgstr "Çöürsé Lévél Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#" msgstr "Çöürsé Lévél Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#"
#: templates/publisher/course_detail/_approval_widget.html
msgid "APPROVALS"
msgstr "ÀPPRÖVÀLS Ⱡ'σяєм ιρѕυм ∂σł#"
#: templates/publisher/course_detail/_approval_widget.html
msgid "day in ownership"
msgstr "däý ïn öwnérshïp Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#"
#: templates/publisher/course_detail/_widgets.html #: templates/publisher/course_detail/_widgets.html
#: templates/publisher/course_run_detail/_approval_widget.html #: templates/publisher/course_run_detail/_approval_widget.html
msgid "EDIT" msgid "EDIT"
......
...@@ -7,14 +7,14 @@ msgid "" ...@@ -7,14 +7,14 @@ 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: 2017-02-10 14:57+0500\n" "POT-Creation-Date: 2017-02-13 14:02+0500\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"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: static/js/catalogs-change-form.js #: static/js/catalogs-change-form.js
......
...@@ -122,6 +122,23 @@ $(document).ready(function(){ ...@@ -122,6 +122,23 @@ $(document).ready(function(){
} }
}); });
$('.btn-change-state').click(function (e) {
$.ajax({
type: "PATCH",
url: $(this).data('change-state-url'),
data: JSON.stringify({name: $(this).data('state-name')}),
contentType: "application/json",
success: function (response) {
location.reload();
},
error: function (response) {
$('#stateChangeError').html(response.responseJSON.name);
$('#stateChangeAlert').show();
console.log(response);
}
});
});
}); });
$(document).on('change', '#id_organization', function (e) { $(document).on('change', '#id_organization', function (e) {
......
...@@ -340,7 +340,7 @@ ...@@ -340,7 +340,7 @@
.approval-widget, .course-widgets { .approval-widget, .course-widgets {
.btn-course-edit, .btn-courserun-edit { .btn-course-edit, .btn-courserun-edit, .btn-change-state {
@include padding(2px, 20px, 3px, 20px); @include padding(2px, 20px, 3px, 20px);
@include float(right); @include float(right);
font-weight: 400; font-weight: 400;
...@@ -354,6 +354,10 @@ ...@@ -354,6 +354,10 @@
background: #065683; background: #065683;
color: #f2f8fb; color: #f2f8fb;
} }
&:disabled {
background: rgba(242, 242, 242, 1);
}
} }
.role-heading { .role-heading {
...@@ -362,6 +366,12 @@ ...@@ -362,6 +366,12 @@
.role-assignment-container { .role-assignment-container {
.state-status {
@include float(right);
margin-top: 10px;
font-size: 12px;
}
.field-readonly { .field-readonly {
@include margin-right(10px); @include margin-right(10px);
} }
...@@ -389,6 +399,11 @@ ...@@ -389,6 +399,11 @@
} }
} }
.btn-change-state {
margin-top: 15px;
}
}//approval-widget (END) }//approval-widget (END)
...@@ -399,7 +414,7 @@ ...@@ -399,7 +414,7 @@
font-size: 24px; font-size: 24px;
} }
.course-runs-heading { .course-runs-heading, .approvals-heading {
display: inline-block; display: inline-block;
} }
......
...@@ -8,6 +8,13 @@ ...@@ -8,6 +8,13 @@
{% block page_content %} {% block page_content %}
{% include 'alert_messages.html' %} {% include 'alert_messages.html' %}
<div id="stateChangeAlert" class="alert-messages hidden">
<div class="alert alert-error" role="alert" tabindex="-1">
<div>
<p id="stateChangeError" class="alert-copy"></p>
</div>
</div>
</div>
<div class="layout-1t2t layout-flush publisher-container course-detail"> <div class="layout-1t2t layout-flush publisher-container course-detail">
<main class="layout-col layout-col-b layout-col-b-custom"> <main class="layout-col layout-col-b layout-col-b-custom">
<div class="course-information"> <div class="course-information">
......
{% load i18n %}
<div class="approval-widget">
<div class="margin-top20">
<h5 class="hd-5 emphasized approvals-heading">{% trans "APPROVALS" %}</h5>
</div>
{% for role_widget in role_widgets %}
<div class="role-widget">
<div class="role-assignment-container">
<div class="layout-1q3q layout-reversed">
<div class="layout-col layout-col-a">
{% if role_widget.state_button %}
<button class="btn btn-neutral btn-change-state" data-change-state-url="{% url 'publisher:api:change_course_state' object.course_state.id %}" data-state-name="{{ role_widget.state_button.value }}"{% if role_widget.button_disabled %} disabled{% endif %} type="button">
{{ role_widget.state_button.text }}
</button>
{% elif role_widget.sent_for_review %}
<span class="state-status">
<span class="icon fa fa-check" aria-hidden="true"></span>
{% trans "Send for Review" %}<br>
{{ role_widget.sent_for_review|date:'m/d/y H:i a' }}
</span>
{% endif %}
</div>
<div class="layout-col layout-col-b">
<span class="role-heading">
<strong>{{ role_widget.heading }}</strong>
</span>
{% if role_widget.ownership %}
<span>{{ role_widget.ownership.days }} {% trans "day in ownership" %}</span>
{% endif %}
<div class="field-readonly user-full-name">
{{ role_widget.course_role.user.full_name }}
</div>
</div>
</div>
</div>
</div>
<hr>
{% endfor %}
</div>
...@@ -39,5 +39,7 @@ ...@@ -39,5 +39,7 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% include 'publisher/_history_widget.html' %} {% include 'publisher/_history_widget.html' %}
{% include 'publisher/course_detail/_approval_widget.html' %}
</div> </div>
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