Merge pull request #7082 from edx/ziafazal/skip-entrance-exam-am
Skip entrance exam feature on instructor dashboard
This commit is contained in:
@@ -0,0 +1,203 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import datetime
|
||||||
|
from south.db import db
|
||||||
|
from south.v2 import SchemaMigration
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(SchemaMigration):
|
||||||
|
|
||||||
|
def forwards(self, orm):
|
||||||
|
# Adding model 'EntranceExamConfiguration'
|
||||||
|
db.create_table('student_entranceexamconfiguration', (
|
||||||
|
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||||
|
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
|
||||||
|
('course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255, db_index=True)),
|
||||||
|
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, db_index=True, blank=True)),
|
||||||
|
('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
|
||||||
|
('skip_entrance_exam', self.gf('django.db.models.fields.BooleanField')(default=True)),
|
||||||
|
))
|
||||||
|
db.send_create_signal('student', ['EntranceExamConfiguration'])
|
||||||
|
|
||||||
|
# Adding unique constraint on 'EntranceExamConfiguration', fields ['user', 'course_id']
|
||||||
|
db.create_unique('student_entranceexamconfiguration', ['user_id', 'course_id'])
|
||||||
|
|
||||||
|
|
||||||
|
def backwards(self, orm):
|
||||||
|
# Removing unique constraint on 'EntranceExamConfiguration', fields ['user', 'course_id']
|
||||||
|
db.delete_unique('student_entranceexamconfiguration', ['user_id', 'course_id'])
|
||||||
|
|
||||||
|
# Deleting model 'EntranceExamConfiguration'
|
||||||
|
db.delete_table('student_entranceexamconfiguration')
|
||||||
|
|
||||||
|
|
||||||
|
models = {
|
||||||
|
'auth.group': {
|
||||||
|
'Meta': {'object_name': 'Group'},
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||||
|
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||||
|
},
|
||||||
|
'auth.permission': {
|
||||||
|
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||||
|
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||||
|
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||||
|
},
|
||||||
|
'auth.user': {
|
||||||
|
'Meta': {'object_name': 'User'},
|
||||||
|
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||||
|
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||||
|
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||||
|
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||||
|
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||||
|
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||||
|
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||||
|
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||||
|
},
|
||||||
|
'contenttypes.contenttype': {
|
||||||
|
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||||
|
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||||
|
},
|
||||||
|
'student.anonymoususerid': {
|
||||||
|
'Meta': {'object_name': 'AnonymousUserId'},
|
||||||
|
'anonymous_user_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
|
||||||
|
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||||
|
},
|
||||||
|
'student.courseaccessrole': {
|
||||||
|
'Meta': {'unique_together': "(('user', 'org', 'course_id', 'role'),)", 'object_name': 'CourseAccessRole'},
|
||||||
|
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'org': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'blank': 'True'}),
|
||||||
|
'role': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
|
||||||
|
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||||
|
},
|
||||||
|
'student.courseenrollment': {
|
||||||
|
'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
|
||||||
|
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}),
|
||||||
|
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||||
|
},
|
||||||
|
'student.courseenrollmentallowed': {
|
||||||
|
'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'},
|
||||||
|
'auto_enroll': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||||
|
'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||||
|
},
|
||||||
|
'student.dashboardconfiguration': {
|
||||||
|
'Meta': {'object_name': 'DashboardConfiguration'},
|
||||||
|
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||||
|
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
|
||||||
|
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'recent_enrollment_time_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
|
||||||
|
},
|
||||||
|
'student.entranceexamconfiguration': {
|
||||||
|
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'EntranceExamConfiguration'},
|
||||||
|
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'skip_entrance_exam': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||||
|
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||||
|
},
|
||||||
|
'student.linkedinaddtoprofileconfiguration': {
|
||||||
|
'Meta': {'object_name': 'LinkedInAddToProfileConfiguration'},
|
||||||
|
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||||
|
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
|
||||||
|
'dashboard_tracking_code': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||||
|
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||||
|
},
|
||||||
|
'student.loginfailures': {
|
||||||
|
'Meta': {'object_name': 'LoginFailures'},
|
||||||
|
'failure_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'lockout_until': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
|
||||||
|
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||||
|
},
|
||||||
|
'student.passwordhistory': {
|
||||||
|
'Meta': {'object_name': 'PasswordHistory'},
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||||
|
'time_set': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||||
|
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||||
|
},
|
||||||
|
'student.pendingemailchange': {
|
||||||
|
'Meta': {'object_name': 'PendingEmailChange'},
|
||||||
|
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||||
|
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||||
|
},
|
||||||
|
'student.pendingnamechange': {
|
||||||
|
'Meta': {'object_name': 'PendingNameChange'},
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||||
|
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||||
|
},
|
||||||
|
'student.registration': {
|
||||||
|
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
|
||||||
|
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||||
|
},
|
||||||
|
'student.userprofile': {
|
||||||
|
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
|
||||||
|
'allow_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'city': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||||
|
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'null': 'True', 'blank': 'True'}),
|
||||||
|
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
|
||||||
|
'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||||
|
'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||||
|
'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||||
|
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||||
|
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||||
|
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||||
|
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}),
|
||||||
|
'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
|
||||||
|
},
|
||||||
|
'student.usersignupsource': {
|
||||||
|
'Meta': {'object_name': 'UserSignupSource'},
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'site': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||||
|
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||||
|
},
|
||||||
|
'student.userstanding': {
|
||||||
|
'Meta': {'object_name': 'UserStanding'},
|
||||||
|
'account_status': ('django.db.models.fields.CharField', [], {'max_length': '31', 'blank': 'True'}),
|
||||||
|
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'blank': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'standing_last_changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
|
||||||
|
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'standing'", 'unique': 'True', 'to': "orm['auth.User']"})
|
||||||
|
},
|
||||||
|
'student.usertestgroup': {
|
||||||
|
'Meta': {'object_name': 'UserTestGroup'},
|
||||||
|
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||||
|
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
complete_apps = ['student']
|
||||||
@@ -1500,3 +1500,43 @@ class LinkedInAddToProfileConfiguration(ConfigurationModel):
|
|||||||
)
|
)
|
||||||
if self.trk_partner_name else None
|
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
|
||||||
|
|||||||
@@ -523,6 +523,13 @@ class StudentAdminPage(PageObject):
|
|||||||
"""
|
"""
|
||||||
return self.q(css='{} input[name=rescore-entrance-exam]'.format(self.EE_CONTAINER))
|
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
|
@property
|
||||||
def delete_student_state_button(self):
|
def delete_student_state_button(self):
|
||||||
"""
|
"""
|
||||||
@@ -592,6 +599,12 @@ class StudentAdminPage(PageObject):
|
|||||||
"""
|
"""
|
||||||
return self.rescore_submission_button.click()
|
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):
|
def click_delete_student_state_button(self):
|
||||||
"""
|
"""
|
||||||
clicks delete student state button.
|
clicks delete student state button.
|
||||||
|
|||||||
@@ -76,10 +76,10 @@ class SettingsPage(CoursePage):
|
|||||||
"""
|
"""
|
||||||
press_the_notification_button(self, "save")
|
press_the_notification_button(self, "save")
|
||||||
if wait_for_confirmation:
|
if wait_for_confirmation:
|
||||||
EmptyPromise(
|
self.wait_for_element_visibility(
|
||||||
lambda: self.q(css='#alert-confirmation-title').present,
|
'#alert-confirmation-title',
|
||||||
'Save is confirmed'
|
'Save confirmation message is visible'
|
||||||
).fulfill()
|
)
|
||||||
|
|
||||||
def refresh_page(self, wait_for_confirmation=True):
|
def refresh_page(self, wait_for_confirmation=True):
|
||||||
"""
|
"""
|
||||||
@@ -91,3 +91,4 @@ class SettingsPage(CoursePage):
|
|||||||
lambda: self.q(css='body.view-settings').present,
|
lambda: self.q(css='body.view-settings').present,
|
||||||
'Page is refreshed'
|
'Page is refreshed'
|
||||||
).fulfill()
|
).fulfill()
|
||||||
|
self.wait_for_ajax()
|
||||||
|
|||||||
@@ -241,9 +241,9 @@ def element_has_text(page, css_selector, text):
|
|||||||
def get_modal_alert(browser):
|
def get_modal_alert(browser):
|
||||||
"""
|
"""
|
||||||
Returns instance of modal alert box shown in browser after waiting
|
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
|
return browser.switch_to.alert
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -204,6 +204,45 @@ class EntranceExamGradeTest(UniqueCourseTest):
|
|||||||
self.student_admin_section.wait_for_ajax()
|
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.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):
|
def test_clicking_delete_student_attempts_button_with_success(self):
|
||||||
"""
|
"""
|
||||||
Scenario: Clicking on the Delete Student State for entrance exam button
|
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 edxmako.shortcuts import render_to_string
|
||||||
from eventtracking import tracker
|
from eventtracking import tracker
|
||||||
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
|
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.core import XBlock
|
||||||
from xblock.fields import Scope
|
from xblock.fields import Scope
|
||||||
from xblock.runtime import KvsFieldData, KeyValueStore
|
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']):
|
if milestone_path.get('content') and len(milestone_path['content']):
|
||||||
for content in milestone_path['content']:
|
for content in milestone_path['content']:
|
||||||
required_content.append(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
|
return required_content
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from django.conf import settings
|
|||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from courseware.access import has_access
|
from courseware.access import has_access
|
||||||
from student.models import CourseEnrollment
|
from student.models import CourseEnrollment, EntranceExamConfiguration
|
||||||
from xmodule.tabs import CourseTabList
|
from xmodule.tabs import CourseTabList
|
||||||
|
|
||||||
if settings.FEATURES.get('MILESTONES_APP', False):
|
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():
|
for __, value in course_milestones_paths.iteritems():
|
||||||
if len(value.get('content', [])):
|
if len(value.get('content', [])):
|
||||||
for content in value['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
|
entrance_exam_mode = True
|
||||||
break
|
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.client import RequestFactory
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
from courseware.model_data import FieldDataCache
|
from courseware.model_data import FieldDataCache
|
||||||
from courseware.module_render import get_module, toc_for_course
|
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 import api as milestones_api
|
||||||
from milestones.models import MilestoneRelationshipType
|
from milestones.models import MilestoneRelationshipType
|
||||||
from xmodule.modulestore.django import modulestore
|
from xmodule.modulestore.django import modulestore
|
||||||
@@ -57,14 +58,15 @@ class EntranceExamTestCases(ModuleStoreTestCase):
|
|||||||
self.entrance_exam = ItemFactory.create(
|
self.entrance_exam = ItemFactory.create(
|
||||||
parent=self.course,
|
parent=self.course,
|
||||||
category="chapter",
|
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(
|
self.exam_1 = ItemFactory.create(
|
||||||
parent=self.entrance_exam,
|
parent=self.entrance_exam,
|
||||||
category='sequential',
|
category='sequential',
|
||||||
display_name="Exam Sequential - Subsection 1",
|
display_name="Exam Sequential - Subsection 1",
|
||||||
graded=True,
|
graded=True,
|
||||||
metadata={'in_entrance_exam': True}
|
in_entrance_exam=True
|
||||||
)
|
)
|
||||||
subsection = ItemFactory.create(
|
subsection = ItemFactory.create(
|
||||||
parent=self.exam_1,
|
parent=self.exam_1,
|
||||||
@@ -130,13 +132,7 @@ class EntranceExamTestCases(ModuleStoreTestCase):
|
|||||||
self.course.entrance_exam_id = unicode(self.entrance_exam.scope_ids.usage_id)
|
self.course.entrance_exam_id = unicode(self.entrance_exam.scope_ids.usage_id)
|
||||||
modulestore().update_item(self.course, user.id) # pylint: disable=no-member
|
modulestore().update_item(self.course, user.id) # pylint: disable=no-member
|
||||||
|
|
||||||
def test_entrance_exam_gating(self):
|
self.expected_locked_toc = (
|
||||||
"""
|
|
||||||
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 = (
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
'active': True,
|
'active': True,
|
||||||
@@ -155,60 +151,7 @@ class EntranceExamTestCases(ModuleStoreTestCase):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
locked_toc = toc_for_course(
|
self.expected_unlocked_toc = (
|
||||||
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 = (
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
'active': False,
|
'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(
|
unlocked_toc = toc_for_course(
|
||||||
self.request,
|
self.request,
|
||||||
self.course,
|
self.course,
|
||||||
@@ -271,5 +272,39 @@ class EntranceExamTestCases(ModuleStoreTestCase):
|
|||||||
self.field_data_cache
|
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)
|
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.courses import get_course_by_id
|
||||||
from courseware.tests.helpers import get_request_for_user, LoginEnrollmentTestCase
|
from courseware.tests.helpers import get_request_for_user, LoginEnrollmentTestCase
|
||||||
|
from courseware.tests.factories import InstructorFactory
|
||||||
from xmodule import tabs
|
from xmodule import tabs
|
||||||
from xmodule.modulestore.tests.django_utils import (
|
from xmodule.modulestore.tests.django_utils import (
|
||||||
TEST_DATA_MIXED_TOY_MODULESTORE, TEST_DATA_MIXED_CLOSED_MODULESTORE
|
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[0]['name'], 'Entrance Exam')
|
||||||
self.assertEqual(course_tab_list[1]['tab_id'], 'instructor')
|
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):
|
class TextBookTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from django.http import HttpRequest, HttpResponse
|
|||||||
from django.test import RequestFactory, TestCase
|
from django.test import RequestFactory, TestCase
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
from django.utils.timezone import utc
|
from django.utils.timezone import utc
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from mock import Mock, patch
|
from mock import Mock, patch
|
||||||
from nose.tools import raises
|
from nose.tools import raises
|
||||||
@@ -2375,13 +2376,13 @@ class TestEntranceExamInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollm
|
|||||||
student=self.student,
|
student=self.student,
|
||||||
course_id=self.course.id,
|
course_id=self.course.id,
|
||||||
module_state_key=self.ee_problem_1.location,
|
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(
|
ee_module_to_reset2 = StudentModule.objects.create(
|
||||||
student=self.student,
|
student=self.student,
|
||||||
course_id=self.course.id,
|
course_id=self.course.id,
|
||||||
module_state_key=self.ee_problem_2.location,
|
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]
|
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
|
# check response
|
||||||
tasks = json.loads(response.content)['tasks']
|
tasks = json.loads(response.content)['tasks']
|
||||||
self.assertEqual(len(tasks), 1)
|
self.assertEqual(len(tasks), 1)
|
||||||
|
self.assertEqual(tasks[0]['status'], _('Complete'))
|
||||||
|
|
||||||
def test_list_entrance_exam_instructor_tasks_all_student(self):
|
def test_list_entrance_exam_instructor_tasks_all_student(self):
|
||||||
""" Test list task history for entrance exam AND all student. """
|
""" Test list task history for entrance exam AND all student. """
|
||||||
@@ -2541,6 +2543,27 @@ class TestEntranceExamInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollm
|
|||||||
})
|
})
|
||||||
self.assertEqual(response.status_code, 400)
|
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)
|
@override_settings(MODULESTORE=TEST_DATA_MOCK_MODULESTORE)
|
||||||
@patch('bulk_email.models.html_to_text', Mock(return_value='Mocking CourseEmail.text_message'))
|
@patch('bulk_email.models.html_to_text', Mock(return_value='Mocking CourseEmail.text_message'))
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ from shoppingcart.models import (
|
|||||||
CourseMode,
|
CourseMode,
|
||||||
CourseRegistrationCodeInvoiceItem,
|
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
|
import instructor_task.api
|
||||||
from instructor_task.api_helper import AlreadyRunningError
|
from instructor_task.api_helper import AlreadyRunningError
|
||||||
from instructor_task.models import ReportStore
|
from instructor_task.models import ReportStore
|
||||||
@@ -2322,3 +2322,27 @@ def spoc_gradebook(request, course_id):
|
|||||||
'staff_access': True,
|
'staff_access': True,
|
||||||
'ordered_grades': sorted(course.grade_cutoffs.items(), key=lambda i: i[1], reverse=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)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
# pylint: disable=bad-continuation
|
|
||||||
"""
|
"""
|
||||||
Instructor API endpoint urls.
|
Instructor API endpoint urls.
|
||||||
"""
|
"""
|
||||||
@@ -38,15 +37,27 @@ urlpatterns = patterns(
|
|||||||
'instructor.views.api.get_student_progress_url', name="get_student_progress_url"),
|
'instructor.views.api.get_student_progress_url', name="get_student_progress_url"),
|
||||||
url(r'^reset_student_attempts$',
|
url(r'^reset_student_attempts$',
|
||||||
'instructor.views.api.reset_student_attempts', name="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"),
|
url( # pylint: disable=bad-continuation
|
||||||
# entrance exam tasks
|
r'^rescore_problem$',
|
||||||
url(r'^reset_student_attempts_for_entrance_exam$',
|
'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',
|
'instructor.views.api.reset_student_attempts_for_entrance_exam',
|
||||||
name="reset_student_attempts_for_entrance_exam"),
|
name="reset_student_attempts_for_entrance_exam"
|
||||||
url(r'^rescore_entrance_exam$',
|
), url(
|
||||||
'instructor.views.api.rescore_entrance_exam', name="rescore_entrance_exam"),
|
r'^rescore_entrance_exam$',
|
||||||
url(r'^list_entrance_exam_instructor_tasks',
|
'instructor.views.api.rescore_entrance_exam',
|
||||||
'instructor.views.api.list_entrance_exam_instructor_tasks', name="list_entrance_exam_instructor_tasks"),
|
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$',
|
url(r'^list_instructor_tasks$',
|
||||||
'instructor.views.api.list_instructor_tasks', name="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_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)}),
|
'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_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',
|
'list_entrace_exam_instructor_tasks_url': reverse('list_entrance_exam_instructor_tasks',
|
||||||
kwargs={'course_id': unicode(course_key)}),
|
kwargs={'course_id': unicode(course_key)}),
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ def get_task_completion_info(instructor_task):
|
|||||||
else:
|
else:
|
||||||
student = task_input.get('student')
|
student = task_input.get('student')
|
||||||
problem_url = task_input.get('problem_url')
|
problem_url = task_input.get('problem_url')
|
||||||
|
entrance_exam_url = task_input.get('entrance_exam_url')
|
||||||
email_id = task_input.get('email_id')
|
email_id = task_input.get('email_id')
|
||||||
|
|
||||||
if instructor_task.task_state == PROGRESS:
|
if instructor_task.task_state == PROGRESS:
|
||||||
@@ -167,6 +168,17 @@ def get_task_completion_info(instructor_task):
|
|||||||
succeeded = True
|
succeeded = True
|
||||||
# Translators: {action} is a past-tense verb that is localized separately. {student} is a student identifier.
|
# Translators: {action} is a past-tense verb that is localized separately. {student} is a student identifier.
|
||||||
msg_format = _("Problem successfully {action} for student '{student}'")
|
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:
|
elif student is None and problem_url is not None:
|
||||||
# this reports on actions on problems for all students:
|
# this reports on actions on problems for all students:
|
||||||
if num_attempted == 0:
|
if num_attempted == 0:
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class @StudentAdmin
|
|||||||
@$btn_reset_entrance_exam_attempts = @$section.find "input[name='reset-entrance-exam-attempts']"
|
@$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_delete_entrance_exam_state = @$section.find "input[name='delete-entrance-exam-state']"
|
||||||
@$btn_rescore_entrance_exam = @$section.find "input[name='rescore-entrance-exam']"
|
@$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']"
|
@$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"
|
@$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})
|
full_error_message = interpolate_text(error_message, {student_id: unique_student_identifier})
|
||||||
@$request_response_error_ee.text full_error_message
|
@$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
|
# delete student state for entrance exam
|
||||||
@$btn_delete_entrance_exam_state.click =>
|
@$btn_delete_entrance_exam_state.click =>
|
||||||
unique_student_identifier = @$field_entrance_exam_student_select_grade.val()
|
unique_student_identifier = @$field_entrance_exam_student_select_grade.val()
|
||||||
@@ -249,7 +272,7 @@ class @StudentAdmin
|
|||||||
@$btn_entrance_exam_task_history.click =>
|
@$btn_entrance_exam_task_history.click =>
|
||||||
unique_student_identifier = @$field_entrance_exam_student_select_grade.val()
|
unique_student_identifier = @$field_entrance_exam_student_select_grade.val()
|
||||||
if not unique_student_identifier
|
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 =
|
send_data =
|
||||||
unique_student_identifier: unique_student_identifier
|
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="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="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>
|
<p>
|
||||||
<label> You can also delete all of the student's answers and scores for the entire entrance exam.
|
<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();
|
studentadmin.$btn_reset_entrance_exam_attempts.click();
|
||||||
// expect error to be shown since student identifier is not set
|
// 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("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);
|
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
|
// Spy on AJAX requests
|
||||||
var requests = AjaxHelpers.requests(this);
|
var requests = AjaxHelpers.requests(this);
|
||||||
studentadmin.$field_entrance_exam_student_select_grade.val(unique_student_identifier)
|
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);
|
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();
|
studentadmin.$btn_delete_entrance_exam_state.click();
|
||||||
// expect error to be shown since student identifier is not set
|
// 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("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);
|
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
|
// Spy on AJAX requests
|
||||||
var requests = AjaxHelpers.requests(this);
|
var requests = AjaxHelpers.requests(this);
|
||||||
studentadmin.$field_entrance_exam_student_select_grade.val(unique_student_identifier)
|
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);
|
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();
|
studentadmin.$btn_entrance_exam_task_history.click();
|
||||||
// expect error to be shown since student identifier is not set
|
// 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 success_message = gettext("Entrance exam state is being deleted for student '{student_id}'.");
|
||||||
var full_success_message = interpolate_text(success_message, {
|
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();
|
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
|
// Spy on AJAX requests
|
||||||
var requests = AjaxHelpers.requests(this);
|
var requests = AjaxHelpers.requests(this);
|
||||||
studentadmin.$field_entrance_exam_student_select_grade.val(unique_student_identifier)
|
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']:
|
%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'] }">
|
<input type="button" name="rescore-entrance-exam" value="${_('Rescore All Problems')}" data-endpoint="${ section_data['rescore_entrance_exam_url'] }">
|
||||||
%endif
|
%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>
|
<p>
|
||||||
%if section_data['access']['instructor']:
|
%if section_data['access']['instructor']:
|
||||||
|
|||||||
Reference in New Issue
Block a user