Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 40 additions & 7 deletions cms/djangoapps/contentstore/views/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@
CourseInstructorRole,
CourseStaffRole,
GlobalStaff,
UserBasedRole
UserBasedRole,
CourseRerunCreatorRole,
OrgRerunCreatorRole,
OrgCourseCreatorRole,
)
from common.djangoapps.util.course import get_link_for_about_page
from common.djangoapps.util.date_utils import get_default_time_display
Expand Down Expand Up @@ -304,10 +307,11 @@ def course_rerun_handler(request, course_key_string):
GET
html: return html page with form to rerun a course for the given course id
"""
# Only global staff (PMs) are able to rerun courses during the soft launch
if not GlobalStaff().has_user(request.user):
raise PermissionDenied()
course_key = CourseKey.from_string(course_key_string)

if not GlobalStaff().has_user(request.user) and not _rerun_permission(request.user, course_key):
raise PermissionDenied()

with modulestore().bulk_operations(course_key):
course_module = get_course_and_check_access(course_key, request.user, depth=3)
if request.method == 'GET':
Expand Down Expand Up @@ -550,6 +554,8 @@ def format_in_process_course_view(uca):
split_archived = settings.FEATURES.get('ENABLE_SEPARATE_ARCHIVED_COURSES', False)
active_courses, archived_courses = _process_courses_list(courses_iter, in_process_course_actions, split_archived)
in_process_course_actions = [format_in_process_course_view(uca) for uca in in_process_course_actions]
active_courses = _set_rerun_permission_for_courses(user, active_courses)
archived_courses = _set_rerun_permission_for_courses(user, archived_courses)

return render_to_response('index.html', {
'courses': active_courses,
Expand Down Expand Up @@ -849,11 +855,17 @@ def _create_or_rerun_course(request):
Returns the destination course_key and overriding fields for the new course.
Raises DuplicateCourseError and InvalidKeyError
"""
if not auth.user_has_role(request.user, CourseCreatorRole()):
raise PermissionDenied()

try:
org = request.json.get('org')

rerun_permission = (
OrgRerunCreatorRole(org).has_user(request.user)
or OrgCourseCreatorRole(org).has_user(request.user)
)

if not auth.user_has_role(request.user, CourseCreatorRole()) and not rerun_permission:
raise PermissionDenied()

course = request.json.get('number', request.json.get('course'))
display_name = request.json.get('display_name')
# force the start date for reruns and allow us to override start via the client
Expand Down Expand Up @@ -1863,3 +1875,24 @@ def _get_course_creator_status(user):
course_creator_status = 'granted'

return course_creator_status


def _rerun_permission(user, course_key):
"""
Helper method to check if user can rerun-course
"""
return (
CourseRerunCreatorRole(course_key).has_user(user)
or OrgRerunCreatorRole(course_key.org).has_user(user)
)


def _set_rerun_permission_for_courses(user, courses):
"""
iterate over courses dict and set the key 'rerun_permission'
"""
for course in courses:
course_key = CourseKey.from_string(course['course_key'])
course['rerun_permission'] = _rerun_permission(user, course_key)

return courses
4 changes: 2 additions & 2 deletions cms/djangoapps/contentstore/views/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ class OrganizationListView(View):
@method_decorator(login_required)
def get(self, request, *args, **kwargs): # lint-amnesty, pylint: disable=unused-argument
"""Returns organization list as json."""
organizations = get_organizations()
org_names_list = [(org["short_name"]) for org in organizations]
# EDUNEXT: Organizations list must not be visible for users, reason why an empty array is returned
org_names_list = []
return HttpResponse(dump_js_escaped_json(org_names_list), content_type='application/json; charset=utf-8') # lint-amnesty, pylint: disable=http-response-with-content-type-json
12 changes: 6 additions & 6 deletions cms/djangoapps/contentstore/views/tests/test_course_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,13 +385,13 @@ def check_index_page(self, separate_archived_courses, org):

