Commit e21ff203 by Zia Fazal

backend changes based on feedback

Conflicts:
	lms/djangoapps/courseware/courses.py
	lms/djangoapps/instructor_task/api.py

refined entrance exam student attempts reset

Quality improvements 1/16

added rescore, delete state and task history functionality

added unit tests for entrance exam reset attempts

added unit tests for re scoring of entrance exam and task history

improved test coverage

Got rid of pep violation

feedback changes and added jasmine test

added more jasmine tests for Javascript changes

added bok-choy tests for UI changes

replaced input containing <p> tags with <label>

Removed ee element assertions to avoid js error

Added call to super.setUp()

changes based on feedback on 2/18

Writing tests in JS instead of coffee script

commit related to skip entrance exam 2/13

fixed bad-continuation quality error

fixed broken bok-choy test

changes based on feedback on 2/18

added js tests and removed coffee script tests

fixed broken bok-choy and unit tests

changes left while rebasing

rephrase test titles

do not need these libs

changes based on feedback on 2/24

changes text which was left

Changes based on feedback on 3/3
parent 7bad6039
......@@ -1500,3 +1500,43 @@ class LinkedInAddToProfileConfiguration(ConfigurationModel):
)
if self.trk_partner_name else None
)
class EntranceExamConfiguration(models.Model):
"""
Represents a Student's entrance exam specific data for a single Course
"""
user = models.ForeignKey(User, db_index=True)
course_id = CourseKeyField(max_length=255, db_index=True)
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
updated = models.DateTimeField(auto_now=True, db_index=True)
# if skip_entrance_exam is True, then student can skip entrance exam
# for the course
skip_entrance_exam = models.BooleanField(default=True)
class Meta(object):
"""
Meta class to make user and course_id unique in the table
"""
unique_together = (('user', 'course_id'), )
def __unicode__(self):
return "[EntranceExamConfiguration] %s: %s (%s) = %s" % (
self.user, self.course_id, self.created, self.skip_entrance_exam
)
@classmethod
def user_can_skip_entrance_exam(cls, user, course_key):
"""
Return True if given user can skip entrance exam for given course otherwise False.
"""
can_skip = False
if settings.FEATURES.get('ENTRANCE_EXAMS', False):
try:
record = EntranceExamConfiguration.objects.get(user=user, course_id=course_key)
can_skip = record.skip_entrance_exam
except EntranceExamConfiguration.DoesNotExist:
can_skip = False
return can_skip
......@@ -524,6 +524,13 @@ class StudentAdminPage(PageObject):
return self.q(css='{} input[name=rescore-entrance-exam]'.format(self.EE_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))
@property
def delete_student_state_button(self):
"""
Returns delete student state button.
......@@ -592,6 +599,12 @@ 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.
......
......@@ -76,10 +76,10 @@ class SettingsPage(CoursePage):
"""
press_the_notification_button(self, "save")
if wait_for_confirmation:
EmptyPromise(
lambda: self.q(css='#alert-confirmation-title').present,
'Save is confirmed'
).fulfill()
self.wait_for_element_visibility(
'#alert-confirmation-title',
'Save confirmation message is visible'
)
def refresh_page(self, wait_for_confirmation=True):
"""
......@@ -91,3 +91,4 @@ class SettingsPage(CoursePage):
lambda: self.q(css='body.view-settings').present,
'Page is refreshed'
).fulfill()
self.wait_for_ajax()
......@@ -241,9 +241,9 @@ def element_has_text(page, css_selector, text):
def get_modal_alert(browser):
"""
Returns instance of modal alert box shown in browser after waiting
for 4 seconds
for 6 seconds
"""
WebDriverWait(browser, 4).until(EC.alert_is_present())
WebDriverWait(browser, 6).until(EC.alert_is_present())
return browser.switch_to.alert
......
......@@ -204,6 +204,45 @@ class EntranceExamGradeTest(UniqueCourseTest):
self.student_admin_section.wait_for_ajax()
self.assertGreater(len(self.student_admin_section.top_notification.text[0]), 0)
def test_clicking_skip_entrance_exam_button_with_success(self):
"""
Scenario: Clicking on the Let Student Skip Entrance Exam button with
valid student email address or username should result in success prompt.
Given that I am on the Student Admin tab on the Instructor Dashboard
When I click the Let Student Skip Entrance Exam Button under
Entrance Exam Grade 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_skip_entrance_exam_button()
#first we have window.confirm
alert = get_modal_alert(self.student_admin_section.browser)
alert.accept()
# then we have alert confirming action
alert = get_modal_alert(self.student_admin_section.browser)
alert.dismiss()
def test_clicking_skip_entrance_exam_button_with_error(self):
"""
Scenario: Clicking on the Let Student Skip Entrance Exam button with
email address or username of a non existing student should result in error message.
Given that I am on the Student Admin tab on the Instructor Dashboard
When I click the Let Student Skip Entrance Exam Button under
Entrance Exam Grade Adjustment after entering 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_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)
def test_clicking_delete_student_attempts_button_with_success(self):
"""
Scenario: Clicking on the Delete Student State for entrance exam button
......
......@@ -36,7 +36,7 @@ from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig
from edxmako.shortcuts import render_to_string
from eventtracking import tracker
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
from student.models import anonymous_id_for_user, user_by_anonymous_id
from student.models import anonymous_id_for_user, user_by_anonymous_id, EntranceExamConfiguration
from xblock.core import XBlock
from xblock.fields import Scope
from xblock.runtime import KvsFieldData, KeyValueStore
......@@ -129,6 +129,15 @@ def _get_required_content(course, user):
if milestone_path.get('content') and len(milestone_path['content']):
for content in milestone_path['content']:
required_content.append(content)
can_skip_entrance_exam = EntranceExamConfiguration.user_can_skip_entrance_exam(user, course.id)
# check if required_content has any entrance exam and user is allowed to skip it
# then remove it from required content
if required_content and getattr(course, 'entrance_exam_enabled', False) and can_skip_entrance_exam:
descriptors = [modulestore().get_item(UsageKey.from_string(content)) for content in required_content]
entrance_exam_contents = [unicode(descriptor.location)
for descriptor in descriptors if descriptor.is_entrance_exam]
required_content = list(set(required_content) - set(entrance_exam_contents))
return required_content
......
......@@ -6,7 +6,7 @@ from django.conf import settings
from django.utils.translation import ugettext as _
from courseware.access import has_access
from student.models import CourseEnrollment
from student.models import CourseEnrollment, EntranceExamConfiguration
from xmodule.tabs import CourseTabList
if settings.FEATURES.get('MILESTONES_APP', False):
......@@ -40,7 +40,8 @@ def get_course_tab_list(course, user):
for __, value in course_milestones_paths.iteritems():
if len(value.get('content', [])):
for content in value['content']:
if content == course.entrance_exam_id:
if content == course.entrance_exam_id \
and not EntranceExamConfiguration.user_can_skip_entrance_exam(user, course.id):
entrance_exam_mode = True
break
......
......@@ -3,10 +3,11 @@ Tests use cases related to LMS Entrance Exam behavior, such as gated content acc
"""
from django.test.client import RequestFactory
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from courseware.model_data import FieldDataCache
from courseware.module_render import get_module, toc_for_course
from courseware.tests.factories import UserFactory
from courseware.tests.factories import UserFactory, InstructorFactory
from milestones import api as milestones_api
from milestones.models import MilestoneRelationshipType
from xmodule.modulestore.django import modulestore
......@@ -57,14 +58,15 @@ class EntranceExamTestCases(ModuleStoreTestCase):
self.entrance_exam = ItemFactory.create(
parent=self.course,
category="chapter",
display_name="Entrance Exam Section - Chapter 1"
display_name="Entrance Exam Section - Chapter 1",
is_entrance_exam=True
)
self.exam_1 = ItemFactory.create(
parent=self.entrance_exam,
category='sequential',
display_name="Exam Sequential - Subsection 1",
graded=True,
metadata={'in_entrance_exam': True}
in_entrance_exam=True
)
subsection = ItemFactory.create(
parent=self.exam_1,
......@@ -130,13 +132,7 @@ class EntranceExamTestCases(ModuleStoreTestCase):
self.course.entrance_exam_id = unicode(self.entrance_exam.scope_ids.usage_id)
modulestore().update_item(self.course, user.id) # pylint: disable=no-member
def test_entrance_exam_gating(self):
"""
Unit Test: test_entrance_exam_gating
"""
# This user helps to cover a discovered bug in the milestone fulfillment logic
chaos_user = UserFactory()
expected_locked_toc = (
self.expected_locked_toc = (
[
{
'active': True,
......@@ -155,60 +151,7 @@ class EntranceExamTestCases(ModuleStoreTestCase):
}
]
)
locked_toc = toc_for_course(
self.request,
self.course,
self.entrance_exam.url_name,
self.exam_1.url_name,
self.field_data_cache
)
for toc_section in expected_locked_toc:
self.assertIn(toc_section, locked_toc)
# Set up the chaos user
# pylint: disable=maybe-no-member,no-member
grade_dict = {'value': 1, 'max_value': 1, 'user_id': chaos_user.id}
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
self.course.id,
chaos_user,
self.course,
depth=2
)
# pylint: disable=protected-access
module = get_module(
chaos_user,
self.request,
self.problem_1.scope_ids.usage_id,
field_data_cache,
)._xmodule
module.system.publish(self.problem_1, 'grade', grade_dict)
# pylint: disable=maybe-no-member,no-member
grade_dict = {'value': 1, 'max_value': 1, 'user_id': self.request.user.id}
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
self.course.id,
self.request.user,
self.course,
depth=2
)
# pylint: disable=protected-access
module = get_module(
self.request.user,
self.request,
self.problem_1.scope_ids.usage_id,
field_data_cache,
)._xmodule
module.system.publish(self.problem_1, 'grade', grade_dict)
module = get_module(
self.request.user,
self.request,
self.problem_2.scope_ids.usage_id,
field_data_cache,
)._xmodule # pylint: disable=protected-access
module.system.publish(self.problem_2, 'grade', grade_dict)
expected_unlocked_toc = (
self.expected_unlocked_toc = (
[
{
'active': False,
......@@ -263,6 +206,64 @@ class EntranceExamTestCases(ModuleStoreTestCase):
]
)
def test_entrance_exam_gating(self):
"""
Unit Test: test_entrance_exam_gating
"""
# This user helps to cover a discovered bug in the milestone fulfillment logic
chaos_user = UserFactory()
locked_toc = toc_for_course(
self.request,
self.course,
self.entrance_exam.url_name,
self.exam_1.url_name,
self.field_data_cache
)
for toc_section in self.expected_locked_toc:
self.assertIn(toc_section, locked_toc)
# Set up the chaos user
# pylint: disable=maybe-no-member,no-member
grade_dict = {'value': 1, 'max_value': 1, 'user_id': chaos_user.id}
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
self.course.id,
chaos_user,
self.course,
depth=2
)
# pylint: disable=protected-access
module = get_module(
chaos_user,
self.request,
self.problem_1.scope_ids.usage_id,
field_data_cache,
)._xmodule
module.system.publish(self.problem_1, 'grade', grade_dict)
# pylint: disable=maybe-no-member,no-member
grade_dict = {'value': 1, 'max_value': 1, 'user_id': self.request.user.id}
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
self.course.id,
self.request.user,
self.course,
depth=2
)
# pylint: disable=protected-access
module = get_module(
self.request.user,
self.request,
self.problem_1.scope_ids.usage_id,
field_data_cache,
)._xmodule
module.system.publish(self.problem_1, 'grade', grade_dict)
module = get_module(
self.request.user,
self.request,
self.problem_2.scope_ids.usage_id,
field_data_cache,
)._xmodule # pylint: disable=protected-access
module.system.publish(self.problem_2, 'grade', grade_dict)
unlocked_toc = toc_for_course(
self.request,
self.course,
......@@ -271,5 +272,39 @@ class EntranceExamTestCases(ModuleStoreTestCase):
self.field_data_cache
)
for toc_section in expected_unlocked_toc:
for toc_section in self.expected_unlocked_toc:
self.assertIn(toc_section, unlocked_toc)
def test_skip_entrance_exame_gating(self):
"""
Tests gating is disabled if skip entrance exam is set for a user.
"""
# make sure toc is locked before allowing user to skip entrance exam
locked_toc = toc_for_course(
self.request,
self.course,
self.entrance_exam.url_name,
self.exam_1.url_name,
self.field_data_cache
)
for toc_section in self.expected_locked_toc:
self.assertIn(toc_section, locked_toc)
# hit skip entrance exam api in instructor app
instructor = InstructorFactory(course_key=self.course.id)
self.client.login(username=instructor.username, password='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,
})
self.assertEqual(response.status_code, 200)
unlocked_toc = toc_for_course(
self.request,
self.course,
self.entrance_exam.url_name,
self.exam_1.url_name,
self.field_data_cache
)
for toc_section in self.expected_unlocked_toc:
self.assertIn(toc_section, unlocked_toc)
......@@ -10,6 +10,7 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
from courseware.courses import get_course_by_id
from courseware.tests.helpers import get_request_for_user, LoginEnrollmentTestCase
from courseware.tests.factories import InstructorFactory
from xmodule import tabs
from xmodule.modulestore.tests.django_utils import (
TEST_DATA_MIXED_TOY_MODULESTORE, TEST_DATA_MIXED_CLOSED_MODULESTORE
......@@ -177,6 +178,29 @@ class EntranceExamsTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
self.assertEqual(course_tab_list[0]['name'], 'Entrance Exam')
self.assertEqual(course_tab_list[1]['tab_id'], 'instructor')
def test_get_course_tabs_list_skipped_entrance_exam(self):
"""
Tests tab list is not limited if user is allowed to skip entrance exam.
"""
#create a user
student = UserFactory()
# login as instructor hit skip entrance exam api in instructor app
instructor = InstructorFactory(course_key=self.course.id)
self.client.logout()
self.client.login(username=instructor.username, password='test')
url = reverse('mark_student_can_skip_entrance_exam', kwargs={'course_id': unicode(self.course.id)})
response = self.client.post(url, {
'unique_student_identifier': student.email,
})
self.assertEqual(response.status_code, 200)
# log in again as student
self.client.logout()
self.login(self.email, self.password)
course_tab_list = get_course_tab_list(self.course, self.user)
self.assertEqual(len(course_tab_list), 5)
class TextBookTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
"""
......
......@@ -23,6 +23,7 @@ from django.http import HttpRequest, HttpResponse
from django.test import RequestFactory, TestCase
from django.test.utils import override_settings
from django.utils.timezone import utc
from django.utils.translation import ugettext as _
from mock import Mock, patch
from nose.tools import raises
......@@ -2375,13 +2376,13 @@ class TestEntranceExamInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollm
student=self.student,
course_id=self.course.id,
module_state_key=self.ee_problem_1.location,
state=json.dumps({'attempts': 10}),
state=json.dumps({'attempts': 10, 'done': True}),
)
ee_module_to_reset2 = StudentModule.objects.create(
student=self.student,
course_id=self.course.id,
module_state_key=self.ee_problem_2.location,
state=json.dumps({'attempts': 10}),
state=json.dumps({'attempts': 10, 'done': True}),
)
self.ee_modules = [ee_module_to_reset1.module_state_key, ee_module_to_reset2.module_state_key]
......@@ -2521,6 +2522,7 @@ class TestEntranceExamInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollm
# check response
tasks = json.loads(response.content)['tasks']
self.assertEqual(len(tasks), 1)
self.assertEqual(tasks[0]['status'], _('Complete'))
def test_list_entrance_exam_instructor_tasks_all_student(self):
""" Test list task history for entrance exam AND all student. """
......@@ -2541,6 +2543,27 @@ class TestEntranceExamInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollm
})
self.assertEqual(response.status_code, 400)
def test_skip_entrance_exam_student(self):
""" Test skip entrance exam api for student. """
# create a re-score entrance exam task
url = reverse('mark_student_can_skip_entrance_exam', kwargs={'course_id': unicode(self.course.id)})
response = self.client.post(url, {
'unique_student_identifier': self.student.email,
})
self.assertEqual(response.status_code, 200)
# check response
message = _('This student (%s) will skip the entrance exam.') % self.student.email
self.assertContains(response, message)
# post again with same student
response = self.client.post(url, {
'unique_student_identifier': self.student.email,
})
# This time response message should be different
message = _('This student (%s) is already allowed to skip the entrance exam.') % self.student.email
self.assertContains(response, message)
@override_settings(MODULESTORE=TEST_DATA_MOCK_MODULESTORE)
@patch('bulk_email.models.html_to_text', Mock(return_value='Mocking CourseEmail.text_message'))
......
......@@ -58,7 +58,7 @@ from shoppingcart.models import (
CourseMode,
CourseRegistrationCodeInvoiceItem,
)
from student.models import CourseEnrollment, unique_id_for_user, anonymous_id_for_user
from student.models import CourseEnrollment, unique_id_for_user, anonymous_id_for_user, EntranceExamConfiguration
import instructor_task.api
from instructor_task.api_helper import AlreadyRunningError
from instructor_task.models import ReportStore
......@@ -2322,3 +2322,27 @@ def spoc_gradebook(request, course_id):
'staff_access': True,
'ordered_grades': sorted(course.grade_cutoffs.items(), key=lambda i: i[1], reverse=True),
})
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff')
@require_POST
def mark_student_can_skip_entrance_exam(request, course_id): # pylint: disable=invalid-name
"""
Mark a student to skip entrance exam.
Takes `unique_student_identifier` as required POST parameter.
"""
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
student_identifier = request.POST.get('unique_student_identifier')
student = get_student_from_identifier(student_identifier)
__, created = EntranceExamConfiguration.objects.get_or_create(user=student, course_id=course_id)
if created:
message = _('This student (%s) will skip the entrance exam.') % student_identifier
else:
message = _('This student (%s) is already allowed to skip the entrance exam.') % student_identifier
response_payload = {
'message': message,
}
return JsonResponse(response_payload)
# pylint: disable=bad-continuation
"""
Instructor API endpoint urls.
"""
......@@ -38,15 +37,27 @@ urlpatterns = patterns(
'instructor.views.api.get_student_progress_url', name="get_student_progress_url"),
url(r'^reset_student_attempts$',
'instructor.views.api.reset_student_attempts', name="reset_student_attempts"),
url(r'^rescore_problem$', 'instructor.views.api.rescore_problem', name="rescore_problem"),
# entrance exam tasks
url(r'^reset_student_attempts_for_entrance_exam$',
url( # pylint: disable=bad-continuation
r'^rescore_problem$',
'instructor.views.api.rescore_problem',
name="rescore_problem"
), url(
r'^reset_student_attempts_for_entrance_exam$',
'instructor.views.api.reset_student_attempts_for_entrance_exam',
name="reset_student_attempts_for_entrance_exam"),
url(r'^rescore_entrance_exam$',
'instructor.views.api.rescore_entrance_exam', name="rescore_entrance_exam"),
url(r'^list_entrance_exam_instructor_tasks',
'instructor.views.api.list_entrance_exam_instructor_tasks', name="list_entrance_exam_instructor_tasks"),
name="reset_student_attempts_for_entrance_exam"
), url(
r'^rescore_entrance_exam$',
'instructor.views.api.rescore_entrance_exam',
name="rescore_entrance_exam"
), url(
r'^list_entrance_exam_instructor_tasks',
'instructor.views.api.list_entrance_exam_instructor_tasks',
name="list_entrance_exam_instructor_tasks"
), url(
r'^mark_student_can_skip_entrance_exam',
'instructor.views.api.mark_student_can_skip_entrance_exam',
name="mark_student_can_skip_entrance_exam"
),
url(r'^list_instructor_tasks$',
'instructor.views.api.list_instructor_tasks', name="list_instructor_tasks"),
......
......@@ -310,6 +310,10 @@ def _section_student_admin(course, access):
),
'rescore_problem_url': reverse('rescore_problem', kwargs={'course_id': unicode(course_key)}),
'rescore_entrance_exam_url': reverse('rescore_entrance_exam', kwargs={'course_id': unicode(course_key)}),
'student_can_skip_entrance_exam_url': reverse(
'mark_student_can_skip_entrance_exam',
kwargs={'course_id': unicode(course_key)},
),
'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': unicode(course_key)}),
'list_entrace_exam_instructor_tasks_url': reverse('list_entrance_exam_instructor_tasks',
kwargs={'course_id': unicode(course_key)}),
......
......@@ -149,6 +149,7 @@ def get_task_completion_info(instructor_task):
else:
student = task_input.get('student')
problem_url = task_input.get('problem_url')
entrance_exam_url = task_input.get('entrance_exam_url')
email_id = task_input.get('email_id')
if instructor_task.task_state == PROGRESS:
......@@ -167,6 +168,17 @@ def get_task_completion_info(instructor_task):
succeeded = True
# Translators: {action} is a past-tense verb that is localized separately. {student} is a student identifier.
msg_format = _("Problem successfully {action} for student '{student}'")
elif student is not None and entrance_exam_url is not None:
# this reports on actions on entrance exam for a particular student:
if num_attempted == 0:
# Translators: {action} is a past-tense verb that is localized separately.
# {student} is a student identifier.
msg_format = _("Unable to find entrance exam submission to be {action} for student '{student}'")
else:
succeeded = True
# Translators: {action} is a past-tense verb that is localized separately.
# {student} is a student identifier.
msg_format = _("Entrance exam successfully {action} for student '{student}'")
elif student is None and problem_url is not None:
# this reports on actions on problems for all students:
if num_attempted == 0:
......
......@@ -46,6 +46,7 @@ class @StudentAdmin
@$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"
......@@ -223,6 +224,28 @@ 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
@$btn_skip_entrance_exam.click =>
unique_student_identifier = @$field_entrance_exam_student_select_grade.val()
if not unique_student_identifier
return @$request_response_error_ee.text gettext("Enter a student's username or email address.")
confirm_message = gettext("Do you want to allow this student ('{student_id}') to skip the entrance exam?")
full_confirm_message = interpolate_text(confirm_message, {student_id: unique_student_identifier})
if window.confirm full_confirm_message
send_data =
unique_student_identifier: unique_student_identifier
$.ajax
dataType: 'json'
url: @$btn_skip_entrance_exam.data 'endpoint'
data: send_data
type: 'POST'
success: @clear_errors_then (data) ->
alert data.message
error: std_ajax_err =>
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
@$btn_delete_entrance_exam_state.click =>
unique_student_identifier = @$field_entrance_exam_student_select_grade.val()
......@@ -249,7 +272,7 @@ class @StudentAdmin
@$btn_entrance_exam_task_history.click =>
unique_student_identifier = @$field_entrance_exam_student_select_grade.val()
if not unique_student_identifier
return @$request_response_error_ee.text gettext("Please enter a student email address or username.")
return @$request_response_error_ee.text gettext("Enter a student's username or email address.")
send_data =
unique_student_identifier: unique_student_identifier
......
......@@ -94,6 +94,7 @@
<input type="button" name="reset-entrance-exam-attempts" value="Reset Number of Student Attempts" data-endpoint="/courses/PU/FSc/2014_T4/instructor/api/reset_student_attempts_for_entrance_exam">
<input type="button" name="rescore-entrance-exam" value="Rescore All Problems" data-endpoint="/courses/PU/FSc/2014_T4/instructor/api/rescore_entrance_exam">
<input type="button" name="skip-entrance-exam" value="Let Student Skip Entrance Exam" data-endpoint="/courses/PU/FSc/2014_T4/instructor/api/mark_student_can_skip_entrance_exam">
<p>
<label> You can also delete all of the student's answers and scores for the entire entrance exam.
......
......@@ -23,7 +23,7 @@ define(['jquery', 'coffee/src/instructor_dashboard/student_admin', 'js/common_he
});
it('binds reset entrance exam ajax call and the result will be success', function() {
it('initiates resetting of entrance exam when button is clicked', function() {
studentadmin.$btn_reset_entrance_exam_attempts.click();
// expect error to be shown since student identifier is not set
expect(studentadmin.$request_response_error_ee.text()).toEqual(gettext("Please enter a student email address or username."));
......@@ -53,7 +53,7 @@ define(['jquery', 'coffee/src/instructor_dashboard/student_admin', 'js/common_he
expect(alert_msg).toEqual(full_success_message);
});
it('binds reset entrance exam ajax call and the result will be error', function() {
it('shows an error when resetting of entrance exam fails', function() {
// Spy on AJAX requests
var requests = AjaxHelpers.requests(this);
studentadmin.$field_entrance_exam_student_select_grade.val(unique_student_identifier)
......@@ -129,7 +129,53 @@ define(['jquery', 'coffee/src/instructor_dashboard/student_admin', 'js/common_he
expect(studentadmin.$request_response_error_ee.text()).toEqual(full_error_message);
});
it('binds delete student state for entrance exam ajax call and the result will be success', function() {
it('initiates skip entrance exam when button is clicked', function() {
studentadmin.$btn_skip_entrance_exam.click();
// expect error to be shown since student identifier is not set
expect(studentadmin.$request_response_error_ee.text()).toEqual(gettext("Enter a student's username or email address."));
var success_message = "This student ('{student_id}') will skip the entrance exam.";
var full_success_message = interpolate_text(success_message, {
student_id: unique_student_identifier
});
// Spy on AJAX requests
var requests = AjaxHelpers.requests(this);
studentadmin.$field_entrance_exam_student_select_grade.val(unique_student_identifier)
studentadmin.$btn_skip_entrance_exam.click();
// Verify that the client contacts the server to start instructor task
var url = dashboard_api_url + '/mark_student_can_skip_entrance_exam';
AjaxHelpers.expectRequest(requests, 'POST', url, $.param({
'unique_student_identifier': unique_student_identifier
}));
// Simulate a success response from the server
AjaxHelpers.respondWithJson(requests, {
message: full_success_message
});
expect(alert_msg).toEqual(full_success_message);
});
it('shows an error when skip entrance exam fails', function() {
// Spy on AJAX requests
var requests = AjaxHelpers.requests(this);
studentadmin.$field_entrance_exam_student_select_grade.val(unique_student_identifier)
studentadmin.$btn_skip_entrance_exam.click();
// Verify that the client contacts the server to start instructor task
var url = dashboard_api_url + '/mark_student_can_skip_entrance_exam';
AjaxHelpers.expectRequest(requests, 'POST', url, $.param({
'unique_student_identifier': unique_student_identifier
}));
// Simulate an error response from the server
AjaxHelpers.respondWithError(requests, 400,{});
var error_message = "An error occurred. Make sure that the student's username or email address is correct and try again.";
expect(studentadmin.$request_response_error_ee.text()).toEqual(error_message);
});
it('initiates delete student state for entrance exam when button is clicked', function() {
studentadmin.$btn_delete_entrance_exam_state.click();
// expect error to be shown since student identifier is not set
expect(studentadmin.$request_response_error_ee.text()).toEqual(gettext("Please enter a student email address or username."));
......@@ -159,7 +205,7 @@ define(['jquery', 'coffee/src/instructor_dashboard/student_admin', 'js/common_he
expect(alert_msg).toEqual(full_success_message);
});
it('binds delete student state for entrance exam ajax call and the result will be error', function() {
it('shows an error when delete student state for entrance exam fails', function() {
// Spy on AJAX requests
var requests = AjaxHelpers.requests(this);
studentadmin.$field_entrance_exam_student_select_grade.val(unique_student_identifier)
......@@ -183,10 +229,10 @@ define(['jquery', 'coffee/src/instructor_dashboard/student_admin', 'js/common_he
expect(studentadmin.$request_response_error_ee.text()).toEqual(full_error_message);
});
it('binds list entrance exam task history ajax call and the result will be success', function() {
it('initiates listing of entrance exam task history when button is clicked', function() {
studentadmin.$btn_entrance_exam_task_history.click();
// expect error to be shown since student identifier is not set
expect(studentadmin.$request_response_error_ee.text()).toEqual(gettext("Please enter a student email address or username."));
expect(studentadmin.$request_response_error_ee.text()).toEqual(gettext("Enter a student's username or email address."));
var success_message = gettext("Entrance exam state is being deleted for student '{student_id}'.");
var full_success_message = interpolate_text(success_message, {
......@@ -224,7 +270,7 @@ define(['jquery', 'coffee/src/instructor_dashboard/student_admin', 'js/common_he
expect($('.entrance-exam-task-history-table')).toBeVisible();
});
it('binds list entrance exam task history ajax call and the result will be error', function() {
it('shows an error when listing entrance exam task history fails', function() {
// Spy on AJAX requests
var requests = AjaxHelpers.requests(this);
studentadmin.$field_entrance_exam_student_select_grade.val(unique_student_identifier)
......
......@@ -107,6 +107,7 @@
%if settings.FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS') and section_data['access']['instructor']:
<input type="button" name="rescore-entrance-exam" value="${_('Rescore All Problems')}" data-endpoint="${ section_data['rescore_entrance_exam_url'] }">
%endif
<input type="button" name="skip-entrance-exam" value="${_('Let Student Skip Entrance Exam')}" data-endpoint="${ section_data['student_can_skip_entrance_exam_url'] }">
<p>
%if section_data['access']['instructor']:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment