Commit fea4a3f0 by Waheed Ahmed

Implemented change course role assignment.

ECOM-6207
parent 5b78c872
from django.contrib import admin from django.contrib import admin
from course_discovery.apps.publisher.models import ( from course_discovery.apps.publisher.models import (
Course, CourseRun, OrganizationUserRole, Seat, State, UserAttributes Course, CourseRun, CourseUserRole, OrganizationUserRole, Seat, State, UserAttributes
) )
admin.site.register(Course) admin.site.register(Course)
...@@ -10,3 +10,8 @@ admin.site.register(OrganizationUserRole) ...@@ -10,3 +10,8 @@ admin.site.register(OrganizationUserRole)
admin.site.register(Seat) admin.site.register(Seat)
admin.site.register(State) admin.site.register(State)
admin.site.register(UserAttributes) admin.site.register(UserAttributes)
@admin.register(CourseUserRole)
class CourseUserRoleAdmin(admin.ModelAdmin):
raw_id_fields = ('user',)
from rest_framework.permissions import BasePermission
from course_discovery.apps.publisher.mixins import check_view_permission
from course_discovery.apps.publisher.utils import is_internal_user
class CanViewAssociatedCourse(BasePermission):
""" Permission class to check user can view a publisher course. """
def has_object_permission(self, request, view, obj):
return check_view_permission(request.user, obj.course)
class InternalUserPermission(BasePermission):
""" Permission class to check user is an internal user. """
def has_object_permission(self, request, view, obj):
return is_internal_user(request.user)
"""Publisher API Serializers"""
from rest_framework import serializers
from course_discovery.apps.publisher.models import CourseUserRole
class CourseUserRoleSerializer(serializers.ModelSerializer):
"""Serializer for the `CourseUserRole` model to change role assignment. """
class Meta:
model = CourseUserRole
fields = ('course', 'user', 'role',)
read_only_fields = ('course', 'role')
def validate(self, data):
validated_values = super(CourseUserRoleSerializer, self).validate(data)
request = self.context.get('request')
if request:
validated_values.update({'changed_by': request.user})
return validated_values
"""Tests API Serializers."""
from unittest import TestCase
from django.test import RequestFactory
from course_discovery.apps.publisher.api.serializers import CourseUserRoleSerializer
from course_discovery.apps.publisher.tests.factories import CourseUserRoleFactory
class CourseUserRoleSerializerTests(TestCase):
serializer_class = CourseUserRoleSerializer
def setUp(self):
super(CourseUserRoleSerializerTests, self).setUp()
self.request = RequestFactory()
self.course_user_role = CourseUserRoleFactory()
self.request.user = self.course_user_role.user
def get_expected_data(self):
return {
'course': self.course_user_role.course.id,
'user': self.course_user_role.user.id,
'role': self.course_user_role.role,
'changed_by': self.course_user_role.user
}
def test_validation(self):
serializer = self.serializer_class(self.course_user_role, context={'request': self.request})
validated_data = serializer.validate(serializer.data)
self.assertEqual(validated_data, self.get_expected_data())
# pylint: disable=no-member
import json
import ddt
from django.contrib.auth.models import Group
from django.core.urlresolvers import reverse
from django.test import TestCase
from guardian.shortcuts import assign_perm
from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWORD
from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory
from course_discovery.apps.publisher.choices import PublisherUserRole
from course_discovery.apps.publisher.constants import INTERNAL_USER_GROUP_NAME
from course_discovery.apps.publisher.models import Course
from course_discovery.apps.publisher.tests import factories, JSON_CONTENT_TYPE
@ddt.ddt
class CourseRoleAssignmentViewTests(TestCase):
def setUp(self):
super(CourseRoleAssignmentViewTests, self).setUp()
self.course = factories.CourseFactory()
# Create an internal user group and assign four users.
self.internal_user = UserFactory()
internal_user_group = Group.objects.get(name=INTERNAL_USER_GROUP_NAME)
internal_user_group.user_set.add(self.internal_user)
self.other_internal_users = []
for __ in range(3):
user = UserFactory()
self.other_internal_users.append(user)
internal_user_group.user_set.add(user)
assign_perm(Course.VIEW_PERMISSION, internal_user_group, self.course)
organization = OrganizationFactory()
self.course.organizations.add(organization)
# Create three internal user course roles for internal users against a course
# so we can test change role assignment on these roles.
for user, role in zip(self.other_internal_users, PublisherUserRole.choices):
factories.CourseUserRoleFactory(course=self.course, user=user, role=role)
self.client.login(username=self.internal_user.username, password=USER_PASSWORD)
def get_role_assignment_url(self, user_course_role):
return reverse(
'publisher:api:course_role_assignments', kwargs={'pk': user_course_role.id}
)
def test_role_assignment_with_non_internal_user(self):
""" Verify non-internal users cannot change role assignments. """
non_internal_user = UserFactory()
assign_perm(Course.VIEW_PERMISSION, non_internal_user, self.course)
self.client.logout()
self.client.login(username=non_internal_user.username, password=USER_PASSWORD)
response = self.client.patch(
self.get_role_assignment_url(self.course.course_user_roles.first()),
data=json.dumps({'user': non_internal_user.id}),
content_type=JSON_CONTENT_TYPE
)
self.assertEqual(response.status_code, 403)
def get_user_course_roles(self):
return self.course.course_user_roles.all()
@ddt.data(
PublisherUserRole.PartnerCoordinator,
PublisherUserRole.MarketingReviewer,
PublisherUserRole.Publisher
)
def test_change_role_assignment_with_internal_user(self, role_name):
""" Verify that internal user can change course role assignment for
all three internal user course roles to another internal user.
"""
user_course_role = self.course.course_user_roles.get(role__icontains=role_name)
response = self.client.patch(
self.get_role_assignment_url(user_course_role),
data=json.dumps({'user': self.internal_user.id}),
content_type=JSON_CONTENT_TYPE
)
self.assertEqual(response.status_code, 200)
expected = {
'course': self.course.id,
'user': self.internal_user.id,
'role': user_course_role.role
}
self.assertDictEqual(response.data, expected)
self.assertEqual(self.internal_user, self.course.course_user_roles.get(role=user_course_role.role).user)
""" Publisher API URLs. """
from django.conf.urls import url
from course_discovery.apps.publisher.api.views import CourseRoleAssignmentView
urlpatterns = [
url(
r'^course_role_assignments/(?P<pk>\d+)/$', CourseRoleAssignmentView.as_view(), name='course_role_assignments'
),
]
from rest_framework.generics import UpdateAPIView
from rest_framework.permissions import IsAuthenticated
from course_discovery.apps.publisher.models import CourseUserRole
from course_discovery.apps.publisher.api.permissions import CanViewAssociatedCourse, InternalUserPermission
from course_discovery.apps.publisher.api.serializers import CourseUserRoleSerializer
class CourseRoleAssignmentView(UpdateAPIView):
permission_classes = (IsAuthenticated, CanViewAssociatedCourse, InternalUserPermission,)
queryset = CourseUserRole.objects.all()
serializer_class = CourseUserRoleSerializer
# Name of the administrative group for the Publisher app # Name of the administrative group for the Publisher app
ADMIN_GROUP_NAME = 'Publisher Admins' ADMIN_GROUP_NAME = 'Publisher Admins'
INTERNAL_USER_GROUP_NAME = 'Internal Users'
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
from course_discovery.apps.publisher.constants import INTERNAL_USER_GROUP_NAME
def create_internal_user_group(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
Group.objects.get_or_create(name=INTERNAL_USER_GROUP_NAME)
def remove_internal_user_group(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
Group.objects.filter(name=INTERNAL_USER_GROUP_NAME).delete()
class Migration(migrations.Migration):
dependencies = [
('publisher', '0017_auto_20161201_1501'),
('auth', '0006_require_contenttypes_0002'),
]
operations = [
migrations.RunPython(create_internal_user_group, remove_internal_user_group)
]
"""Publisher Serializers""" """Publisher Serializers"""
import waffle import waffle
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
......
...@@ -28,8 +28,7 @@ class UpdateCourseKeySerializerTests(TestCase): ...@@ -28,8 +28,7 @@ class UpdateCourseKeySerializerTests(TestCase):
def test_validation(self): def test_validation(self):
self.course_run.lms_course_id = 'course-v1:edxTest+TC101+2016_Q1' self.course_run.lms_course_id = 'course-v1:edxTest+TC101+2016_Q1'
self.course_run.save() # pylint: disable=no-member self.course_run.save() # pylint: disable=no-member
serializer = self.serializer_class(self.course_run) serializer = self.serializer_class(self.course_run, context={'request': self.request})
serializer.context['request'] = self.request
expected = serializer.validate(serializer.data) expected = serializer.validate(serializer.data)
self.assertEqual(self.get_expected_data(), expected) self.assertEqual(self.get_expected_data(), expected)
......
...@@ -3,9 +3,11 @@ from django.contrib.auth.models import Group ...@@ -3,9 +3,11 @@ from django.contrib.auth.models import Group
from django.test import TestCase from django.test import TestCase
from course_discovery.apps.core.tests.factories import UserFactory from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.publisher.constants import ADMIN_GROUP_NAME from course_discovery.apps.publisher.constants import ADMIN_GROUP_NAME, INTERNAL_USER_GROUP_NAME
from course_discovery.apps.publisher.tests import factories from course_discovery.apps.publisher.tests import factories
from course_discovery.apps.publisher.utils import is_email_notification_enabled, is_publisher_admin from course_discovery.apps.publisher.utils import (
is_email_notification_enabled, is_publisher_admin, is_internal_user, get_internal_users
)
class PublisherUtilsTests(TestCase): class PublisherUtilsTests(TestCase):
...@@ -49,3 +51,21 @@ class PublisherUtilsTests(TestCase): ...@@ -49,3 +51,21 @@ class PublisherUtilsTests(TestCase):
admin_group = Group.objects.get(name=ADMIN_GROUP_NAME) admin_group = Group.objects.get(name=ADMIN_GROUP_NAME)
self.user.groups.add(admin_group) self.user.groups.add(admin_group)
self.assertTrue(is_publisher_admin(self.user)) self.assertTrue(is_publisher_admin(self.user))
def test_is_internal_user(self):
""" Verify the function returns a boolean indicating if the user
is a member of the internal user group.
"""
self.assertFalse(is_internal_user(self.user))
internal_user_group = Group.objects.get(name=INTERNAL_USER_GROUP_NAME)
self.user.groups.add(internal_user_group)
self.assertTrue(is_internal_user(self.user))
def test_get_internal_user(self):
""" Verify the function returns all internal users. """
internal_user_group = Group.objects.get(name=INTERNAL_USER_GROUP_NAME)
self.assertEqual(get_internal_users(), [])
self.user.groups.add(internal_user_group)
self.assertEqual(get_internal_users(), [self.user])
...@@ -8,6 +8,7 @@ from mock import patch ...@@ -8,6 +8,7 @@ from mock import patch
from django.db import IntegrityError from django.db import IntegrityError
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Group
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core import mail from django.core import mail
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -20,11 +21,16 @@ from course_discovery.apps.core.models import User ...@@ -20,11 +21,16 @@ from course_discovery.apps.core.models import User
from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWORD from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWORD
from course_discovery.apps.core.tests.helpers import make_image_file 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.publisher.choices import PublisherUserRole
from course_discovery.apps.publisher.constants import INTERNAL_USER_GROUP_NAME
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.tests import factories, JSON_CONTENT_TYPE from course_discovery.apps.publisher.tests import factories, JSON_CONTENT_TYPE
from course_discovery.apps.publisher.tests.utils import create_non_staff_user_and_login from course_discovery.apps.publisher.tests.utils import create_non_staff_user_and_login
from course_discovery.apps.publisher.utils import is_email_notification_enabled from course_discovery.apps.publisher.utils import is_email_notification_enabled, get_internal_users
from course_discovery.apps.publisher.views import CourseRunDetailView, logger as publisher_views_logger from course_discovery.apps.publisher.views import (
CourseRunDetailView, logger as publisher_views_logger, ROLE_WIDGET_HEADINGS
)
from course_discovery.apps.publisher.wrappers import CourseRunWrapper from course_discovery.apps.publisher.wrappers import CourseRunWrapper
from course_discovery.apps.publisher_comments.tests.factories import CommentFactory from course_discovery.apps.publisher_comments.tests.factories import CommentFactory
...@@ -897,6 +903,52 @@ class CourseRunDetailTests(TestCase): ...@@ -897,6 +903,52 @@ class CourseRunDetailTests(TestCase):
response = self.client.get(page_url) response = self.client.get(page_url)
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_detail_page_with_role_assignment(self):
""" Verify that detail page contains role assignment data for internal user. """
# Add users in internal user group
pc_user = UserFactory()
marketing_user = UserFactory()
publisher_user = UserFactory()
internal_user_group = Group.objects.get(name=INTERNAL_USER_GROUP_NAME)
internal_user_group.user_set.add(*(self.user, pc_user, marketing_user, publisher_user))
assign_perm(Course.VIEW_PERMISSION, internal_user_group, self.course)
organization = OrganizationFactory()
self.course.organizations.add(organization)
# create three course user roles for internal users
for user, role in zip([pc_user, marketing_user, publisher_user], PublisherUserRole.choices):
factories.CourseUserRoleFactory(course=self.course, user=user, role=role)
response = self.client.get(self.page_url)
expected_roles = []
for user_course_role in self.course.course_user_roles.all():
expected_roles.append(
{'user_course_role': user_course_role, 'heading': ROLE_WIDGET_HEADINGS.get(user_course_role.role)}
)
self.assertEqual(response.context['role_widgets'], expected_roles)
self.assertEqual(list(response.context['user_list']), list(get_internal_users()))
def test_detail_page_role_assignment_with_non_internal_user(self):
""" Verify that non internal user can't see change role assignment widget. """
# Create a non internal user and assign course view permission.
non_internal_user = UserFactory()
assign_perm(Course.VIEW_PERMISSION, non_internal_user, self.course)
self.client.logout()
self.client.login(username=non_internal_user.username, password=USER_PASSWORD)
response = self.client.get(self.page_url)
self.assertNotIn('role_widgets', response.context)
self.assertNotIn('user_list', response.context)
class ChangeStateViewTests(TestCase): class ChangeStateViewTests(TestCase):
""" Tests for the `ChangeStateView`. """ """ Tests for the `ChangeStateView`. """
......
""" """
URLs for the course publisher views. URLs for the course publisher views.
""" """
from django.conf.urls import url from django.conf.urls import url, include
from course_discovery.apps.publisher import views from course_discovery.apps.publisher import views
urlpatterns = [ urlpatterns = [
url(r'^api/', include('course_discovery.apps.publisher.api.urls', namespace='api')),
url(r'^dashboard/$', views.Dashboard.as_view(), name='publisher_dashboard'), url(r'^dashboard/$', views.Dashboard.as_view(), name='publisher_dashboard'),
url(r'^courses/new$', views.CreateCourseView.as_view(), name='publisher_courses_new'), url(r'^courses/new$', views.CreateCourseView.as_view(), name='publisher_courses_new'),
url(r'^courses/(?P<pk>\d+)/view/$', views.ReadOnlyView.as_view(), name='publisher_courses_readonly'), url(r'^courses/(?P<pk>\d+)/view/$', views.ReadOnlyView.as_view(), name='publisher_courses_readonly'),
......
""" Publisher Utils.""" """ Publisher Utils."""
from course_discovery.apps.publisher.constants import ADMIN_GROUP_NAME from course_discovery.apps.core.models import User
from course_discovery.apps.publisher.constants import ADMIN_GROUP_NAME, INTERNAL_USER_GROUP_NAME
def is_email_notification_enabled(user): def is_email_notification_enabled(user):
...@@ -27,3 +28,25 @@ def is_publisher_admin(user): ...@@ -27,3 +28,25 @@ def is_publisher_admin(user):
bool: True, if user is an administrator; otherwise, False. bool: True, if user is an administrator; otherwise, False.
""" """
return user.groups.filter(name=ADMIN_GROUP_NAME).exists() return user.groups.filter(name=ADMIN_GROUP_NAME).exists()
def is_internal_user(user):
""" Returns True if the user is an internal user.
Arguments:
user (:obj:`User`): User whose permissions should be checked.
Returns:
bool: True, if user is an internal user; otherwise, False.
"""
return user.groups.filter(name=INTERNAL_USER_GROUP_NAME).exists()
def get_internal_users():
"""
Returns a list of all internal users
Returns:
list
"""
return list(User.objects.filter(groups__name=INTERNAL_USER_GROUP_NAME))
...@@ -17,20 +17,31 @@ from django_fsm import TransitionNotAllowed ...@@ -17,20 +17,31 @@ from django_fsm import TransitionNotAllowed
from guardian.shortcuts import get_objects_for_user from guardian.shortcuts import get_objects_for_user
from rest_framework.generics import UpdateAPIView from rest_framework.generics import UpdateAPIView
from course_discovery.apps.publisher.choices import PublisherUserRole
from course_discovery.apps.publisher.forms import ( from course_discovery.apps.publisher.forms import (
CourseForm, CourseRunForm, SeatForm, CustomCourseForm, CustomCourseRunForm, CustomSeatForm, CourseForm, CourseRunForm, SeatForm, CustomCourseForm, CustomCourseRunForm,
UpdateCourseForm) CustomSeatForm, UpdateCourseForm
)
from course_discovery.apps.publisher import mixins from course_discovery.apps.publisher import mixins
from course_discovery.apps.publisher.models import Course, CourseRun, Seat, State, UserAttributes from course_discovery.apps.publisher.models import (
Course, CourseRun, Seat, State, UserAttributes
)
from course_discovery.apps.publisher.serializers import UpdateCourseKeySerializer from course_discovery.apps.publisher.serializers import UpdateCourseKeySerializer
from course_discovery.apps.publisher.utils import is_internal_user, get_internal_users
from course_discovery.apps.publisher.wrappers import CourseRunWrapper from course_discovery.apps.publisher.wrappers import CourseRunWrapper
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
SEATS_HIDDEN_FIELDS = ['price', 'currency', 'upgrade_deadline', 'credit_provider', 'credit_hours'] SEATS_HIDDEN_FIELDS = ['price', 'currency', 'upgrade_deadline', 'credit_provider', 'credit_hours']
ROLE_WIDGET_HEADINGS = {
PublisherUserRole.PartnerCoordinator: _('PARTNER COORDINATOR'),
PublisherUserRole.MarketingReviewer: _('MARKETING'),
PublisherUserRole.Publisher: _('PUBLISHER'),
PublisherUserRole.CourseTeam: _('Course Team')
}
class Dashboard(mixins.LoginRequiredMixin, ListView): class Dashboard(mixins.LoginRequiredMixin, ListView):
""" Create Course View.""" """ Create Course View."""
...@@ -83,10 +94,32 @@ class CourseRunDetailView(mixins.LoginRequiredMixin, mixins.ViewPermissionMixin, ...@@ -83,10 +94,32 @@ class CourseRunDetailView(mixins.LoginRequiredMixin, mixins.ViewPermissionMixin,
model = CourseRun model = CourseRun
template_name = 'publisher/course_run_detail.html' template_name = 'publisher/course_run_detail.html'
def get_role_widgets_data(self, course_roles):
""" Create role widgets list for course user roles. """
role_widgets = []
for course_role in course_roles:
role_widgets.append(
{
'user_course_role': course_role,
'heading': ROLE_WIDGET_HEADINGS.get(course_role.role)
}
)
return role_widgets
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(CourseRunDetailView, self).get_context_data(**kwargs) context = super(CourseRunDetailView, self).get_context_data(**kwargs)
context['object'] = CourseRunWrapper(context['object'])
context['comment_object'] = self.object.course course_run = CourseRunWrapper(self.get_object())
context['object'] = course_run
context['comment_object'] = course_run.course
# Show role assignment widgets if user is an internal user.
if is_internal_user(self.request.user):
course_roles = course_run.course.course_user_roles.exclude(role=PublisherUserRole.CourseTeam)
context['role_widgets'] = self.get_role_widgets_data(course_roles)
context['user_list'] = get_internal_users()
return context return context
def patch(self, *args, **kwargs): def patch(self, *args, **kwargs):
......
...@@ -7,7 +7,7 @@ msgid "" ...@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-12-01 20:12+0500\n" "POT-Creation-Date: 2016-12-08 15:03+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"
...@@ -421,7 +421,7 @@ msgstr "" ...@@ -421,7 +421,7 @@ msgstr ""
msgid "Publisher" msgid "Publisher"
msgstr "" msgstr ""
#: apps/publisher/choices.py #: apps/publisher/choices.py apps/publisher/views.py
msgid "Course Team" msgid "Course Team"
msgstr "" msgstr ""
...@@ -584,6 +584,18 @@ msgid "Course Role" ...@@ -584,6 +584,18 @@ msgid "Course Role"
msgstr "" msgstr ""
#: apps/publisher/views.py #: apps/publisher/views.py
msgid "PARTNER COORDINATOR"
msgstr ""
#: apps/publisher/views.py
msgid "MARKETING"
msgstr ""
#: apps/publisher/views.py
msgid "PUBLISHER"
msgstr ""
#: apps/publisher/views.py
msgid "Course created successfully." msgid "Course created successfully."
msgstr "" msgstr ""
...@@ -1223,23 +1235,22 @@ msgstr "" ...@@ -1223,23 +1235,22 @@ msgstr ""
msgid "All" msgid "All"
msgstr "" msgstr ""
#. Translators: Studio is an edX tool for course creation.
#: templates/publisher/course_run_detail.html #: templates/publisher/course_run_detail.html
msgid "STUDIO" msgid "STUDIO"
msgstr "" msgstr ""
#. Translators: CAT is an acronym for Course Administration Tool.
#: templates/publisher/course_run_detail.html #: templates/publisher/course_run_detail.html
msgid "CAT" msgid "CAT"
msgstr "" msgstr ""
#. Translators: DRUPAL is an edX marketing site.
#: templates/publisher/course_run_detail.html #: templates/publisher/course_run_detail.html
msgid "DRUPAL" msgid "DRUPAL"
msgstr "" msgstr ""
#: templates/publisher/course_run_detail.html #: templates/publisher/course_run_detail.html
msgid "Salesforce"
msgstr ""
#: templates/publisher/course_run_detail.html
#: templates/publisher/course_run_form.html #: templates/publisher/course_run_form.html
#: templates/publisher/course_runs_list.html #: templates/publisher/course_runs_list.html
msgid "Status" msgid "Status"
...@@ -1384,8 +1395,16 @@ msgstr "" ...@@ -1384,8 +1395,16 @@ msgstr ""
msgid "Additional Notes" msgid "Additional Notes"
msgstr "" msgstr ""
#: templates/publisher/course_run_detail/_all.html #: templates/publisher/course_run_detail/_approval_widget.html
msgid "Edit" msgid "EDIT"
msgstr ""
#: templates/publisher/course_run_detail/_approval_widget.html
msgid "change owner"
msgstr ""
#: templates/publisher/course_run_detail/_approval_widget.html
msgid "CHANGE"
msgstr "" msgstr ""
#: templates/publisher/course_run_detail/_cat.html #: templates/publisher/course_run_detail/_cat.html
......
...@@ -7,7 +7,7 @@ msgid "" ...@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-12-01 20:12+0500\n" "POT-Creation-Date: 2016-12-08 15:03+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"
......
...@@ -7,7 +7,7 @@ msgid "" ...@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-12-01 20:12+0500\n" "POT-Creation-Date: 2016-12-08 15:03+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"
...@@ -525,7 +525,7 @@ msgstr "Märkétïng Révïéwér Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α ...@@ -525,7 +525,7 @@ msgstr "Märkétïng Révïéwér Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α
msgid "Publisher" msgid "Publisher"
msgstr "Püßlïshér Ⱡ'σяєм ιρѕυм ∂σł#" msgstr "Püßlïshér Ⱡ'σяєм ιρѕυм ∂σł#"
#: apps/publisher/choices.py #: apps/publisher/choices.py apps/publisher/views.py
msgid "Course Team" msgid "Course Team"
msgstr "Çöürsé Téäm Ⱡ'σяєм ιρѕυм ∂σłσя #" msgstr "Çöürsé Téäm Ⱡ'σяєм ιρѕυм ∂σłσя #"
...@@ -704,6 +704,18 @@ msgid "Course Role" ...@@ -704,6 +704,18 @@ msgid "Course Role"
msgstr "Çöürsé Rölé Ⱡ'σяєм ιρѕυм ∂σłσя #" msgstr "Çöürsé Rölé Ⱡ'σяєм ιρѕυм ∂σłσя #"
#: apps/publisher/views.py #: apps/publisher/views.py
msgid "PARTNER COORDINATOR"
msgstr "PÀRTNÉR ÇÖÖRDÌNÀTÖR Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#"
#: apps/publisher/views.py
msgid "MARKETING"
msgstr "MÀRKÉTÌNG Ⱡ'σяєм ιρѕυм ∂σł#"
#: apps/publisher/views.py
msgid "PUBLISHER"
msgstr "PÛBLÌSHÉR Ⱡ'σяєм ιρѕυм ∂σł#"
#: apps/publisher/views.py
msgid "Course created successfully." msgid "Course created successfully."
msgstr "Çöürsé çréätéd süççéssfüllý. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#" msgstr "Çöürsé çréätéd süççéssfüllý. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#"
...@@ -1486,23 +1498,22 @@ msgstr "Çöürsé Rün Détäïl Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α ...@@ -1486,23 +1498,22 @@ msgstr "Çöürsé Rün Détäïl Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α
msgid "All" msgid "All"
msgstr "Àll Ⱡ'σяєм#" msgstr "Àll Ⱡ'σяєм#"
#. Translators: Studio is an edX tool for course creation.
#: templates/publisher/course_run_detail.html #: templates/publisher/course_run_detail.html
msgid "STUDIO" msgid "STUDIO"
msgstr "STÛDÌÖ Ⱡ'σяєм ιρѕυ#" msgstr "STÛDÌÖ Ⱡ'σяєм ιρѕυ#"
#. Translators: CAT is an acronym for Course Administration Tool.
#: templates/publisher/course_run_detail.html #: templates/publisher/course_run_detail.html
msgid "CAT" msgid "CAT"
msgstr "ÇÀT Ⱡ'σяєм#" msgstr "ÇÀT Ⱡ'σяєм#"
#. Translators: DRUPAL is an edX marketing site.
#: templates/publisher/course_run_detail.html #: templates/publisher/course_run_detail.html
msgid "DRUPAL" msgid "DRUPAL"
msgstr "DRÛPÀL Ⱡ'σяєм ιρѕυ#" msgstr "DRÛPÀL Ⱡ'σяєм ιρѕυ#"
#: templates/publisher/course_run_detail.html #: templates/publisher/course_run_detail.html
msgid "Salesforce"
msgstr "Sälésförçé Ⱡ'σяєм ιρѕυм ∂σłσ#"
#: templates/publisher/course_run_detail.html
#: templates/publisher/course_run_form.html #: templates/publisher/course_run_form.html
#: templates/publisher/course_runs_list.html #: templates/publisher/course_runs_list.html
msgid "Status" msgid "Status"
...@@ -1654,9 +1665,17 @@ msgstr "" ...@@ -1654,9 +1665,17 @@ msgstr ""
msgid "Additional Notes" msgid "Additional Notes"
msgstr "Àddïtïönäl Nötés Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#" msgstr "Àddïtïönäl Nötés Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#"
#: templates/publisher/course_run_detail/_all.html #: templates/publisher/course_run_detail/_approval_widget.html
msgid "Edit" msgid "EDIT"
msgstr "Édït Ⱡ'σяєм ι#" msgstr "ÉDÌT Ⱡ'σяєм ι#"
#: templates/publisher/course_run_detail/_approval_widget.html
msgid "change owner"
msgstr "çhängé öwnér Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#"
#: templates/publisher/course_run_detail/_approval_widget.html
msgid "CHANGE"
msgstr "ÇHÀNGÉ Ⱡ'σяєм ιρѕυ#"
#: templates/publisher/course_run_detail/_cat.html #: templates/publisher/course_run_detail/_cat.html
msgid "Course Type" msgid "Course Type"
......
...@@ -7,7 +7,7 @@ msgid "" ...@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-12-01 20:12+0500\n" "POT-Creation-Date: 2016-12-08 15:03+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"
......
$.ajaxSetup({
headers: {
'X-CSRFToken': Cookies.get('course_discovery_csrftoken')
}
});
$(document).ready(function() {
$('.btn-change-assignment').click(function(e){
var apiEndpoint = $(this).data('api-endpoint'),
roleName = $(this).data('role'),
$selectedOption = $('#selectUsers-' + roleName + ' option:selected'),
userId = $selectedOption.val(),
userName = $selectedOption.text();
e.preventDefault();
$.ajax({
url: apiEndpoint,
type: 'PATCH',
data: JSON.stringify({'user': userId}),
contentType: 'application/json',
success: function (response) {
$('#userFullName-' + roleName).text(userName);
$selectedOption.val(userId);
$('#userRoleContainer-' + roleName).show();
$('#changeRoleContainer-' + roleName).hide();
}
});
});
$('.change-role-assignment').click(function (e) {
var roleName = $(this).data('role');
e.preventDefault();
$('#changeRoleContainer-' + roleName).show();
$('#userRoleContainer-' + roleName).hide();
});
});
...@@ -17,9 +17,6 @@ $(document).ready(function() { ...@@ -17,9 +17,6 @@ $(document).ready(function() {
courseKeyValue = courseKeyInput.val().trim(), courseKeyValue = courseKeyInput.val().trim(),
courseTitleTag = $courseRunParentTag.find("#course-title").html().trim(), courseTitleTag = $courseRunParentTag.find("#course-title").html().trim(),
startDateTag = $courseRunParentTag.find("#course-start").html().trim(), startDateTag = $courseRunParentTag.find("#course-start").html().trim(),
headers = {
'X-CSRFToken': Cookies.get('course_discovery_csrftoken')
},
$studioInstanceSuccess = $(".studio-instance-success"), $studioInstanceSuccess = $(".studio-instance-success"),
successMessage = interpolateString( successMessage = interpolateString(
gettext("You have successfully created a studio instance ({studioLinkTag}) for {courseRunDetail} with a start date of {startDate}"), gettext("You have successfully created a studio instance ({studioLinkTag}) for {courseRunDetail} with a start date of {startDate}"),
...@@ -36,7 +33,6 @@ $(document).ready(function() { ...@@ -36,7 +33,6 @@ $(document).ready(function() {
type: "PATCH", type: "PATCH",
data: JSON.stringify({lms_course_id: courseKeyValue}), data: JSON.stringify({lms_course_id: courseKeyValue}),
contentType: "application/json", contentType: "application/json",
headers: headers,
success: function (response) { success: function (response) {
data_table_studio.row($courseRunParentTag).remove().draw(); data_table_studio.row($courseRunParentTag).remove().draw();
$("#studio-count").html(data_table_studio.rows().count()); $("#studio-count").html(data_table_studio.rows().count());
......
...@@ -30,17 +30,13 @@ $(document).ready(function () { ...@@ -30,17 +30,13 @@ $(document).ready(function () {
$("#email-switch").change(function(e) { $("#email-switch").change(function(e) {
var isEnabled = this.checked ? true : false, var isEnabled = this.checked ? true : false,
switchLabel = gettext("OFF"), switchLabel = gettext("OFF");
headers = {
'X-CSRFToken': Cookies.get('course_discovery_csrftoken')
};
e.preventDefault(); e.preventDefault();
$.ajax({ $.ajax({
url: "/publisher/user/toggle/email_settings/", url: "/publisher/user/toggle/email_settings/",
type: "POST", type: "POST",
data: {is_enabled: isEnabled}, data: {is_enabled: isEnabled},
headers: headers,
success: function (response) { success: function (response) {
if (response.is_enabled) { if (response.is_enabled) {
switchLabel = gettext("ON") switchLabel = gettext("ON")
......
...@@ -637,3 +637,54 @@ select { ...@@ -637,3 +637,54 @@ select {
.hidden { .hidden {
display: none; display: none;
} }
.approval-widget {
.btn-course-edit {
@include padding(2px, 20px, 3px, 20px);
font-weight: 400;
font-size: 14px;
background: white;
border-radius: 5px;
&:hover, &:focus {
border-color: #065683;
background: #065683;
color: #f2f8fb;
}
}
.role-heading {
color: #999999;
}
.role-assignment-container {
.field-readonly {
@include margin-right(10px);
}
.change-role-container {
display: none;
.select-users-by-role {
@include margin-right(20px);
}
.btn-change-assignment {
@include padding(2px, 10px, 3px, 10px);
font-weight: 400;
font-size: 14px;
background: white;
border-radius: 5px;
&:hover, &:focus {
border-color: #065683;
background: #065683;
color: #f2f8fb;
}
}
}
}
}
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
{% load comments %} {% load comments %}
{% if user.is_authenticated and comment_object %} {% if user.is_authenticated and comment_object %}
<div class="comments-container"> <div>
<p>{% trans 'Add new comment' %}</p> <p>{% trans 'Add new comment' %}</p>
<div> <div>
{% get_comment_form for comment_object as form %} {% get_comment_form for comment_object as form %}
......
{% load i18n %} {% load i18n %}
{% load comments %} {% load comments %}
{% if comment_object %} {% if comment_object %}
<div class="comments-container"> <div>
{% get_comment_count for comment_object as comment_count %} {% get_comment_count for comment_object as comment_count %}
<h4 class="hd-4"> <h4 class="hd-4">
{% blocktrans with comment_count=comment_count trimmed %} {% blocktrans with comment_count=comment_count trimmed %}
......
...@@ -57,6 +57,7 @@ ...@@ -57,6 +57,7 @@
<script src="{% static 'bower_components/moment/moment.js' %}"></script> <script src="{% static 'bower_components/moment/moment.js' %}"></script>
<script src="{% static 'bower_components/pikaday/pikaday.js' %}"></script> <script src="{% static 'bower_components/pikaday/pikaday.js' %}"></script>
<script src="{% static 'bower_components/datatables/media/js/jquery.dataTables.js' %}"></script> <script src="{% static 'bower_components/datatables/media/js/jquery.dataTables.js' %}"></script>
<script src="{% static 'js/publisher/main.js' %}"></script>
<script src="{% static 'js/publisher/views/navbar.js' %}"></script> <script src="{% static 'js/publisher/views/navbar.js' %}"></script>
<script src="{% static 'js/publisher/utils.js' %}"></script> <script src="{% static 'js/publisher/utils.js' %}"></script>
{% endcompress %} {% endcompress %}
......
...@@ -9,14 +9,18 @@ ...@@ -9,14 +9,18 @@
{% endblock title %} {% endblock title %}
{% block page_content %} {% block page_content %}
<div class="publisher-container course-detail"> <div class="layout-1t2t layout-flush publisher-container course-detail">
<main class="layout-col layout-col-b">
<nav class="administration-nav"> <nav class="administration-nav">
<div class="tab-container"> <div class="tab-container">
<button class="selected" data-tab="#tab-1">{% trans "All" %}</button> <button class="selected" data-tab="#tab-1">{% trans "All" %}</button>
{% comment %}Translators: Studio is an edX tool for course creation.{% endcomment %}
<button data-tab="#tab-2">{% trans "STUDIO" %}</button> <button data-tab="#tab-2">{% trans "STUDIO" %}</button>
{% comment %}Translators: CAT is an acronym for Course Administration Tool.{% endcomment %}
<button data-tab="#tab-3">{% trans "CAT" %}</button> <button data-tab="#tab-3">{% trans "CAT" %}</button>
{% comment %}Translators: DRUPAL is an edX marketing site.{% endcomment %}
<button data-tab="#tab-4">{% trans "DRUPAL" %}</button> <button data-tab="#tab-4">{% trans "DRUPAL" %}</button>
<button data-tab="#tab-5">{% trans "Salesforce" %}</button> <button data-tab="#tab-5">Salesforce</button>
</div> </div>
</nav> </nav>
...@@ -55,18 +59,17 @@ ...@@ -55,18 +59,17 @@
{% include 'publisher/course_run_detail/_salesforce.html' %} {% include 'publisher/course_run_detail/_salesforce.html' %}
</div> </div>
</div> </div>
<div class="actions">
<form action="{% url 'publisher:publisher_change_state' course_run_id=object.id %}" method="post"> {% csrf_token %}
<button type="submit" value="{{ object.change_state_button.value }}" class="btn-brand btn-small btn-states" name="state">{{ object.change_state_button.text }}</button>
</form>
</div>
</div> </div>
</main>
<aside class="layout-col layout-col-a">
{% include 'publisher/course_run_detail/_approval_widget.html' %}
</aside>
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
<script src="{% static 'bower_components/clipboard/dist/clipboard.min.js' %}"></script> <script src="{% static 'bower_components/clipboard/dist/clipboard.min.js' %}"></script>
<script src="{% static 'js/publisher/views/course_detail.js' %}"></script>
<script src="{% static 'js/publisher/publisher.js' %}"></script> <script src="{% static 'js/publisher/publisher.js' %}"></script>
<script src="{% static 'js/publisher/comments.js' %}"></script> <script src="{% static 'js/publisher/comments.js' %}"></script>
<script> <script>
......
...@@ -256,12 +256,4 @@ ...@@ -256,12 +256,4 @@
<div class="copy">{{ object.notes }}</div> <div class="copy">{{ object.notes }}</div>
</div> </div>
</div> </div>
<div class="comment-container">
<a href="{% url 'publisher:publisher_course_runs_edit' pk=object.id %}" class="btn btn-neutral btn-add">
<span class="icon fa fa-edit" aria-hidden="true"></span>&nbsp;&nbsp;{% trans "Edit" %}
</a>
{% include 'comments/comments_list.html' %}
{% include 'comments/add_auth_comments.html' %}
</div>
<div class="clearfix"></div> <div class="clearfix"></div>
{% load i18n %}
<div class="approval-widget">
<a href="{% url 'publisher:publisher_course_runs_edit' pk=object.id %}" class="btn btn-neutral btn-course-edit">{% trans "EDIT" %}</a>
{% for role_widget in role_widgets %}
<div class="role-widget">
<hr>
<span class="role-heading">
<strong>{{ role_widget.heading }}</strong>
</span>
<div class="role-assignment-container">
<div id="userRoleContainer-{{ role_widget.user_course_role.role }}">
<span id="userFullName-{{ role_widget.user_course_role.role }}" class="field-readonly user-full-name">
{{ role_widget.user_course_role.user.full_name }}
</span>
<a class="change-role-assignment" data-role="{{ role_widget.user_course_role.role }}" href="#">
{% trans "change owner" %}
</a>
</div>
<div class="change-role-container" id="changeRoleContainer-{{ role_widget.user_course_role.role }}">
<select class="select-users-by-role" id="selectUsers-{{ role_widget.user_course_role.role }}">
{% for user in user_list %}
<option {% if role_widget.user_course_role.user == user%}selected="selected"{% endif %} value="{{ user.id }}">
{{ user.full_name }}
</option>
{% endfor %}
</select>
<input type="hidden" id="roleName" value="{{ role_widget.user_course_role.role }}">
<button type="button" class="btn-neutral btn-change-assignment" data-role="{{ role_widget.user_course_role.role }}" data-api-endpoint="{% url 'publisher:api:course_role_assignments' role_widget.user_course_role.id %}">
{% trans "CHANGE" %}
</button>
</div>
</div>
</div>
{% endfor %}
<hr>
<div class="actions">
<form action="{% url 'publisher:publisher_change_state' course_run_id=object.id %}" method="post"> {% csrf_token %}
<button type="submit" value="{{ object.change_state_button.value }}" class="btn-brand btn-small btn-states" name="state">{{ object.change_state_button.text }}</button>
</form>
</div>
<div class="comment-container">
{% include 'comments/comments_list.html' %}
{% include 'comments/add_auth_comments.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