@ddt.data(
# Staff user has course staff access
(True, 'staff', None, 3, 18),
(False, 'staff', None, 3, 18),
(True, 'staff', None, 3, 20),
(False, 'staff', None, 3, 20),
# Base user has global staff access
(True, 'user', ORG, 3, 18),
(False, 'user', ORG, 3, 18),
(True, 'user', None, 3, 18),
(False, 'user', None, 3, 18),
(True, 'user', ORG, 3, 20),
(False, 'user', ORG, 3, 20),
(True, 'user', None, 3, 20),
(False, 'user', None, 3, 20),
)
@ddt.unpack
def test_separate_archived_courses(self, separate_archived_courses, username, org, mongo_queries, sql_queries):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ def setUp(self):
self.staff = UserFactory(is_staff=True)
self.client.login(username=self.staff.username, password='test')
self.org_names_listing_url = reverse('organizations')
self.org_short_names = ["alphaX", "betaX", "orgX"]
# EDUNEXT: Organizations list must not be visible for users, reason why an empty array is set
self.org_short_names = []
for index, short_name in enumerate(self.org_short_names):
add_organization(organization_data={
'name': 'Test Organization %s' % index,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function CourseOrLibraryListing(props) {
</a>
{ item.lms_link && item.rerun_link &&
<ul className="item-actions course-actions">
{ allowReruns &&
{ (allowReruns || item.rerun_permission) &&
<li className="action action-rerun">
<a
href={item.rerun_link}
Expand Down
45 changes: 35 additions & 10 deletions cms/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from openedx.core.djangolib.js_utils import (
dump_js_escaped_json
)
from opaque_keys.edx.keys import CourseKey
from common.djangoapps.student.roles import OrgRerunCreatorRole, UserBasedRole
%>

<%inherit file="base.html" />
Expand All @@ -22,6 +24,21 @@
</%block>

<%block name="content">

<%
org_course_creator_status = 'notgranted'
org_course_creator_allowed_org = []
all_roles = UserBasedRole(request.user, 'org_course_creator_group').courses_with_role()
if all_roles:
org_course_creator_status = 'granted'
org_course_creator_allowed_org = [x.org for x in all_roles]
else:
org_course_creator_status = 'notgranted'
org_course_creator_allowed_org = []
course_creator_permission_granted = course_creator_status == 'granted' or org_course_creator_status == 'granted'
course_creator_permission_denied = course_creator_status == 'unrequested' and org_course_creator_status != 'granted'
%>

<div class="wrapper-mast wrapper">
<header class="mast has-actions">
<h1 class="page-header">${_("{studio_name} Home").format(studio_name=settings.STUDIO_SHORT_NAME)}</h1>
Expand All @@ -31,7 +48,7 @@ <h1 class="page-header">${_("{studio_name} Home").format(studio_name=settings.ST
<h3 class="sr">${_("Page Actions")}</h3>
<ul>
<li class="nav-item">
% if course_creator_status=='granted':
% if course_creator_permission_granted:
<a href="#" class="button new-button new-course-button"><span class="icon fa fa-plus icon-inline" aria-hidden="true"></span>
${_("New Course")}</a>
% elif course_creator_status=='disallowed_for_this_site' and settings.FEATURES.get('STUDIO_REQUEST_EMAIL',''):
Expand All @@ -53,7 +70,7 @@ <h3 class="sr">${_("Page Actions")}</h3>
<section class="content">
<article class="content-primary" role="main">

% if course_creator_status=='granted':
% if course_creator_permission_granted:
<div class="wrapper-create-element wrapper-create-course">
<form class="form-create create-course course-info" id="create-course-form" name="create-course-form">
<div class="wrap-error">
Expand Down Expand Up @@ -81,12 +98,20 @@ <h3 class="title">${_("Create a New Course")}</h3>
<label for="new-course-org">${_("Organization")}</label>
## Translators: This is an example for the name of the organization sponsoring a course, seen when filling out the form to create a new course. The organization name cannot contain spaces.
## Translators: "e.g. UniversityX or OrganizationX" is a placeholder displayed when user put no data into this field.
<input class="new-course-org" id="new-course-org" type="text" name="new-course-org" required placeholder="${_('e.g. UniversityX or OrganizationX')}" aria-describedby="tip-new-course-org tip-error-new-course-org" />
<span class="tip" id="tip-new-course-org">${Text(_("The name of the organization sponsoring the course. {strong_start}Note: The organization name is part of the course URL.{strong_end} This cannot be changed, but you can set a different display name in Advanced Settings later.")).format(
strong_start=HTML('<strong>'),
strong_end=HTML('</strong>'),
)}</span>
<span class="tip tip-error is-hiding" id="tip-error-new-course-org"></span>
% if org_course_creator_allowed_org:
<select class="new-course-org" id="new-course-org" name="new-course-org" required aria-describedby="tip-new-course-org tip-error-new-course-org">
%for org in org_course_creator_allowed_org:
<option value="${ org }">${ org }</option>
%endfor
</select>
% else:
<input class="new-course-org" id="new-course-org" type="text" name="new-course-org" required placeholder="${_('e.g. UniversityX or OrganizationX')}" aria-describedby="tip-new-course-org tip-error-new-course-org" />
<span class="tip" id="tip-new-course-org">${Text(_("The name of the organization sponsoring the course. {strong_start}Note: The organization name is part of the course URL.{strong_end} This cannot be changed, but you can set a different display name in Advanced Settings later.")).format(
strong_start=HTML('<strong>'),
strong_end=HTML('</strong>'),
)}</span>
<span class="tip tip-error is-hiding" id="tip-error-new-course-org"></span>
% endif
</li>

<li class="field text required" id="field-course-number">
Expand Down Expand Up @@ -372,7 +397,7 @@ <h3 class="title">${_("Are you staff on an existing {studio_name} course?").form
</div>
</div>

%if course_creator_status == "granted":
%if course_creator_permission_granted:
<div class="notice-item has-actions">
<div class="msg">
<h3 class="title">${_('Create Your First Course')}</h3>
Expand All @@ -393,7 +418,7 @@ <h3 class="title">${_('Create Your First Course')}</h3>
% endif


%if course_creator_status == "unrequested":
%if course_creator_permission_denied:
<div class="wrapper wrapper-creationrights">
<h3 class="title">
<a href="#instruction-creationrights" class="ui-toggle-control show-creationrights"><span class="label">${_('Becoming a Course Creator in {studio_name}').format(studio_name=settings.STUDIO_SHORT_NAME)}</span> <span class="icon fa fa-times-circle" aria-hidden="true"></span></a>
Expand Down
27 changes: 27 additions & 0 deletions common/djangoapps/student/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,33 @@ def __init__(self, *args, **kwargs):
super().__init__(self.ROLE, *args, **kwargs)


@register_access_role
class CourseRerunCreatorRole(CourseRole):
"""A course staff with ability to rerun"""
ROLE = 'rerun_creator'

def __init__(self, *args, **kwargs):
super().__init__(self.ROLE, *args, **kwargs)


@register_access_role
class OrgRerunCreatorRole(OrgRole):
"""An ORG staff with ability to rerun all courses"""
ROLE = 'org_rerun_creator_group'

def __init__(self, *args, **kwargs):
super().__init__(self.ROLE, *args, **kwargs)


@register_access_role
class OrgCourseCreatorRole(OrgRole):
"""An ORG staff with ability to create new courses"""
ROLE = 'org_course_creator_group'

def __init__(self, *args, **kwargs):
super().__init__(self.ROLE, *args, **kwargs)


@register_access_role
class LibraryUserRole(CourseRole):
"""
Expand Down