Merge pull request #7404 from edx/waheed/plat407-decorate-instructor-dashboard-with-sudo-required
Decorated instructor dashboard with sudo_required.
This commit is contained in:
@@ -33,6 +33,7 @@ Feature: CMS.Help
|
||||
Then I should see online help for "grading"
|
||||
|
||||
And I am viewing the course team settings
|
||||
And I get sudo access with password "test"
|
||||
Then I should see online help for "course-team"
|
||||
|
||||
And I select the Advanced Settings
|
||||
|
||||
@@ -1343,6 +1343,7 @@ class ContentStoreTest(ContentStoreTestCase):
|
||||
resp = self._show_course_overview(course_key)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertContains(resp, 'Chapter 2')
|
||||
self.grant_sudo_access(unicode(course_key), self.user_password)
|
||||
|
||||
# go to various pages
|
||||
test_get_html('import_handler')
|
||||
|
||||
@@ -22,10 +22,10 @@ class TestCourseAccess(ModuleStoreTestCase):
|
||||
|
||||
Create a pool of users w/o granting them any permissions
|
||||
"""
|
||||
user_password = super(TestCourseAccess, self).setUp()
|
||||
self.user_password = super(TestCourseAccess, self).setUp()
|
||||
|
||||
self.client = AjaxEnabledTestClient()
|
||||
self.client.login(username=self.user.username, password=user_password)
|
||||
self.client.login(username=self.user.username, password=self.user_password)
|
||||
|
||||
# create a course via the view handler which has a different strategy for permissions than the factory
|
||||
self.course_key = self.store.make_course_key('myu', 'mydept.mycourse', 'myrun')
|
||||
@@ -93,6 +93,7 @@ class TestCourseAccess(ModuleStoreTestCase):
|
||||
user_by_role[role].append(user)
|
||||
self.assertTrue(auth.has_course_author_access(user, self.course_key), "{} does not have access".format(user))
|
||||
|
||||
self.grant_sudo_access(unicode(self.course_key), self.user_password)
|
||||
course_team_url = reverse_course_url('course_team_handler', self.course_key)
|
||||
response = self.client.get_html(course_team_url)
|
||||
for role in [CourseInstructorRole, CourseStaffRole]: # Global and org-based roles don't appear on this page
|
||||
|
||||
@@ -29,6 +29,7 @@ from opaque_keys.edx.keys import UsageKey
|
||||
|
||||
from student.auth import has_course_author_access
|
||||
from django.utils.translation import ugettext as _
|
||||
from sudo.utils import revoke_sudo_privileges
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
|
||||
__all__ = ['OPEN_ENDED_COMPONENT_TYPES',
|
||||
@@ -163,6 +164,12 @@ def container_handler(request, usage_key_string):
|
||||
with modulestore().bulk_operations(usage_key.course_key):
|
||||
try:
|
||||
course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key)
|
||||
|
||||
# Revoke sudo privileges from a request explicitly
|
||||
region = unicode(course.id)
|
||||
if request.is_sudo(region=region):
|
||||
revoke_sudo_privileges(request, region=region)
|
||||
|
||||
except ItemNotFoundError:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django_sudo_helpers.decorators import sudo_required
|
||||
from sudo.utils import revoke_sudo_privileges
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
@@ -68,6 +70,11 @@ def _display_library(library_key_string, request):
|
||||
"""
|
||||
Displays single library
|
||||
"""
|
||||
|
||||
# Revoke sudo privileges from a request explicitly
|
||||
if request.is_sudo(region=library_key_string):
|
||||
revoke_sudo_privileges(request, region=library_key_string)
|
||||
|
||||
library_key = CourseKey.from_string(library_key_string)
|
||||
if not isinstance(library_key, LibraryLocator):
|
||||
log.exception("Non-library key passed to content libraries API.") # Should never happen due to url regex
|
||||
@@ -197,6 +204,7 @@ def library_blocks_view(library, user, response_format):
|
||||
})
|
||||
|
||||
|
||||
@sudo_required
|
||||
def manage_library_users(request, library_key_string):
|
||||
"""
|
||||
Studio UI for editing the users within a library.
|
||||
|
||||
@@ -12,6 +12,7 @@ from django.utils import http
|
||||
|
||||
import contentstore.views.component as views
|
||||
from contentstore.views.tests.utils import StudioPageTestCase
|
||||
from django_sudo_helpers.tests.utils import sudo_middleware_process_request
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.factories import ItemFactory
|
||||
|
||||
@@ -171,6 +172,7 @@ class ContainerPageTestCase(StudioPageTestCase):
|
||||
"""
|
||||
request = RequestFactory().get('foo')
|
||||
request.user = self.user
|
||||
sudo_middleware_process_request(request)
|
||||
|
||||
# Check for invalid 'usage_key_strings'
|
||||
self.assertRaises(
|
||||
|
||||
@@ -114,6 +114,7 @@ class TestCourseIndex(CourseTestCase):
|
||||
"""
|
||||
course_staff_client, course_staff = self.create_non_staff_authed_user_client()
|
||||
for course in [self.course, self.odd_course]:
|
||||
self.grant_sudo_access(unicode(course.id), 'foo')
|
||||
permission_url = reverse_course_url('course_team_handler', course.id, kwargs={'email': course_staff.email})
|
||||
|
||||
self.client.post(
|
||||
|
||||
@@ -30,10 +30,10 @@ class UnitTestLibraries(ModuleStoreTestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
user_password = super(UnitTestLibraries, self).setUp()
|
||||
self.user_password = super(UnitTestLibraries, self).setUp()
|
||||
|
||||
self.client = AjaxEnabledTestClient()
|
||||
self.client.login(username=self.user.username, password=user_password)
|
||||
self.client.login(username=self.user.username, password=self.user_password)
|
||||
|
||||
######################################################
|
||||
# Tests for /library/ - list and create libraries:
|
||||
@@ -207,6 +207,7 @@ class UnitTestLibraries(ModuleStoreTestCase):
|
||||
"""
|
||||
library = LibraryFactory.create()
|
||||
extra_user, _ = self.create_non_staff_user()
|
||||
self.grant_sudo_access(unicode(library.location.library_key), self.user_password)
|
||||
manage_users_url = reverse_library_url('manage_library_users', unicode(library.location.library_key))
|
||||
|
||||
response = self.client.get(manage_users_url)
|
||||
|
||||
@@ -14,6 +14,7 @@ from student import auth
|
||||
class UsersTestCase(CourseTestCase):
|
||||
def setUp(self):
|
||||
super(UsersTestCase, self).setUp()
|
||||
self.grant_sudo_access(unicode(self.course.id), self.user_password)
|
||||
self.ext_user = User.objects.create_user(
|
||||
"joe", "joe@comedycentral.com", "haha")
|
||||
self.ext_user.is_active = True
|
||||
|
||||
@@ -11,6 +11,7 @@ from xmodule.modulestore.django import modulestore
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locator import LibraryLocator
|
||||
from util.json_request import JsonResponse, expect_json
|
||||
from django_sudo_helpers.decorators import sudo_required
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole, LibraryUserRole
|
||||
from course_creators.views import user_requested_access
|
||||
|
||||
@@ -38,6 +39,7 @@ def request_course_creator(request):
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(("GET", "POST", "PUT", "DELETE"))
|
||||
@sudo_required
|
||||
def course_team_handler(request, course_key_string=None, email=None):
|
||||
"""
|
||||
The restful handler for course team users.
|
||||
|
||||
@@ -5,7 +5,7 @@ django admin page for the course creators table
|
||||
from course_creators.models import CourseCreator, update_creator_state, send_user_notification, send_admin_notification
|
||||
from course_creators.views import update_course_creator_group
|
||||
|
||||
from ratelimitbackend import admin
|
||||
from django.contrib import admin
|
||||
from django.conf import settings
|
||||
from django.dispatch import receiver
|
||||
from edxmako.shortcuts import render_to_string
|
||||
|
||||
@@ -11,6 +11,7 @@ import mock
|
||||
from course_creators.admin import CourseCreatorAdmin
|
||||
from course_creators.models import CourseCreator
|
||||
from django.core import mail
|
||||
from sudo.utils import region_name
|
||||
from student.roles import CourseCreatorRole
|
||||
from student import auth
|
||||
|
||||
@@ -46,6 +47,16 @@ class CourseCreatorAdminTest(TestCase):
|
||||
"STUDIO_REQUEST_EMAIL": self.studio_request_email
|
||||
}
|
||||
|
||||
def grant_sudo_access(self, region, password):
|
||||
"""
|
||||
Grant sudo access to staff or instructor user.
|
||||
"""
|
||||
self.client.post(
|
||||
'/sudo/?region={}'.format(region_name(region)),
|
||||
{'password': password},
|
||||
follow=True
|
||||
)
|
||||
|
||||
@mock.patch('course_creators.admin.render_to_string', mock.Mock(side_effect=mock_render_to_string, autospec=True))
|
||||
@mock.patch('django.contrib.auth.models.User.email_user')
|
||||
def test_change_status(self, email_user):
|
||||
@@ -161,6 +172,7 @@ class CourseCreatorAdminTest(TestCase):
|
||||
self.assertFalse(self.creator_admin.has_change_permission(self.request))
|
||||
|
||||
def test_rate_limit_login(self):
|
||||
self.grant_sudo_access('django_admin', 'foo')
|
||||
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_CREATOR_GROUP': True}):
|
||||
post_params = {'username': self.user.username, 'password': 'wrong_password'}
|
||||
# try logging in 30 times, the default limit in the number of failed
|
||||
|
||||
@@ -319,6 +319,9 @@ MIDDLEWARE_CLASSES = (
|
||||
# catches any uncaught RateLimitExceptions and returns a 403 instead of a 500
|
||||
'ratelimitbackend.middleware.RateLimitMiddleware',
|
||||
|
||||
# force re-authentication before activating administrative functions
|
||||
'sudo.middleware.SudoMiddleware',
|
||||
|
||||
# for expiring inactive sessions
|
||||
'session_inactivity_timeout.middleware.SessionInactivityTimeout',
|
||||
|
||||
@@ -761,6 +764,9 @@ INSTALLED_APPS = (
|
||||
'openedx.core.djangoapps.credit',
|
||||
|
||||
'xblock_django',
|
||||
|
||||
# Allows sudo-mode
|
||||
'sudo',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -70,3 +70,129 @@
|
||||
width: 100%;
|
||||
background: $black;
|
||||
}
|
||||
|
||||
.sudo-modal {
|
||||
@extend .modal;
|
||||
background: $shadow-d2;
|
||||
border: 1px solid rgba(0, 0, 0, 0.9);
|
||||
border-radius: 0;
|
||||
box-shadow: 0 15px 80px 15px rgba(0,0,0, 0.5);
|
||||
color: $white;
|
||||
display: none;
|
||||
left: 50%;
|
||||
padding: 8px;
|
||||
position: absolute;
|
||||
width: 480px;
|
||||
height: auto;
|
||||
|
||||
.inner-wrapper {
|
||||
@extend %ui-depth1;
|
||||
background: rgb(245,245,245);
|
||||
border-radius: 0;
|
||||
border: 1px solid rgba(0, 0, 0, 0.9);
|
||||
box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.7);
|
||||
overflow: hidden;
|
||||
padding-left: ($baseline/2);
|
||||
padding-right: ($baseline/2);
|
||||
padding-bottom: ($baseline/2);
|
||||
position: relative;
|
||||
|
||||
header {
|
||||
@extend %ui-depth1;
|
||||
overflow: hidden;
|
||||
padding: 28px $baseline 0;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
@include background-image(radial-gradient(50% 50%, circle closest-side, rgba(255,255,255, 0.8) 0%, rgba(255,255,255, 0) 100%));
|
||||
content: "";
|
||||
display: block;
|
||||
height: 400px;
|
||||
left: 0;
|
||||
margin: 0 auto;
|
||||
position: absolute;
|
||||
top: -140px;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
&::after {
|
||||
bottom: 0;
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
text-shadow: 0 1px rgba(255,255,255, 0.4);
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
margin-bottom: 12px;
|
||||
padding: 0 ($baseline*2) $baseline;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
label {
|
||||
color: rgb(51, 51, 51);
|
||||
|
||||
&.field-error {
|
||||
display: block;
|
||||
color: #8F0E0E;
|
||||
|
||||
+ input, + textarea {
|
||||
border: 1px solid #CA1111;
|
||||
color: #8F0E0E;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type="password"] {
|
||||
background: rgb(255,255,255);
|
||||
display: block;
|
||||
height: 45px;
|
||||
margin-bottom: $baseline;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
border: 1px solid #CFC6C6;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0px 1px 0px 0px #FFF inset;
|
||||
color: #333;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
background-color: #EEE;
|
||||
background-image: linear-gradient(#EEE, #D6CECE);
|
||||
padding: 12px 18px;
|
||||
text-decoration: none;
|
||||
text-shadow: 0px 1px 0px #F9F8F8;
|
||||
background-clip: padding-box;
|
||||
font-size: 0.8125em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#sudo_overlay {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #000;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
42
cms/templates/sudo/sudo.html
Normal file
42
cms/templates/sudo/sudo.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{% block body %}
|
||||
{% load i18n %}
|
||||
{% load compressed %}
|
||||
{% compressed_css 'style-main' %}
|
||||
|
||||
<a href="#sudo-modal" id="sudo-modal-trig" style="display: none;"></a>
|
||||
<section aria-hidden="true" class="modal sudo-modal" id="sudo-modal" style="overflow:auto; display: none;" >
|
||||
<div class="inner-wrapper" style="color:black">
|
||||
<header>
|
||||
<h2>{% trans "Confirm Your Password to Access the Course Team Settings" %}</h2>
|
||||
</header>
|
||||
<hr />
|
||||
<div>
|
||||
<form class="sudo-form" method="post" action="">{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<p>
|
||||
<input type="submit" id="sudo-button" class="sudo-button" value="{% trans 'Submit' %}">
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script type="text/javascript">
|
||||
window.baseUrl = "{{STATIC_URL}}";
|
||||
var require = {baseUrl: window.baseUrl};
|
||||
</script>
|
||||
<script type="text/javascript" src="{{STATIC_URL}}js/vendor/require.js"></script>
|
||||
<script type="text/javascript" src="{{STATIC_URL}}require-config.js"></script>
|
||||
<script type = "text/javascript">
|
||||
require(['domReady', "jquery"], function (domReady, $) {
|
||||
domReady(function () {
|
||||
require(["jquery.leanModal"], function () {
|
||||
var sudoModalTrig = $("#sudo-modal-trig");
|
||||
sudoModalTrig.leanModal();
|
||||
sudoModalTrig.click();
|
||||
$("#lean_overlay").remove();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<div id="sudo_overlay"></div>
|
||||
{% endblock %}
|
||||
@@ -2,7 +2,8 @@ from django.conf import settings
|
||||
from django.conf.urls import patterns, include, url
|
||||
|
||||
# There is a course creators admin table.
|
||||
from ratelimitbackend import admin
|
||||
from edx_admin import admin
|
||||
|
||||
admin.autodiscover()
|
||||
|
||||
# pylint: disable=bad-continuation
|
||||
@@ -50,6 +51,8 @@ urlpatterns = patterns(
|
||||
url(r'^heartbeat$', include('heartbeat.urls')),
|
||||
|
||||
url(r'^user_api/', include('openedx.core.djangoapps.user_api.legacy_urls')),
|
||||
|
||||
url(r'^sudo/$', 'sudo.views.sudo'),
|
||||
)
|
||||
|
||||
# User creation and updating views
|
||||
|
||||
@@ -3,7 +3,7 @@ Django admin page for course modes
|
||||
"""
|
||||
from django.conf import settings
|
||||
from pytz import timezone, UTC
|
||||
from ratelimitbackend import admin
|
||||
from django.contrib import admin
|
||||
from course_modes.models import CourseMode
|
||||
from django import forms
|
||||
|
||||
|
||||
@@ -380,6 +380,7 @@ class AdminCourseModePageTest(ModuleStoreTestCase):
|
||||
}
|
||||
|
||||
self.client.login(username=user.username, password='test')
|
||||
self.grant_sudo_access('django_admin', 'test')
|
||||
|
||||
# creating new course mode from django admin page
|
||||
response = self.client.post(reverse('admin:course_modes_coursemode_add'), data=data)
|
||||
|
||||
0
common/djangoapps/django_sudo_helpers/__init__.py
Normal file
0
common/djangoapps/django_sudo_helpers/__init__.py
Normal file
61
common/djangoapps/django_sudo_helpers/decorators.py
Normal file
61
common/djangoapps/django_sudo_helpers/decorators.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Custom decorator for django-sudo.
|
||||
"""
|
||||
from functools import wraps
|
||||
|
||||
from sudo.settings import RESET_TOKEN
|
||||
from sudo.utils import new_sudo_token_on_activity
|
||||
from sudo.views import redirect_to_sudo
|
||||
from util.json_request import JsonResponse
|
||||
|
||||
|
||||
def sudo_required(func_or_region):
|
||||
"""
|
||||
Enforces a view to have elevated privileges.
|
||||
Should likely be paired with ``@login_required``.
|
||||
|
||||
>>> @sudo_required
|
||||
>>> def secure_page(request):
|
||||
>>> ...
|
||||
|
||||
Can also specify a particular sudo region (to only
|
||||
allow access to that region).
|
||||
|
||||
Also get course_id, course_key_string and library_key_string
|
||||
from kwargs and set as region if region itself is None.
|
||||
|
||||
>>> @sudo_required('admin_page')
|
||||
>>> def secure_admin_page(request):
|
||||
>>> ...
|
||||
"""
|
||||
def wrapper(func): # pylint: disable=missing-docstring
|
||||
@wraps(func)
|
||||
def inner(request, *args, **kwargs): # pylint: disable=missing-docstring
|
||||
course_specific_region = kwargs.get('course_id')
|
||||
if 'course_key_string' in kwargs:
|
||||
course_specific_region = kwargs.get('course_key_string')
|
||||
if 'library_key_string' in kwargs:
|
||||
course_specific_region = kwargs.get('library_key_string')
|
||||
|
||||
# N.B. region is captured from the enclosing sudo_required function
|
||||
if not request.is_sudo(region=region or course_specific_region):
|
||||
response_format = request.REQUEST.get('format', 'html')
|
||||
if (response_format == 'json' or
|
||||
'application/json' in request.META.get('HTTP_ACCEPT', 'application/json')):
|
||||
return JsonResponse({'error': 'Unauthorized'}, status=401)
|
||||
|
||||
return redirect_to_sudo(request.get_full_path(), region=region or course_specific_region)
|
||||
|
||||
if RESET_TOKEN is True:
|
||||
# Provide new sudo token content and reset timeout on activity
|
||||
new_sudo_token_on_activity(request, region=region or course_specific_region)
|
||||
|
||||
return func(request, *args, **kwargs)
|
||||
return inner
|
||||
|
||||
if callable(func_or_region):
|
||||
region = None
|
||||
return wrapper(func_or_region)
|
||||
else:
|
||||
region = func_or_region
|
||||
return wrapper
|
||||
15
common/djangoapps/django_sudo_helpers/tests/utils.py
Normal file
15
common/djangoapps/django_sudo_helpers/tests/utils.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
django_sudo_heplers.utils
|
||||
"""
|
||||
import django.contrib.sessions.middleware
|
||||
import sudo.middleware
|
||||
|
||||
|
||||
def sudo_middleware_process_request(request):
|
||||
"""
|
||||
Initialize the session and is_sudo on request object.
|
||||
"""
|
||||
session_middleware = django.contrib.sessions.middleware.SessionMiddleware()
|
||||
session_middleware.process_request(request)
|
||||
sudo_middleware = sudo.middleware.SudoMiddleware()
|
||||
sudo_middleware.process_request(request)
|
||||
0
common/djangoapps/edx_admin/__init__.py
Normal file
0
common/djangoapps/edx_admin/__init__.py
Normal file
31
common/djangoapps/edx_admin/admin.py
Normal file
31
common/djangoapps/edx_admin/admin.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
RatelimitSudoAdminSite
|
||||
"""
|
||||
|
||||
from django.contrib.admin import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from django.contrib.admin import (site as django_site,
|
||||
autodiscover as django_autodiscover)
|
||||
from ratelimitbackend.admin import RateLimitAdminSite
|
||||
from sudo.admin import SudoAdminSite
|
||||
|
||||
|
||||
class RatelimitSudoAdminSite(RateLimitAdminSite, SudoAdminSite):
|
||||
"""
|
||||
A class that includes the features of both RateLimitAdminSite and SudoAdminSite
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
site = RatelimitSudoAdminSite() # pylint: disable=invalid-name
|
||||
|
||||
|
||||
def autodiscover(): # pylint: disable=function-redefined
|
||||
"""
|
||||
Auto-Discover admin models.
|
||||
"""
|
||||
django_autodiscover()
|
||||
|
||||
# pylint: disable=protected-access
|
||||
for model, modeladmin in django_site._registry.items():
|
||||
if model not in site._registry:
|
||||
site.register(model, modeladmin.__class__)
|
||||
3
common/djangoapps/edx_admin/models.py
Normal file
3
common/djangoapps/edx_admin/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
This space intentionally left blank
|
||||
"""
|
||||
@@ -3,7 +3,7 @@ django admin pages for courseware model
|
||||
'''
|
||||
|
||||
from external_auth.models import *
|
||||
from ratelimitbackend import admin
|
||||
from django.contrib import admin
|
||||
|
||||
|
||||
class ExternalAuthMapAdmin(admin.ModelAdmin):
|
||||
|
||||
@@ -9,7 +9,7 @@ from student.models import UserProfile, UserTestGroup, CourseEnrollmentAllowed,
|
||||
from student.models import (
|
||||
CourseEnrollment, Registration, PendingNameChange, CourseAccessRole, LinkedInAddToProfileConfiguration
|
||||
)
|
||||
from ratelimitbackend import admin
|
||||
from django.contrib import admin
|
||||
from student.roles import REGISTERED_ACCESS_ROLES
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
@@ -17,6 +17,9 @@ class AdminCourseRolesPageTest(ModuleStoreTestCase):
|
||||
self.user.save()
|
||||
self.course = CourseFactory.create(org='edx')
|
||||
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
self.grant_sudo_access('django_admin', 'test')
|
||||
|
||||
def test_save_valid_data(self):
|
||||
|
||||
data = {
|
||||
@@ -26,8 +29,6 @@ class AdminCourseRolesPageTest(ModuleStoreTestCase):
|
||||
'email': self.user.email
|
||||
}
|
||||
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
|
||||
# # adding new role from django admin page
|
||||
response = self.client.post(reverse('admin:student_courseaccessrole_add'), data=data)
|
||||
self.assertRedirects(response, reverse('admin:student_courseaccessrole_changelist'))
|
||||
@@ -51,8 +52,6 @@ class AdminCourseRolesPageTest(ModuleStoreTestCase):
|
||||
'course_id': unicode(self.course.id)
|
||||
}
|
||||
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
|
||||
# # adding new role from django admin page
|
||||
response = self.client.post(reverse('admin:student_courseaccessrole_add'), data=data)
|
||||
self.assertRedirects(response, reverse('admin:student_courseaccessrole_changelist'))
|
||||
@@ -69,8 +68,6 @@ class AdminCourseRolesPageTest(ModuleStoreTestCase):
|
||||
|
||||
}
|
||||
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
|
||||
# # adding new role from django admin page
|
||||
response = self.client.post(reverse('admin:student_courseaccessrole_add'), data=data)
|
||||
self.assertRedirects(response, reverse('admin:student_courseaccessrole_changelist'))
|
||||
@@ -88,8 +85,6 @@ class AdminCourseRolesPageTest(ModuleStoreTestCase):
|
||||
|
||||
}
|
||||
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
|
||||
# # adding new role from django admin page
|
||||
response = self.client.post(reverse('admin:student_courseaccessrole_add'), data=data)
|
||||
self.assertRedirects(response, reverse('admin:student_courseaccessrole_changelist'))
|
||||
@@ -109,8 +104,6 @@ class AdminCourseRolesPageTest(ModuleStoreTestCase):
|
||||
'email': email
|
||||
}
|
||||
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
|
||||
# Adding new role with invalid data
|
||||
response = self.client.post(reverse('admin:student_courseaccessrole_add'), data=data)
|
||||
self.assertContains(
|
||||
@@ -136,8 +129,6 @@ class AdminCourseRolesPageTest(ModuleStoreTestCase):
|
||||
'email': self.user.email
|
||||
}
|
||||
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
|
||||
# # adding new role from django admin page
|
||||
response = self.client.post(reverse('admin:student_courseaccessrole_add'), data=data)
|
||||
self.assertContains(
|
||||
|
||||
@@ -241,3 +241,23 @@ def view_course_team_settings(_step, whom):
|
||||
world.click_course_settings()
|
||||
link_css = 'li.nav-course-settings-team a'
|
||||
world.css_click(link_css)
|
||||
|
||||
|
||||
@step('I get sudo access with password "([^"]*)"$')
|
||||
def i_get_sudo_access(_step, password):
|
||||
"""
|
||||
Get sudo access for instructor or staff user.
|
||||
Set the password value of the element to the specified password.
|
||||
Note that wait_for empty is due to password field
|
||||
It will return password like this **** not text.
|
||||
"""
|
||||
|
||||
sudo_form = world.css_find('form.sudo-form')
|
||||
# check if sudo form is available then submit password to get sudo access
|
||||
# otherwise return True because sudo access already given.
|
||||
if len(sudo_form) > 0:
|
||||
css_selector = 'input[id=id_password]'
|
||||
world.retry_on_exception(lambda: world.css_find(css_selector)[0].fill(password))
|
||||
world.wait_for(lambda _: not world.css_has_value(css_selector, '', index=0))
|
||||
world.css_click('input[type=submit]')
|
||||
return True
|
||||
|
||||
@@ -3,6 +3,6 @@ django admin pages for courseware model
|
||||
'''
|
||||
|
||||
from track.models import TrackingLog
|
||||
from ratelimitbackend import admin
|
||||
from django.contrib import admin
|
||||
|
||||
admin.site.register(TrackingLog)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Admin interface for the util app. """
|
||||
|
||||
from ratelimitbackend import admin
|
||||
from django.contrib import admin
|
||||
from util.models import RateLimitConfiguration
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from request_cache.middleware import RequestCache
|
||||
from courseware.field_overrides import OverrideFieldData # pylint: disable=import-error
|
||||
from openedx.core.lib.tempdir import mkdtemp_clean
|
||||
|
||||
from sudo.utils import region_name
|
||||
from xmodule.contentstore.django import _CONTENTSTORE
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore, clear_existing_modulestores
|
||||
@@ -422,3 +423,13 @@ class ModuleStoreTestCase(TestCase):
|
||||
fields={"display_name": "Syllabus"},
|
||||
)
|
||||
return self.toy_loc
|
||||
|
||||
def grant_sudo_access(self, region, password):
|
||||
"""
|
||||
Grant sudo access to staff or instructor user.
|
||||
"""
|
||||
self.client.post(
|
||||
'/sudo/?region={}'.format(region_name(region)),
|
||||
{'password': password},
|
||||
follow=True
|
||||
)
|
||||
|
||||
57
common/test/acceptance/pages/common/sudo_page.py
Normal file
57
common/test/acceptance/pages/common/sudo_page.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Django sudo page to get sudo access.
|
||||
"""
|
||||
|
||||
from bok_choy.javascript import wait_for_js
|
||||
from bok_choy.page_object import PageObject
|
||||
|
||||
|
||||
class SudoPage(PageObject):
|
||||
"""
|
||||
Sudo page to get sudo access
|
||||
"""
|
||||
SUDO_FORM = 'form.sudo-form'
|
||||
|
||||
def __init__(self, browser, redirect_page):
|
||||
super(SudoPage, self).__init__(browser)
|
||||
self.redirect_page = redirect_page
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return self.q(css=self.SUDO_FORM).present
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""
|
||||
Construct a URL to the page which needs sudo access.
|
||||
"""
|
||||
return self.redirect_page.url
|
||||
|
||||
@property
|
||||
def sudo_password_input(self):
|
||||
"""
|
||||
Returns sudo password input box.
|
||||
"""
|
||||
return self.q(css='{} input[id=id_password]'.format(self.SUDO_FORM))
|
||||
|
||||
@property
|
||||
def submit_button(self):
|
||||
"""
|
||||
Returns submit button.
|
||||
"""
|
||||
return self.q(css='{} input[type=submit]'.format(self.SUDO_FORM))
|
||||
|
||||
@wait_for_js
|
||||
def submit_sudo_password_and_get_access(self, password):
|
||||
"""
|
||||
Fill password in input field and click submit.
|
||||
"""
|
||||
input_box = self.sudo_password_input.first.results[0]
|
||||
input_box.send_keys(password)
|
||||
self.click_submit()
|
||||
self.redirect_page.wait_for_page()
|
||||
|
||||
def click_submit(self):
|
||||
"""
|
||||
Click on submit button.
|
||||
"""
|
||||
return self.submit_button.click()
|
||||
@@ -759,12 +759,14 @@ class DataDownloadPage(PageObject):
|
||||
return self.report_download_links.map(lambda el: el.text)
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
class StudentAdminPage(PageObject):
|
||||
"""
|
||||
Student admin section of the Instructor dashboard.
|
||||
"""
|
||||
url = None
|
||||
EE_CONTAINER = ".entrance-exam-grade-container"
|
||||
ENTRANCE_EXAM_CONTAINER = ".entrance-exam-grade-container"
|
||||
SG_CONTAINER = ".student-grade-container"
|
||||
|
||||
def is_browser_on_page(self):
|
||||
"""
|
||||
@@ -773,89 +775,161 @@ class StudentAdminPage(PageObject):
|
||||
return self.q(css='a[data-section=student_admin].active-section').present
|
||||
|
||||
@property
|
||||
def student_email_input(self):
|
||||
def entrance_exam_student_email_input(self):
|
||||
"""
|
||||
Returns email address/username input box.
|
||||
Returns email address/username input box for entrance exam.
|
||||
"""
|
||||
return self.q(css='{} input[name=entrance-exam-student-select-grade]'.format(self.EE_CONTAINER))
|
||||
return self.q(css='{} input[name=entrance-exam-student-select-grade]'.format(self.ENTRANCE_EXAM_CONTAINER))
|
||||
|
||||
@property
|
||||
def reset_attempts_button(self):
|
||||
def entrance_exam_reset_attempts_button(self):
|
||||
"""
|
||||
Returns reset student attempts button.
|
||||
Returns reset student attempts button for entrance exam.
|
||||
"""
|
||||
return self.q(css='{} input[name=reset-entrance-exam-attempts]'.format(self.EE_CONTAINER))
|
||||
return self.q(css='{} input[name=reset-entrance-exam-attempts]'.format(self.ENTRANCE_EXAM_CONTAINER))
|
||||
|
||||
@property
|
||||
def rescore_submission_button(self):
|
||||
def entrance_exam_rescore_submission_button(self):
|
||||
"""
|
||||
Returns rescore student submission button.
|
||||
Returns rescore student submission button for entrance exam.
|
||||
"""
|
||||
return self.q(css='{} input[name=rescore-entrance-exam]'.format(self.EE_CONTAINER))
|
||||
return self.q(css='{} input[name=rescore-entrance-exam]'.format(self.ENTRANCE_EXAM_CONTAINER))
|
||||
|
||||
@property
|
||||
def skip_entrance_exam_button(self):
|
||||
"""
|
||||
Return Let Student Skip Entrance Exam button.
|
||||
"""
|
||||
return self.q(css='{} input[name=skip-entrance-exam]'.format(self.EE_CONTAINER))
|
||||
return self.q(css='{} input[name=skip-entrance-exam]'.format(self.ENTRANCE_EXAM_CONTAINER))
|
||||
|
||||
@property
|
||||
def entrance_exam_delete_student_state_button(self):
|
||||
"""
|
||||
Returns delete student state button for entrance exam.
|
||||
"""
|
||||
return self.q(css='{} input[name=delete-entrance-exam-state]'.format(self.ENTRANCE_EXAM_CONTAINER))
|
||||
|
||||
@property
|
||||
def background_task_history_button(self):
|
||||
"""
|
||||
Returns show background task history for student button for entrance exam.
|
||||
"""
|
||||
return self.q(css='{} input[name=entrance-exam-task-history]'.format(self.ENTRANCE_EXAM_CONTAINER))
|
||||
|
||||
@property
|
||||
def entrance_exam_top_notification(self):
|
||||
"""
|
||||
Returns show background task history for student button for entrance exam.
|
||||
"""
|
||||
return self.q(css='{} .request-response-error'.format(self.ENTRANCE_EXAM_CONTAINER)).first
|
||||
|
||||
@property
|
||||
def reset_attempts_button(self):
|
||||
"""
|
||||
Returns reset student attempts button.
|
||||
"""
|
||||
return self.q(css='{} input[name=reset-attempts-single]'.format(self.SG_CONTAINER))
|
||||
|
||||
@property
|
||||
def rescore_submission_button(self):
|
||||
"""
|
||||
Returns rescore student submission button.
|
||||
"""
|
||||
return self.q(css='{} input[name=rescore-problem-single]'.format(self.SG_CONTAINER))
|
||||
|
||||
@property
|
||||
def delete_student_state_button(self):
|
||||
"""
|
||||
Returns delete student state button.
|
||||
"""
|
||||
return self.q(css='{} input[name=delete-entrance-exam-state]'.format(self.EE_CONTAINER))
|
||||
|
||||
@property
|
||||
def background_task_history_button(self):
|
||||
"""
|
||||
Returns show background task history for student button.
|
||||
"""
|
||||
return self.q(css='{} input[name=entrance-exam-task-history]'.format(self.EE_CONTAINER))
|
||||
return self.q(css='{} input[name=delete-state-single]'.format(self.SG_CONTAINER))
|
||||
|
||||
@property
|
||||
def top_notification(self):
|
||||
"""
|
||||
Returns show background task history for student button.
|
||||
"""
|
||||
return self.q(css='{} .request-response-error'.format(self.EE_CONTAINER)).first
|
||||
return self.q(css='{} .request-response-error'.format(self.SG_CONTAINER)).first
|
||||
|
||||
def is_student_email_input_visible(self):
|
||||
def is_entrance_exam_student_email_input_visible(self):
|
||||
"""
|
||||
Returns True if student email address/username input box is present.
|
||||
Returns True if student email address/username input box is present
|
||||
for entrance exam.
|
||||
"""
|
||||
return self.student_email_input.is_present()
|
||||
return self.entrance_exam_student_email_input.is_present()
|
||||
|
||||
def is_reset_attempts_button_visible(self):
|
||||
def is_entrance_exam_reset_attempts_button_visible(self):
|
||||
"""
|
||||
Returns True if reset student attempts button is present.
|
||||
Returns True if reset student attempts button is present
|
||||
for entrance exam.
|
||||
"""
|
||||
return self.reset_attempts_button.is_present()
|
||||
return self.entrance_exam_reset_attempts_button.is_present()
|
||||
|
||||
def is_rescore_submission_button_visible(self):
|
||||
def is_entrance_exam_rescore_submission_button_visible(self):
|
||||
"""
|
||||
Returns True if rescore student submission button is present.
|
||||
Returns True if rescore student submission button is present
|
||||
for entrance exam.
|
||||
"""
|
||||
return self.rescore_submission_button.is_present()
|
||||
return self.entrance_exam_rescore_submission_button.is_present()
|
||||
|
||||
def is_delete_student_state_button_visible(self):
|
||||
def is_entrance_exam_delete_student_state_button_visible(self):
|
||||
"""
|
||||
Returns True if delete student state for entrance exam button is present.
|
||||
Returns True if delete student state for entrance exam button is present
|
||||
for entrance exam.
|
||||
"""
|
||||
return self.delete_student_state_button.is_present()
|
||||
return self.entrance_exam_delete_student_state_button.is_present()
|
||||
|
||||
def is_background_task_history_button_visible(self):
|
||||
"""
|
||||
Returns True if show background task history for student button is present.
|
||||
Returns True if show background task history for student button is present
|
||||
for entrance exam.
|
||||
"""
|
||||
return self.background_task_history_button.is_present()
|
||||
|
||||
def is_background_task_history_table_visible(self):
|
||||
"""
|
||||
Returns True if background task history table is present.
|
||||
Returns True if background task history table is present
|
||||
for entrance exam.
|
||||
"""
|
||||
return self.q(css='{} .entrance-exam-task-history-table'.format(self.EE_CONTAINER)).is_present()
|
||||
return self.q(css='{} .entrance-exam-task-history-table'.format(self.ENTRANCE_EXAM_CONTAINER)).is_present()
|
||||
|
||||
def entrance_exam_click_reset_attempts_button(self):
|
||||
"""
|
||||
clicks reset student attempts button for entrance exam.
|
||||
"""
|
||||
return self.entrance_exam_reset_attempts_button.click()
|
||||
|
||||
def entrance_exam_click_rescore_submissions_button(self):
|
||||
"""
|
||||
clicks rescore submissions button for entrance exam.
|
||||
"""
|
||||
return self.entrance_exam_rescore_submission_button.click()
|
||||
|
||||
def click_skip_entrance_exam_button(self):
|
||||
"""
|
||||
clicks let student skip entrance exam button for entrance exam.
|
||||
"""
|
||||
return self.skip_entrance_exam_button.click()
|
||||
|
||||
def entrance_exam_click_delete_student_state_button(self):
|
||||
"""
|
||||
clicks delete student state button for entrance exam.
|
||||
"""
|
||||
return self.entrance_exam_delete_student_state_button.click()
|
||||
|
||||
def entrance_exam_click_task_history_button(self):
|
||||
"""
|
||||
clicks background task history button for entrance exam.
|
||||
"""
|
||||
return self.background_task_history_button.click()
|
||||
|
||||
def set_student_email_for_ee(self, email_addres):
|
||||
"""
|
||||
Sets given email address as value of student email address/username input box
|
||||
for entrance exam.
|
||||
"""
|
||||
input_box = self.entrance_exam_student_email_input.first.results[0]
|
||||
input_box.send_keys(email_addres)
|
||||
|
||||
def click_reset_attempts_button(self):
|
||||
"""
|
||||
@@ -869,30 +943,13 @@ class StudentAdminPage(PageObject):
|
||||
"""
|
||||
return self.rescore_submission_button.click()
|
||||
|
||||
def click_skip_entrance_exam_button(self):
|
||||
"""
|
||||
clicks let student skip entrance exam button.
|
||||
"""
|
||||
return self.skip_entrance_exam_button.click()
|
||||
|
||||
def click_delete_student_state_button(self):
|
||||
"""
|
||||
clicks delete student state button.
|
||||
clicks delete student state button and confirm the action.
|
||||
"""
|
||||
return self.delete_student_state_button.click()
|
||||
|
||||
def click_task_history_button(self):
|
||||
"""
|
||||
clicks background task history button.
|
||||
"""
|
||||
return self.background_task_history_button.click()
|
||||
|
||||
def set_student_email(self, email_addres):
|
||||
"""
|
||||
Sets given email address as value of student email address/username input box.
|
||||
"""
|
||||
input_box = self.student_email_input.first.results[0]
|
||||
input_box.send_keys(email_addres)
|
||||
with self.handle_alert(confirm=True):
|
||||
self.delete_student_state_button.click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
|
||||
class CertificatesPage(PageObject):
|
||||
|
||||
@@ -68,31 +68,14 @@ class StaffDebugPage(PageObject):
|
||||
def is_browser_on_page(self):
|
||||
return self.q(css='section.staff-modal').present
|
||||
|
||||
def reset_attempts(self, user=None):
|
||||
def click_student_grade_adjustments(self, user=None):
|
||||
"""
|
||||
This clicks on the reset attempts link with an optionally
|
||||
specified user.
|
||||
"""
|
||||
if user:
|
||||
self.q(css='input[id^=sd_fu_]').first.fill(user)
|
||||
self.q(css='section.staff-modal a.staff-debug-reset').click()
|
||||
|
||||
def delete_state(self, user=None):
|
||||
"""
|
||||
This delete's a student's state for the problem
|
||||
"""
|
||||
if user:
|
||||
self.q(css='input[id^=sd_fu_]').fill(user)
|
||||
self.q(css='section.staff-modal a.staff-debug-sdelete').click()
|
||||
|
||||
def rescore(self, user=None):
|
||||
"""
|
||||
This clicks on the reset attempts link with an optionally
|
||||
specified user.
|
||||
"""
|
||||
if user:
|
||||
self.q(css='input[id^=sd_fu_]').first.fill(user)
|
||||
self.q(css='section.staff-modal a.staff-debug-rescore').click()
|
||||
self.q(css='section.staff-modal a.staff-debug-grade-adjustments').click()
|
||||
|
||||
@property
|
||||
def idash_msg(self):
|
||||
|
||||
@@ -9,7 +9,7 @@ from pytz import UTC, utc
|
||||
from bok_choy.promise import EmptyPromise
|
||||
from nose.plugins.attrib import attr
|
||||
from .helpers import CohortTestMixin
|
||||
from ..helpers import UniqueCourseTest, EventsTestMixin, create_user_partition_json
|
||||
from ..helpers import UniqueCourseTest, EventsTestMixin, create_user_partition_json, get_sudo_access
|
||||
from xmodule.partitions.partitions import Group
|
||||
from ...fixtures.course import CourseFixture, XBlockFixtureDesc
|
||||
from ...pages.lms.auto_auth import AutoAuthPage
|
||||
@@ -53,14 +53,16 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin
|
||||
).visit().get_user_id()
|
||||
|
||||
# login as an instructor
|
||||
instructor_password = 'test'
|
||||
self.instructor_name = "instructor_user"
|
||||
self.instructor_id = AutoAuthPage(
|
||||
self.browser, username=self.instructor_name, email="instructor_user@example.com",
|
||||
course_id=self.course_id, staff=True
|
||||
course_id=self.course_id, staff=True, password=instructor_password
|
||||
).visit().get_user_id()
|
||||
|
||||
# go to the membership page on the instructor dashboard
|
||||
self.instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id)
|
||||
get_sudo_access(self.browser, self.instructor_dashboard_page, instructor_password)
|
||||
self.instructor_dashboard_page.visit()
|
||||
self.cohort_management_page = self.instructor_dashboard_page.select_cohort_management()
|
||||
|
||||
@@ -648,14 +650,16 @@ class CohortDiscussionTopicsTest(UniqueCourseTest, CohortTestMixin):
|
||||
self.cohort_id = self.add_manual_cohort(self.course_fixture, self.cohort_name)
|
||||
|
||||
# login as an instructor
|
||||
self.instructor_password = 'test'
|
||||
self.instructor_name = "instructor_user"
|
||||
self.instructor_id = AutoAuthPage(
|
||||
self.browser, username=self.instructor_name, email="instructor_user@example.com",
|
||||
course_id=self.course_id, staff=True
|
||||
course_id=self.course_id, staff=True, password=self.instructor_password
|
||||
).visit().get_user_id()
|
||||
|
||||
# go to the membership page on the instructor dashboard
|
||||
self.instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id)
|
||||
get_sudo_access(self.browser, self.instructor_dashboard_page, self.instructor_password)
|
||||
self.instructor_dashboard_page.visit()
|
||||
self.cohort_management_page = self.instructor_dashboard_page.select_cohort_management()
|
||||
self.cohort_management_page.wait_for_page()
|
||||
@@ -940,14 +944,16 @@ class CohortContentGroupAssociationTest(UniqueCourseTest, CohortTestMixin):
|
||||
})
|
||||
|
||||
# login as an instructor
|
||||
instructor_password = 'test'
|
||||
self.instructor_name = "instructor_user"
|
||||
self.instructor_id = AutoAuthPage(
|
||||
self.browser, username=self.instructor_name, email="instructor_user@example.com",
|
||||
course_id=self.course_id, staff=True
|
||||
course_id=self.course_id, staff=True, password=instructor_password
|
||||
).visit().get_user_id()
|
||||
|
||||
# go to the membership page on the instructor dashboard
|
||||
self.instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id)
|
||||
get_sudo_access(self.browser, self.instructor_dashboard_page, instructor_password)
|
||||
self.instructor_dashboard_page.visit()
|
||||
self.cohort_management_page = self.instructor_dashboard_page.select_cohort_management()
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ from selenium.webdriver.support.select import Select
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from unittest import TestCase
|
||||
from ..pages.common.sudo_page import SudoPage
|
||||
|
||||
|
||||
from ..pages.common import BASE_URL
|
||||
@@ -684,3 +685,12 @@ class TestWithSearchIndexMixin(object):
|
||||
def _cleanup_index_file(self):
|
||||
""" Removes search index backing file """
|
||||
remove_file(self.TEST_INDEX_FILENAME)
|
||||
|
||||
|
||||
def get_sudo_access(browser, redirect_page, password):
|
||||
"""
|
||||
Get sudo access for instructor or staff user.
|
||||
"""
|
||||
sudo_password_page = SudoPage(browser, redirect_page)
|
||||
sudo_password_page.visit()
|
||||
sudo_password_page.submit_sudo_password_and_get_access(password)
|
||||
|
||||
@@ -9,6 +9,7 @@ from ...pages.studio.overview import CourseOutlinePage
|
||||
from ...pages.lms.courseware_search import CoursewareSearchPage
|
||||
from ...pages.lms.staff_view import StaffPage
|
||||
from ...fixtures.course import XBlockFixtureDesc
|
||||
from ..helpers import get_sudo_access
|
||||
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
@@ -83,13 +84,13 @@ class CoursewareSearchCohortTest(ContainerBase):
|
||||
super(CoursewareSearchCohortTest, self).tearDown()
|
||||
os.remove(self.TEST_INDEX_FILENAME)
|
||||
|
||||
def _auto_auth(self, username, email, staff):
|
||||
def _auto_auth(self, username, email, staff, password='test'):
|
||||
"""
|
||||
Logout and login with given credentials.
|
||||
"""
|
||||
LogoutPage(self.browser).visit()
|
||||
StudioAutoAuthPage(self.browser, username=username, email=email,
|
||||
course_id=self.course_id, staff=staff).visit()
|
||||
course_id=self.course_id, staff=staff, password=password).visit()
|
||||
|
||||
def _studio_reindex(self):
|
||||
"""
|
||||
@@ -193,7 +194,7 @@ class CoursewareSearchCohortTest(ContainerBase):
|
||||
Each cohort is assigned one student.
|
||||
"""
|
||||
instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id)
|
||||
instructor_dashboard_page.visit()
|
||||
get_sudo_access(self.browser, instructor_dashboard_page, 'test')
|
||||
cohort_management_page = instructor_dashboard_page.select_cohort_management()
|
||||
|
||||
def add_cohort_with_student(cohort_name, content_group, student):
|
||||
|
||||
@@ -6,7 +6,7 @@ End-to-end tests for the LMS Instructor Dashboard.
|
||||
from nose.plugins.attrib import attr
|
||||
from bok_choy.promise import EmptyPromise
|
||||
|
||||
from ..helpers import UniqueCourseTest, get_modal_alert, EventsTestMixin
|
||||
from ..helpers import UniqueCourseTest, get_modal_alert, EventsTestMixin, get_sudo_access
|
||||
from ...pages.common.logout import LogoutPage
|
||||
from ...pages.lms.auto_auth import AutoAuthPage
|
||||
from ...pages.lms.instructor_dashboard import InstructorDashboardPage
|
||||
@@ -22,7 +22,9 @@ class BaseInstructorDashboardTest(EventsTestMixin, UniqueCourseTest):
|
||||
Logs in as an instructor and returns the id.
|
||||
"""
|
||||
username = "test_instructor_{uuid}".format(uuid=self.unique_id[0:6])
|
||||
auto_auth_page = AutoAuthPage(self.browser, username=username, course_id=self.course_id, staff=True)
|
||||
auto_auth_page = AutoAuthPage(
|
||||
self.browser, username=username, course_id=self.course_id, staff=True, password="test"
|
||||
)
|
||||
return username, auto_auth_page.visit().get_user_id()
|
||||
|
||||
def visit_instructor_dashboard(self):
|
||||
@@ -30,6 +32,7 @@ class BaseInstructorDashboardTest(EventsTestMixin, UniqueCourseTest):
|
||||
Visits the instructor dashboard.
|
||||
"""
|
||||
instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id)
|
||||
get_sudo_access(self.browser, instructor_dashboard_page, "test")
|
||||
instructor_dashboard_page.visit()
|
||||
return instructor_dashboard_page
|
||||
|
||||
@@ -142,10 +145,10 @@ class EntranceExamGradeTest(BaseInstructorDashboardTest):
|
||||
Then I see Student Email input box, Reset Student Attempt, Rescore Student Submission,
|
||||
Delete Student State for entrance exam and Show Background Task History for Student buttons
|
||||
"""
|
||||
self.assertTrue(self.student_admin_section.is_student_email_input_visible())
|
||||
self.assertTrue(self.student_admin_section.is_reset_attempts_button_visible())
|
||||
self.assertTrue(self.student_admin_section.is_rescore_submission_button_visible())
|
||||
self.assertTrue(self.student_admin_section.is_delete_student_state_button_visible())
|
||||
self.assertTrue(self.student_admin_section.is_entrance_exam_student_email_input_visible())
|
||||
self.assertTrue(self.student_admin_section.is_entrance_exam_reset_attempts_button_visible())
|
||||
self.assertTrue(self.student_admin_section.is_entrance_exam_rescore_submission_button_visible())
|
||||
self.assertTrue(self.student_admin_section.is_entrance_exam_delete_student_state_button_visible())
|
||||
self.assertTrue(self.student_admin_section.is_background_task_history_button_visible())
|
||||
|
||||
def test_clicking_reset_student_attempts_button_without_email_shows_error(self):
|
||||
@@ -158,10 +161,10 @@ class EntranceExamGradeTest(BaseInstructorDashboardTest):
|
||||
Then I should be shown an Error Notification
|
||||
And The Notification message should read 'Please enter a student email address or username.'
|
||||
"""
|
||||
self.student_admin_section.click_reset_attempts_button()
|
||||
self.student_admin_section.entrance_exam_click_reset_attempts_button()
|
||||
self.assertEqual(
|
||||
'Please enter a student email address or username.',
|
||||
self.student_admin_section.top_notification.text[0]
|
||||
self.student_admin_section.entrance_exam_top_notification.text[0]
|
||||
)
|
||||
|
||||
def test_clicking_reset_student_attempts_button_with_success(self):
|
||||
@@ -174,8 +177,8 @@ class EntranceExamGradeTest(BaseInstructorDashboardTest):
|
||||
email address or username
|
||||
Then I should be shown an alert with success message
|
||||
"""
|
||||
self.student_admin_section.set_student_email(self.student_identifier)
|
||||
self.student_admin_section.click_reset_attempts_button()
|
||||
self.student_admin_section.set_student_email_for_ee(self.student_identifier)
|
||||
self.student_admin_section.entrance_exam_click_reset_attempts_button()
|
||||
alert = get_modal_alert(self.student_admin_section.browser)
|
||||
alert.dismiss()
|
||||
|
||||
@@ -188,10 +191,10 @@ class EntranceExamGradeTest(BaseInstructorDashboardTest):
|
||||
Adjustment after non existing student email address or username
|
||||
Then I should be shown an error message
|
||||
"""
|
||||
self.student_admin_section.set_student_email('non_existing@example.com')
|
||||
self.student_admin_section.click_reset_attempts_button()
|
||||
self.student_admin_section.set_student_email_for_ee('non_existing@example.com')
|
||||
self.student_admin_section.entrance_exam_click_reset_attempts_button()
|
||||
self.student_admin_section.wait_for_ajax()
|
||||
self.assertGreater(len(self.student_admin_section.top_notification.text[0]), 0)
|
||||
self.assertGreater(len(self.student_admin_section.entrance_exam_top_notification.text[0]), 0)
|
||||
|
||||
def test_clicking_rescore_submission_button_with_success(self):
|
||||
"""
|
||||
@@ -202,8 +205,8 @@ class EntranceExamGradeTest(BaseInstructorDashboardTest):
|
||||
Adjustment after entering a valid student email address or username
|
||||
Then I should be shown an alert with success message
|
||||
"""
|
||||
self.student_admin_section.set_student_email(self.student_identifier)
|
||||
self.student_admin_section.click_rescore_submissions_button()
|
||||
self.student_admin_section.set_student_email_for_ee(self.student_identifier)
|
||||
self.student_admin_section.entrance_exam_click_rescore_submissions_button()
|
||||
alert = get_modal_alert(self.student_admin_section.browser)
|
||||
alert.dismiss()
|
||||
|
||||
@@ -216,10 +219,10 @@ class EntranceExamGradeTest(BaseInstructorDashboardTest):
|
||||
Adjustment after non existing student email address or username
|
||||
Then I should be shown an error message
|
||||
"""
|
||||
self.student_admin_section.set_student_email('non_existing@example.com')
|
||||
self.student_admin_section.click_rescore_submissions_button()
|
||||
self.student_admin_section.set_student_email_for_ee('non_existing@example.com')
|
||||
self.student_admin_section.entrance_exam_click_rescore_submissions_button()
|
||||
self.student_admin_section.wait_for_ajax()
|
||||
self.assertGreater(len(self.student_admin_section.top_notification.text[0]), 0)
|
||||
self.assertGreater(len(self.student_admin_section.entrance_exam_top_notification.text[0]), 0)
|
||||
|
||||
def test_clicking_skip_entrance_exam_button_with_success(self):
|
||||
"""
|
||||
@@ -231,7 +234,7 @@ class EntranceExamGradeTest(BaseInstructorDashboardTest):
|
||||
email address or username
|
||||
Then I should be shown an alert with success message
|
||||
"""
|
||||
self.student_admin_section.set_student_email(self.student_identifier)
|
||||
self.student_admin_section.set_student_email_for_ee(self.student_identifier)
|
||||
self.student_admin_section.click_skip_entrance_exam_button()
|
||||
#first we have window.confirm
|
||||
alert = get_modal_alert(self.student_admin_section.browser)
|
||||
@@ -251,14 +254,14 @@ class EntranceExamGradeTest(BaseInstructorDashboardTest):
|
||||
student email address or username
|
||||
Then I should be shown an error message
|
||||
"""
|
||||
self.student_admin_section.set_student_email('non_existing@example.com')
|
||||
self.student_admin_section.set_student_email_for_ee('non_existing@example.com')
|
||||
self.student_admin_section.click_skip_entrance_exam_button()
|
||||
#first we have window.confirm
|
||||
alert = get_modal_alert(self.student_admin_section.browser)
|
||||
alert.accept()
|
||||
|
||||
self.student_admin_section.wait_for_ajax()
|
||||
self.assertGreater(len(self.student_admin_section.top_notification.text[0]), 0)
|
||||
self.assertGreater(len(self.student_admin_section.entrance_exam_top_notification.text[0]), 0)
|
||||
|
||||
def test_clicking_delete_student_attempts_button_with_success(self):
|
||||
"""
|
||||
@@ -270,8 +273,8 @@ class EntranceExamGradeTest(BaseInstructorDashboardTest):
|
||||
email address or username
|
||||
Then I should be shown an alert with success message
|
||||
"""
|
||||
self.student_admin_section.set_student_email(self.student_identifier)
|
||||
self.student_admin_section.click_delete_student_state_button()
|
||||
self.student_admin_section.set_student_email_for_ee(self.student_identifier)
|
||||
self.student_admin_section.entrance_exam_click_delete_student_state_button()
|
||||
alert = get_modal_alert(self.student_admin_section.browser)
|
||||
alert.dismiss()
|
||||
|
||||
@@ -286,10 +289,10 @@ class EntranceExamGradeTest(BaseInstructorDashboardTest):
|
||||
email address or username
|
||||
Then I should be shown an error message
|
||||
"""
|
||||
self.student_admin_section.set_student_email('non_existing@example.com')
|
||||
self.student_admin_section.click_delete_student_state_button()
|
||||
self.student_admin_section.set_student_email_for_ee('non_existing@example.com')
|
||||
self.student_admin_section.entrance_exam_click_delete_student_state_button()
|
||||
self.student_admin_section.wait_for_ajax()
|
||||
self.assertGreater(len(self.student_admin_section.top_notification.text[0]), 0)
|
||||
self.assertGreater(len(self.student_admin_section.entrance_exam_top_notification.text[0]), 0)
|
||||
|
||||
def test_clicking_task_history_button_with_success(self):
|
||||
"""
|
||||
@@ -301,8 +304,8 @@ class EntranceExamGradeTest(BaseInstructorDashboardTest):
|
||||
email address or username
|
||||
Then I should be shown an table listing all background tasks
|
||||
"""
|
||||
self.student_admin_section.set_student_email(self.student_identifier)
|
||||
self.student_admin_section.click_task_history_button()
|
||||
self.student_admin_section.set_student_email_for_ee(self.student_identifier)
|
||||
self.student_admin_section.entrance_exam_click_task_history_button()
|
||||
self.assertTrue(self.student_admin_section.is_background_task_history_table_visible())
|
||||
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
Tests the "preview" selector in the LMS that allows changing between Staff, Student, and Content Groups.
|
||||
"""
|
||||
|
||||
from ..helpers import UniqueCourseTest, create_user_partition_json
|
||||
from ..helpers import UniqueCourseTest, create_user_partition_json, get_modal_alert
|
||||
from ...pages.studio.auto_auth import AutoAuthPage
|
||||
from ...pages.lms.courseware import CoursewarePage
|
||||
from ...pages.lms.instructor_dashboard import InstructorDashboardPage, StudentAdminPage
|
||||
from ...pages.lms.staff_view import StaffPage
|
||||
from ...pages.common.sudo_page import SudoPage
|
||||
from ...fixtures.course import CourseFixture, XBlockFixtureDesc
|
||||
from xmodule.partitions.partitions import Group
|
||||
from textwrap import dedent
|
||||
@@ -36,8 +38,9 @@ class StaffViewTest(UniqueCourseTest):
|
||||
|
||||
# Auto-auth register for the course.
|
||||
# Do this as global staff so that you will see the Staff View
|
||||
self.staff_password = 'test'
|
||||
AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL,
|
||||
course_id=self.course_id, staff=True).visit()
|
||||
course_id=self.course_id, staff=True, password=self.staff_password).visit()
|
||||
|
||||
def _goto_staff_page(self):
|
||||
"""
|
||||
@@ -99,26 +102,41 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
"""
|
||||
Tests that verify the staff debug info.
|
||||
"""
|
||||
|
||||
def _goto_student_admin_section(self):
|
||||
"""
|
||||
Get sudo access and return student admin section.
|
||||
"""
|
||||
instructor_page = InstructorDashboardPage(self.browser, self.course_id)
|
||||
sudo_page = SudoPage(self.browser, instructor_page)
|
||||
sudo_page.wait_for_page()
|
||||
sudo_page.submit_sudo_password_and_get_access(self.staff_password)
|
||||
|
||||
student_admin_section = StudentAdminPage(self.browser)
|
||||
student_admin_section.wait_for_page()
|
||||
return student_admin_section
|
||||
|
||||
def test_reset_attempts_empty(self):
|
||||
"""
|
||||
Test that we reset even when there is no student state
|
||||
"""
|
||||
|
||||
staff_debug_page = self._goto_staff_page().open_staff_debug_info()
|
||||
staff_debug_page.reset_attempts()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully reset the attempts '
|
||||
'for user {}'.format(self.USERNAME), msg)
|
||||
staff_debug_page.click_student_grade_adjustments()
|
||||
student_admin_section = self._goto_student_admin_section()
|
||||
student_admin_section.click_reset_attempts_button()
|
||||
alert = get_modal_alert(student_admin_section.browser)
|
||||
alert.dismiss()
|
||||
|
||||
def test_delete_state_empty(self):
|
||||
"""
|
||||
Test that we delete properly even when there isn't state to delete.
|
||||
"""
|
||||
staff_debug_page = self._goto_staff_page().open_staff_debug_info()
|
||||
staff_debug_page.delete_state()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully deleted student state '
|
||||
'for user {}'.format(self.USERNAME), msg)
|
||||
staff_debug_page.click_student_grade_adjustments()
|
||||
student_admin_section = self._goto_student_admin_section()
|
||||
student_admin_section.click_delete_student_state_button()
|
||||
self.assertEqual(len(student_admin_section.top_notification.text[0]), 0)
|
||||
|
||||
def test_reset_attempts_state(self):
|
||||
"""
|
||||
@@ -128,10 +146,11 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
staff_page.answer_problem()
|
||||
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.reset_attempts()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully reset the attempts '
|
||||
'for user {}'.format(self.USERNAME), msg)
|
||||
staff_debug_page.click_student_grade_adjustments()
|
||||
student_admin_section = self._goto_student_admin_section()
|
||||
student_admin_section.click_reset_attempts_button()
|
||||
alert = get_modal_alert(student_admin_section.browser)
|
||||
alert.dismiss()
|
||||
|
||||
def test_rescore_state(self):
|
||||
"""
|
||||
@@ -141,9 +160,11 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
staff_page.answer_problem()
|
||||
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.rescore()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully rescored problem for user STAFF_TESTER', msg)
|
||||
staff_debug_page.click_student_grade_adjustments()
|
||||
student_admin_section = self._goto_student_admin_section()
|
||||
student_admin_section.click_rescore_submissions_button()
|
||||
alert = get_modal_alert(student_admin_section.browser)
|
||||
alert.dismiss()
|
||||
|
||||
def test_student_state_delete(self):
|
||||
"""
|
||||
@@ -153,10 +174,10 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
staff_page.answer_problem()
|
||||
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.delete_state()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully deleted student state '
|
||||
'for user {}'.format(self.USERNAME), msg)
|
||||
staff_debug_page.click_student_grade_adjustments()
|
||||
student_admin_section = self._goto_student_admin_section()
|
||||
student_admin_section.click_delete_student_state_button()
|
||||
self.assertEqual(len(student_admin_section.top_notification.text[0]), 0)
|
||||
|
||||
def test_student_by_email(self):
|
||||
"""
|
||||
@@ -166,10 +187,11 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
staff_page.answer_problem()
|
||||
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.reset_attempts(self.EMAIL)
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully reset the attempts '
|
||||
'for user {}'.format(self.EMAIL), msg)
|
||||
staff_debug_page.click_student_grade_adjustments(self.EMAIL)
|
||||
student_admin_section = self._goto_student_admin_section()
|
||||
student_admin_section.click_reset_attempts_button()
|
||||
alert = get_modal_alert(student_admin_section.browser)
|
||||
alert.dismiss()
|
||||
|
||||
def test_bad_student(self):
|
||||
"""
|
||||
@@ -179,10 +201,10 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
staff_page.answer_problem()
|
||||
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.delete_state('INVALIDUSER')
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Failed to delete student state. '
|
||||
'User does not exist.', msg)
|
||||
staff_debug_page.click_student_grade_adjustments('INVALIDUSER')
|
||||
student_admin_section = self._goto_student_admin_section()
|
||||
student_admin_section.click_delete_student_state_button()
|
||||
self.assertGreater(len(student_admin_section.top_notification.text[0]), 0)
|
||||
|
||||
def test_reset_attempts_for_problem_loaded_via_ajax(self):
|
||||
"""
|
||||
@@ -193,10 +215,11 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
staff_page.answer_problem()
|
||||
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.reset_attempts()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully reset the attempts '
|
||||
'for user {}'.format(self.USERNAME), msg)
|
||||
staff_debug_page.click_student_grade_adjustments()
|
||||
student_admin_section = self._goto_student_admin_section()
|
||||
student_admin_section.click_reset_attempts_button()
|
||||
alert = get_modal_alert(student_admin_section.browser)
|
||||
alert.dismiss()
|
||||
|
||||
def test_rescore_state_for_problem_loaded_via_ajax(self):
|
||||
"""
|
||||
@@ -207,9 +230,11 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
staff_page.answer_problem()
|
||||
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.rescore()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully rescored problem for user STAFF_TESTER', msg)
|
||||
staff_debug_page.click_student_grade_adjustments()
|
||||
student_admin_section = self._goto_student_admin_section()
|
||||
student_admin_section.click_rescore_submissions_button()
|
||||
alert = get_modal_alert(student_admin_section.browser)
|
||||
alert.dismiss()
|
||||
|
||||
def test_student_state_delete_for_problem_loaded_via_ajax(self):
|
||||
"""
|
||||
@@ -220,10 +245,10 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
staff_page.answer_problem()
|
||||
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.delete_state()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully deleted student state '
|
||||
'for user {}'.format(self.USERNAME), msg)
|
||||
staff_debug_page.click_student_grade_adjustments()
|
||||
student_admin_section = self._goto_student_admin_section()
|
||||
student_admin_section.click_delete_student_state_button()
|
||||
self.assertEqual(len(student_admin_section.top_notification.text[0]), 0)
|
||||
|
||||
|
||||
class CourseWithContentGroupsTest(StaffViewTest):
|
||||
|
||||
@@ -5,6 +5,7 @@ from flaky import flaky
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
from .base_studio_test import StudioCourseTest
|
||||
from ..helpers import get_sudo_access
|
||||
from ...pages.studio.auto_auth import AutoAuthPage
|
||||
|
||||
from ...pages.studio.users import CourseTeamPage
|
||||
@@ -38,6 +39,7 @@ class CourseTeamPageTest(StudioCourseTest):
|
||||
self.page = CourseTeamPage( # pylint:disable=attribute-defined-outside-init
|
||||
self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run']
|
||||
)
|
||||
get_sudo_access(self.browser, self.page, self.user.get('password'))
|
||||
self._go_to_course_team_page()
|
||||
|
||||
def _go_to_course_team_page(self):
|
||||
@@ -125,6 +127,7 @@ class CourseTeamPageTest(StudioCourseTest):
|
||||
self.page.add_user_to_course(self.other_user.get('email'))
|
||||
self._assert_user_present(self.other_user, present=True)
|
||||
self.log_in(self.other_user)
|
||||
get_sudo_access(self.browser, self.page, self.other_user.get('password'))
|
||||
self._assert_current_course(visible=True)
|
||||
|
||||
@flaky # TODO fix this, see TNL-2667
|
||||
@@ -143,6 +146,7 @@ class CourseTeamPageTest(StudioCourseTest):
|
||||
self._assert_user_present(self.other_user, present=True)
|
||||
|
||||
self.log_in(self.other_user)
|
||||
get_sudo_access(self.browser, self.page, self.other_user.get('password'))
|
||||
self._assert_current_course(visible=True)
|
||||
self._go_to_course_team_page()
|
||||
|
||||
@@ -204,6 +208,7 @@ class CourseTeamPageTest(StudioCourseTest):
|
||||
self._assert_is_admin(other)
|
||||
|
||||
self.log_in(self.other_user)
|
||||
get_sudo_access(self.browser, self.page, self.other_user.get('password'))
|
||||
self._go_to_course_team_page()
|
||||
other = self.page.get_user(self.other_user.get('email'))
|
||||
self.assertTrue(other.is_current_user)
|
||||
@@ -235,12 +240,14 @@ class CourseTeamPageTest(StudioCourseTest):
|
||||
|
||||
# precondition check - frank is an admin and can add/delete/promote/demote users
|
||||
self.log_in(self.other_user)
|
||||
get_sudo_access(self.browser, self.page, self.other_user.get('password'))
|
||||
self._go_to_course_team_page()
|
||||
other = self.page.get_user(self.other_user.get('email'))
|
||||
self.assertTrue(other.is_current_user)
|
||||
self._assert_can_manage_users()
|
||||
|
||||
self.log_in(self.user)
|
||||
get_sudo_access(self.browser, self.page, self.user.get('password'))
|
||||
self._go_to_course_team_page()
|
||||
other = self.page.get_user(self.other_user.get('email'))
|
||||
other.click_demote()
|
||||
@@ -249,6 +256,7 @@ class CourseTeamPageTest(StudioCourseTest):
|
||||
self._assert_is_staff(other)
|
||||
|
||||
self.log_in(self.other_user)
|
||||
get_sudo_access(self.browser, self.page, self.other_user.get('password'))
|
||||
self._go_to_course_team_page()
|
||||
other = self.page.get_user(self.other_user.get('email'))
|
||||
self.assertTrue(other.is_current_user)
|
||||
@@ -334,6 +342,7 @@ class CourseTeamPageTest(StudioCourseTest):
|
||||
self.assertFalse(current.can_promote)
|
||||
|
||||
self.log_in(self.other_user)
|
||||
get_sudo_access(self.browser, self.page, self.other_user.get('password'))
|
||||
self._go_to_course_team_page()
|
||||
|
||||
current = self.page.get_user(self.user.get('email'))
|
||||
|
||||
@@ -7,6 +7,7 @@ from flaky import flaky
|
||||
|
||||
from .base_studio_test import StudioLibraryTest
|
||||
from ...fixtures.course import XBlockFixtureDesc
|
||||
from ..helpers import get_sudo_access
|
||||
from ...pages.studio.auto_auth import AutoAuthPage
|
||||
from ...pages.studio.utils import add_component
|
||||
from ...pages.studio.library import LibraryEditPage
|
||||
@@ -514,6 +515,7 @@ class LibraryUsersPageTest(StudioLibraryTest):
|
||||
AutoAuthPage(self.browser, username="second", email="second@example.com", no_login=True).visit()
|
||||
|
||||
self.page = LibraryUsersPage(self.browser, self.library_key)
|
||||
get_sudo_access(self.browser, self.page, self.user.get("password"))
|
||||
self.page.visit()
|
||||
|
||||
def _refresh_page(self):
|
||||
|
||||
@@ -11,6 +11,7 @@ from ..pages.studio.settings_group_configurations import GroupConfigurationsPage
|
||||
from ..pages.studio.auto_auth import AutoAuthPage as StudioAutoAuthPage
|
||||
from ..fixtures.course import XBlockFixtureDesc
|
||||
from ..fixtures import LMS_BASE_URL
|
||||
from .helpers import get_sudo_access
|
||||
from ..pages.studio.component_editor import ComponentVisibilityEditorView
|
||||
from ..pages.lms.instructor_dashboard import InstructorDashboardPage
|
||||
from ..pages.lms.courseware import CoursewarePage
|
||||
@@ -54,8 +55,12 @@ class EndToEndCohortedCoursewareTest(ContainerBase):
|
||||
).visit()
|
||||
|
||||
# Start logged in as the staff user.
|
||||
self.instructor_password = 'test'
|
||||
StudioAutoAuthPage(
|
||||
self.browser, username=self.staff_user["username"], email=self.staff_user["email"]
|
||||
self.browser,
|
||||
username=self.staff_user["username"],
|
||||
email=self.staff_user["email"],
|
||||
password=self.instructor_password
|
||||
).visit()
|
||||
|
||||
def populate_course_fixture(self, course_fixture):
|
||||
@@ -138,6 +143,7 @@ class EndToEndCohortedCoursewareTest(ContainerBase):
|
||||
Each cohort is assigned one student.
|
||||
"""
|
||||
instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id)
|
||||
get_sudo_access(self.browser, instructor_dashboard_page, self.instructor_password)
|
||||
instructor_dashboard_page.visit()
|
||||
cohort_management_page = instructor_dashboard_page.select_cohort_management()
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
|
||||
def setUp(self):
|
||||
super(TestOptoutCourseEmails, self).setUp()
|
||||
course_title = u"ẗëṡẗ title イ乇丂イ ᄊ乇丂丂ムg乇 キo尺 ムレレ тэѕт мэѕѕаБэ"
|
||||
self.course = CourseFactory.create(display_name=course_title)
|
||||
self.course = CourseFactory.create(display_name=course_title, run='T12015')
|
||||
self.instructor = AdminFactory.create()
|
||||
self.student = UserFactory.create()
|
||||
CourseEnrollmentFactory.create(user=self.student, course_id=self.course.id)
|
||||
@@ -47,6 +47,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
|
||||
"""Navigate to the instructor dash's email view"""
|
||||
# Pull up email view on instructor dashboard
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
response = self.client.get(url)
|
||||
email_section = '<div class="vert-left send-email" id="section-send-email">'
|
||||
# If this fails, it is likely because ENABLE_INSTRUCTOR_EMAIL is set to False
|
||||
|
||||
@@ -53,7 +53,7 @@ class EmailSendFromDashboardTestCase(ModuleStoreTestCase):
|
||||
def setUp(self):
|
||||
super(EmailSendFromDashboardTestCase, self).setUp()
|
||||
course_title = u"ẗëṡẗ title イ乇丂イ ᄊ乇丂丂ムg乇 キo尺 ムレレ тэѕт мэѕѕаБэ"
|
||||
self.course = CourseFactory.create(display_name=course_title)
|
||||
self.course = CourseFactory.create(display_name=course_title, run="1T2015")
|
||||
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
|
||||
@@ -75,6 +75,7 @@ class EmailSendFromDashboardTestCase(ModuleStoreTestCase):
|
||||
self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
# Response loads the whole instructor dashboard, so no need to explicitly
|
||||
# navigate to a particular email section
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
response = self.client.get(self.url)
|
||||
email_section = '<div class="vert-left send-email" id="section-send-email">'
|
||||
# If this fails, it is likely because ENABLE_INSTRUCTOR_EMAIL is set to False
|
||||
|
||||
@@ -47,9 +47,10 @@ class TestEmailErrors(ModuleStoreTestCase):
|
||||
def setUp(self):
|
||||
super(TestEmailErrors, self).setUp()
|
||||
course_title = u"ẗëṡẗ title イ乇丂イ ᄊ乇丂丂ムg乇 キo尺 ムレレ тэѕт мэѕѕаБэ"
|
||||
self.course = CourseFactory.create(display_name=course_title)
|
||||
self.course = CourseFactory.create(display_name=course_title, run="1T2015")
|
||||
self.instructor = AdminFactory.create()
|
||||
self.client.login(username=self.instructor.username, password="test")
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
|
||||
# load initial content (since we don't run migrations as part of tests):
|
||||
call_command("loaddata", "course_email_template.json")
|
||||
|
||||
@@ -3,7 +3,7 @@ django admin pages for courseware model
|
||||
'''
|
||||
|
||||
from courseware.models import StudentModule, OfflineComputedGrade, OfflineComputedGradeLog
|
||||
from ratelimitbackend import admin
|
||||
from django.contrib import admin
|
||||
|
||||
admin.site.register(StudentModule)
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ Feature: LMS.LTI component
|
||||
Then I see text "Problem Scores: 5/10"
|
||||
And I see graph with total progress "5%"
|
||||
Then I click on the "Instructor" tab
|
||||
Then I get sudo access with password "test"
|
||||
And I click on the "Student Admin" tab
|
||||
And I click on the "View Gradebook" link
|
||||
And I see in the gradebook table that "HW" is "50"
|
||||
@@ -90,6 +91,7 @@ Feature: LMS.LTI component
|
||||
Then I see text "Problem Scores: 8/10"
|
||||
And I see graph with total progress "8%"
|
||||
Then I click on the "Instructor" tab
|
||||
Then I get sudo access with password "test"
|
||||
And I click on the "Student Admin" tab
|
||||
And I click on the "View Gradebook" link
|
||||
And I see in the gradebook table that "HW" is "80"
|
||||
@@ -116,6 +118,7 @@ Feature: LMS.LTI component
|
||||
Then I see text "Problem Scores: 0/10"
|
||||
And I see graph with total progress "0%"
|
||||
Then I click on the "Instructor" tab
|
||||
Then I get sudo access with password "test"
|
||||
And I click on the "Student Admin" tab
|
||||
And I click on the "View Gradebook" link
|
||||
And I see in the gradebook table that "HW" is "0"
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
@shard_1
|
||||
Feature: LMS.Debug staff info links
|
||||
As a course staff in an edX course
|
||||
In order to test my understanding of the material
|
||||
I want to click on staff debug info links
|
||||
|
||||
Scenario: I can reset student attempts
|
||||
When i am staff member for the course "model_course"
|
||||
And I am viewing a "multiple choice" problem
|
||||
And I can view staff debug info
|
||||
Then I can reset student attempts
|
||||
Then I cannot see delete student state link
|
||||
Then I cannot see rescore student submission link
|
||||
@@ -1,51 +0,0 @@
|
||||
"""
|
||||
Steps for staff_debug_info.feature lettuce tests
|
||||
"""
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from lettuce import world, step
|
||||
from common import create_course, course_id
|
||||
from courseware.courses import get_course_by_id
|
||||
from instructor.access import allow_access
|
||||
|
||||
|
||||
@step(u'i am staff member for the course "([^"]*)"$')
|
||||
def i_am_staff_member_for_the_course(step, course_number):
|
||||
# Create the course
|
||||
create_course(step, course_number)
|
||||
course = get_course_by_id(course_id(course_number))
|
||||
|
||||
# Create the user
|
||||
world.create_user('robot', 'test')
|
||||
user = User.objects.get(username='robot')
|
||||
|
||||
# Add user as a course staff.
|
||||
allow_access(course, user, "staff")
|
||||
|
||||
world.log_in(username='robot', password='test')
|
||||
|
||||
|
||||
@step(u'I can view staff debug info')
|
||||
def view_staff_debug_info(step):
|
||||
css_selector = "a.instructor-info-action"
|
||||
world.css_click(css_selector)
|
||||
world.wait_for_visible("section.staff-modal")
|
||||
|
||||
|
||||
@step(u'I can reset student attempts')
|
||||
def view_staff_debug_info(step):
|
||||
css_selector = "a.staff-debug-reset"
|
||||
world.css_click(css_selector)
|
||||
world.wait_for_ajax_complete()
|
||||
|
||||
|
||||
@step(u'I cannot see delete student state link')
|
||||
def view_staff_debug_info(step):
|
||||
css_selector = "a.staff-debug-sdelete"
|
||||
world.is_css_not_present(css_selector)
|
||||
|
||||
|
||||
@step(u'I cannot see rescore student submission link')
|
||||
def view_staff_debug_info(step):
|
||||
css_selector = "a.staff-debug-rescore"
|
||||
world.is_css_not_present(css_selector)
|
||||
@@ -386,6 +386,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
# hit skip entrance exam api in instructor app
|
||||
instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.client.login(username=instructor.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
url = reverse('mark_student_can_skip_entrance_exam', kwargs={'course_id': unicode(self.course.id)})
|
||||
response = self.client.post(url, {
|
||||
'unique_student_identifier': self.request.user.email,
|
||||
|
||||
@@ -383,6 +383,7 @@ class EntranceExamsTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.client.logout()
|
||||
self.client.login(username=instructor.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
|
||||
url = reverse('mark_student_can_skip_entrance_exam', kwargs={'course_id': unicode(self.course.id)})
|
||||
response = self.client.post(url, {
|
||||
|
||||
@@ -67,6 +67,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
'book_index': index})
|
||||
for index, __ in enumerate(course.textbooks)
|
||||
])
|
||||
self.grant_sudo_access(unicode(course.id), 'test')
|
||||
for url in urls:
|
||||
self.assert_request_status_code(404, url)
|
||||
|
||||
@@ -81,6 +82,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
'book_index': index})
|
||||
for index in xrange(len(course.textbooks))
|
||||
])
|
||||
self.grant_sudo_access(unicode(course.id), 'test')
|
||||
for url in urls:
|
||||
self.assert_request_status_code(200, url)
|
||||
|
||||
@@ -207,6 +209,8 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
urls = [reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()}),
|
||||
reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id.to_deprecated_string()})]
|
||||
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
self.grant_sudo_access(unicode(self.test_course.id), 'test')
|
||||
# Shouldn't be able to get to the instructor pages
|
||||
for url in urls:
|
||||
self.assert_request_status_code(404, url)
|
||||
@@ -218,6 +222,8 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"""
|
||||
self.login(self.staff_user)
|
||||
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
self.grant_sudo_access(unicode(self.test_course.id), 'test')
|
||||
# Now should be able to get to self.course, but not self.test_course
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
self.assert_request_status_code(200, url)
|
||||
@@ -232,6 +238,8 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"""
|
||||
self.login(self.instructor_user)
|
||||
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
self.grant_sudo_access(unicode(self.test_course.id), 'test')
|
||||
# Now should be able to get to self.course, but not self.test_course
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
self.assert_request_status_code(200, url)
|
||||
@@ -245,6 +253,9 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
and student profile pages for course in their org.
|
||||
"""
|
||||
self.login(self.org_staff_user)
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
self.grant_sudo_access(unicode(self.test_course.id), 'test')
|
||||
self.grant_sudo_access(unicode(self.other_org_course.id), 'test')
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
self.assert_request_status_code(200, url)
|
||||
|
||||
@@ -260,6 +271,9 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
and student profile pages for course in their org.
|
||||
"""
|
||||
self.login(self.org_instructor_user)
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
self.grant_sudo_access(unicode(self.test_course.id), 'test')
|
||||
self.grant_sudo_access(unicode(self.other_org_course.id), 'test')
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
self.assert_request_status_code(200, url)
|
||||
|
||||
@@ -275,6 +289,8 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"""
|
||||
self.login(self.global_staff_user)
|
||||
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
self.grant_sudo_access(unicode(self.test_course.id), 'test')
|
||||
# and now should be able to load both
|
||||
urls = [reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()}),
|
||||
reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id.to_deprecated_string()})]
|
||||
@@ -387,6 +403,26 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.login(self.global_staff_user)
|
||||
self.assertTrue(self.enroll(self.course))
|
||||
|
||||
def test_org_instructor_cannot_access_without_sudo(self):
|
||||
"""
|
||||
Test that org instructor cannot load the instructor dashboard without sudo access
|
||||
and it redirect org instructor to sudo password page.
|
||||
"""
|
||||
self.login(self.org_instructor_user)
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': unicode(self.course.id)})
|
||||
response = self.assert_request_status_code(401, url)
|
||||
self.assertIn('Unauthorized', response.content)
|
||||
|
||||
def test_org_staff_cannot_access_without_sudo(self):
|
||||
"""
|
||||
Test that org staff cannot load the instructor dashboard without sudo access
|
||||
and it redirect org staff to sudo password page.
|
||||
"""
|
||||
self.login(self.org_staff_user)
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': unicode(self.course.id)})
|
||||
response = self.assert_request_status_code(401, url)
|
||||
self.assertIn('Unauthorized', response.content)
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class TestBetatesterAccess(ModuleStoreTestCase, CourseAccessTestMixin):
|
||||
|
||||
@@ -32,6 +32,7 @@ from course_modes.models import CourseMode
|
||||
from courseware.testutils import RenderXBlockTestMixin
|
||||
from courseware.tests.factories import StudentModuleFactory
|
||||
from edxmako.tests import mako_middleware_process_request
|
||||
from django_sudo_helpers.tests.utils import sudo_middleware_process_request
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import AdminFactory, UserFactory, CourseEnrollmentFactory
|
||||
from util.tests.test_date_utils import fake_ugettext, fake_pgettext
|
||||
@@ -1152,6 +1153,7 @@ class TestIndexView(ModuleStoreTestCase):
|
||||
)
|
||||
request.user = user
|
||||
mako_middleware_process_request(request)
|
||||
sudo_middleware_process_request(request)
|
||||
|
||||
# Trigger the assertions embedded in the ViewCheckerBlocks
|
||||
response = views.index(request, unicode(course.id), chapter=chapter.url_name, section=section.url_name)
|
||||
|
||||
@@ -25,6 +25,7 @@ from django.shortcuts import redirect
|
||||
from certificates import api as certs_api
|
||||
from edxmako.shortcuts import render_to_response, render_to_string, marketing_link
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from sudo.utils import revoke_sudo_privileges
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.db import transaction
|
||||
from markupsafe import escape
|
||||
@@ -340,6 +341,10 @@ def index(request, course_id, chapter=None, section=None,
|
||||
- HTTPresponse
|
||||
"""
|
||||
|
||||
# Revoke sudo privileges from a request explicitly
|
||||
if request.is_sudo(region=course_id):
|
||||
revoke_sudo_privileges(request, region=course_id)
|
||||
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
|
||||
user = User.objects.prefetch_related("groups").get(id=request.user.id)
|
||||
|
||||
@@ -7,6 +7,8 @@ Feature: LMS.Instructor Dash Bulk Email
|
||||
Scenario: Send bulk email
|
||||
Given there is a course with a staff, instructor and student
|
||||
And I am logged in to the course as "<Role>"
|
||||
Then I go to instructor tab
|
||||
Then I get sudo access with password "test"
|
||||
When I send email to "<Recipient>"
|
||||
Then Email is sent to "<Recipient>"
|
||||
|
||||
|
||||
@@ -96,6 +96,13 @@ def log_into_the_course(step, role): # pylint: disable=unused-argument
|
||||
world.expected_addresses['myself'] = [my_email]
|
||||
|
||||
|
||||
@step("I go to instructor tab")
|
||||
def i_got_to_instructor_tab(step): # pylint: disable=unused-argument
|
||||
url = '/courses/{}'.format(world.bulk_email_course_key)
|
||||
world.visit(url)
|
||||
world.css_click('a[href="{}/instructor"]'.format(url))
|
||||
|
||||
|
||||
@step(u'I send email to "([^"]*)"')
|
||||
def when_i_send_an_email(step, recipient): # pylint: disable=unused-argument
|
||||
|
||||
@@ -115,9 +122,6 @@ def when_i_send_an_email(step, recipient): # pylint: disable=unused-argument
|
||||
call_command('loaddata', 'course_email_template.json')
|
||||
|
||||
# Go to the email section of the instructor dash
|
||||
url = '/courses/{}'.format(world.bulk_email_course_key)
|
||||
world.visit(url)
|
||||
world.css_click('a[href="{}/instructor"]'.format(url))
|
||||
world.css_click('a[data-section="send_email"]')
|
||||
|
||||
# Select the recipient
|
||||
|
||||
@@ -12,6 +12,7 @@ from mock import patch
|
||||
from nose.tools import assert_in # pylint: disable=no-name-in-module
|
||||
|
||||
from courseware.tests.factories import StaffFactory, InstructorFactory
|
||||
from terrain.steps import i_get_sudo_access
|
||||
|
||||
|
||||
@step(u'Given I am "([^"]*)" for a very large course')
|
||||
@@ -71,11 +72,17 @@ def i_am_staff_or_instructor(step, role): # pylint: disable=unused-argument
|
||||
)
|
||||
|
||||
|
||||
def go_to_section(section_name):
|
||||
def go_to_instructor_tab(step):
|
||||
# section name should be one of
|
||||
# course_info, membership, student_admin, data_download, analytics, send_email
|
||||
world.visit(u'/courses/{}'.format(world.course_key))
|
||||
world.css_click(u'a[href="/courses/{}/instructor"]'.format(world.course_key))
|
||||
i_get_sudo_access(step, 'test')
|
||||
|
||||
|
||||
def go_to_section(section_name):
|
||||
# section name should be one of
|
||||
# course_info, membership, student_admin, data_download, analytics, send_email
|
||||
world.css_click('a[data-section="{0}"]'.format(section_name))
|
||||
|
||||
|
||||
@@ -84,6 +91,7 @@ def click_a_button(step, button): # pylint: disable=unused-argument
|
||||
|
||||
if button == "Generate Grade Report":
|
||||
# Go to the data download section of the instructor dash
|
||||
go_to_instructor_tab(step)
|
||||
go_to_section("data_download")
|
||||
|
||||
# Click generate grade report button
|
||||
@@ -101,18 +109,21 @@ def click_a_button(step, button): # pylint: disable=unused-argument
|
||||
|
||||
elif button == "Grading Configuration":
|
||||
# Go to the data download section of the instructor dash
|
||||
go_to_instructor_tab(step)
|
||||
go_to_section("data_download")
|
||||
|
||||
world.css_click('input[name="dump-gradeconf"]')
|
||||
|
||||
elif button == "List enrolled students' profile information":
|
||||
# Go to the data download section of the instructor dash
|
||||
go_to_instructor_tab(step)
|
||||
go_to_section("data_download")
|
||||
|
||||
world.css_click('input[name="list-profiles"]')
|
||||
|
||||
elif button == "Download profile information as a CSV":
|
||||
# Go to the data download section of the instructor dash
|
||||
go_to_instructor_tab(step)
|
||||
go_to_section("data_download")
|
||||
|
||||
world.css_click('input[name="list-profiles-csv"]')
|
||||
@@ -132,4 +143,5 @@ def click_a_button(step, tab_name): # pylint: disable=unused-argument
|
||||
'Analytics': 'analytics',
|
||||
'Email': 'send_email',
|
||||
}
|
||||
go_to_instructor_tab(step)
|
||||
go_to_section(tab_name_dict[tab_name])
|
||||
|
||||
@@ -283,6 +283,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
CourseEnrollment.enroll(staff_member, self.course.id)
|
||||
CourseFinanceAdminRole(self.course.id).add_users(staff_member)
|
||||
self.client.login(username=staff_member.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
# Try to promote to forums admin - not working
|
||||
# update_forum_role(self.course.id, staff_member, FORUM_ROLE_ADMINISTRATOR, 'allow')
|
||||
|
||||
@@ -305,6 +306,23 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"Staff member should not be allowed to access endpoint " + endpoint
|
||||
)
|
||||
|
||||
def test_staff_level_without_sudo_access(self):
|
||||
"""
|
||||
Ensure that a staff member redirected to sudo password page without sudo access.
|
||||
"""
|
||||
staff_member = StaffFactory(course_key=self.course.id)
|
||||
CourseEnrollment.enroll(staff_member, self.course.id)
|
||||
CourseFinanceAdminRole(self.course.id).add_users(staff_member)
|
||||
self.client.login(username=staff_member.username, password='test')
|
||||
|
||||
for endpoint, args in self.staff_level_endpoints:
|
||||
self._access_endpoint(
|
||||
endpoint,
|
||||
args,
|
||||
401,
|
||||
""
|
||||
)
|
||||
|
||||
def test_instructor_level(self):
|
||||
"""
|
||||
Ensure that an instructor member can access all endpoints.
|
||||
@@ -314,6 +332,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
|
||||
CourseFinanceAdminRole(self.course.id).add_users(inst)
|
||||
self.client.login(username=inst.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
|
||||
for endpoint, args in self.staff_level_endpoints:
|
||||
# TODO: make these work
|
||||
@@ -337,6 +356,31 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"Instructor should be allowed to access endpoint " + endpoint
|
||||
)
|
||||
|
||||
def test_instructor_level_without_sudo_access(self):
|
||||
"""
|
||||
Ensure that an instructor member redirected to sudo password page without sudo access.
|
||||
"""
|
||||
inst = InstructorFactory(course_key=self.course.id)
|
||||
CourseEnrollment.enroll(inst, self.course.id)
|
||||
CourseFinanceAdminRole(self.course.id).add_users(inst)
|
||||
self.client.login(username=inst.username, password='test')
|
||||
|
||||
for endpoint, args in self.staff_level_endpoints:
|
||||
self._access_endpoint(
|
||||
endpoint,
|
||||
args,
|
||||
401,
|
||||
""
|
||||
)
|
||||
|
||||
for endpoint, args in self.instructor_level_endpoints:
|
||||
self._access_endpoint(
|
||||
endpoint,
|
||||
args,
|
||||
401,
|
||||
""
|
||||
)
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
@patch.dict(settings.FEATURES, {'ALLOW_AUTOMATED_SIGNUPS': True})
|
||||
@@ -351,6 +395,7 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(ModuleStoreTestCase, Log
|
||||
self.course = CourseFactory.create()
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
self.url = reverse('register_and_enroll_students', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
|
||||
self.not_enrolled_student = UserFactory(
|
||||
@@ -655,6 +700,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.course = CourseFactory.create()
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
|
||||
self.enrolled_student = UserFactory(username='EnrolledStudent', first_name='Enrolled', last_name='Student')
|
||||
CourseEnrollment.enroll(
|
||||
@@ -1231,6 +1277,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
manually enrolling the students for the paid courses.
|
||||
"""
|
||||
paid_course = self.create_paid_course()
|
||||
self.grant_sudo_access(unicode(paid_course.id), 'test')
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': paid_course.id.to_deprecated_string()})
|
||||
params = {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': False,
|
||||
'auto_enroll': False}
|
||||
@@ -1256,6 +1303,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
test to unenroll allow to enroll user.
|
||||
"""
|
||||
paid_course = self.create_paid_course()
|
||||
self.grant_sudo_access(unicode(paid_course.id), 'test')
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': paid_course.id.to_deprecated_string()})
|
||||
params = {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': False,
|
||||
'auto_enroll': False, 'reason': 'testing..'}
|
||||
@@ -1306,6 +1354,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
test unenrolled user already not enrolled in a course.
|
||||
"""
|
||||
paid_course = self.create_paid_course()
|
||||
self.grant_sudo_access(unicode(paid_course.id), 'test')
|
||||
course_enrollment = CourseEnrollment.objects.filter(
|
||||
user__email=self.notregistered_email, course_id=paid_course.id
|
||||
)
|
||||
@@ -1403,6 +1452,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
|
||||
self.course = CourseFactory.create()
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
|
||||
self.beta_tester = BetaTesterFactory(course_key=self.course.id)
|
||||
CourseEnrollment.enroll(
|
||||
@@ -1731,6 +1781,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase
|
||||
self.course = CourseFactory.create()
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
|
||||
self.other_instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.other_staff = StaffFactory(course_key=self.course.id)
|
||||
@@ -1969,6 +2020,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
self.course_mode.save()
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
self.cart = Order.get_cart_for_user(self.instructor)
|
||||
self.coupon_code = 'abcde'
|
||||
self.coupon = Coupon(code=self.coupon_code, description='testing code', course_id=self.course.id,
|
||||
@@ -2396,6 +2448,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
UserProfileFactory.create(user=self.students[0], meta='{"company": "asdasda"}')
|
||||
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
url = reverse('get_enrollment_report', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {})
|
||||
self.assertIn('Your detailed enrollment report is being generated!', response.content)
|
||||
@@ -2445,6 +2498,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
|
||||
CourseFinanceAdminRole(self.course.id).add_users(self.instructor)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
|
||||
url = reverse('get_enrollment_report', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {})
|
||||
@@ -2467,6 +2521,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
|
||||
CourseFinanceAdminRole(self.course.id).add_users(self.instructor)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
|
||||
url = reverse('get_enrollment_report', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {})
|
||||
@@ -2492,6 +2547,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
|
||||
CourseFinanceAdminRole(self.course.id).add_users(self.instructor)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
|
||||
url = reverse('get_enrollment_report', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {})
|
||||
@@ -2649,6 +2705,7 @@ class TestInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollmentTestCase)
|
||||
self.course = CourseFactory.create()
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
|
||||
self.student = UserFactory()
|
||||
CourseEnrollment.enroll(self.student, self.course.id)
|
||||
@@ -2818,6 +2875,8 @@ class TestEntranceExamInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollm
|
||||
# Add instructor to invalid ee course
|
||||
CourseInstructorRole(self.course_with_invalid_ee.id).add_users(self.instructor)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course_with_invalid_ee.id), 'test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
|
||||
self.student = UserFactory()
|
||||
CourseEnrollment.enroll(self.student, self.course.id)
|
||||
@@ -2927,6 +2986,7 @@ class TestEntranceExamInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollm
|
||||
self.client.logout()
|
||||
staff_user = StaffFactory(course_key=self.course.id)
|
||||
self.client.login(username=staff_user.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
url = reverse('reset_student_attempts_for_entrance_exam',
|
||||
kwargs={'course_id': unicode(self.course.id)})
|
||||
response = self.client.get(url, {
|
||||
@@ -3057,6 +3117,7 @@ class TestInstructorSendEmail(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.course = CourseFactory.create()
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
test_subject = u'\u1234 test subject'
|
||||
test_message = u'\u6824 test message'
|
||||
self.full_test_message = {
|
||||
@@ -3182,6 +3243,7 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
)
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
|
||||
self.student = UserFactory()
|
||||
CourseEnrollment.enroll(self.student, self.course.id)
|
||||
@@ -3298,6 +3360,7 @@ class TestInstructorEmailContentList(ModuleStoreTestCase, LoginEnrollmentTestCas
|
||||
self.course = CourseFactory.create()
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
self.tasks = {}
|
||||
self.emails = {}
|
||||
self.emails_info = {}
|
||||
@@ -3552,6 +3615,7 @@ class TestDueDateExtensions(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
|
||||
self.instructor = InstructorFactory(course_key=course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
|
||||
def test_change_due_date(self):
|
||||
url = reverse('change_due_date', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
@@ -3676,6 +3740,7 @@ class TestCourseRegistrationCodes(ModuleStoreTestCase):
|
||||
CourseModeFactory.create(course_id=self.course.id, min_price=50)
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
CourseSalesAdminRole(self.course.id).add_users(self.instructor)
|
||||
|
||||
url = reverse('generate_registration_codes',
|
||||
@@ -4151,6 +4216,7 @@ class TestBulkCohorting(ModuleStoreTestCase):
|
||||
Verify that we get the error we expect for a given file input.
|
||||
"""
|
||||
self.client.login(username=self.staff_user.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
response = self.call_add_users_to_cohorts(file_content, suffix=file_suffix)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
result = json.loads(response.content)
|
||||
@@ -4164,6 +4230,7 @@ class TestBulkCohorting(ModuleStoreTestCase):
|
||||
"""
|
||||
mock_store_upload.return_value = (None, 'fake_file_name.csv')
|
||||
self.client.login(username=self.staff_user.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
response = self.call_add_users_to_cohorts(file_content)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertTrue(mock_store_upload.called)
|
||||
|
||||
@@ -33,6 +33,7 @@ class TestInstructorAPIEnrollmentEmailLocalization(ModuleStoreTestCase):
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
set_user_preference(self.instructor, LANGUAGE_KEY, 'zh-cn')
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
|
||||
self.student = UserFactory.create()
|
||||
set_user_preference(self.student, LANGUAGE_KEY, 'fr')
|
||||
|
||||
@@ -96,6 +96,7 @@ class CertificatesInstructorDashTest(ModuleStoreTestCase):
|
||||
|
||||
def _assert_certificates_visible(self, is_visible):
|
||||
"""Check that the certificates section is visible on the instructor dash. """
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
response = self.client.get(self.url)
|
||||
if is_visible:
|
||||
self.assertContains(response, "Certificates")
|
||||
@@ -122,6 +123,7 @@ class CertificatesInstructorDashTest(ModuleStoreTestCase):
|
||||
|
||||
def _assert_certificate_status(self, cert_name, expected_status):
|
||||
"""Check the certificate status display on the instructor dash. """
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
response = self.client.get(self.url)
|
||||
|
||||
if expected_status == 'started':
|
||||
@@ -138,6 +140,7 @@ class CertificatesInstructorDashTest(ModuleStoreTestCase):
|
||||
|
||||
def _assert_enable_certs_button_is_disabled(self):
|
||||
"""Check that the "enable student-generated certificates" button is disabled. """
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
response = self.client.get(self.url)
|
||||
expected_html = '<button class="is-disabled" disabled>Enable Student-Generated Certificates</button>'
|
||||
self.assertContains(response, expected_html)
|
||||
@@ -179,11 +182,13 @@ class CertificatesInstructorApiTest(ModuleStoreTestCase):
|
||||
|
||||
# Global staff have access
|
||||
self.client.login(username=self.global_staff.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
response = self.client.post(url)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_generate_example_certificates(self):
|
||||
self.client.login(username=self.global_staff.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
url = reverse(
|
||||
'generate_example_certificates',
|
||||
kwargs={'course_id': unicode(self.course.id)}
|
||||
@@ -202,6 +207,7 @@ class CertificatesInstructorApiTest(ModuleStoreTestCase):
|
||||
@ddt.data(True, False)
|
||||
def test_enable_certificate_generation(self, is_enabled):
|
||||
self.client.login(username=self.global_staff.username, password='test')
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
url = reverse(
|
||||
'enable_certificate_generation',
|
||||
kwargs={'course_id': unicode(self.course.id)}
|
||||
|
||||
@@ -29,6 +29,7 @@ class TestECommerceDashboardViews(ModuleStoreTestCase):
|
||||
# Create instructor account
|
||||
self.instructor = AdminFactory.create()
|
||||
self.client.login(username=self.instructor.username, password="test")
|
||||
self.grant_sudo_access(unicode(self.course.id), "test")
|
||||
mode = CourseMode(
|
||||
course_id=self.course.id.to_deprecated_string(), mode_slug='honor',
|
||||
mode_display_name='honor', min_price=10, currency='usd'
|
||||
|
||||
@@ -29,6 +29,7 @@ class TestNewInstructorDashboardEmailViewMongoBacked(ModuleStoreTestCase):
|
||||
# Create instructor account
|
||||
instructor = AdminFactory.create()
|
||||
self.client.login(username=instructor.username, password="test")
|
||||
self.grant_sudo_access(unicode(self.course.id), "test")
|
||||
|
||||
# URL for instructor dash
|
||||
self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
|
||||
@@ -35,6 +35,8 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
|
||||
|
||||
self.course = CourseFactory.create()
|
||||
|
||||
self.grant_sudo_access(unicode(self.course.id), "test")
|
||||
|
||||
self.users = [
|
||||
UserFactory.create(username="student%d" % i, email="student%d@test.com" % i)
|
||||
for i in xrange(USER_COUNT)
|
||||
@@ -52,7 +54,6 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
|
||||
"""
|
||||
|
||||
course = self.course
|
||||
|
||||
# Run the Un-enroll students command
|
||||
url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
response = self.client.post(
|
||||
|
||||
@@ -44,6 +44,7 @@ class TestRawGradeCSV(TestSubmittingProblems):
|
||||
"""
|
||||
# Answer second problem correctly with 2nd user to expose bug
|
||||
self.login(self.instructor, self.password)
|
||||
self.grant_sudo_access(unicode(self.course.id), self.password)
|
||||
resp = self.submit_question_answer('p2', {'2_1': 'Correct'})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
|
||||
@@ -52,9 +52,10 @@ class TestXss(ModuleStoreTestCase):
|
||||
)
|
||||
req.user = self._instructor
|
||||
req.session = {}
|
||||
req.is_sudo = lambda region=None: True
|
||||
|
||||
mako_middleware_process_request(req)
|
||||
resp = legacy.instructor_dashboard(req, self._course.id.to_deprecated_string())
|
||||
resp = legacy.instructor_dashboard(request=req, course_id=self._course.id.to_deprecated_string())
|
||||
respUnicode = resp.content.decode(settings.DEFAULT_CHARSET)
|
||||
self.assertNotIn(self._evil_student.profile.name, respUnicode)
|
||||
self.assertIn(escape(self._evil_student.profile.name), respUnicode)
|
||||
|
||||
@@ -39,6 +39,7 @@ class TestGradebook(ModuleStoreTestCase):
|
||||
kwargs['grading_policy'] = self.grading_policy
|
||||
|
||||
self.course = CourseFactory.create(**kwargs)
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
chapter = ItemFactory.create(
|
||||
parent_location=self.course.location,
|
||||
category="sequential",
|
||||
|
||||
@@ -43,6 +43,7 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
# Create instructor account
|
||||
self.instructor = AdminFactory.create()
|
||||
self.client.login(username=self.instructor.username, password="test")
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
|
||||
# URL for instructor dash
|
||||
self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
@@ -202,6 +203,7 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
student_cart.purchase()
|
||||
|
||||
self.client.login(username=self.instructor.username, password="test")
|
||||
self.grant_sudo_access(unicode(self.course.id), 'test')
|
||||
CourseFinanceAdminRole(self.course.id).add_users(self.instructor)
|
||||
single_purchase_total = PaidCourseRegistration.get_total_amount_of_purchased_item(self.course.id)
|
||||
bulk_purchase_total = CourseRegCodeItem.get_total_amount_of_purchased_item(self.course.id)
|
||||
@@ -234,3 +236,13 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
expected_result,
|
||||
'CCX Coaches are able to create their own Custom Courses based on this course' in response.content
|
||||
)
|
||||
|
||||
def test_sudo_required_on_dashboard(self):
|
||||
"""
|
||||
Test that sudo_required redirect user to password page.
|
||||
"""
|
||||
# Logout to remove sudo access.
|
||||
self.client.logout()
|
||||
self.client.login(username=self.instructor.username, password="test")
|
||||
response = self.client.get(self.url, content_type='html', HTTP_ACCEPT='html')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
@@ -52,6 +52,7 @@ from django_comment_common.models import (
|
||||
)
|
||||
from edxmako.shortcuts import render_to_response, render_to_string
|
||||
from courseware.models import StudentModule
|
||||
from django_sudo_helpers.decorators import sudo_required
|
||||
from shoppingcart.models import (
|
||||
Coupon,
|
||||
CourseRegistrationCode,
|
||||
@@ -317,6 +318,7 @@ COUNTRY_INDEX = 3
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@sudo_required
|
||||
def register_and_enroll_students(request, course_id): # pylint: disable=too-many-statements
|
||||
"""
|
||||
Create new account and Enroll students in this course.
|
||||
@@ -517,6 +519,7 @@ def create_and_enroll_user(email, username, name, country, password, course_id):
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@require_post_params(action="enroll or unenroll", identifiers="stringified list of emails and/or usernames")
|
||||
@sudo_required
|
||||
def students_update_enrollment(request, course_id):
|
||||
"""
|
||||
Enroll or unenroll students by email.
|
||||
@@ -684,6 +687,7 @@ def students_update_enrollment(request, course_id):
|
||||
identifiers="stringified list of emails and/or usernames",
|
||||
action="add or remove",
|
||||
)
|
||||
@sudo_required
|
||||
def bulk_beta_modify_access(request, course_id):
|
||||
"""
|
||||
Enroll or unenroll users in beta testing program.
|
||||
@@ -765,6 +769,7 @@ def bulk_beta_modify_access(request, course_id):
|
||||
rolename="'instructor', 'staff', 'beta', or 'ccx_coach'",
|
||||
action="'allow' or 'revoke'"
|
||||
)
|
||||
@sudo_required
|
||||
def modify_access(request, course_id):
|
||||
"""
|
||||
Modify staff/instructor access of other user.
|
||||
@@ -840,6 +845,7 @@ def modify_access(request, course_id):
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('instructor')
|
||||
@require_query_params(rolename="'instructor', 'staff', or 'beta'")
|
||||
@sudo_required
|
||||
def list_course_role_members(request, course_id):
|
||||
"""
|
||||
List instructors and staff.
|
||||
@@ -890,6 +896,7 @@ def list_course_role_members(request, course_id):
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@sudo_required
|
||||
def get_grading_config(request, course_id):
|
||||
"""
|
||||
Respond with json which contains a html formatted grade summary.
|
||||
@@ -910,6 +917,7 @@ def get_grading_config(request, course_id):
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@sudo_required
|
||||
def get_sale_records(request, course_id, csv=False): # pylint: disable=unused-argument, redefined-outer-name
|
||||
"""
|
||||
return the summary of all sales records for a particular course
|
||||
@@ -941,6 +949,7 @@ def get_sale_records(request, course_id, csv=False): # pylint: disable=unused-a
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@sudo_required
|
||||
def get_sale_order_records(request, course_id): # pylint: disable=unused-argument, redefined-outer-name
|
||||
"""
|
||||
return the summary of all sales records for a particular course
|
||||
@@ -982,6 +991,7 @@ def get_sale_order_records(request, course_id): # pylint: disable=unused-argume
|
||||
|
||||
@require_level('staff')
|
||||
@require_POST
|
||||
@sudo_required
|
||||
def sale_validation(request, course_id):
|
||||
"""
|
||||
This method either invalidate or re validate the sale against the invoice number depending upon the event type
|
||||
@@ -1047,6 +1057,7 @@ def re_validate_invoice(obj_invoice):
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@sudo_required
|
||||
def get_students_features(request, course_id, csv=False): # pylint: disable=redefined-outer-name
|
||||
"""
|
||||
Respond with json which contains a summary of all enrolled students profile information.
|
||||
@@ -1122,6 +1133,7 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@sudo_required
|
||||
def get_students_who_may_enroll(request, course_id):
|
||||
"""
|
||||
Initiate generation of a CSV file containing information about
|
||||
@@ -1153,6 +1165,7 @@ def get_students_who_may_enroll(request, course_id):
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_POST
|
||||
@require_level('staff')
|
||||
@sudo_required
|
||||
def add_users_to_cohorts(request, course_id):
|
||||
"""
|
||||
View method that accepts an uploaded file (using key "uploaded-file")
|
||||
@@ -1197,6 +1210,7 @@ def add_users_to_cohorts(request, course_id):
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@sudo_required
|
||||
def get_coupon_codes(request, course_id): # pylint: disable=unused-argument
|
||||
"""
|
||||
Respond with csv which contains a summary of all Active Coupons.
|
||||
@@ -1226,6 +1240,7 @@ def get_coupon_codes(request, course_id): # pylint: disable=unused-argument
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@sudo_required
|
||||
@require_finance_admin
|
||||
def get_enrollment_report(request, course_id):
|
||||
"""
|
||||
@@ -1251,6 +1266,7 @@ def get_enrollment_report(request, course_id):
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@sudo_required
|
||||
@require_finance_admin
|
||||
def get_exec_summary_report(request, course_id):
|
||||
"""
|
||||
@@ -1350,6 +1366,7 @@ def random_code_generator():
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@require_POST
|
||||
@sudo_required
|
||||
def get_registration_codes(request, course_id): # pylint: disable=unused-argument
|
||||
"""
|
||||
Respond with csv which contains a summary of all Registration Codes.
|
||||
@@ -1553,6 +1570,7 @@ def generate_registration_codes(request, course_id):
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@require_POST
|
||||
@sudo_required
|
||||
def active_registration_codes(request, course_id): # pylint: disable=unused-argument
|
||||
"""
|
||||
Respond with csv which contains a summary of all Active Registration Codes.
|
||||
@@ -1584,6 +1602,7 @@ def active_registration_codes(request, course_id): # pylint: disable=unused-arg
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@require_POST
|
||||
@sudo_required
|
||||
def spent_registration_codes(request, course_id): # pylint: disable=unused-argument
|
||||
"""
|
||||
Respond with csv which contains a summary of all Spent(used) Registration Codes.
|
||||
@@ -1614,6 +1633,7 @@ def spent_registration_codes(request, course_id): # pylint: disable=unused-argu
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@sudo_required
|
||||
def get_anon_ids(request, course_id): # pylint: disable=unused-argument
|
||||
"""
|
||||
Respond with 2-column CSV output of user-id, anonymized-user-id
|
||||
@@ -1652,6 +1672,7 @@ def get_anon_ids(request, course_id): # pylint: disable=unused-argument
|
||||
@require_query_params(
|
||||
unique_student_identifier="email or username of student for whom to get progress url"
|
||||
)
|
||||
@sudo_required
|
||||
def get_student_progress_url(request, course_id):
|
||||
"""
|
||||
Get the progress url of a student.
|
||||
@@ -1681,6 +1702,7 @@ def get_student_progress_url(request, course_id):
|
||||
problem_to_reset="problem urlname to reset"
|
||||
)
|
||||
@common_exceptions_400
|
||||
@sudo_required
|
||||
def reset_student_attempts(request, course_id):
|
||||
"""
|
||||
|
||||
@@ -1759,6 +1781,7 @@ def reset_student_attempts(request, course_id):
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@common_exceptions_400
|
||||
@sudo_required
|
||||
def reset_student_attempts_for_entrance_exam(request, course_id): # pylint: disable=invalid-name
|
||||
"""
|
||||
|
||||
@@ -1825,6 +1848,7 @@ def reset_student_attempts_for_entrance_exam(request, course_id): # pylint: dis
|
||||
@require_level('instructor')
|
||||
@require_query_params(problem_to_reset="problem urlname to reset")
|
||||
@common_exceptions_400
|
||||
@sudo_required
|
||||
def rescore_problem(request, course_id):
|
||||
"""
|
||||
Starts a background process a students attempts counter. Optionally deletes student state for a problem.
|
||||
@@ -1879,6 +1903,7 @@ def rescore_problem(request, course_id):
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('instructor')
|
||||
@common_exceptions_400
|
||||
@sudo_required
|
||||
def rescore_entrance_exam(request, course_id):
|
||||
"""
|
||||
Starts a background process a students attempts counter for entrance exam.
|
||||
@@ -1930,6 +1955,7 @@ def rescore_entrance_exam(request, course_id):
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@sudo_required
|
||||
def list_background_email_tasks(request, course_id): # pylint: disable=unused-argument
|
||||
"""
|
||||
List background email tasks.
|
||||
@@ -1948,6 +1974,7 @@ def list_background_email_tasks(request, course_id): # pylint: disable=unused-a
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@sudo_required
|
||||
def list_email_content(request, course_id): # pylint: disable=unused-argument
|
||||
"""
|
||||
List the content of bulk emails sent
|
||||
@@ -1966,6 +1993,7 @@ def list_email_content(request, course_id): # pylint: disable=unused-argument
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@sudo_required
|
||||
def list_instructor_tasks(request, course_id):
|
||||
"""
|
||||
List instructor tasks.
|
||||
@@ -2011,6 +2039,7 @@ def list_instructor_tasks(request, course_id):
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@sudo_required
|
||||
def list_entrance_exam_instructor_tasks(request, course_id): # pylint: disable=invalid-name
|
||||
"""
|
||||
List entrance exam related instructor tasks.
|
||||
@@ -2045,6 +2074,7 @@ def list_entrance_exam_instructor_tasks(request, course_id): # pylint: disable=
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@sudo_required
|
||||
def list_report_downloads(_request, course_id):
|
||||
"""
|
||||
List grade CSV files that are available for download for this course.
|
||||
@@ -2064,6 +2094,7 @@ def list_report_downloads(_request, course_id):
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@sudo_required
|
||||
@require_finance_admin
|
||||
def list_financial_report_downloads(_request, course_id):
|
||||
"""
|
||||
@@ -2084,6 +2115,7 @@ def list_financial_report_downloads(_request, course_id):
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@sudo_required
|
||||
def calculate_grades_csv(request, course_id):
|
||||
"""
|
||||
AlreadyRunningError is raised if the course's grades are already being updated.
|
||||
@@ -2108,6 +2140,7 @@ def calculate_grades_csv(request, course_id):
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@sudo_required
|
||||
def problem_grade_report(request, course_id):
|
||||
"""
|
||||
Request a CSV showing students' grades for all problems in the
|
||||
@@ -2137,6 +2170,7 @@ def problem_grade_report(request, course_id):
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@require_query_params('rolename')
|
||||
@sudo_required
|
||||
def list_forum_members(request, course_id):
|
||||
"""
|
||||
Lists forum members of a certain rolename.
|
||||
@@ -2199,6 +2233,7 @@ def list_forum_members(request, course_id):
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@require_post_params(send_to="sending to whom", subject="subject line", message="message text")
|
||||
@sudo_required
|
||||
def send_email(request, course_id):
|
||||
"""
|
||||
Send an email to self, staff, or everyone involved in a course.
|
||||
@@ -2257,6 +2292,7 @@ def send_email(request, course_id):
|
||||
action="'allow' or 'revoke'",
|
||||
)
|
||||
@common_exceptions_400
|
||||
@sudo_required
|
||||
def update_forum_role_membership(request, course_id):
|
||||
"""
|
||||
Modify user's forum role.
|
||||
@@ -2342,6 +2378,7 @@ def _display_unit(unit):
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@require_query_params('student', 'url', 'due_datetime')
|
||||
@sudo_required
|
||||
def change_due_date(request, course_id):
|
||||
"""
|
||||
Grants a due date extension to a student for a particular unit.
|
||||
@@ -2363,6 +2400,7 @@ def change_due_date(request, course_id):
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@require_query_params('student', 'url')
|
||||
@sudo_required
|
||||
def reset_due_date(request, course_id):
|
||||
"""
|
||||
Rescinds a due date extension for a student on a particular unit.
|
||||
@@ -2389,6 +2427,7 @@ def reset_due_date(request, course_id):
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@require_query_params('url')
|
||||
@sudo_required
|
||||
def show_unit_extensions(request, course_id):
|
||||
"""
|
||||
Shows all of the students which have due date extensions for the given unit.
|
||||
@@ -2403,6 +2442,7 @@ def show_unit_extensions(request, course_id):
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@require_query_params('student')
|
||||
@sudo_required
|
||||
def show_student_extensions(request, course_id):
|
||||
"""
|
||||
Shows all of the due date extensions granted to a particular student in a
|
||||
@@ -2489,6 +2529,7 @@ def enable_certificate_generation(request, course_id=None):
|
||||
#---- Gradebook (shown to small courses only) ----
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@sudo_required
|
||||
def spoc_gradebook(request, course_id):
|
||||
"""
|
||||
Show the gradebook for this course:
|
||||
@@ -2530,6 +2571,7 @@ def spoc_gradebook(request, course_id):
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@require_POST
|
||||
@sudo_required
|
||||
def mark_student_can_skip_entrance_exam(request, course_id): # pylint: disable=invalid-name
|
||||
"""
|
||||
Mark a student to skip entrance exam.
|
||||
|
||||
@@ -34,6 +34,7 @@ from courseware.courses import get_course_by_id, get_studio_url
|
||||
from django_comment_client.utils import has_forum_access
|
||||
from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR
|
||||
from student.models import CourseEnrollment
|
||||
from django_sudo_helpers.decorators import sudo_required
|
||||
from shoppingcart.models import Coupon, PaidCourseRegistration, CourseRegCodeItem
|
||||
from course_modes.models import CourseMode, CourseModesArchive
|
||||
from student.roles import CourseFinanceAdminRole, CourseSalesAdminRole
|
||||
@@ -65,8 +66,33 @@ class InstructorDashboardTab(CourseTab):
|
||||
return user and has_access(user, 'staff', course, course.id)
|
||||
|
||||
|
||||
def check_staff_or_404():
|
||||
"""
|
||||
Decorator with argument that requires an access level of the requesting
|
||||
user. If the requirement is not satisfied, returns an
|
||||
Http404 (404).
|
||||
|
||||
Assumes that request is in args[0].
|
||||
Assumes that course_id is in kwargs['course_id'].
|
||||
"""
|
||||
|
||||
def decorator(func): # pylint: disable=missing-docstring
|
||||
def wrapped(*args, **kwargs): # pylint: disable=missing-docstring
|
||||
request = args[0]
|
||||
course = get_course_by_id(CourseKey.from_string(kwargs['course_id']))
|
||||
user_is_staff = has_access(request.user, "staff", course)
|
||||
if user_is_staff:
|
||||
return func(*args, **kwargs)
|
||||
else:
|
||||
raise Http404
|
||||
return wrapped
|
||||
return decorator
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@check_staff_or_404()
|
||||
@sudo_required
|
||||
def instructor_dashboard_2(request, course_id):
|
||||
""" Display the instructor dashboard for a course. """
|
||||
try:
|
||||
@@ -86,16 +112,16 @@ def instructor_dashboard_2(request, course_id):
|
||||
'forum_admin': has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR),
|
||||
}
|
||||
|
||||
if not access['staff']:
|
||||
raise Http404()
|
||||
|
||||
is_white_label = CourseMode.is_white_label(course_key)
|
||||
|
||||
unique_student_identifier = request.GET.get("unique_student_identifier", "")
|
||||
problem_to_reset = request.GET.get("problem_to_reset", "")
|
||||
|
||||
sections = [
|
||||
_section_course_info(course, access),
|
||||
_section_membership(course, access, is_white_label),
|
||||
_section_cohort_management(course, access),
|
||||
_section_student_admin(course, access),
|
||||
_section_student_admin(course, access, unique_student_identifier, problem_to_reset),
|
||||
_section_data_download(course, access),
|
||||
]
|
||||
|
||||
@@ -407,11 +433,21 @@ def _is_small_course(course_key):
|
||||
return is_small_course
|
||||
|
||||
|
||||
def _section_student_admin(course, access):
|
||||
def _section_student_admin(course, access, unique_student_identifier, problem_to_reset):
|
||||
""" Provide data for the corresponding dashboard section """
|
||||
course_key = course.id
|
||||
is_small_course = _is_small_course(course_key)
|
||||
|
||||
problem_url = None
|
||||
if problem_to_reset:
|
||||
problem_url = reverse(
|
||||
'jump_to',
|
||||
kwargs={
|
||||
'course_id': unicode(course_key),
|
||||
'location': problem_to_reset
|
||||
}
|
||||
)
|
||||
|
||||
section_data = {
|
||||
'section_key': 'student_admin',
|
||||
'section_display_name': _('Student Admin'),
|
||||
@@ -434,6 +470,9 @@ def _section_student_admin(course, access):
|
||||
'list_entrace_exam_instructor_tasks_url': reverse('list_entrance_exam_instructor_tasks',
|
||||
kwargs={'course_id': unicode(course_key)}),
|
||||
'spoc_gradebook_url': reverse('spoc_gradebook', kwargs={'course_id': unicode(course_key)}),
|
||||
'unique_student_identifier': unique_student_identifier,
|
||||
'problem_to_reset': problem_to_reset,
|
||||
'problem_url': problem_url,
|
||||
}
|
||||
return section_data
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ from student.models import (
|
||||
CourseEnrollment,
|
||||
CourseEnrollmentAllowed,
|
||||
)
|
||||
from django_sudo_helpers.decorators import sudo_required
|
||||
import track.views
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
@@ -79,6 +80,7 @@ def split_by_comma_and_whitespace(a_str):
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@sudo_required
|
||||
def instructor_dashboard(request, course_id):
|
||||
"""Display the instructor dashboard for a course."""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Django admin interface for the shopping cart models. """
|
||||
from ratelimitbackend import admin
|
||||
from django.contrib import admin
|
||||
from shoppingcart.models import (
|
||||
PaidCourseRegistrationAnnotation,
|
||||
Coupon,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
from ratelimitbackend import admin
|
||||
"""
|
||||
django admin pages for verify_student models
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
from config_models.admin import ConfigurationModelAdmin
|
||||
from verify_student.models import (
|
||||
SoftwareSecurePhotoVerification,
|
||||
|
||||
@@ -1176,6 +1176,10 @@ MIDDLEWARE_CLASSES = (
|
||||
|
||||
# catches any uncaught RateLimitExceptions and returns a 403 instead of a 500
|
||||
'ratelimitbackend.middleware.RateLimitMiddleware',
|
||||
|
||||
# force re-authentication before activating administrative functions
|
||||
'sudo.middleware.SudoMiddleware',
|
||||
|
||||
# needs to run after locale middleware (or anything that modifies the request context)
|
||||
'edxmako.middleware.MakoMiddleware',
|
||||
|
||||
@@ -1897,6 +1901,9 @@ INSTALLED_APPS = (
|
||||
# Surveys
|
||||
'survey',
|
||||
|
||||
# Allows sudo-mode
|
||||
'sudo',
|
||||
|
||||
'lms.djangoapps.lms_xblock',
|
||||
|
||||
'openedx.core.djangoapps.content.course_overviews',
|
||||
|
||||
@@ -27,13 +27,13 @@ class DataDownload
|
||||
@$problem_grade_report_csv_btn = @$section.find("input[name='problem-grade-report']'")
|
||||
|
||||
# response areas
|
||||
@$download = @$section.find '.data-download-container'
|
||||
@$download_display_text = @$download.find '.data-display-text'
|
||||
@$download = @$section.find '.data-download-container'
|
||||
@$download_display_text = @$download.find '.data-display-text'
|
||||
@$download_request_response_error = @$download.find '.request-response-error'
|
||||
@$reports = @$section.find '.reports-download-container'
|
||||
@$download_display_table = @$reports.find '.data-display-table'
|
||||
@$reports_request_response = @$reports.find '.request-response'
|
||||
@$reports_request_response_error = @$reports.find '.request-response-error'
|
||||
@$reports = @$section.find '.reports-download-container'
|
||||
@$download_display_table = @$reports.find '.data-display-table'
|
||||
@$reports_request_response = @$reports.find '.request-response'
|
||||
@$reports_request_response_error = @$reports.find '.request-response-error'
|
||||
|
||||
@report_downloads = new (ReportDownloads()) @$section
|
||||
@instructor_tasks = new (PendingInstructorTasks()) @$section
|
||||
@@ -58,12 +58,14 @@ class DataDownload
|
||||
$.ajax
|
||||
dataType: 'json'
|
||||
url: url
|
||||
error: (std_ajax_err) =>
|
||||
error: std_ajax_err((=>
|
||||
@$reports_request_response_error.text gettext("Error generating student profile information. Please try again.")
|
||||
$(".msg-error").css({"display":"block"})
|
||||
$(".msg-error").css({"display": "block"})
|
||||
), true)
|
||||
|
||||
success: (data) =>
|
||||
@$reports_request_response.text data['status']
|
||||
$(".msg-confirm").css({"display":"block"})
|
||||
$(".msg-confirm").css({"display": "block"})
|
||||
|
||||
@$list_studs_btn.click (e) =>
|
||||
url = @$list_studs_btn.data 'endpoint'
|
||||
@@ -76,9 +78,11 @@ class DataDownload
|
||||
$.ajax
|
||||
dataType: 'json'
|
||||
url: url
|
||||
error: (std_ajax_err) =>
|
||||
error: std_ajax_err((=>
|
||||
@clear_display()
|
||||
@$download_request_response_error.text gettext("Error getting student list.")
|
||||
), true)
|
||||
|
||||
success: (data) =>
|
||||
@clear_display()
|
||||
|
||||
@@ -95,7 +99,7 @@ class DataDownload
|
||||
$table_placeholder = $ '<div/>', class: 'slickgrid'
|
||||
@$download_display_table.append $table_placeholder
|
||||
grid = new Slick.Grid($table_placeholder, grid_data, columns, options)
|
||||
# grid.autosizeColumns()
|
||||
# grid.autosizeColumns()
|
||||
|
||||
@$list_may_enroll_csv_btn.click (e) =>
|
||||
@clear_display()
|
||||
@@ -104,12 +108,14 @@ class DataDownload
|
||||
$.ajax
|
||||
dataType: 'json'
|
||||
url: url
|
||||
error: (std_ajax_err) =>
|
||||
error: std_ajax_err((=>
|
||||
@$reports_request_response_error.text gettext("Error generating list of students who may enroll. Please try again.")
|
||||
$(".msg-error").css({"display":"block"})
|
||||
$(".msg-error").css({"display": "block"})
|
||||
), true)
|
||||
|
||||
success: (data) =>
|
||||
@$reports_request_response.text data['status']
|
||||
$(".msg-confirm").css({"display":"block"})
|
||||
$(".msg-confirm").css({"display": "block"})
|
||||
|
||||
@$grade_config_btn.click (e) =>
|
||||
url = @$grade_config_btn.data 'endpoint'
|
||||
@@ -117,9 +123,10 @@ class DataDownload
|
||||
$.ajax
|
||||
dataType: 'json'
|
||||
url: url
|
||||
error: (std_ajax_err) =>
|
||||
error: std_ajax_err((=>
|
||||
@clear_display()
|
||||
@$download_request_response_error.text gettext("Error retrieving grading configuration.")
|
||||
), true)
|
||||
success: (data) =>
|
||||
@clear_display()
|
||||
@$download_display_text.html data['grading_config_summary']
|
||||
@@ -131,20 +138,22 @@ class DataDownload
|
||||
@onClickGradeDownload @$problem_grade_report_csv_btn, gettext("Error generating problem grade report. Please try again.")
|
||||
|
||||
onClickGradeDownload: (button, errorMessage) ->
|
||||
# Clear any CSS styling from the request-response areas
|
||||
#$(".msg-confirm").css({"display":"none"})
|
||||
#$(".msg-error").css({"display":"none"})
|
||||
@clear_display()
|
||||
url = button.data 'endpoint'
|
||||
$.ajax
|
||||
dataType: 'json'
|
||||
url: url
|
||||
error: (std_ajax_err) =>
|
||||
@$reports_request_response_error.text errorMessage
|
||||
$(".msg-error").css({"display":"block"})
|
||||
success: (data) =>
|
||||
@$reports_request_response.text data['status']
|
||||
$(".msg-confirm").css({"display":"block"})
|
||||
# Clear any CSS styling from the request-response areas
|
||||
#$(".msg-confirm").css({"display":"none"})
|
||||
#$(".msg-error").css({"display":"none"})
|
||||
@clear_display()
|
||||
url = button.data 'endpoint'
|
||||
$.ajax
|
||||
dataType: 'json'
|
||||
url: url
|
||||
error: std_ajax_err((=>
|
||||
@.$reports_request_response_error.text gettext('Error generating student profile information. Please try again.')
|
||||
$('.msg-error').css 'display': 'block'
|
||||
), true
|
||||
)
|
||||
success: (data) =>
|
||||
@$reports_request_response.text data['status']
|
||||
$(".msg-confirm").css({"display": "block"})
|
||||
|
||||
# handler for when the section title is clicked.
|
||||
onClickTitle: ->
|
||||
@@ -166,8 +175,8 @@ class DataDownload
|
||||
@$reports_request_response.empty()
|
||||
@$reports_request_response_error.empty()
|
||||
# Clear any CSS styling from the request-response areas
|
||||
$(".msg-confirm").css({"display":"none"})
|
||||
$(".msg-error").css({"display":"none"})
|
||||
$(".msg-confirm").css({"display": "none"})
|
||||
$(".msg-error").css({"display": "none"})
|
||||
|
||||
# export for use
|
||||
# create parent namespaces if they do not already exist.
|
||||
|
||||
@@ -14,7 +14,7 @@ emailStudents = false
|
||||
class MemberListWidget
|
||||
# create a MemberListWidget `$container` is a jquery object to embody.
|
||||
# `params` holds template parameters. `params` should look like the defaults below.
|
||||
constructor: (@$container, params={}) ->
|
||||
constructor: (@$container, params = {}) ->
|
||||
params = _.defaults params,
|
||||
title: "Member List"
|
||||
info: """
|
||||
@@ -117,14 +117,15 @@ class AuthListWidget extends MemberListWidget
|
||||
|
||||
# create revoke button and insert it into the row
|
||||
label_trans = gettext("Revoke access")
|
||||
$revoke_btn = $ _.template('<div class="revoke"><i class="icon fa fa-times-circle"></i> <%= label %></div>', {label: label_trans}),
|
||||
$revoke_btn = $ _.template('<div class="revoke"><i class="icon fa fa-times-circle"></i> <%= label %></div>',
|
||||
{label: label_trans}),
|
||||
class: 'revoke'
|
||||
$revoke_btn.click =>
|
||||
@modify_member_access member.email, 'revoke', (error) =>
|
||||
# abort on error
|
||||
return @show_errors error unless error is null
|
||||
@clear_errors()
|
||||
@reload_list()
|
||||
@modify_member_access member.email, 'revoke', (error) =>
|
||||
# abort on error
|
||||
return @show_errors error unless error is null
|
||||
@clear_errors()
|
||||
@reload_list()
|
||||
@add_row [member.username, member.email, $revoke_btn]
|
||||
|
||||
# clear error display
|
||||
@@ -139,11 +140,13 @@ class AuthListWidget extends MemberListWidget
|
||||
$.ajax
|
||||
dataType: 'json'
|
||||
url: @list_endpoint
|
||||
data: rolename: @rolename
|
||||
data:
|
||||
rolename: @rolename
|
||||
success: (data) => cb? null, data[@rolename]
|
||||
error: std_ajax_err =>
|
||||
`// Translators: A rolename appears this sentence. A rolename is something like "staff" or "beta tester".`
|
||||
error: std_ajax_err((=>
|
||||
# Translators: A rolename appears this sentence. A rolename is something like "staff" or "beta tester".
|
||||
cb? gettext("Error fetching list for role") + " '#{@rolename}'"
|
||||
), true)
|
||||
|
||||
# send ajax request to modify access
|
||||
# (add or remove them from the list)
|
||||
@@ -158,7 +161,9 @@ class AuthListWidget extends MemberListWidget
|
||||
rolename: @rolename
|
||||
action: action
|
||||
success: (data) => @member_response data
|
||||
error: std_ajax_err => cb? gettext "Error changing user's permissions."
|
||||
error: std_ajax_err((=>
|
||||
cb? gettext "Error changing user's permissions."
|
||||
), true)
|
||||
|
||||
member_response: (data) ->
|
||||
@clear_errors()
|
||||
@@ -204,15 +209,15 @@ class @AutoEnrollmentViaCsv
|
||||
event.preventDefault()
|
||||
data = new FormData(event.currentTarget)
|
||||
$.ajax
|
||||
dataType: 'json'
|
||||
type: 'POST'
|
||||
url: event.currentTarget.action
|
||||
data: data
|
||||
processData: false
|
||||
contentType: false
|
||||
success: (data) =>
|
||||
@processing = false
|
||||
@display_response data
|
||||
dataType: 'json'
|
||||
type: 'POST'
|
||||
url: event.currentTarget.action
|
||||
data: data
|
||||
processData: false
|
||||
contentType: false
|
||||
success: (data) =>
|
||||
@processing = false
|
||||
@display_response data
|
||||
|
||||
return false
|
||||
|
||||
@@ -246,7 +251,7 @@ class @AutoEnrollmentViaCsv
|
||||
if student_result.is_general_error
|
||||
details.push student_result.response
|
||||
else
|
||||
response_message = student_result.username + ' ('+ student_result.email + '): ' + ' (' + student_result.response + ')'
|
||||
response_message = student_result.username + ' (' + student_result.email + '): ' + ' (' + student_result.response + ')'
|
||||
details.push response_message
|
||||
|
||||
@$results.append @render_notification_view type, title, message, details
|
||||
@@ -261,23 +266,23 @@ class @AutoEnrollmentViaCsv
|
||||
render_notification_view: (type, title, message, details) ->
|
||||
notification_model = new NotificationModel()
|
||||
notification_model.set({
|
||||
'type': type,
|
||||
'title': title,
|
||||
'message': message,
|
||||
'details': details,
|
||||
'type': type,
|
||||
'title': title,
|
||||
'message': message,
|
||||
'details': details,
|
||||
});
|
||||
view = new NotificationView(model:notification_model);
|
||||
view = new NotificationView(model: notification_model);
|
||||
view.render()
|
||||
return view.$el.html()
|
||||
|
||||
class BetaTesterBulkAddition
|
||||
constructor: (@$container) ->
|
||||
# gather elements
|
||||
@$identifier_input = @$container.find("textarea[name='student-ids-for-beta']")
|
||||
@$btn_beta_testers = @$container.find("input[name='beta-testers']")
|
||||
@$checkbox_autoenroll = @$container.find("input[name='auto-enroll']")
|
||||
@$identifier_input = @$container.find("textarea[name='student-ids-for-beta']")
|
||||
@$btn_beta_testers = @$container.find("input[name='beta-testers']")
|
||||
@$checkbox_autoenroll = @$container.find("input[name='auto-enroll']")
|
||||
@$checkbox_emailstudents = @$container.find("input[name='email-students-beta']")
|
||||
@$task_response = @$container.find(".request-response")
|
||||
@$task_response = @$container.find(".request-response")
|
||||
@$request_response_error = @$container.find(".request-response-error")
|
||||
|
||||
# click handlers
|
||||
@@ -296,7 +301,10 @@ class BetaTesterBulkAddition
|
||||
url: @$btn_beta_testers.data 'endpoint'
|
||||
data: send_data
|
||||
success: (data) => @display_response data
|
||||
error: std_ajax_err => @fail_with_error gettext "Error adding/removing users as beta testers."
|
||||
error: std_ajax_err((=>
|
||||
@fail_with_error gettext "Error adding/removing users as beta testers."
|
||||
), true)
|
||||
|
||||
|
||||
# clear the input text field
|
||||
clear_input: ->
|
||||
@@ -365,13 +373,13 @@ class BetaTesterBulkAddition
|
||||
class BatchEnrollment
|
||||
constructor: (@$container) ->
|
||||
# gather elements
|
||||
@$identifier_input = @$container.find("textarea[name='student-ids']")
|
||||
@$enrollment_button = @$container.find(".enrollment-button")
|
||||
@$is_course_white_label = @$container.find("#is_course_white_label").val()
|
||||
@$reason_field = @$container.find("textarea[name='reason-field']")
|
||||
@$checkbox_autoenroll = @$container.find("input[name='auto-enroll']")
|
||||
@$identifier_input = @$container.find("textarea[name='student-ids']")
|
||||
@$enrollment_button = @$container.find(".enrollment-button")
|
||||
@$is_course_white_label = @$container.find("#is_course_white_label").val()
|
||||
@$reason_field = @$container.find("textarea[name='reason-field']")
|
||||
@$checkbox_autoenroll = @$container.find("input[name='auto-enroll']")
|
||||
@$checkbox_emailstudents = @$container.find("input[name='email-students']")
|
||||
@$task_response = @$container.find(".request-response")
|
||||
@$task_response = @$container.find(".request-response")
|
||||
@$request_response_error = @$container.find(".request-response-error")
|
||||
|
||||
# attach click handler for enrollment buttons
|
||||
@@ -395,8 +403,9 @@ class BatchEnrollment
|
||||
url: $(event.target).data 'endpoint'
|
||||
data: send_data
|
||||
success: (data) => @display_response data
|
||||
error: std_ajax_err => @fail_with_error gettext "Error enrolling/unenrolling users."
|
||||
|
||||
error: std_ajax_err((=>
|
||||
@fail_with_error gettext "Error enrolling/unenrolling users."
|
||||
), true)
|
||||
|
||||
# clear the input text field
|
||||
clear_input: ->
|
||||
@@ -478,7 +487,7 @@ class BatchEnrollment
|
||||
else
|
||||
allowed.push student_results
|
||||
|
||||
# The instructor is trying to unenroll someone who is not enrolled or allowed to enroll; non-sensical action.
|
||||
# The instructor is trying to unenroll someone who is not enrolled or allowed to enroll; non-sensical action.
|
||||
else if data_from_server.action is 'unenroll' and not (student_results.before.enrollment) and not (student_results.before.allowed)
|
||||
notunenrolled.push student_results
|
||||
|
||||
@@ -572,11 +581,11 @@ class AuthList
|
||||
# rolename is the name of Role for forums for the forum endpoints
|
||||
constructor: (@$container, @rolename) ->
|
||||
# gather elements
|
||||
@$display_table = @$container.find('.auth-list-table')
|
||||
@$display_table = @$container.find('.auth-list-table')
|
||||
@$request_response_error = @$container.find('.request-response-error')
|
||||
@$add_section = @$container.find('.auth-list-add')
|
||||
@$allow_field = @$add_section.find("input[name='email']")
|
||||
@$allow_button = @$add_section.find("input[name='allow']")
|
||||
@$add_section = @$container.find('.auth-list-add')
|
||||
@$allow_field = @$add_section.find("input[name='email']")
|
||||
@$allow_button = @$add_section.find("input[name='allow']")
|
||||
|
||||
# attach click handler
|
||||
@$allow_button.click =>
|
||||
@@ -597,7 +606,7 @@ class AuthList
|
||||
options =
|
||||
enableCellNavigation: true
|
||||
enableColumnReorder: false
|
||||
# autoHeight: true
|
||||
# autoHeight: true
|
||||
forceFitColumns: true
|
||||
|
||||
# this is a hack to put a button/link in a slick grid cell
|
||||
@@ -618,10 +627,10 @@ class AuthList
|
||||
field: 'first_name'
|
||||
name: 'First Name'
|
||||
,
|
||||
# id: 'last_name'
|
||||
# field: 'last_name'
|
||||
# name: 'Last Name'
|
||||
# ,
|
||||
# id: 'last_name'
|
||||
# field: 'last_name'
|
||||
# name: 'Last Name'
|
||||
# ,
|
||||
id: 'revoke'
|
||||
field: 'revoke'
|
||||
name: 'Revoke'
|
||||
@@ -646,9 +655,13 @@ class AuthList
|
||||
$.ajax
|
||||
dataType: 'json'
|
||||
url: @$display_table.data 'endpoint'
|
||||
data: rolename: @rolename
|
||||
data:
|
||||
rolename: @rolename
|
||||
success: load_auth_list
|
||||
error: std_ajax_err => @$request_response_error.text "Error fetching list for '#{@rolename}'"
|
||||
error: std_ajax_err((=>
|
||||
@$request_response_error.text "Error fetching list for '#{@rolename}'"
|
||||
), true)
|
||||
|
||||
|
||||
|
||||
# slickgrid's layout collapses when rendered
|
||||
@@ -670,8 +683,9 @@ class AuthList
|
||||
rolename: @rolename
|
||||
action: action
|
||||
success: (data) -> cb?(data)
|
||||
error: std_ajax_err => @$request_response_error.text gettext "Error changing user's permissions."
|
||||
|
||||
error: std_ajax_err((=>
|
||||
@$request_response_error.text gettext "Error changing user's permissions."
|
||||
), true)
|
||||
|
||||
# Membership Section
|
||||
class Membership
|
||||
|
||||
@@ -79,9 +79,9 @@ class SendEmail
|
||||
data: send_data
|
||||
success: (data) =>
|
||||
@display_response success_message
|
||||
|
||||
error: std_ajax_err =>
|
||||
error: std_ajax_err((=>
|
||||
@fail_with_error gettext('Error sending email.')
|
||||
), true)
|
||||
|
||||
else
|
||||
@$task_response.empty()
|
||||
@@ -99,38 +99,41 @@ class SendEmail
|
||||
else
|
||||
@$history_request_response_error.text gettext("There is no email history for this course.")
|
||||
# Enable the msg-warning css display
|
||||
@$history_request_response_error.css({"display":"block"})
|
||||
error: std_ajax_err =>
|
||||
@$history_request_response_error.css({"display": "block"})
|
||||
error: std_ajax_err((=>
|
||||
@$history_request_response_error.text gettext("There was an error obtaining email task history for this course.")
|
||||
), true)
|
||||
|
||||
# List content history for emails sent
|
||||
@$btn_task_history_email_content.click =>
|
||||
url = @$btn_task_history_email_content.data 'endpoint'
|
||||
$.ajax
|
||||
dataType: 'json'
|
||||
url : url
|
||||
url: url
|
||||
success: (data) =>
|
||||
if data.emails.length
|
||||
create_email_content_table @$table_email_content_history, @$email_content_table_inner, data.emails
|
||||
create_email_message_views @$email_messages_wrapper, data.emails
|
||||
else
|
||||
@$content_request_response_error.text gettext("There is no email history for this course.")
|
||||
@$content_request_response_error.css({"display":"block"})
|
||||
error: std_ajax_err =>
|
||||
@$content_request_response_error.css({"display": "block"})
|
||||
error: std_ajax_err((=>
|
||||
@$content_request_response_error.text gettext("There was an error obtaining email content history for this course.")
|
||||
), true)
|
||||
|
||||
|
||||
fail_with_error: (msg) ->
|
||||
console.warn msg
|
||||
@$task_response.empty()
|
||||
@$request_response_error.empty()
|
||||
@$request_response_error.text msg
|
||||
$(".msg-confirm").css({"display":"none"})
|
||||
$(".msg-confirm").css({"display": "none"})
|
||||
|
||||
display_response: (data_from_server) ->
|
||||
@$task_response.empty()
|
||||
@$request_response_error.empty()
|
||||
@$task_response.text(data_from_server)
|
||||
$(".msg-confirm").css({"display":"block"})
|
||||
$(".msg-confirm").css({"display": "block"})
|
||||
|
||||
|
||||
# Email Section
|
||||
|
||||
@@ -32,37 +32,42 @@ class @StudentAdmin
|
||||
# some buttons are optional because they can be flipped by the instructor task feature switch
|
||||
# student-specific
|
||||
@$field_student_select_progress = find_and_assert @$section, "input[name='student-select-progress']"
|
||||
@$field_student_select_grade = find_and_assert @$section, "input[name='student-select-grade']"
|
||||
@$progress_link = find_and_assert @$section, "a.progress-link"
|
||||
@$field_student_select_grade = find_and_assert @$section, "input[name='student-select-grade']"
|
||||
@$progress_link = find_and_assert @$section, "a.progress-link"
|
||||
@$field_problem_select_single = find_and_assert @$section, "input[name='problem-select-single']"
|
||||
@$btn_reset_attempts_single = find_and_assert @$section, "input[name='reset-attempts-single']"
|
||||
@$btn_delete_state_single = @$section.find "input[name='delete-state-single']"
|
||||
@$btn_rescore_problem_single = @$section.find "input[name='rescore-problem-single']"
|
||||
@$btn_task_history_single = @$section.find "input[name='task-history-single']"
|
||||
@$table_task_history_single = @$section.find ".task-history-single-table"
|
||||
@$btn_reset_attempts_single = find_and_assert @$section, "input[name='reset-attempts-single']"
|
||||
@$btn_delete_state_single = @$section.find "input[name='delete-state-single']"
|
||||
@$btn_rescore_problem_single = @$section.find "input[name='rescore-problem-single']"
|
||||
@$btn_task_history_single = @$section.find "input[name='task-history-single']"
|
||||
@$table_task_history_single = @$section.find ".task-history-single-table"
|
||||
|
||||
# entrance-exam-specific
|
||||
@$field_entrance_exam_student_select_grade = @$section.find "input[name='entrance-exam-student-select-grade']"
|
||||
@$btn_reset_entrance_exam_attempts = @$section.find "input[name='reset-entrance-exam-attempts']"
|
||||
@$btn_delete_entrance_exam_state = @$section.find "input[name='delete-entrance-exam-state']"
|
||||
@$btn_rescore_entrance_exam = @$section.find "input[name='rescore-entrance-exam']"
|
||||
@$btn_skip_entrance_exam = @$section.find "input[name='skip-entrance-exam']"
|
||||
@$btn_entrance_exam_task_history = @$section.find "input[name='entrance-exam-task-history']"
|
||||
@$table_entrance_exam_task_history = @$section.find ".entrance-exam-task-history-table"
|
||||
@$field_entrance_exam_student_select_grade = @$section.find "input[name='entrance-exam-student-select-grade']"
|
||||
@$btn_reset_entrance_exam_attempts = @$section.find "input[name='reset-entrance-exam-attempts']"
|
||||
@$btn_delete_entrance_exam_state = @$section.find "input[name='delete-entrance-exam-state']"
|
||||
@$btn_rescore_entrance_exam = @$section.find "input[name='rescore-entrance-exam']"
|
||||
@$btn_skip_entrance_exam = @$section.find "input[name='skip-entrance-exam']"
|
||||
@$btn_entrance_exam_task_history = @$section.find "input[name='entrance-exam-task-history']"
|
||||
@$table_entrance_exam_task_history = @$section.find ".entrance-exam-task-history-table"
|
||||
|
||||
# course-specific
|
||||
@$field_problem_select_all = @$section.find "input[name='problem-select-all']"
|
||||
@$btn_reset_attempts_all = @$section.find "input[name='reset-attempts-all']"
|
||||
@$btn_rescore_problem_all = @$section.find "input[name='rescore-problem-all']"
|
||||
@$btn_task_history_all = @$section.find "input[name='task-history-all']"
|
||||
@$table_task_history_all = @$section.find ".task-history-all-table"
|
||||
@instructor_tasks = new (PendingInstructorTasks()) @$section
|
||||
@$field_problem_select_all = @$section.find "input[name='problem-select-all']"
|
||||
@$btn_reset_attempts_all = @$section.find "input[name='reset-attempts-all']"
|
||||
@$btn_rescore_problem_all = @$section.find "input[name='rescore-problem-all']"
|
||||
@$btn_task_history_all = @$section.find "input[name='task-history-all']"
|
||||
@$table_task_history_all = @$section.find ".task-history-all-table"
|
||||
@instructor_tasks = new (PendingInstructorTasks()) @$section
|
||||
|
||||
# response areas
|
||||
@$request_response_error_progress = find_and_assert @$section, ".student-specific-container .request-response-error"
|
||||
@$request_response_error_grade = find_and_assert @$section, ".student-grade-container .request-response-error"
|
||||
@$request_response_error_ee = @$section.find ".entrance-exam-grade-container .request-response-error"
|
||||
@$request_response_error_all = @$section.find ".course-specific-container .request-response-error"
|
||||
@$request_response_error_ee = @$section.find ".entrance-exam-grade-container .request-response-error"
|
||||
@$request_response_error_all = @$section.find ".course-specific-container .request-response-error"
|
||||
|
||||
$student_grade_container = find_and_assert @$section, ".student-grade-container"
|
||||
unique_student_identifier = @$field_student_select_grade.val()
|
||||
if unique_student_identifier
|
||||
@scroll_to_section($student_grade_container)
|
||||
|
||||
# attach click handlers
|
||||
|
||||
@@ -78,10 +83,13 @@ class @StudentAdmin
|
||||
$.ajax
|
||||
dataType: 'json'
|
||||
url: @$progress_link.data 'endpoint'
|
||||
data: unique_student_identifier: unique_student_identifier
|
||||
data:
|
||||
unique_student_identifier: unique_student_identifier
|
||||
success: @clear_errors_then (data) ->
|
||||
window.location = data.progress_url
|
||||
error: std_ajax_err => @$request_response_error_progress.text full_error_message
|
||||
error: std_ajax_err((=>
|
||||
@$request_response_error_progress.text full_error_message
|
||||
), true)
|
||||
|
||||
# reset attempts for student on problem
|
||||
@$btn_reset_attempts_single.click =>
|
||||
@@ -97,15 +105,19 @@ class @StudentAdmin
|
||||
delete_module: false
|
||||
success_message = gettext("Success! Problem attempts reset for problem '<%= problem_id %>' and student '<%= student_id %>'.")
|
||||
error_message = gettext("Error resetting problem attempts for problem '<%= problem_id %>' and student '<%= student_id %>'. Make sure that the problem and student identifiers are complete and correct.")
|
||||
full_success_message = _.template(success_message, {problem_id: problem_to_reset, student_id: unique_student_identifier})
|
||||
full_error_message = _.template(error_message, {problem_id: problem_to_reset, student_id: unique_student_identifier})
|
||||
full_success_message = _.template(success_message,
|
||||
{problem_id: problem_to_reset, student_id: unique_student_identifier})
|
||||
full_error_message = _.template(error_message,
|
||||
{problem_id: problem_to_reset, student_id: unique_student_identifier})
|
||||
|
||||
$.ajax
|
||||
dataType: 'json'
|
||||
url: @$btn_reset_attempts_single.data 'endpoint'
|
||||
data: send_data
|
||||
success: @clear_errors_then -> alert full_success_message
|
||||
error: std_ajax_err => @$request_response_error_grade.text full_error_message
|
||||
error: std_ajax_err((=>
|
||||
@$request_response_error_grade.text full_error_message
|
||||
), true)
|
||||
|
||||
# delete state for student on problem
|
||||
@$btn_delete_state_single.click =>
|
||||
@@ -116,7 +128,8 @@ class @StudentAdmin
|
||||
if not problem_to_reset
|
||||
return @$request_response_error_grade.text gettext("Please enter a problem location.")
|
||||
confirm_message = gettext("Delete student '<%= student_id %>'s state on problem '<%= problem_id %>'?")
|
||||
full_confirm_message = _.template(confirm_message, {student_id: unique_student_identifier, problem_id: problem_to_reset})
|
||||
full_confirm_message = _.template(confirm_message,
|
||||
{student_id: unique_student_identifier, problem_id: problem_to_reset})
|
||||
|
||||
if window.confirm full_confirm_message
|
||||
send_data =
|
||||
@@ -124,14 +137,17 @@ class @StudentAdmin
|
||||
problem_to_reset: problem_to_reset
|
||||
delete_module: true
|
||||
error_message = gettext("Error deleting student '<%= student_id %>'s state on problem '<%= problem_id %>'. Make sure that the problem and student identifiers are complete and correct.")
|
||||
full_error_message = _.template(error_message, {student_id: unique_student_identifier, problem_id: problem_to_reset})
|
||||
full_error_message = _.template(error_message,
|
||||
{student_id: unique_student_identifier, problem_id: problem_to_reset})
|
||||
|
||||
$.ajax
|
||||
dataType: 'json'
|
||||
url: @$btn_delete_state_single.data 'endpoint'
|
||||
data: send_data
|
||||
success: @clear_errors_then -> alert gettext('Module state successfully deleted.')
|
||||
error: std_ajax_err => @$request_response_error_grade.text full_error_message
|
||||
error: std_ajax_err((=>
|
||||
@$request_response_error_grade.text full_error_message
|
||||
), true)
|
||||
else
|
||||
# Clear error messages if "Cancel" was chosen on confirmation alert
|
||||
@clear_errors()
|
||||
@@ -148,16 +164,20 @@ class @StudentAdmin
|
||||
unique_student_identifier: unique_student_identifier
|
||||
problem_to_reset: problem_to_reset
|
||||
success_message = gettext("Started rescore problem task for problem '<%= problem_id %>' and student '<%= student_id %>'. Click the 'Show Background Task History for Student' button to see the status of the task.")
|
||||
full_success_message = _.template(success_message, {student_id: unique_student_identifier, problem_id: problem_to_reset})
|
||||
full_success_message = _.template(success_message,
|
||||
{student_id: unique_student_identifier, problem_id: problem_to_reset})
|
||||
error_message = gettext("Error starting a task to rescore problem '<%= problem_id %>' for student '<%= student_id %>'. Make sure that the the problem and student identifiers are complete and correct.")
|
||||
full_error_message = _.template(error_message, {student_id: unique_student_identifier, problem_id: problem_to_reset})
|
||||
full_error_message = _.template(error_message,
|
||||
{student_id: unique_student_identifier, problem_id: problem_to_reset})
|
||||
|
||||
$.ajax
|
||||
dataType: 'json'
|
||||
url: @$btn_rescore_problem_single.data 'endpoint'
|
||||
data: send_data
|
||||
success: @clear_errors_then -> alert full_success_message
|
||||
error: std_ajax_err => @$request_response_error_grade.text full_error_message
|
||||
error: std_ajax_err((=>
|
||||
@$request_response_error_grade.text full_error_message
|
||||
), true)
|
||||
|
||||
# list task history for student+problem
|
||||
@$btn_task_history_single.click =>
|
||||
@@ -171,7 +191,8 @@ class @StudentAdmin
|
||||
unique_student_identifier: unique_student_identifier
|
||||
problem_location_str: problem_to_reset
|
||||
error_message = gettext("Error getting task history for problem '<%= problem_id %>' and student '<%= student_id %>'. Make sure that the problem and student identifiers are complete and correct.")
|
||||
full_error_message = _.template(error_message, {student_id: unique_student_identifier, problem_id: problem_to_reset})
|
||||
full_error_message = _.template(error_message,
|
||||
{student_id: unique_student_identifier, problem_id: problem_to_reset})
|
||||
|
||||
$.ajax
|
||||
dataType: 'json'
|
||||
@@ -179,9 +200,11 @@ class @StudentAdmin
|
||||
data: send_data
|
||||
success: @clear_errors_then (data) =>
|
||||
create_task_list_table @$table_task_history_single, data.tasks
|
||||
error: std_ajax_err => @$request_response_error_grade.text full_error_message
|
||||
error: std_ajax_err((=>
|
||||
@$request_response_error_grade.text full_error_message
|
||||
), true)
|
||||
|
||||
# reset entrance exam attempts for student
|
||||
# reset entrance exam attempts for student
|
||||
@$btn_reset_entrance_exam_attempts.click =>
|
||||
unique_student_identifier = @$field_entrance_exam_student_select_grade.val()
|
||||
if not unique_student_identifier
|
||||
@@ -198,12 +221,13 @@ class @StudentAdmin
|
||||
success_message = gettext("Entrance exam attempts is being reset for student '{student_id}'.")
|
||||
full_success_message = interpolate_text(success_message, {student_id: unique_student_identifier})
|
||||
alert full_success_message
|
||||
error: std_ajax_err =>
|
||||
error: std_ajax_err((=>
|
||||
error_message = gettext("Error resetting entrance exam attempts for student '{student_id}'. Make sure student identifier is correct.")
|
||||
full_error_message = interpolate_text(error_message, {student_id: unique_student_identifier})
|
||||
@$request_response_error_ee.text full_error_message
|
||||
), true)
|
||||
|
||||
# start task to rescore entrance exam for student
|
||||
# start task to rescore entrance exam for student
|
||||
@$btn_rescore_entrance_exam.click =>
|
||||
unique_student_identifier = @$field_entrance_exam_student_select_grade.val()
|
||||
if not unique_student_identifier
|
||||
@@ -224,7 +248,7 @@ class @StudentAdmin
|
||||
full_error_message = interpolate_text(error_message, {student_id: unique_student_identifier})
|
||||
@$request_response_error_ee.text full_error_message
|
||||
|
||||
# Mark a student to skip entrance exam
|
||||
# Mark a student to skip entrance exam
|
||||
@$btn_skip_entrance_exam.click =>
|
||||
unique_student_identifier = @$field_entrance_exam_student_select_grade.val()
|
||||
if not unique_student_identifier
|
||||
@@ -246,7 +270,7 @@ class @StudentAdmin
|
||||
error_message = gettext("An error occurred. Make sure that the student's username or email address is correct and try again.")
|
||||
@$request_response_error_ee.text error_message
|
||||
|
||||
# delete student state for entrance exam
|
||||
# delete student state for entrance exam
|
||||
@$btn_delete_entrance_exam_state.click =>
|
||||
unique_student_identifier = @$field_entrance_exam_student_select_grade.val()
|
||||
if not unique_student_identifier
|
||||
@@ -308,7 +332,9 @@ class @StudentAdmin
|
||||
url: @$btn_reset_attempts_all.data 'endpoint'
|
||||
data: send_data
|
||||
success: @clear_errors_then -> alert full_success_message
|
||||
error: std_ajax_err => @$request_response_error_all.text full_error_message
|
||||
error: std_ajax_err((=>
|
||||
@$request_response_error_all.text full_error_message
|
||||
), true)
|
||||
else
|
||||
# Clear error messages if "Cancel" was chosen on confirmation alert
|
||||
@clear_errors()
|
||||
@@ -334,7 +360,9 @@ class @StudentAdmin
|
||||
url: @$btn_rescore_problem_all.data 'endpoint'
|
||||
data: send_data
|
||||
success: @clear_errors_then -> alert full_success_message
|
||||
error: std_ajax_err => @$request_response_error_all.text full_error_message
|
||||
error: std_ajax_err((=>
|
||||
@$request_response_error_all.text full_error_message
|
||||
), true)
|
||||
else
|
||||
# Clear error messages if "Cancel" was chosen on confirmation alert
|
||||
@clear_errors()
|
||||
@@ -353,7 +381,9 @@ class @StudentAdmin
|
||||
data: send_data
|
||||
success: @clear_errors_then (data) =>
|
||||
create_task_list_table @$table_task_history_all, data.tasks
|
||||
error: std_ajax_err => @$request_response_error_all.text gettext("Error listing task history for this student and problem.")
|
||||
error: std_ajax_err((=>
|
||||
@$request_response_error_all.text gettext("Error listing task history for this student and problem.")
|
||||
), true)
|
||||
|
||||
# wraps a function, but first clear the error displays
|
||||
clear_errors_then: (cb) ->
|
||||
@@ -371,6 +401,10 @@ class @StudentAdmin
|
||||
@$request_response_error_ee.empty()
|
||||
@$request_response_error_all.empty()
|
||||
|
||||
|
||||
scroll_to_section: (element) ->
|
||||
$(window).scrollTop(element.offset().top).scrollLeft(element.offset().left)
|
||||
|
||||
# handler for when the section title is clicked.
|
||||
onClickTitle: -> @instructor_tasks.task_poller.start()
|
||||
|
||||
|
||||
@@ -19,10 +19,12 @@ find_and_assert = ($root, selector) ->
|
||||
#
|
||||
# wraps a `handler` function so that first
|
||||
# it prints basic error information to the console.
|
||||
@std_ajax_err = (handler) -> (jqXHR, textStatus, errorThrown) ->
|
||||
@std_ajax_err = (handler, sudo_reload=false) -> (jqXHR, textStatus, errorThrown) ->
|
||||
console.warn """ajax error
|
||||
textStatus: #{textStatus}
|
||||
errorThrown: #{errorThrown}"""
|
||||
if sudo_reload == true and jqXHR.status == 401
|
||||
window.location.reload()
|
||||
handler.apply this, arguments
|
||||
|
||||
|
||||
@@ -297,7 +299,10 @@ class @PendingInstructorTasks
|
||||
@$no_tasks_message.empty()
|
||||
@$no_tasks_message.append $('<p>').text gettext("No tasks currently running.")
|
||||
@$no_tasks_message.show()
|
||||
error: std_ajax_err => console.error "Error finding pending tasks to display"
|
||||
error: std_ajax_err((=>
|
||||
console.error "Error finding pending tasks to display"
|
||||
), true)
|
||||
|
||||
### /Pending Instructor Tasks Section ####
|
||||
|
||||
class KeywordValidator
|
||||
|
||||
@@ -4,13 +4,18 @@ define(['backbone', 'jquery', 'js/staff_debug_actions'],
|
||||
describe('StaffDebugActions', function () {
|
||||
var location = 'i4x://edX/Open_DemoX/edx_demo_course/problem/test_loc';
|
||||
var locationName = 'test_loc';
|
||||
var action = {location: location, locationName: locationName};
|
||||
var fixture_id = 'sd_fu_' + locationName;
|
||||
var fixture = $('<input>', { id: fixture_id, placeholder: "userman" });
|
||||
|
||||
describe('get_url ', function () {
|
||||
it('defines url to courseware ajax entry point', function () {
|
||||
spyOn(StaffDebug, "get_current_url").andReturn("/courses/edX/Open_DemoX/edx_demo_course/courseware/stuff");
|
||||
expect(StaffDebug.get_url('rescore_problem')).toBe('/courses/edX/Open_DemoX/edx_demo_course/instructor/api/rescore_problem');
|
||||
$('body').append(fixture);
|
||||
var expected_url = '/courses/edX/Open_DemoX/edx_demo_course/instructor?unique_student_identifier=userman&problem_to_reset=' + encodeURIComponent(action.location);
|
||||
expect(StaffDebug.get_url(action)).toBe(expected_url);
|
||||
|
||||
$('#' + fixture_id).remove();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,63 +41,20 @@ define(['backbone', 'jquery', 'js/staff_debug_actions'],
|
||||
$('#' + fixture_id).remove();
|
||||
});
|
||||
});
|
||||
describe('reset', function () {
|
||||
describe('student_grade_adjustemnts', function () {
|
||||
it('makes an ajax call with the expected parameters', function () {
|
||||
$('body').append(fixture);
|
||||
|
||||
spyOn($, 'ajax');
|
||||
StaffDebug.reset(locationName, location);
|
||||
spyOn(StaffDebug, 'goto_student_admin');
|
||||
|
||||
expect($.ajax.mostRecentCall.args[0]['type']).toEqual('GET');
|
||||
expect($.ajax.mostRecentCall.args[0]['data']).toEqual({
|
||||
'problem_to_reset': location,
|
||||
'unique_student_identifier': 'userman',
|
||||
'delete_module': false
|
||||
});
|
||||
expect($.ajax.mostRecentCall.args[0]['url']).toEqual(
|
||||
'/instructor/api/reset_student_attempts'
|
||||
);
|
||||
$('#' + fixture_id).remove();
|
||||
});
|
||||
});
|
||||
describe('sdelete', function () {
|
||||
it('makes an ajax call with the expected parameters', function () {
|
||||
$('body').append(fixture);
|
||||
StaffDebug.student_grade_adjustemnts(locationName, location);
|
||||
|
||||
spyOn($, 'ajax');
|
||||
StaffDebug.sdelete(locationName, location);
|
||||
var expected_url = get_url(action) + '#view-student_admin';
|
||||
|
||||
expect($.ajax.mostRecentCall.args[0]['type']).toEqual('GET');
|
||||
expect($.ajax.mostRecentCall.args[0]['data']).toEqual({
|
||||
'problem_to_reset': location,
|
||||
'unique_student_identifier': 'userman',
|
||||
'delete_module': true
|
||||
});
|
||||
expect($.ajax.mostRecentCall.args[0]['url']).toEqual(
|
||||
'/instructor/api/reset_student_attempts'
|
||||
);
|
||||
expect(StaffDebug.goto_student_admin).toHaveBeenCalledWith(expected_url);
|
||||
|
||||
$('#' + fixture_id).remove();
|
||||
});
|
||||
});
|
||||
describe('rescore', function () {
|
||||
it('makes an ajax call with the expected parameters', function () {
|
||||
$('body').append(fixture);
|
||||
|
||||
spyOn($, 'ajax');
|
||||
StaffDebug.rescore(locationName, location);
|
||||
|
||||
expect($.ajax.mostRecentCall.args[0]['type']).toEqual('GET');
|
||||
expect($.ajax.mostRecentCall.args[0]['data']).toEqual({
|
||||
'problem_to_reset': location,
|
||||
'unique_student_identifier': 'userman',
|
||||
'delete_module': false
|
||||
});
|
||||
expect($.ajax.mostRecentCall.args[0]['url']).toEqual(
|
||||
'/instructor/api/rescore_problem'
|
||||
);
|
||||
$('#' + fixture_id).remove();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,13 +3,15 @@ var StaffDebug = (function(){
|
||||
|
||||
get_current_url = function() {
|
||||
return window.location.pathname;
|
||||
}
|
||||
};
|
||||
|
||||
get_url = function(action){
|
||||
var problem_to_reset = encodeURIComponent(action.location);
|
||||
var unique_student_identifier = get_user(action.locationName);
|
||||
var pathname = this.get_current_url();
|
||||
var url = pathname.substr(0,pathname.indexOf('/courseware')) + '/instructor/api/' + action;
|
||||
var url = pathname.substr(0,pathname.indexOf('/courseware')) + '/instructor'+ '?unique_student_identifier=' + unique_student_identifier + '&problem_to_reset=' + problem_to_reset;
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
sanitized_string = function(string) {
|
||||
return string.replace(/[.*+?^:${}()|[\]\\]/g, "\\$&");
|
||||
@@ -22,95 +24,21 @@ var StaffDebug = (function(){
|
||||
uname = $('#sd_fu_' + locname).attr('placeholder');
|
||||
}
|
||||
return uname;
|
||||
}
|
||||
};
|
||||
|
||||
do_idash_action = function(action){
|
||||
var pdata = {
|
||||
'problem_to_reset': action.location,
|
||||
'unique_student_identifier': get_user(action.locationName),
|
||||
'delete_module': action.delete_module
|
||||
}
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: get_url(action.method),
|
||||
data: pdata,
|
||||
success: function(data){
|
||||
var text = _.template(
|
||||
action.success_msg,
|
||||
{user: data.student},
|
||||
{interpolate: /\{(.+?)\}/g}
|
||||
)
|
||||
var html = _.template(
|
||||
'<p id="idash_msg" class="success">{text}</p>',
|
||||
{text: text},
|
||||
{interpolate: /\{(.+?)\}/g}
|
||||
)
|
||||
$("#result_"+action.locationName).html(html);
|
||||
},
|
||||
error: function(request, status, error) {
|
||||
var response_json;
|
||||
try {
|
||||
response_json = $.parseJSON(request.responseText);
|
||||
} catch(e) {
|
||||
response_json = { error: gettext('Unknown Error Occurred.') };
|
||||
}
|
||||
var text = _.template(
|
||||
'{error_msg} {error}',
|
||||
{
|
||||
error_msg: action.error_msg,
|
||||
error: response_json.error
|
||||
},
|
||||
{interpolate: /\{(.+?)\}/g}
|
||||
)
|
||||
var html = _.template(
|
||||
'<p id="idash_msg" class="error">{text}</p>',
|
||||
{text: text},
|
||||
{interpolate: /\{(.+?)\}/g}
|
||||
)
|
||||
$("#result_"+action.locationName).html(html);
|
||||
},
|
||||
dataType: 'json'
|
||||
});
|
||||
}
|
||||
goto_student_admin = function(location) {
|
||||
window.location = location;
|
||||
};
|
||||
|
||||
reset = function(locname, location){
|
||||
this.do_idash_action({
|
||||
locationName: locname,
|
||||
location: location,
|
||||
method: 'reset_student_attempts',
|
||||
success_msg: gettext('Successfully reset the attempts for user {user}'),
|
||||
error_msg: gettext('Failed to reset attempts.'),
|
||||
delete_module: false
|
||||
});
|
||||
}
|
||||
|
||||
sdelete = function(locname, location){
|
||||
this.do_idash_action({
|
||||
locationName: locname,
|
||||
location: location,
|
||||
method: 'reset_student_attempts',
|
||||
success_msg: gettext('Successfully deleted student state for user {user}'),
|
||||
error_msg: gettext('Failed to delete student state.'),
|
||||
delete_module: true
|
||||
});
|
||||
}
|
||||
|
||||
rescore = function(locname, location){
|
||||
this.do_idash_action({
|
||||
locationName: locname,
|
||||
location: location,
|
||||
method: 'rescore_problem',
|
||||
success_msg: gettext('Successfully rescored problem for user {user}'),
|
||||
error_msg: gettext('Failed to rescore problem.'),
|
||||
delete_module: false
|
||||
});
|
||||
}
|
||||
student_grade_adjustemnts = function(locname, location){
|
||||
var action = {locationName: locname, location: location};
|
||||
var instructor_tab_url = get_url(action);
|
||||
this.goto_student_admin(instructor_tab_url + '#view-student_admin');
|
||||
};
|
||||
|
||||
return {
|
||||
reset: reset,
|
||||
sdelete: sdelete,
|
||||
rescore: rescore,
|
||||
do_idash_action: do_idash_action,
|
||||
student_grade_adjustemnts: student_grade_adjustemnts,
|
||||
goto_student_admin: goto_student_admin,
|
||||
get_current_url: get_current_url,
|
||||
get_url: get_url,
|
||||
get_user: get_user,
|
||||
@@ -121,16 +49,8 @@ var StaffDebug = (function(){
|
||||
// Register click handlers
|
||||
$(document).ready(function() {
|
||||
var $courseContent = $('.course-content');
|
||||
$courseContent.on("click", '.staff-debug-reset', function() {
|
||||
StaffDebug.reset($(this).parent().data('location-name'), $(this).parent().data('location'));
|
||||
return false;
|
||||
});
|
||||
$courseContent.on("click", '.staff-debug-sdelete', function() {
|
||||
StaffDebug.sdelete($(this).parent().data('location-name'), $(this).parent().data('location'));
|
||||
return false;
|
||||
});
|
||||
$courseContent.on("click", '.staff-debug-rescore', function() {
|
||||
StaffDebug.rescore($(this).parent().data('location-name'), $(this).parent().data('location'));
|
||||
$courseContent.on("click", '.staff-debug-grade-adjustments', function() {
|
||||
StaffDebug.student_grade_adjustemnts($(this).parent().data('location-name'), $(this).parent().data('location'));
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,17 +39,22 @@
|
||||
|
||||
<div class="student-grade-container action-type-container">
|
||||
<h2>${_("Student-specific grade adjustment")}</h2>
|
||||
%if section_data['problem_url']:
|
||||
<div class="wrap-instructor-info" aria-hidden="true">
|
||||
<a href="${ section_data['problem_url'] }" class="instructor-info-action">${_("Go Back To Problem")}</a>
|
||||
</div>
|
||||
%endif
|
||||
<div class="request-response-error"></div>
|
||||
<p>
|
||||
<label>
|
||||
${_("Specify the {platform_name} email address or username of a student here:").format(platform_name=settings.PLATFORM_NAME)}
|
||||
<input type="text" name="student-select-grade" placeholder="${_("Student Email or Username")}">
|
||||
<input type="text" name="student-select-grade" value="${ section_data['unique_student_identifier'] }" placeholder="${_("Student Email or Username")}">
|
||||
</label>
|
||||
</p>
|
||||
<br>
|
||||
|
||||
<label> ${_("Specify a problem in the course here with its complete location:")}
|
||||
<input type="text" name="problem-select-single" placeholder="${_("Problem location")}">
|
||||
<input type="text" name="problem-select-single" value="${ section_data['problem_to_reset'] }" placeholder="${_("Problem location")}">
|
||||
</label>
|
||||
|
||||
## Translators: A location (string of text) follows this sentence.
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! from django.template.defaultfilters import escapejs %>
|
||||
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
@@ -67,15 +70,7 @@ ${block_content}
|
||||
<input type="text" id="sd_fu_${location.name | h}" placeholder="${user.username}"/>
|
||||
</div>
|
||||
<div data-location="${location | h}" data-location-name="${location.name | h}">
|
||||
[
|
||||
<a href="#" class="staff-debug-reset">${_('Reset Student Attempts')}</a>
|
||||
% if has_instructor_access:
|
||||
|
|
||||
<a href="#" class="staff-debug-sdelete">${_('Delete Student State')}</a>
|
||||
|
|
||||
<a href="#" class="staff-debug-rescore">${_('Rescore Student Submission')}</a>
|
||||
% endif
|
||||
]
|
||||
[<a href="#" class="staff-debug-grade-adjustments">${_("Modify Student's State for Problem")}</a>]
|
||||
</div>
|
||||
<div id="result_${location.name | h}"/>
|
||||
</div>
|
||||
|
||||
21
lms/templates/sudo/sudo.html
Normal file
21
lms/templates/sudo/sudo.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends "main_django.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block body %}
|
||||
<section style="margin: 0 auto; width: 480px; padding: 50px;">
|
||||
<div class="inner-wrapper">
|
||||
<header>
|
||||
<h2 style="text-align: center;">{% trans "Confirm Your Password to Access the Instructor Dashboard" %}</h2>
|
||||
</header>
|
||||
<hr />
|
||||
<div style="margin: 0px auto; width: 218px;">
|
||||
<form class="sudo-form" method="post">{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<p>
|
||||
<input type="submit" value="Submit" />
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -1,11 +1,11 @@
|
||||
from django.conf import settings
|
||||
from django.conf.urls import patterns, include, url
|
||||
from ratelimitbackend import admin
|
||||
from django.conf.urls.static import static
|
||||
|
||||
import django.contrib.auth.views
|
||||
from microsite_configuration import microsite
|
||||
import auth_exchange.views
|
||||
from edx_admin import admin
|
||||
|
||||
# Uncomment the next two lines to enable the admin:
|
||||
if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
|
||||
@@ -80,6 +80,8 @@ urlpatterns = (
|
||||
# Course content API
|
||||
url(r'^api/course_structure/', include('course_structure_api.urls', namespace='course_structure_api')),
|
||||
|
||||
url(r'^sudo/$', 'sudo.views.sudo'),
|
||||
|
||||
# User API endpoints
|
||||
url(r'^api/user/', include('openedx.core.djangoapps.user_api.urls')),
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
from ratelimitbackend import admin
|
||||
"""
|
||||
django admin pages for course_structures model
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import CourseStructure
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"""
|
||||
Django admin page for credit eligibility
|
||||
"""
|
||||
|
||||
from ratelimitbackend import admin
|
||||
from openedx.core.djangoapps.credit.models import (
|
||||
CreditCourse, CreditProvider, CreditEligibility, CreditRequest
|
||||
)
|
||||
from django.contrib import admin
|
||||
|
||||
|
||||
class CreditCourseAdmin(admin.ModelAdmin):
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
-e git+https://github.com/edx/django-pipeline.git@88ec8a011e481918fdc9d2682d4017c835acd8be#egg=django-pipeline
|
||||
-e git+https://github.com/edx/django-wiki.git@cd0b2b31997afccde519fe5b3365e61a9edb143f#egg=django-wiki
|
||||
-e git+https://github.com/edx/django-oauth2-provider.git@0.2.7-fork-edx-5#egg=django-oauth2-provider
|
||||
-e git+https://github.com/edx/django-sudo.git@5ceb91236b477ce2726c538a2d8631884bda2684#egg=django-sudo
|
||||
-e git+https://github.com/edx/MongoDBProxy.git@25b99097615bda06bd7cdfe5669ed80dc2a7fed0#egg=mongodb_proxy
|
||||
git+https://github.com/edx/nltk.git@2.0.6#egg=nltk==2.0.6
|
||||
-e git+https://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
|
||||
|
||||
Reference in New Issue
Block a user