Merge pull request #7350 from edx/learner-profiles
Feature branch for learner profiles
@@ -36,7 +36,12 @@ import lms.envs.common
|
||||
# Although this module itself may not use these imported variables, other dependent modules may.
|
||||
from lms.envs.common import (
|
||||
USE_TZ, TECH_SUPPORT_EMAIL, PLATFORM_NAME, BUGS_EMAIL, DOC_STORE_CONFIG, DATA_DIR, ALL_LANGUAGES, WIKI_ENABLED,
|
||||
update_module_store_settings, ASSET_IGNORE_REGEX, COPYRIGHT_YEAR
|
||||
update_module_store_settings, ASSET_IGNORE_REGEX, COPYRIGHT_YEAR, PARENTAL_CONSENT_AGE_LIMIT,
|
||||
# The following PROFILE_IMAGE_* settings are included as they are
|
||||
# indirectly accessed through the email opt-in API, which is
|
||||
# technically accessible through the CMS via legacy URLs.
|
||||
PROFILE_IMAGE_BACKEND, PROFILE_IMAGE_DEFAULT_FILENAME, PROFILE_IMAGE_DEFAULT_FILE_EXTENSION,
|
||||
PROFILE_IMAGE_SECRET_KEY, PROFILE_IMAGE_MIN_BYTES, PROFILE_IMAGE_MAX_BYTES,
|
||||
)
|
||||
from path import path
|
||||
from warnings import simplefilter
|
||||
|
||||
@@ -249,6 +249,9 @@ FEATURES['USE_MICROSITES'] = True
|
||||
# the one in lms/envs/test.py
|
||||
FEATURES['ENABLE_DISCUSSION_SERVICE'] = False
|
||||
|
||||
# Enable a parental consent age limit for testing
|
||||
PARENTAL_CONSENT_AGE_LIMIT = 13
|
||||
|
||||
# Enable content libraries code for the tests
|
||||
FEATURES['ENABLE_CONTENT_LIBRARIES'] = True
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ define([
|
||||
|
||||
patchAndVerifyRequest(requests, url, notificationSpy);
|
||||
|
||||
AjaxHelpers.respondToDelete(requests);
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
ViewHelpers.verifyNotificationHidden(notificationSpy);
|
||||
expect($(SELECTORS.itemView)).not.toExist();
|
||||
};
|
||||
|
||||
@@ -281,7 +281,7 @@ define(["jquery", "sinon", "js/common_helpers/ajax_helpers", "js/views/utils/vie
|
||||
expect($('.wrapper-alert-announcement')).not.toHaveClass('is-hidden');
|
||||
$('.dismiss-button').click();
|
||||
AjaxHelpers.expectJsonRequest(requests, 'DELETE', 'dummy_dismiss_url');
|
||||
AjaxHelpers.respondToDelete(requests);
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
expect($('.wrapper-alert-announcement')).toHaveClass('is-hidden');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ define(["jquery", "js/common_helpers/ajax_helpers", "js/spec_helpers/view_helper
|
||||
var reloadSpy = spyOn(ViewUtils, 'reload');
|
||||
$('.dismiss-button').click();
|
||||
AjaxHelpers.expectJsonRequest(requests, 'DELETE', 'dummy_dismiss_url');
|
||||
AjaxHelpers.respondToDelete(requests);
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
expect(reloadSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,10 @@ from opaque_keys.edx.keys import CourseKey
|
||||
from embargo import api as embargo_api
|
||||
from cors_csrf.authentication import SessionAuthenticationCrossDomainCsrf
|
||||
from cors_csrf.decorators import ensure_csrf_cookie_cross_domain
|
||||
from util.authentication import SessionAuthenticationAllowInactiveUser, OAuth2AuthenticationAllowInactiveUser
|
||||
from openedx.core.lib.api.authentication import (
|
||||
SessionAuthenticationAllowInactiveUser,
|
||||
OAuth2AuthenticationAllowInactiveUser,
|
||||
)
|
||||
from util.disable_rate_limit import can_disable_rate_limit
|
||||
from enrollment import api
|
||||
from enrollment.errors import (
|
||||
|
||||
@@ -121,9 +121,11 @@ class Migration(SchemaMigration):
|
||||
'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'}),
|
||||
'company_identifier': ('django.db.models.fields.TextField', [], {}),
|
||||
'dashboard_tracking_code': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'trk_partner_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '10', 'blank': 'True'})
|
||||
},
|
||||
'student.loginfailures': {
|
||||
'Meta': {'object_name': 'LoginFailures'},
|
||||
|
||||
194
common/djangoapps/student/migrations/0047_add_bio_field.py
Normal file
@@ -0,0 +1,194 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from south.utils import datetime_utils as datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding field 'UserProfile.bio'
|
||||
db.add_column('auth_userprofile', 'bio',
|
||||
self.gf('django.db.models.fields.CharField')(max_length=3000, null=True, blank=True, db_index=False),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'UserProfile.bio'
|
||||
db.delete_column('auth_userprofile', 'bio')
|
||||
|
||||
|
||||
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'}),
|
||||
'company_identifier': ('django.db.models.fields.TextField', [], {}),
|
||||
'dashboard_tracking_code': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'trk_partner_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '10', 'blank': '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'}),
|
||||
'bio': ('django.db.models.fields.CharField', [], {'db_index': 'False', 'null': 'True', 'blank': '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']
|
||||
@@ -0,0 +1,195 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from south.utils import datetime_utils as datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding field 'UserProfile.profile_image_uploaded_at'
|
||||
db.add_column('auth_userprofile', 'profile_image_uploaded_at',
|
||||
self.gf('django.db.models.fields.DateTimeField')(null=True),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'UserProfile.profile_image_uploaded_at'
|
||||
db.delete_column('auth_userprofile', 'profile_image_uploaded_at')
|
||||
|
||||
|
||||
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'}),
|
||||
'company_identifier': ('django.db.models.fields.TextField', [], {}),
|
||||
'dashboard_tracking_code': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'trk_partner_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '10', 'blank': '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'}),
|
||||
'bio': ('django.db.models.fields.CharField', [], {'db_index': 'False', 'null': 'True', 'blank': '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'}),
|
||||
'has_profile_image': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'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']
|
||||
@@ -0,0 +1,210 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from south.utils import datetime_utils as 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 'LanguageProficiency'
|
||||
db.create_table('student_languageproficiency', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('user_profile', self.gf('django.db.models.fields.related.ForeignKey')(related_name='language_proficiencies', to=orm['student.UserProfile'])),
|
||||
('code', self.gf('django.db.models.fields.CharField')(max_length=16)),
|
||||
))
|
||||
db.send_create_signal('student', ['LanguageProficiency'])
|
||||
|
||||
# Adding unique constraint on 'LanguageProficiency', fields ['code', 'user_profile']
|
||||
db.create_unique('student_languageproficiency', ['code', 'user_profile_id'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing unique constraint on 'LanguageProficiency', fields ['code', 'user_profile']
|
||||
db.delete_unique('student_languageproficiency', ['code', 'user_profile_id'])
|
||||
|
||||
# Deleting model 'LanguageProficiency'
|
||||
db.delete_table('student_languageproficiency')
|
||||
|
||||
|
||||
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.languageproficiency': {
|
||||
'Meta': {'unique_together': "(('code', 'user_profile'),)", 'object_name': 'LanguageProficiency'},
|
||||
'code': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user_profile': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'language_proficiencies'", 'to': "orm['student.UserProfile']"})
|
||||
},
|
||||
'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'}),
|
||||
'company_identifier': ('django.db.models.fields.TextField', [], {}),
|
||||
'dashboard_tracking_code': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'trk_partner_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '10', 'blank': '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'}),
|
||||
'bio': ('django.db.models.fields.CharField', [], {'db_index': 'False', 'null': 'True', 'blank': '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'}),
|
||||
'profile_image_uploaded_at': ('django.db.models.fields.DateTimeField', [], {'null': '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']
|
||||
@@ -28,6 +28,7 @@ from django.contrib.auth.hashers import make_password
|
||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||
from django.db import models, IntegrityError
|
||||
from django.db.models import Count
|
||||
from django.db.models.signals import pre_save, post_save
|
||||
from django.dispatch import receiver, Signal
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.translation import ugettext_noop
|
||||
@@ -40,6 +41,7 @@ from importlib import import_module
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
import lms.lib.comment_client as cc
|
||||
from util.model_utils import emit_field_changed_events, get_changed_fields_dict
|
||||
from util.query import use_read_replica_if_available
|
||||
from xmodule_django.models import CourseKeyField, NoneToEmptyManager
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
@@ -249,6 +251,16 @@ class UserProfile(models.Model):
|
||||
country = CountryField(blank=True, null=True)
|
||||
goals = models.TextField(blank=True, null=True)
|
||||
allow_certificate = models.BooleanField(default=1)
|
||||
bio = models.CharField(blank=True, null=True, max_length=3000, db_index=False)
|
||||
profile_image_uploaded_at = models.DateTimeField(null=True)
|
||||
|
||||
@property
|
||||
def has_profile_image(self):
|
||||
"""
|
||||
Convenience method that returns a boolean indicating whether or not
|
||||
this user has uploaded a profile image.
|
||||
"""
|
||||
return self.profile_image_uploaded_at is not None
|
||||
|
||||
def get_meta(self): # pylint: disable=missing-docstring
|
||||
js_str = self.meta
|
||||
@@ -276,6 +288,96 @@ class UserProfile(models.Model):
|
||||
self.set_meta(meta)
|
||||
self.save()
|
||||
|
||||
def requires_parental_consent(self, date=None, age_limit=None, default_requires_consent=True):
|
||||
"""Returns true if this user requires parental consent.
|
||||
|
||||
Args:
|
||||
date (Date): The date for which consent needs to be tested (defaults to now).
|
||||
age_limit (int): The age limit at which parental consent is no longer required.
|
||||
This defaults to the value of the setting 'PARENTAL_CONTROL_AGE_LIMIT'.
|
||||
default_requires_consent (bool): True if users require parental consent if they
|
||||
have no specified year of birth (default is True).
|
||||
|
||||
Returns:
|
||||
True if the user requires parental consent.
|
||||
"""
|
||||
if age_limit is None:
|
||||
age_limit = getattr(settings, 'PARENTAL_CONSENT_AGE_LIMIT', None)
|
||||
if age_limit is None:
|
||||
return False
|
||||
|
||||
# Return True if either:
|
||||
# a) The user has a year of birth specified and that year is fewer years in the past than the limit.
|
||||
# b) The user has no year of birth specified and the default is to require consent.
|
||||
#
|
||||
# Note: we have to be conservative using the user's year of birth as their birth date could be
|
||||
# December 31st. This means that if the number of years since their birth year is exactly equal
|
||||
# to the age limit then we have to assume that they might still not be old enough.
|
||||
year_of_birth = self.year_of_birth
|
||||
if year_of_birth is None:
|
||||
return default_requires_consent
|
||||
if date is None:
|
||||
date = datetime.now(UTC)
|
||||
return date.year - year_of_birth <= age_limit # pylint: disable=maybe-no-member
|
||||
|
||||
|
||||
@receiver(pre_save, sender=UserProfile)
|
||||
def user_profile_pre_save_callback(sender, **kwargs):
|
||||
"""
|
||||
Ensure consistency of a user profile before saving it.
|
||||
"""
|
||||
user_profile = kwargs['instance']
|
||||
|
||||
# Remove profile images for users who require parental consent
|
||||
if user_profile.requires_parental_consent() and user_profile.has_profile_image:
|
||||
user_profile.profile_image_uploaded_at = None
|
||||
|
||||
# Cache "old" field values on the model instance so that they can be
|
||||
# retrieved in the post_save callback when we emit an event with new and
|
||||
# old field values.
|
||||
user_profile._changed_fields = get_changed_fields_dict(user_profile, sender)
|
||||
|
||||
|
||||
@receiver(post_save, sender=UserProfile)
|
||||
def user_profile_post_save_callback(sender, **kwargs):
|
||||
"""
|
||||
Emit analytics events after saving the UserProfile.
|
||||
"""
|
||||
user_profile = kwargs['instance']
|
||||
# pylint: disable=protected-access
|
||||
emit_field_changed_events(
|
||||
user_profile,
|
||||
user_profile.user,
|
||||
sender._meta.db_table,
|
||||
excluded_fields=['meta']
|
||||
)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=User)
|
||||
def user_pre_save_callback(sender, **kwargs):
|
||||
"""
|
||||
Capture old fields on the user instance before save and cache them as a
|
||||
private field on the current model for use in the post_save callback.
|
||||
"""
|
||||
user = kwargs['instance']
|
||||
user._changed_fields = get_changed_fields_dict(user, sender)
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def user_post_save_callback(sender, **kwargs):
|
||||
"""
|
||||
Emit analytics events after saving the User.
|
||||
"""
|
||||
user = kwargs['instance']
|
||||
# pylint: disable=protected-access
|
||||
emit_field_changed_events(
|
||||
user,
|
||||
user,
|
||||
sender._meta.db_table,
|
||||
excluded_fields=['last_login'],
|
||||
hidden_fields=['password']
|
||||
)
|
||||
|
||||
|
||||
class UserSignupSource(models.Model):
|
||||
"""
|
||||
@@ -1561,3 +1663,25 @@ class EntranceExamConfiguration(models.Model):
|
||||
except EntranceExamConfiguration.DoesNotExist:
|
||||
can_skip = False
|
||||
return can_skip
|
||||
|
||||
|
||||
class LanguageProficiency(models.Model):
|
||||
"""
|
||||
Represents a user's language proficiency.
|
||||
|
||||
Note that we have not found a way to emit analytics change events by using signals directly on this
|
||||
model or on UserProfile. Therefore if you are changing LanguageProficiency values, it is important
|
||||
to go through the accounts API (AccountsView) defined in
|
||||
/edx-platform/openedx/core/djangoapps/user_api/accounts/views.py or its associated api method
|
||||
(update_account_settings) so that the events are emitted.
|
||||
"""
|
||||
class Meta:
|
||||
unique_together = (('code', 'user_profile'),)
|
||||
|
||||
user_profile = models.ForeignKey(UserProfile, db_index=True, related_name='language_proficiencies')
|
||||
code = models.CharField(
|
||||
max_length=16,
|
||||
blank=False,
|
||||
choices=settings.ALL_LANGUAGES,
|
||||
help_text=ugettext_lazy("The ISO 639-1 language code for this language.")
|
||||
)
|
||||
|
||||
@@ -43,7 +43,9 @@ class AutoAuthEnabledTestCase(UrlResetMixin, TestCase):
|
||||
"""
|
||||
self._auto_auth()
|
||||
self.assertEqual(User.objects.count(), 1)
|
||||
self.assertTrue(User.objects.all()[0].is_active)
|
||||
user = User.objects.all()[0]
|
||||
self.assertTrue(user.is_active)
|
||||
self.assertFalse(user.profile.requires_parental_consent())
|
||||
|
||||
def test_create_same_user(self):
|
||||
self._auto_auth(username='test')
|
||||
|
||||
@@ -5,7 +5,8 @@ import unittest
|
||||
|
||||
from student.tests.factories import UserFactory, RegistrationFactory, PendingEmailChangeFactory
|
||||
from student.views import (
|
||||
reactivation_email_for_user, change_email_request, do_email_change_request, confirm_email_change
|
||||
reactivation_email_for_user, change_email_request, do_email_change_request, confirm_email_change,
|
||||
SETTING_CHANGE_INITIATED
|
||||
)
|
||||
from student.models import UserProfile, PendingEmailChange
|
||||
from django.core.urlresolvers import reverse
|
||||
@@ -19,6 +20,7 @@ from django.conf import settings
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from edxmako.tests import mako_middleware_process_request
|
||||
from util.request import safe_get_host
|
||||
from util.testing import EventTestMixin
|
||||
|
||||
|
||||
class TestException(Exception):
|
||||
@@ -198,10 +200,11 @@ class ReactivationEmailTests(EmailTestMixin, TestCase):
|
||||
self.assertTrue(response_data['success'])
|
||||
|
||||
|
||||
class EmailChangeRequestTests(TestCase):
|
||||
class EmailChangeRequestTests(EventTestMixin, TestCase):
|
||||
"""Test changing a user's email address"""
|
||||
|
||||
def setUp(self):
|
||||
super(EmailChangeRequestTests, self).setUp('student.views.tracker')
|
||||
self.user = UserFactory.create()
|
||||
self.new_email = 'new.email@edx.org'
|
||||
self.req_factory = RequestFactory()
|
||||
@@ -275,6 +278,7 @@ class EmailChangeRequestTests(TestCase):
|
||||
send_mail.side_effect = [Exception, None]
|
||||
self.request.POST['new_email'] = "valid@email.com"
|
||||
self.assertFailedRequest(self.run_request(), 'Unable to send email activation link. Please try again later.')
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
@patch('django.core.mail.send_mail')
|
||||
@patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True))
|
||||
@@ -295,6 +299,9 @@ class EmailChangeRequestTests(TestCase):
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
[new_email]
|
||||
)
|
||||
self.assert_event_emitted(
|
||||
SETTING_CHANGE_INITIATED, user_id=self.user.id, setting=u'email', old=old_email, new=new_email
|
||||
)
|
||||
|
||||
|
||||
@patch('django.contrib.auth.models.User.email_user')
|
||||
|
||||
@@ -194,7 +194,7 @@ class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase):
|
||||
"""Change the student's enrollment status in a course.
|
||||
|
||||
Args:
|
||||
action (string): The action to perform (either "enroll" or "unenroll")
|
||||
action (str): The action to perform (either "enroll" or "unenroll")
|
||||
|
||||
Keyword Args:
|
||||
course_id (unicode): If provided, use this course ID. Otherwise, use the
|
||||
|
||||
148
common/djangoapps/student/tests/test_events.py
Normal file
@@ -0,0 +1,148 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Test that various events are fired for models in the student app.
|
||||
"""
|
||||
from django.test import TestCase
|
||||
|
||||
from django_countries.fields import Country
|
||||
|
||||
from student.models import PasswordHistory
|
||||
from student.tests.factories import UserFactory
|
||||
from student.tests.tests import UserSettingsEventTestMixin
|
||||
import mock
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
|
||||
class TestUserProfileEvents(UserSettingsEventTestMixin, TestCase):
|
||||
"""
|
||||
Test that we emit field change events when UserProfile models are changed.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestUserProfileEvents, self).setUp()
|
||||
self.table = 'auth_userprofile'
|
||||
self.user = UserFactory.create()
|
||||
self.profile = self.user.profile
|
||||
self.reset_tracker()
|
||||
|
||||
def test_change_one_field(self):
|
||||
"""
|
||||
Verify that we emit an event when a single field changes on the user
|
||||
profile.
|
||||
"""
|
||||
self.profile.year_of_birth = 1900
|
||||
self.profile.save()
|
||||
self.assert_user_setting_event_emitted(setting='year_of_birth', old=None, new=self.profile.year_of_birth)
|
||||
|
||||
# Verify that we remove the temporary `_changed_fields` property from
|
||||
# the model after we're done emitting events.
|
||||
with self.assertRaises(AttributeError):
|
||||
getattr(self.profile, '_changed_fields')
|
||||
|
||||
def test_change_many_fields(self):
|
||||
"""
|
||||
Verify that we emit one event per field when many fields change on the
|
||||
user profile in one transaction.
|
||||
"""
|
||||
self.profile.gender = u'o'
|
||||
self.profile.bio = 'test bio'
|
||||
self.profile.save()
|
||||
self.assert_user_setting_event_emitted(setting='bio', old=None, new=self.profile.bio)
|
||||
self.assert_user_setting_event_emitted(setting='gender', old=u'm', new=u'o')
|
||||
|
||||
def test_unicode(self):
|
||||
"""
|
||||
Verify that the events we emit can handle unicode characters.
|
||||
"""
|
||||
old_name = self.profile.name
|
||||
self.profile.name = u'Dånîél'
|
||||
self.profile.save()
|
||||
self.assert_user_setting_event_emitted(setting='name', old=old_name, new=self.profile.name)
|
||||
|
||||
def test_country(self):
|
||||
"""
|
||||
Verify that we properly serialize the JSON-unfriendly Country field.
|
||||
"""
|
||||
self.profile.country = Country(u'AL', 'dummy_flag_url')
|
||||
self.profile.save()
|
||||
self.assert_user_setting_event_emitted(setting='country', old=None, new=self.profile.country)
|
||||
|
||||
def test_excluded_field(self):
|
||||
"""
|
||||
Verify that we don't emit events for ignored fields.
|
||||
"""
|
||||
self.profile.meta = {u'foo': u'bar'}
|
||||
self.profile.save()
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
@mock.patch('student.models.UserProfile.save', side_effect=IntegrityError)
|
||||
def test_no_event_if_save_failed(self, _save_mock):
|
||||
"""
|
||||
Verify no event is triggered if the save does not complete. Note that the pre_save
|
||||
signal is not called in this case either, but the intent is to make it clear that this model
|
||||
should never emit an event if save fails.
|
||||
"""
|
||||
self.profile.gender = "unknown"
|
||||
with self.assertRaises(IntegrityError):
|
||||
self.profile.save()
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
|
||||
class TestUserEvents(UserSettingsEventTestMixin, TestCase):
|
||||
"""
|
||||
Test that we emit field change events when User models are changed.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestUserEvents, self).setUp()
|
||||
self.user = UserFactory.create()
|
||||
self.reset_tracker()
|
||||
self.table = 'auth_user'
|
||||
|
||||
def test_change_one_field(self):
|
||||
"""
|
||||
Verify that we emit an event when a single field changes on the user.
|
||||
"""
|
||||
old_username = self.user.username
|
||||
self.user.username = u'new username'
|
||||
self.user.save()
|
||||
self.assert_user_setting_event_emitted(setting='username', old=old_username, new=self.user.username)
|
||||
|
||||
def test_change_many_fields(self):
|
||||
"""
|
||||
Verify that we emit one event per field when many fields change on the
|
||||
user in one transaction.
|
||||
"""
|
||||
old_email = self.user.email
|
||||
old_is_staff = self.user.is_staff
|
||||
self.user.email = u'foo@bar.com'
|
||||
self.user.is_staff = True
|
||||
self.user.save()
|
||||
self.assert_user_setting_event_emitted(setting='email', old=old_email, new=self.user.email)
|
||||
self.assert_user_setting_event_emitted(setting='is_staff', old=old_is_staff, new=self.user.is_staff)
|
||||
|
||||
def test_password(self):
|
||||
"""
|
||||
Verify that password values are not included in the event payload.
|
||||
"""
|
||||
self.user.password = u'new password'
|
||||
self.user.save()
|
||||
self.assert_user_setting_event_emitted(setting='password', old=None, new=None)
|
||||
|
||||
def test_related_fields_ignored(self):
|
||||
"""
|
||||
Verify that we don't emit events for related fields.
|
||||
"""
|
||||
self.user.passwordhistory_set.add(PasswordHistory(password='new_password'))
|
||||
self.user.save()
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
@mock.patch('django.contrib.auth.models.User.save', side_effect=IntegrityError)
|
||||
def test_no_event_if_save_failed(self, _save_mock):
|
||||
"""
|
||||
Verify no event is triggered if the save does not complete. Note that the pre_save
|
||||
signal is not called in this case either, but the intent is to make it clear that this model
|
||||
should never emit an event if save fails.
|
||||
"""
|
||||
self.user.password = u'new password'
|
||||
with self.assertRaises(IntegrityError):
|
||||
self.user.save()
|
||||
self.assert_no_events_were_emitted()
|
||||
86
common/djangoapps/student/tests/test_parental_controls.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""Unit tests for parental controls."""
|
||||
|
||||
import datetime
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from student.models import UserProfile
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
|
||||
class ProfileParentalControlsTest(TestCase):
|
||||
"""Unit tests for requires_parental_consent."""
|
||||
|
||||
password = "test"
|
||||
|
||||
def setUp(self):
|
||||
super(ProfileParentalControlsTest, self).setUp()
|
||||
self.user = UserFactory.create(password=self.password)
|
||||
self.profile = UserProfile.objects.get(id=self.user.id)
|
||||
|
||||
def set_year_of_birth(self, year_of_birth):
|
||||
"""
|
||||
Helper method that creates a mock profile for the specified user.
|
||||
"""
|
||||
self.profile.year_of_birth = year_of_birth
|
||||
self.profile.save()
|
||||
|
||||
def test_no_year_of_birth(self):
|
||||
"""Verify the behavior for users with no specified year of birth."""
|
||||
self.assertTrue(self.profile.requires_parental_consent())
|
||||
self.assertTrue(self.profile.requires_parental_consent(default_requires_consent=True))
|
||||
self.assertFalse(self.profile.requires_parental_consent(default_requires_consent=False))
|
||||
|
||||
@override_settings(PARENTAL_CONSENT_AGE_LIMIT=None)
|
||||
def test_no_parental_controls(self):
|
||||
"""Verify the behavior for all users when parental controls are not enabled."""
|
||||
self.assertFalse(self.profile.requires_parental_consent())
|
||||
self.assertFalse(self.profile.requires_parental_consent(default_requires_consent=True))
|
||||
self.assertFalse(self.profile.requires_parental_consent(default_requires_consent=False))
|
||||
|
||||
# Verify that even a child does not require parental consent
|
||||
current_year = datetime.datetime.now().year
|
||||
self.set_year_of_birth(current_year - 10)
|
||||
self.assertFalse(self.profile.requires_parental_consent())
|
||||
|
||||
def test_adult_user(self):
|
||||
"""Verify the behavior for an adult."""
|
||||
current_year = datetime.datetime.now().year
|
||||
self.set_year_of_birth(current_year - 20)
|
||||
self.assertFalse(self.profile.requires_parental_consent())
|
||||
self.assertTrue(self.profile.requires_parental_consent(age_limit=21))
|
||||
|
||||
def test_child_user(self):
|
||||
"""Verify the behavior for a child."""
|
||||
current_year = datetime.datetime.now().year
|
||||
|
||||
# Verify for a child born 13 years agp
|
||||
self.set_year_of_birth(current_year - 13)
|
||||
self.assertTrue(self.profile.requires_parental_consent())
|
||||
self.assertTrue(self.profile.requires_parental_consent(date=datetime.date(current_year, 12, 31)))
|
||||
self.assertFalse(self.profile.requires_parental_consent(date=datetime.date(current_year + 1, 1, 1)))
|
||||
|
||||
# Verify for a child born 14 years ago
|
||||
self.set_year_of_birth(current_year - 14)
|
||||
self.assertFalse(self.profile.requires_parental_consent())
|
||||
self.assertFalse(self.profile.requires_parental_consent(date=datetime.date(current_year, 1, 1)))
|
||||
|
||||
def test_profile_image(self):
|
||||
"""Verify that a profile's image obeys parental controls."""
|
||||
|
||||
# Verify that an image cannot be set for a user with no year of birth set
|
||||
self.profile.profile_image_uploaded_at = datetime.datetime.now()
|
||||
self.profile.save()
|
||||
self.assertFalse(self.profile.has_profile_image)
|
||||
|
||||
# Verify that an image can be set for an adult user
|
||||
current_year = datetime.datetime.now().year
|
||||
self.set_year_of_birth(current_year - 20)
|
||||
self.profile.profile_image_uploaded_at = datetime.datetime.now()
|
||||
self.profile.save()
|
||||
self.assertTrue(self.profile.has_profile_image)
|
||||
|
||||
# verify that a user's profile image is removed when they switch to requiring parental controls
|
||||
self.set_year_of_birth(current_year - 10)
|
||||
self.profile.save()
|
||||
self.assertFalse(self.profile.has_profile_image)
|
||||
@@ -17,20 +17,22 @@ from django.utils.http import int_to_base36
|
||||
from mock import Mock, patch
|
||||
import ddt
|
||||
|
||||
from student.views import password_reset, password_reset_confirm_wrapper
|
||||
from student.views import password_reset, password_reset_confirm_wrapper, SETTING_CHANGE_INITIATED
|
||||
from student.tests.factories import UserFactory
|
||||
from student.tests.test_email import mock_render_to_string
|
||||
from util.testing import EventTestMixin
|
||||
|
||||
from test_microsite import fake_site_name
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class ResetPasswordTests(TestCase):
|
||||
class ResetPasswordTests(EventTestMixin, TestCase):
|
||||
""" Tests that clicking reset password sends email, and doesn't activate the user
|
||||
"""
|
||||
request_factory = RequestFactory()
|
||||
|
||||
def setUp(self):
|
||||
super(ResetPasswordTests, self).setUp('student.views.tracker')
|
||||
self.user = UserFactory.create()
|
||||
self.user.is_active = False
|
||||
self.user.save()
|
||||
@@ -55,6 +57,7 @@ class ResetPasswordTests(TestCase):
|
||||
'success': True,
|
||||
'value': "('registration/password_reset_done.html', [])",
|
||||
})
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
@patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True))
|
||||
def test_nonexist_email_password_reset(self):
|
||||
@@ -71,6 +74,7 @@ class ResetPasswordTests(TestCase):
|
||||
'success': True,
|
||||
'value': "('registration/password_reset_done.html', [])",
|
||||
})
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
@patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True))
|
||||
def test_password_reset_ratelimited(self):
|
||||
@@ -88,6 +92,7 @@ class ResetPasswordTests(TestCase):
|
||||
bad_req = self.request_factory.post('/password_reset/', {'email': 'thisdoesnotexist@foo.com'})
|
||||
bad_resp = password_reset(bad_req)
|
||||
self.assertEquals(bad_resp.status_code, 403)
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
cache.clear()
|
||||
|
||||
@@ -98,6 +103,7 @@ class ResetPasswordTests(TestCase):
|
||||
"""Tests contents of reset password email, and that user is not active"""
|
||||
|
||||
good_req = self.request_factory.post('/password_reset/', {'email': self.user.email})
|
||||
good_req.user = self.user
|
||||
good_resp = password_reset(good_req)
|
||||
self.assertEquals(good_resp.status_code, 200)
|
||||
obj = json.loads(good_resp.content)
|
||||
@@ -113,6 +119,10 @@ class ResetPasswordTests(TestCase):
|
||||
self.assertEquals(len(to_addrs), 1)
|
||||
self.assertIn(self.user.email, to_addrs)
|
||||
|
||||
self.assert_event_emitted(
|
||||
SETTING_CHANGE_INITIATED, user_id=self.user.id, setting=u'password', old=None, new=None,
|
||||
)
|
||||
|
||||
#test that the user is not active
|
||||
self.user = User.objects.get(pk=self.user.pk)
|
||||
self.assertFalse(self.user.is_active)
|
||||
@@ -130,12 +140,17 @@ class ResetPasswordTests(TestCase):
|
||||
'/password_reset/', {'email': self.user.email}
|
||||
)
|
||||
req.is_secure = Mock(return_value=is_secure)
|
||||
resp = password_reset(req)
|
||||
req.user = self.user
|
||||
password_reset(req)
|
||||
_, msg, _, _ = send_email.call_args[0]
|
||||
expected_msg = "Please go to the following page and choose a new password:\n\n" + protocol
|
||||
|
||||
self.assertIn(expected_msg, msg)
|
||||
|
||||
self.assert_event_emitted(
|
||||
SETTING_CHANGE_INITIATED, user_id=self.user.id, setting=u'password', old=None, new=None
|
||||
)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS")
|
||||
@patch('django.core.mail.send_mail')
|
||||
@ddt.data(('Crazy Awesome Site', 'Crazy Awesome Site'), (None, 'edX'))
|
||||
@@ -150,7 +165,8 @@ class ResetPasswordTests(TestCase):
|
||||
'/password_reset/', {'email': self.user.email}
|
||||
)
|
||||
req.get_host = Mock(return_value=domain_override)
|
||||
resp = password_reset(req)
|
||||
req.user = self.user
|
||||
password_reset(req)
|
||||
_, msg, _, _ = send_email.call_args[0]
|
||||
|
||||
reset_msg = "you requested a password reset for your user account at {}"
|
||||
@@ -164,6 +180,10 @@ class ResetPasswordTests(TestCase):
|
||||
sign_off = "The {} Team".format(platform_name)
|
||||
self.assertIn(sign_off, msg)
|
||||
|
||||
self.assert_event_emitted(
|
||||
SETTING_CHANGE_INITIATED, user_id=self.user.id, setting=u'password', old=None, new=None
|
||||
)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS")
|
||||
@patch("microsite_configuration.microsite.get_value", fake_site_name)
|
||||
@patch('django.core.mail.send_mail')
|
||||
@@ -176,13 +196,18 @@ class ResetPasswordTests(TestCase):
|
||||
'/password_reset/', {'email': self.user.email}
|
||||
)
|
||||
req.get_host = Mock(return_value=None)
|
||||
resp = password_reset(req)
|
||||
req.user = self.user
|
||||
password_reset(req)
|
||||
_, msg, _, _ = send_email.call_args[0]
|
||||
|
||||
reset_msg = "you requested a password reset for your user account at openedx.localhost"
|
||||
|
||||
self.assertIn(reset_msg, msg)
|
||||
|
||||
self.assert_event_emitted(
|
||||
SETTING_CHANGE_INITIATED, user_id=self.user.id, setting=u'password', old=None, new=None
|
||||
)
|
||||
|
||||
@patch('student.views.password_reset_confirm')
|
||||
def test_reset_password_bad_token(self, reset_confirm):
|
||||
"""Tests bad token and uidb36 in password reset"""
|
||||
|
||||
@@ -21,12 +21,13 @@ from mock import Mock, patch
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
from student.models import (
|
||||
anonymous_id_for_user, user_by_anonymous_id, CourseEnrollment, unique_id_for_user,
|
||||
LinkedInAddToProfileConfiguration
|
||||
anonymous_id_for_user, user_by_anonymous_id, CourseEnrollment, unique_id_for_user, LinkedInAddToProfileConfiguration
|
||||
)
|
||||
from student.views import (process_survey_link, _cert_info,
|
||||
change_enrollment, complete_course_mode_info)
|
||||
from student.tests.factories import UserFactory, CourseModeFactory
|
||||
from util.testing import EventTestMixin
|
||||
from util.model_utils import USER_SETTINGS_CHANGED_EVENT_NAME
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
@@ -485,19 +486,33 @@ class DashboardTest(ModuleStoreTestCase):
|
||||
self.assertContains(response, expected_url)
|
||||
|
||||
|
||||
class EnrollmentEventTestMixin(object):
|
||||
""" Mixin with assertions for validating enrollment events. """
|
||||
|
||||
class UserSettingsEventTestMixin(EventTestMixin):
|
||||
"""
|
||||
Mixin for verifying that user setting events were emitted during a test.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(EnrollmentEventTestMixin, self).setUp()
|
||||
patcher = patch('student.models.tracker')
|
||||
self.mock_tracker = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
super(UserSettingsEventTestMixin, self).setUp('util.model_utils.tracker')
|
||||
|
||||
def assert_no_events_were_emitted(self):
|
||||
"""Ensures no events were emitted since the last event related assertion"""
|
||||
self.assertFalse(self.mock_tracker.emit.called) # pylint: disable=maybe-no-member
|
||||
self.mock_tracker.reset_mock()
|
||||
def assert_user_setting_event_emitted(self, **kwargs):
|
||||
"""
|
||||
Helper method to assert that we emit the expected user settings events.
|
||||
|
||||
Expected settings are passed in via `kwargs`.
|
||||
"""
|
||||
if 'truncated' not in kwargs:
|
||||
kwargs['truncated'] = []
|
||||
self.assert_event_emitted(
|
||||
USER_SETTINGS_CHANGED_EVENT_NAME,
|
||||
table=self.table, # pylint: disable=no-member
|
||||
user_id=self.user.id,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
class EnrollmentEventTestMixin(EventTestMixin):
|
||||
""" Mixin with assertions for validating enrollment events. """
|
||||
def setUp(self):
|
||||
super(EnrollmentEventTestMixin, self).setUp('student.models.tracker')
|
||||
|
||||
def assert_enrollment_mode_change_event_was_emitted(self, user, course_key, mode):
|
||||
"""Ensures an enrollment mode change event was emitted"""
|
||||
|
||||
@@ -129,6 +129,8 @@ AUDIT_LOG = logging.getLogger("audit")
|
||||
|
||||
ReverifyInfo = namedtuple('ReverifyInfo', 'course_id course_name course_number date status display') # pylint: disable=invalid-name
|
||||
|
||||
SETTING_CHANGE_INITIATED = 'edx.user.settings.change_initiated'
|
||||
|
||||
|
||||
def csrf_token(context):
|
||||
"""A csrf token that can be included in a form."""
|
||||
@@ -620,40 +622,11 @@ def dashboard(request):
|
||||
|
||||
enrolled_courses_either_paid = frozenset(course.id for course, _enrollment in course_enrollment_pairs
|
||||
if _enrollment.is_paid_course())
|
||||
# get info w.r.t ExternalAuthMap
|
||||
external_auth_map = None
|
||||
try:
|
||||
external_auth_map = ExternalAuthMap.objects.get(user=user)
|
||||
except ExternalAuthMap.DoesNotExist:
|
||||
pass
|
||||
|
||||
# If there are *any* denied reverifications that have not been toggled off,
|
||||
# we'll display the banner
|
||||
denied_banner = any(item.display for item in reverifications["denied"])
|
||||
|
||||
language_options = DarkLangConfig.current().released_languages_list
|
||||
|
||||
# add in the default language if it's not in the list of released languages
|
||||
if settings.LANGUAGE_CODE not in language_options:
|
||||
language_options.append(settings.LANGUAGE_CODE)
|
||||
# Re-alphabetize language options
|
||||
language_options.sort()
|
||||
|
||||
# try to get the preferred language for the user
|
||||
preferred_language_code = preferences_api.get_user_preference(request.user, LANGUAGE_KEY)
|
||||
# try and get the current language of the user
|
||||
current_language_code = get_language()
|
||||
if preferred_language_code and preferred_language_code in settings.LANGUAGE_DICT:
|
||||
# if the user has a preference, get the name from the code
|
||||
current_language = settings.LANGUAGE_DICT[preferred_language_code]
|
||||
elif current_language_code in settings.LANGUAGE_DICT:
|
||||
# if the user's browser is showing a particular language,
|
||||
# use that as the current language
|
||||
current_language = settings.LANGUAGE_DICT[current_language_code]
|
||||
else:
|
||||
# otherwise, use the default language
|
||||
current_language = settings.LANGUAGE_DICT[settings.LANGUAGE_CODE]
|
||||
|
||||
# Populate the Order History for the side-bar.
|
||||
order_history_list = order_history(user, course_org_filter=course_org_filter, org_filter_out_set=org_filter_out_set)
|
||||
|
||||
@@ -678,7 +651,6 @@ def dashboard(request):
|
||||
'course_enrollment_pairs': course_enrollment_pairs,
|
||||
'course_optouts': course_optouts,
|
||||
'message': message,
|
||||
'external_auth_map': external_auth_map,
|
||||
'staff_access': staff_access,
|
||||
'errored_courses': errored_courses,
|
||||
'show_courseware_links_for': show_courseware_links_for,
|
||||
@@ -693,11 +665,7 @@ def dashboard(request):
|
||||
'block_courses': block_courses,
|
||||
'denied_banner': denied_banner,
|
||||
'billing_email': settings.PAYMENT_SUPPORT_EMAIL,
|
||||
'language_options': language_options,
|
||||
'current_language': current_language,
|
||||
'current_language_code': current_language_code,
|
||||
'user': user,
|
||||
'duplicate_provider': None,
|
||||
'logout_url': reverse(logout_user),
|
||||
'platform_name': platform_name,
|
||||
'enrolled_courses_either_paid': enrolled_courses_either_paid,
|
||||
@@ -707,10 +675,6 @@ def dashboard(request):
|
||||
'ccx_membership_triplets': ccx_membership_triplets,
|
||||
}
|
||||
|
||||
if third_party_auth.is_enabled():
|
||||
context['duplicate_provider'] = pipeline.get_duplicate_provider(messages.get_messages(request))
|
||||
context['provider_user_states'] = pipeline.get_provider_user_states(user)
|
||||
|
||||
return render_to_response('dashboard.html', context)
|
||||
|
||||
|
||||
@@ -1758,13 +1722,14 @@ def auto_auth(request):
|
||||
# If successful, this will return a tuple containing
|
||||
# the new user object.
|
||||
try:
|
||||
user, _profile, reg = _do_create_account(form)
|
||||
user, profile, reg = _do_create_account(form)
|
||||
except AccountValidationError:
|
||||
# Attempt to retrieve the existing user.
|
||||
user = User.objects.get(username=username)
|
||||
user.email = email
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
profile = UserProfile.objects.get(user=user)
|
||||
reg = Registration.objects.get(user=user)
|
||||
|
||||
# Set the user's global staff bit
|
||||
@@ -1776,6 +1741,12 @@ def auto_auth(request):
|
||||
reg.activate()
|
||||
reg.save()
|
||||
|
||||
# ensure parental consent threshold is met
|
||||
year = datetime.date.today().year
|
||||
age_limit = settings.PARENTAL_CONSENT_AGE_LIMIT
|
||||
profile.year_of_birth = (year - age_limit) - 1
|
||||
profile.save()
|
||||
|
||||
# Enroll the user in a course
|
||||
if course_key is not None:
|
||||
CourseEnrollment.enroll(user, course_key)
|
||||
@@ -1864,6 +1835,18 @@ def password_reset(request):
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
request=request,
|
||||
domain_override=request.get_host())
|
||||
# When password change is complete, a "edx.user.settings.changed" event will be emitted.
|
||||
# But because changing the password is multi-step, we also emit an event here so that we can
|
||||
# track where the request was initiated.
|
||||
tracker.emit(
|
||||
SETTING_CHANGE_INITIATED,
|
||||
{
|
||||
"setting": "password",
|
||||
"old": None,
|
||||
"new": None,
|
||||
"user_id": request.user.id,
|
||||
}
|
||||
)
|
||||
else:
|
||||
# bad user? tick the rate limiter counter
|
||||
AUDIT_LOG.info("Bad password_reset user passed in.")
|
||||
@@ -2080,6 +2063,19 @@ def do_email_change_request(user, new_email, activation_key=uuid.uuid4().hex):
|
||||
log.error(u'Unable to send email activation link to user from "%s"', from_address, exc_info=True)
|
||||
raise ValueError(_('Unable to send email activation link. Please try again later.'))
|
||||
|
||||
# When the email address change is complete, a "edx.user.settings.changed" event will be emitted.
|
||||
# But because changing the email address is multi-step, we also emit an event here so that we can
|
||||
# track where the request was initiated.
|
||||
tracker.emit(
|
||||
SETTING_CHANGE_INITIATED,
|
||||
{
|
||||
"setting": "email",
|
||||
"old": context['old_email'],
|
||||
"new": context['new_email'],
|
||||
"user_id": user.id,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@transaction.commit_manually
|
||||
|
||||
@@ -114,9 +114,9 @@ AUTH_EMAIL_OPT_IN_KEY = 'email_opt_in'
|
||||
|
||||
|
||||
# The following are various possible values for the AUTH_ENTRY_KEY.
|
||||
AUTH_ENTRY_DASHBOARD = 'dashboard'
|
||||
AUTH_ENTRY_LOGIN = 'login'
|
||||
AUTH_ENTRY_REGISTER = 'register'
|
||||
AUTH_ENTRY_ACCOUNT_SETTINGS = 'account_settings'
|
||||
|
||||
# This is left-over from an A/B test
|
||||
# of the new combined login/registration page (ECOM-369)
|
||||
@@ -142,9 +142,9 @@ def is_api(auth_entry):
|
||||
# We don't use "reverse" here because doing so may cause modules
|
||||
# to load that depend on this module.
|
||||
AUTH_DISPATCH_URLS = {
|
||||
AUTH_ENTRY_DASHBOARD: '/dashboard',
|
||||
AUTH_ENTRY_LOGIN: '/login',
|
||||
AUTH_ENTRY_REGISTER: '/register',
|
||||
AUTH_ENTRY_ACCOUNT_SETTINGS: '/account/settings',
|
||||
|
||||
# This is left-over from an A/B test
|
||||
# of the new combined login/registration page (ECOM-369)
|
||||
@@ -156,9 +156,9 @@ AUTH_DISPATCH_URLS = {
|
||||
}
|
||||
|
||||
_AUTH_ENTRY_CHOICES = frozenset([
|
||||
AUTH_ENTRY_DASHBOARD,
|
||||
AUTH_ENTRY_LOGIN,
|
||||
AUTH_ENTRY_REGISTER,
|
||||
AUTH_ENTRY_ACCOUNT_SETTINGS,
|
||||
|
||||
# This is left-over from an A/B test
|
||||
# of the new combined login/registration page (ECOM-369)
|
||||
@@ -577,7 +577,7 @@ def login_analytics(strategy, auth_entry, *args, **kwargs):
|
||||
event_name = None
|
||||
if auth_entry in [AUTH_ENTRY_LOGIN, AUTH_ENTRY_LOGIN_2]:
|
||||
event_name = 'edx.bi.user.account.authenticated'
|
||||
elif auth_entry in [AUTH_ENTRY_DASHBOARD]:
|
||||
elif auth_entry in [AUTH_ENTRY_ACCOUNT_SETTINGS]:
|
||||
event_name = 'edx.bi.user.account.linked'
|
||||
|
||||
if event_name is not None:
|
||||
@@ -623,7 +623,7 @@ def change_enrollment(strategy, auth_entry=None, user=None, *args, **kwargs):
|
||||
user (User): The user being authenticated.
|
||||
"""
|
||||
# We skip enrollment if the user entered the flow from the "link account"
|
||||
# button on the student dashboard. At this point, either:
|
||||
# button on the account settings page. At this point, either:
|
||||
#
|
||||
# 1) The user already had a linked account when they started the enrollment flow,
|
||||
# in which case they would have been enrolled during the normal authentication process.
|
||||
@@ -633,7 +633,7 @@ def change_enrollment(strategy, auth_entry=None, user=None, *args, **kwargs):
|
||||
# args when sending users to this page, successfully authenticating through this page
|
||||
# would also enroll the student in the course.
|
||||
enroll_course_id = strategy.session_get('enroll_course_id')
|
||||
if enroll_course_id and auth_entry != AUTH_ENTRY_DASHBOARD:
|
||||
if enroll_course_id and auth_entry != AUTH_ENTRY_ACCOUNT_SETTINGS:
|
||||
course_id = CourseKey.from_string(enroll_course_id)
|
||||
modes = CourseMode.modes_for_course_dict(course_id)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from social.apps.django_app import utils as social_utils
|
||||
from social.apps.django_app import views as social_views
|
||||
from student import models as student_models
|
||||
from student import views as student_views
|
||||
from student_account.views import account_settings_context
|
||||
|
||||
from third_party_auth import middleware, pipeline
|
||||
from third_party_auth import settings as auth_settings
|
||||
@@ -110,41 +111,25 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
|
||||
self.client = test.Client()
|
||||
self.request_factory = test.RequestFactory()
|
||||
|
||||
def assert_dashboard_response_looks_correct(self, response, user, duplicate=False, linked=None):
|
||||
"""Asserts the user's dashboard is in the expected state.
|
||||
def assert_account_settings_context_looks_correct(self, context, user, duplicate=False, linked=None):
|
||||
"""Asserts the user's account settings page context is in the expected state.
|
||||
|
||||
We check unconditionally that the dashboard 200s and contains the
|
||||
user's info. If duplicate is True, we expect the duplicate account
|
||||
association error to be present. If linked is passed, we conditionally
|
||||
check the content and controls in the Account Links section of the
|
||||
sidebar.
|
||||
If duplicate is True, we expect context['duplicate_provider'] to contain
|
||||
the duplicate provider object. If linked is passed, we conditionally
|
||||
check that the provider is included in context['auth']['providers'] and
|
||||
its connected state is correct.
|
||||
"""
|
||||
duplicate_account_error_needle = '<section class="dashboard-banner third-party-auth">'
|
||||
assert_duplicate_presence_fn = self.assertIn if duplicate else self.assertNotIn
|
||||
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertIn(user.email, response.content.decode('UTF-8'))
|
||||
self.assertIn(user.username, response.content.decode('UTF-8'))
|
||||
assert_duplicate_presence_fn(duplicate_account_error_needle, response.content)
|
||||
if duplicate:
|
||||
self.assertEqual(context['duplicate_provider'].NAME, self.PROVIDER_CLASS.NAME)
|
||||
else:
|
||||
self.assertIsNone(context['duplicate_provider'])
|
||||
|
||||
if linked is not None:
|
||||
|
||||
if linked:
|
||||
expected_control_text = pipeline.ProviderUserState(
|
||||
self.PROVIDER_CLASS, user, False).get_unlink_form_name()
|
||||
else:
|
||||
expected_control_text = pipeline.get_login_url(self.PROVIDER_CLASS.NAME, pipeline.AUTH_ENTRY_DASHBOARD)
|
||||
|
||||
provider_name = re.search(r'<span class="provider">([^<]+)', response.content, re.DOTALL).groups()[0]
|
||||
|
||||
self.assertIn(expected_control_text, response.content)
|
||||
if linked:
|
||||
self.assertIn("fa fa-link", response.content)
|
||||
self.assertNotIn("fa fa-unlink", response.content)
|
||||
else:
|
||||
self.assertNotIn("fa fa-link", response.content)
|
||||
self.assertIn("fa fa-unlink", response.content)
|
||||
self.assertEqual(self.PROVIDER_CLASS.NAME, provider_name)
|
||||
expected_provider = [
|
||||
provider for provider in context['auth']['providers'] if provider['name'] == self.PROVIDER_CLASS.NAME
|
||||
][0]
|
||||
self.assertIsNotNone(expected_provider)
|
||||
self.assertEqual(expected_provider['connected'], linked)
|
||||
|
||||
def assert_exception_redirect_looks_correct(self, expected_uri, auth_entry=None):
|
||||
"""Tests middleware conditional redirection.
|
||||
@@ -406,6 +391,11 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
|
||||
def test_canceling_authentication_redirects_to_login_when_auth_register_2(self):
|
||||
self.assert_exception_redirect_looks_correct('/account/register/', auth_entry=pipeline.AUTH_ENTRY_REGISTER_2)
|
||||
|
||||
def test_canceling_authentication_redirects_to_account_settings_when_auth_entry_account_settings(self):
|
||||
self.assert_exception_redirect_looks_correct(
|
||||
'/account/settings', auth_entry=pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS
|
||||
)
|
||||
|
||||
def test_canceling_authentication_redirects_to_root_when_auth_entry_not_set(self):
|
||||
self.assert_exception_redirect_looks_correct('/')
|
||||
|
||||
@@ -432,7 +422,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
|
||||
|
||||
# First we expect that we're in the unlinked state, and that there
|
||||
# really is no association in the backend.
|
||||
self.assert_dashboard_response_looks_correct(student_views.dashboard(request), request.user, linked=False)
|
||||
self.assert_account_settings_context_looks_correct(account_settings_context(request), request.user, linked=False)
|
||||
self.assert_social_auth_does_not_exist_for_user(request.user, strategy)
|
||||
|
||||
# We should be redirected back to the complete page, setting
|
||||
@@ -452,7 +442,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
|
||||
|
||||
# Now we expect to be in the linked state, with a backend entry.
|
||||
self.assert_social_auth_exists_for_user(request.user, strategy)
|
||||
self.assert_dashboard_response_looks_correct(student_views.dashboard(request), request.user, linked=True)
|
||||
self.assert_account_settings_context_looks_correct(account_settings_context(request), request.user, linked=True)
|
||||
|
||||
def test_full_pipeline_succeeds_for_unlinking_account(self):
|
||||
# First, create, the request and strategy that store pipeline state,
|
||||
@@ -479,7 +469,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
|
||||
actions.do_complete(strategy, social_views._do_login, user=user) # pylint: disable-msg=protected-access
|
||||
|
||||
# First we expect that we're in the linked state, with a backend entry.
|
||||
self.assert_dashboard_response_looks_correct(student_views.dashboard(request), user, linked=True)
|
||||
self.assert_account_settings_context_looks_correct(account_settings_context(request), user, linked=True)
|
||||
self.assert_social_auth_exists_for_user(request.user, strategy)
|
||||
|
||||
# Fire off the disconnect pipeline to unlink.
|
||||
@@ -487,7 +477,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
|
||||
request.social_strategy, request.user, None, redirect_field_name=auth.REDIRECT_FIELD_NAME))
|
||||
|
||||
# Now we expect to be in the unlinked state, with no backend entry.
|
||||
self.assert_dashboard_response_looks_correct(student_views.dashboard(request), user, linked=False)
|
||||
self.assert_account_settings_context_looks_correct(account_settings_context(request), user, linked=False)
|
||||
self.assert_social_auth_does_not_exist_for_user(user, strategy)
|
||||
|
||||
def test_linking_already_associated_account_raises_auth_already_associated(self):
|
||||
@@ -541,8 +531,8 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
|
||||
request,
|
||||
exceptions.AuthAlreadyAssociated(self.PROVIDER_CLASS.BACKEND_CLASS.name, 'account is already in use.'))
|
||||
|
||||
self.assert_dashboard_response_looks_correct(
|
||||
student_views.dashboard(request), user, duplicate=True, linked=True)
|
||||
self.assert_account_settings_context_looks_correct(
|
||||
account_settings_context(request), user, duplicate=True, linked=True)
|
||||
|
||||
def test_full_pipeline_succeeds_for_signing_in_to_existing_active_account(self):
|
||||
# First, create, the request and strategy that store pipeline state,
|
||||
@@ -593,7 +583,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
|
||||
|
||||
self.assert_redirect_to_dashboard_looks_correct(
|
||||
actions.do_complete(strategy, social_views._do_login, user=user))
|
||||
self.assert_dashboard_response_looks_correct(student_views.dashboard(request), user)
|
||||
self.assert_account_settings_context_looks_correct(account_settings_context(request), user)
|
||||
|
||||
def test_signin_fails_if_account_not_active(self):
|
||||
_, strategy = self.get_request_and_strategy(
|
||||
@@ -710,7 +700,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
|
||||
self.assert_redirect_to_dashboard_looks_correct(
|
||||
actions.do_complete(strategy, social_views._do_login, user=created_user))
|
||||
self.assert_social_auth_exists_for_user(created_user, strategy)
|
||||
self.assert_dashboard_response_looks_correct(student_views.dashboard(request), created_user, linked=True)
|
||||
self.assert_account_settings_context_looks_correct(account_settings_context(request), created_user, linked=True)
|
||||
|
||||
def test_new_account_registration_assigns_distinct_username_on_collision(self):
|
||||
original_username = self.get_username()
|
||||
|
||||
@@ -145,9 +145,9 @@ class PipelineEnrollmentTest(UrlResetMixin, ModuleStoreTestCase):
|
||||
strategy = self._fake_strategy()
|
||||
strategy.session_set('enroll_course_id', unicode(self.course.id))
|
||||
|
||||
# Simulate completing the pipeline from the student dashboard's
|
||||
# Simulate completing the pipeline from the student account settings
|
||||
# "link account" button.
|
||||
result = pipeline.change_enrollment(strategy, 1, user=self.user, auth_entry=pipeline.AUTH_ENTRY_DASHBOARD) # pylint: disable=assignment-from-no-return,redundant-keyword-arg
|
||||
result = pipeline.change_enrollment(strategy, 1, user=self.user, auth_entry=pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS) # pylint: disable=assignment-from-no-return,redundant-keyword-arg
|
||||
|
||||
# Verify that we were NOT enrolled
|
||||
self.assertEqual(result, {})
|
||||
|
||||
147
common/djangoapps/util/model_utils.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
Utilities for django models.
|
||||
"""
|
||||
from eventtracking import tracker
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models.fields.related import RelatedField
|
||||
|
||||
from django_countries.fields import Country
|
||||
|
||||
# The setting name used for events when "settings" (account settings, preferences, profile information) change.
|
||||
USER_SETTINGS_CHANGED_EVENT_NAME = u'edx.user.settings.changed'
|
||||
|
||||
|
||||
def get_changed_fields_dict(instance, model_class):
|
||||
"""
|
||||
Helper method for tracking field changes on a model.
|
||||
|
||||
Given a model instance and class, return a dict whose keys are that
|
||||
instance's fields which differ from the last saved ones and whose values
|
||||
are the old values of those fields. Related fields are not considered.
|
||||
|
||||
Args:
|
||||
instance (Model instance): the model instance with changes that are
|
||||
being tracked
|
||||
model_class (Model class): the class of the model instance we are
|
||||
tracking
|
||||
|
||||
Returns:
|
||||
dict: a mapping of field names to current database values of those
|
||||
fields, or an empty dict if the model is new
|
||||
"""
|
||||
try:
|
||||
old_model = model_class.objects.get(pk=instance.pk)
|
||||
except model_class.DoesNotExist:
|
||||
# Object is new, so fields haven't technically changed. We'll return
|
||||
# an empty dict as a default value.
|
||||
return {}
|
||||
else:
|
||||
field_names = [
|
||||
field[0].name for field in model_class._meta.get_fields_with_model()
|
||||
]
|
||||
changed_fields = {
|
||||
field_name: getattr(old_model, field_name) for field_name in field_names
|
||||
if getattr(old_model, field_name) != getattr(instance, field_name)
|
||||
}
|
||||
|
||||
return changed_fields
|
||||
|
||||
|
||||
def emit_field_changed_events(instance, user, db_table, excluded_fields=None, hidden_fields=None):
|
||||
"""Emits a settings changed event for each field that has changed.
|
||||
|
||||
Note that this function expects that a `_changed_fields` dict has been set
|
||||
as an attribute on `instance` (see `get_changed_fields_dict`.
|
||||
|
||||
Args:
|
||||
instance (Model instance): the model instance that is being saved
|
||||
user (User): the user that this instance is associated with
|
||||
db_table (str): the name of the table that we're modifying
|
||||
excluded_fields (list): a list of field names for which events should
|
||||
not be emitted
|
||||
hidden_fields (list): a list of field names specifying fields whose
|
||||
values should not be included in the event (None will be used
|
||||
instead)
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
def clean_field(field_name, value):
|
||||
"""
|
||||
Prepare a field to be emitted in a JSON serializable format. If
|
||||
`field_name` is a hidden field, return None.
|
||||
"""
|
||||
if field_name in hidden_fields:
|
||||
return None
|
||||
# Country is not JSON serializable. Return the country code.
|
||||
if isinstance(value, Country):
|
||||
if value.code:
|
||||
return value.code
|
||||
else:
|
||||
return None
|
||||
return value
|
||||
|
||||
excluded_fields = excluded_fields or []
|
||||
hidden_fields = hidden_fields or []
|
||||
changed_fields = getattr(instance, '_changed_fields', {})
|
||||
for field_name in changed_fields:
|
||||
if field_name not in excluded_fields:
|
||||
old_value = clean_field(field_name, changed_fields[field_name])
|
||||
new_value = clean_field(field_name, getattr(instance, field_name))
|
||||
emit_setting_changed_event(user, db_table, field_name, old_value, new_value)
|
||||
# Remove the now inaccurate _changed_fields attribute.
|
||||
if hasattr(instance, '_changed_fields'):
|
||||
del instance._changed_fields
|
||||
|
||||
|
||||
def emit_setting_changed_event(user, db_table, setting_name, old_value, new_value):
|
||||
"""Emits an event for a change in a setting.
|
||||
|
||||
Args:
|
||||
user (User): the user that this setting is associated with.
|
||||
db_table (str): the name of the table that we're modifying.
|
||||
setting_name (str): the name of the setting being changed.
|
||||
old_value (object): the value before the change.
|
||||
new_value (object): the new value being saved.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# Compute the maximum value length so that two copies can fit into the maximum event size
|
||||
# in addition to all the other fields recorded.
|
||||
max_value_length = settings.TRACK_MAX_EVENT / 4
|
||||
|
||||
serialized_old_value, old_was_truncated = _get_truncated_setting_value(old_value, max_length=max_value_length)
|
||||
serialized_new_value, new_was_truncated = _get_truncated_setting_value(new_value, max_length=max_value_length)
|
||||
truncated_values = []
|
||||
if old_was_truncated:
|
||||
truncated_values.append("old")
|
||||
if new_was_truncated:
|
||||
truncated_values.append("new")
|
||||
tracker.emit(
|
||||
USER_SETTINGS_CHANGED_EVENT_NAME,
|
||||
{
|
||||
"setting": setting_name,
|
||||
"old": serialized_old_value,
|
||||
"new": serialized_new_value,
|
||||
"truncated": truncated_values,
|
||||
"user_id": user.id,
|
||||
"table": db_table,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _get_truncated_setting_value(value, max_length=None):
|
||||
"""
|
||||
Returns the truncated form of a setting value.
|
||||
|
||||
Returns:
|
||||
truncated_value (object): the possibly truncated version of the value.
|
||||
was_truncated (bool): returns true if the serialized value was truncated.
|
||||
"""
|
||||
if isinstance(value, basestring) and max_length is not None and len(value) > max_length:
|
||||
return value[0:max_length], True
|
||||
else:
|
||||
return value, False
|
||||
@@ -1,5 +1,7 @@
|
||||
import sys
|
||||
|
||||
from mock import patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import clear_url_caches, resolve
|
||||
|
||||
@@ -55,3 +57,36 @@ class UrlResetMixin(object):
|
||||
|
||||
self._reset_urls(urlconf_modules)
|
||||
self.addCleanup(lambda: self._reset_urls(urlconf_modules))
|
||||
|
||||
|
||||
class EventTestMixin(object):
|
||||
"""
|
||||
Generic mixin for verifying that events were emitted during a test.
|
||||
"""
|
||||
def setUp(self, tracker):
|
||||
super(EventTestMixin, self).setUp()
|
||||
self.tracker = tracker
|
||||
patcher = patch(self.tracker)
|
||||
self.mock_tracker = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def assert_no_events_were_emitted(self):
|
||||
"""
|
||||
Ensures no events were emitted since the last event related assertion.
|
||||
"""
|
||||
self.assertFalse(self.mock_tracker.emit.called) # pylint: disable=maybe-no-member
|
||||
|
||||
def assert_event_emitted(self, event_name, **kwargs):
|
||||
"""
|
||||
Verify that an event was emitted with the given parameters.
|
||||
"""
|
||||
self.mock_tracker.emit.assert_any_call( # pylint: disable=maybe-no-member
|
||||
event_name,
|
||||
kwargs
|
||||
)
|
||||
|
||||
def reset_tracker(self):
|
||||
"""
|
||||
Reset the mock tracker in order to forget about old events.
|
||||
"""
|
||||
self.mock_tracker.reset_mock()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
define(['sinon', 'underscore'], function(sinon, _) {
|
||||
var fakeServer, fakeRequests, expectRequest, expectJsonRequest,
|
||||
respondWithJson, respondWithError, respondWithTextError, respondToDelete;
|
||||
respondWithJson, respondWithError, respondWithTextError, responseWithNoContent;
|
||||
|
||||
/* These utility methods are used by Jasmine tests to create a mock server or
|
||||
* get reference to mock requests. In either case, the cleanup (restore) is done with
|
||||
@@ -109,7 +109,7 @@ define(['sinon', 'underscore'], function(sinon, _) {
|
||||
);
|
||||
};
|
||||
|
||||
respondToDelete = function(requests, requestIndex) {
|
||||
respondWithNoContent = function(requests, requestIndex) {
|
||||
if (_.isUndefined(requestIndex)) {
|
||||
requestIndex = requests.length - 1;
|
||||
}
|
||||
@@ -125,6 +125,6 @@ define(['sinon', 'underscore'], function(sinon, _) {
|
||||
'respondWithJson': respondWithJson,
|
||||
'respondWithError': respondWithError,
|
||||
'respondWithTextError': respondWithTextError,
|
||||
'respondToDelete': respondToDelete
|
||||
'respondWithNoContent': respondWithNoContent,
|
||||
};
|
||||
});
|
||||
|
||||
59
common/test/acceptance/pages/lms/account_settings.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Base class for account settings page.
|
||||
"""
|
||||
from . import BASE_URL
|
||||
|
||||
from bok_choy.page_object import PageObject
|
||||
from bok_choy.promise import EmptyPromise
|
||||
|
||||
from .fields import FieldsMixin
|
||||
|
||||
|
||||
class AccountSettingsPage(FieldsMixin, PageObject):
|
||||
"""
|
||||
Tests for Account Settings Page.
|
||||
"""
|
||||
|
||||
url = "{base}/{settings}".format(base=BASE_URL, settings='account/settings')
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return self.q(css='.account-settings-container').present
|
||||
|
||||
def sections_structure(self):
|
||||
"""
|
||||
Return list of section titles and field titles for each section.
|
||||
|
||||
Example: [
|
||||
{
|
||||
'title': 'Section Title'
|
||||
'fields': ['Field 1 title', 'Field 2 title',...]
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
structure = []
|
||||
|
||||
sections = self.q(css='.section')
|
||||
for section in sections:
|
||||
section_title_element = section.find_element_by_class_name('section-header')
|
||||
field_title_elements = section.find_elements_by_class_name('u-field-title')
|
||||
|
||||
structure.append({
|
||||
'title': section_title_element.text,
|
||||
'fields': [element.text for element in field_title_elements],
|
||||
})
|
||||
|
||||
return structure
|
||||
|
||||
def _is_loading_in_progress(self):
|
||||
"""
|
||||
Check if loading indicator is visible.
|
||||
"""
|
||||
query = self.q(css='.ui-loading-indicator')
|
||||
return query.present and 'is-hidden' not in query.attrs('class')[0].split()
|
||||
|
||||
def wait_for_loading_indicator(self):
|
||||
"""
|
||||
Wait for loading indicator to become visible.
|
||||
"""
|
||||
EmptyPromise(self._is_loading_in_progress, "Loading is in progress.").fulfill()
|
||||
@@ -50,19 +50,18 @@ class DashboardPage(PageObject):
|
||||
return self.q(css='h3.course-title > a').map(_get_course_name).results
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
"""Return the displayed value for the user's full name"""
|
||||
return self.q(css='li.info--username .data').text[0]
|
||||
def sidebar_menu_title(self):
|
||||
"""
|
||||
Return the title value for sidebar menu.
|
||||
"""
|
||||
return self.q(css='.user-info span.title').text[0]
|
||||
|
||||
@property
|
||||
def email(self):
|
||||
"""Return the displayed value for the user's email address"""
|
||||
return self.q(css='li.info--email .data').text[0]
|
||||
|
||||
@property
|
||||
def username(self):
|
||||
"""Return the displayed value for the user's username"""
|
||||
return self.q(css='.username-label').text[0]
|
||||
def sidebar_menu_description(self):
|
||||
"""
|
||||
Return the description text for sidebar menu.
|
||||
"""
|
||||
return self.q(css='.user-info span.copy').text[0]
|
||||
|
||||
def get_enrollment_mode(self, course_name):
|
||||
"""Get the enrollment mode for a given course on the dashboard.
|
||||
@@ -149,27 +148,6 @@ class DashboardPage(PageObject):
|
||||
else:
|
||||
return None
|
||||
|
||||
def change_language(self, code):
|
||||
"""
|
||||
Change the language on the dashboard to the language corresponding with `code`.
|
||||
"""
|
||||
self.q(css=".edit-language").first.click()
|
||||
self.q(css='select[name="language"] option[value="{}"]'.format(code)).first.click()
|
||||
self.q(css="#submit-lang").first.click()
|
||||
|
||||
# Clicking the submit-lang button does a jquery ajax post, so make sure that
|
||||
# has completed before continuing on.
|
||||
self.wait_for_ajax()
|
||||
|
||||
self._changed_lang_promise(code).fulfill()
|
||||
|
||||
def _changed_lang_promise(self, code):
|
||||
def _check_func():
|
||||
language_is_selected = self.q(css='select[name="language"] option[value="{}"]'.format(code)).selected
|
||||
modal_is_visible = self.q(css='section#change_language.modal').visible
|
||||
return (language_is_selected and not modal_is_visible)
|
||||
return EmptyPromise(_check_func, "language changed and modal hidden")
|
||||
|
||||
def pre_requisite_message_displayed(self):
|
||||
"""
|
||||
Verify if pre-requisite course messages are being displayed.
|
||||
@@ -183,3 +161,28 @@ class DashboardPage(PageObject):
|
||||
def get_course_social_sharing_widget(self, widget_name):
|
||||
""" Retrieves the specified social sharing widget by its classification """
|
||||
return self.q(css='a.action-{}'.format(widget_name))
|
||||
|
||||
def click_username_dropdown(self):
|
||||
"""
|
||||
Click username dropdown.
|
||||
"""
|
||||
self.q(css='.dropdown').first.click()
|
||||
|
||||
@property
|
||||
def username_dropdown_link_text(self):
|
||||
"""
|
||||
Return list username dropdown links.
|
||||
"""
|
||||
return self.q(css='.dropdown-menu li a').text
|
||||
|
||||
def click_account_settings_link(self):
|
||||
"""
|
||||
Click on `Account Settings` link.
|
||||
"""
|
||||
self.q(css='.dropdown-menu li a').first.click()
|
||||
|
||||
def click_my_profile_link(self):
|
||||
"""
|
||||
Click on `My Profile` link.
|
||||
"""
|
||||
self.q(css='.dropdown-menu li a').nth(1).click()
|
||||
|
||||
@@ -444,9 +444,9 @@ class DiscussionUserProfilePage(CoursePage):
|
||||
return (
|
||||
self.q(css='section.discussion-user-threads[data-course-id="{}"]'.format(self.course_id)).present
|
||||
and
|
||||
self.q(css='section.user-profile div.sidebar-username').present
|
||||
self.q(css='section.user-profile a.leaner-profile-link').present
|
||||
and
|
||||
self.q(css='section.user-profile div.sidebar-username').text[0] == self.username
|
||||
self.q(css='section.user-profile a.leaner-profile-link').text[0] == self.username
|
||||
)
|
||||
|
||||
@wait_for_js
|
||||
@@ -526,6 +526,10 @@ class DiscussionUserProfilePage(CoursePage):
|
||||
"Window is on top"
|
||||
).fulfill()
|
||||
|
||||
def click_on_sidebar_username(self):
|
||||
self.wait_for_page()
|
||||
self.q(css='.leaner-profile-link').first.click()
|
||||
|
||||
|
||||
class DiscussionTabHomePage(CoursePage, DiscussionPageMixin):
|
||||
|
||||
|
||||
222
common/test/acceptance/pages/lms/fields.py
Normal file
@@ -0,0 +1,222 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Mixins for fields.
|
||||
"""
|
||||
from bok_choy.promise import EmptyPromise
|
||||
|
||||
from ...tests.helpers import get_selected_option_text, select_option_by_text
|
||||
|
||||
|
||||
class FieldsMixin(object):
|
||||
"""
|
||||
Methods for testing fields in pages.
|
||||
"""
|
||||
|
||||
def field(self, field_id):
|
||||
"""
|
||||
Return field with field_id.
|
||||
"""
|
||||
query = self.q(css='.u-field-{}'.format(field_id))
|
||||
return query.text[0] if query.present else None
|
||||
|
||||
def wait_for_field(self, field_id):
|
||||
"""
|
||||
Wait for a field to appear in DOM.
|
||||
"""
|
||||
EmptyPromise(
|
||||
lambda: self.field(field_id) is not None,
|
||||
"Field with id \"{0}\" is in DOM.".format(field_id)
|
||||
).fulfill()
|
||||
|
||||
def mode_for_field(self, field_id):
|
||||
"""
|
||||
Extract current field mode.
|
||||
|
||||
Returns:
|
||||
`placeholder`/`edit`/`display`
|
||||
"""
|
||||
self.wait_for_field(field_id)
|
||||
|
||||
query = self.q(css='.u-field-{}'.format(field_id))
|
||||
|
||||
if not query.present:
|
||||
return None
|
||||
|
||||
field_classes = query.attrs('class')[0].split()
|
||||
|
||||
if 'mode-placeholder' in field_classes:
|
||||
return 'placeholder'
|
||||
|
||||
if 'mode-display' in field_classes:
|
||||
return 'display'
|
||||
|
||||
if 'mode-edit' in field_classes:
|
||||
return 'edit'
|
||||
|
||||
def icon_for_field(self, field_id, icon_id):
|
||||
"""
|
||||
Check if field icon is present.
|
||||
"""
|
||||
self.wait_for_field(field_id)
|
||||
|
||||
query = self.q(css='.u-field-{} .u-field-icon'.format(field_id))
|
||||
return query.present and icon_id in query.attrs('class')[0].split()
|
||||
|
||||
def title_for_field(self, field_id):
|
||||
"""
|
||||
Return the title of a field.
|
||||
"""
|
||||
self.wait_for_field(field_id)
|
||||
|
||||
query = self.q(css='.u-field-{} .u-field-title'.format(field_id))
|
||||
return query.text[0] if query.present else None
|
||||
|
||||
def message_for_field(self, field_id):
|
||||
"""
|
||||
Return the current message in a field.
|
||||
"""
|
||||
self.wait_for_field(field_id)
|
||||
|
||||
query = self.q(css='.u-field-{} .u-field-message'.format(field_id))
|
||||
return query.text[0] if query.present else None
|
||||
|
||||
def wait_for_messsage(self, field_id, message):
|
||||
"""
|
||||
Wait for a message to appear in a field.
|
||||
"""
|
||||
EmptyPromise(
|
||||
lambda: message in (self.message_for_field(field_id) or ''),
|
||||
"Messsage \"{0}\" is visible.".format(message)
|
||||
).fulfill()
|
||||
|
||||
def indicator_for_field(self, field_id):
|
||||
"""
|
||||
Return the name of the current indicator in a field.
|
||||
"""
|
||||
self.wait_for_field(field_id)
|
||||
|
||||
query = self.q(css='.u-field-{} .u-field-message i'.format(field_id))
|
||||
return [
|
||||
class_name for class_name
|
||||
in query.attrs('class')[0].split(' ')
|
||||
if class_name.startswith('message')
|
||||
][0].partition('-')[2] if query.present else None
|
||||
|
||||
def wait_for_indicator(self, field_id, indicator):
|
||||
"""
|
||||
Wait for an indicator to appear in a field.
|
||||
"""
|
||||
EmptyPromise(
|
||||
lambda: indicator == self.indicator_for_field(field_id),
|
||||
"Indicator \"{0}\" is visible.".format(self.indicator_for_field(field_id))
|
||||
).fulfill()
|
||||
|
||||
def make_field_editable(self, field_id):
|
||||
"""
|
||||
Make a field editable.
|
||||
"""
|
||||
query = self.q(css='.u-field-{}'.format(field_id))
|
||||
|
||||
if not query.present:
|
||||
return None
|
||||
|
||||
field_classes = query.attrs('class')[0].split()
|
||||
|
||||
if 'mode-placeholder' in field_classes or 'mode-display' in field_classes:
|
||||
if field_id == 'bio':
|
||||
self.q(css='.u-field-bio > .wrapper-u-field').first.click()
|
||||
else:
|
||||
self.q(css='.u-field-{}'.format(field_id)).first.click()
|
||||
|
||||
def value_for_readonly_field(self, field_id):
|
||||
"""
|
||||
Return the value in a readonly field.
|
||||
"""
|
||||
self.wait_for_field(field_id)
|
||||
|
||||
return self.value_for_text_field(field_id)
|
||||
|
||||
def value_for_text_field(self, field_id, value=None):
|
||||
"""
|
||||
Get or set the value of a text field.
|
||||
"""
|
||||
self.wait_for_field(field_id)
|
||||
|
||||
query = self.q(css='.u-field-{} input'.format(field_id))
|
||||
if not query.present:
|
||||
return None
|
||||
|
||||
if value is not None:
|
||||
current_value = query.attrs('value')[0]
|
||||
query.results[0].send_keys(u'\ue003' * len(current_value)) # Delete existing value.
|
||||
query.results[0].send_keys(value) # Input new value
|
||||
query.results[0].send_keys(u'\ue007') # Press Enter
|
||||
return query.attrs('value')[0]
|
||||
|
||||
def value_for_textarea_field(self, field_id, value=None):
|
||||
"""
|
||||
Get or set the value of a textarea field.
|
||||
"""
|
||||
self.wait_for_field(field_id)
|
||||
|
||||
self.make_field_editable(field_id)
|
||||
|
||||
query = self.q(css='.u-field-{} textarea'.format(field_id))
|
||||
if not query.present:
|
||||
return None
|
||||
|
||||
if value is not None:
|
||||
query.fill(value)
|
||||
query.results[0].send_keys(u'\ue004') # Focus Out using TAB
|
||||
|
||||
if self.mode_for_field(field_id) == 'edit':
|
||||
return query.text[0]
|
||||
else:
|
||||
return self.get_non_editable_mode_value(field_id)
|
||||
|
||||
def get_non_editable_mode_value(self, field_id):
|
||||
"""
|
||||
Return value of field in `display` or `placeholder` mode.
|
||||
"""
|
||||
self.wait_for_field(field_id)
|
||||
|
||||
return self.q(css='.u-field-{} .u-field-value .u-field-value-readonly'.format(field_id)).text[0]
|
||||
|
||||
def value_for_dropdown_field(self, field_id, value=None):
|
||||
"""
|
||||
Get or set the value in a dropdown field.
|
||||
"""
|
||||
self.wait_for_field(field_id)
|
||||
|
||||
self.make_field_editable(field_id)
|
||||
|
||||
query = self.q(css='.u-field-{} select'.format(field_id))
|
||||
if not query.present:
|
||||
return None
|
||||
|
||||
if value is not None:
|
||||
select_option_by_text(query, value)
|
||||
|
||||
if self.mode_for_field(field_id) == 'edit':
|
||||
return get_selected_option_text(query)
|
||||
else:
|
||||
return self.get_non_editable_mode_value(field_id)
|
||||
|
||||
def link_title_for_link_field(self, field_id):
|
||||
"""
|
||||
Return the title of the link in a link field.
|
||||
"""
|
||||
self.wait_for_field(field_id)
|
||||
|
||||
query = self.q(css='.u-field-link-title-{}'.format(field_id))
|
||||
return query.text[0] if query.present else None
|
||||
|
||||
def click_on_link_in_link_field(self, field_id):
|
||||
"""
|
||||
Click the link in a link field.
|
||||
"""
|
||||
self.wait_for_field(field_id)
|
||||
|
||||
query = self.q(css='.u-field-{} a'.format(field_id))
|
||||
if query.present:
|
||||
query.first.click()
|
||||
271
common/test/acceptance/pages/lms/learner_profile.py
Normal file
@@ -0,0 +1,271 @@
|
||||
"""
|
||||
Bok-Choy PageObject class for learner profile page.
|
||||
"""
|
||||
from . import BASE_URL
|
||||
from bok_choy.page_object import PageObject
|
||||
from .fields import FieldsMixin
|
||||
from bok_choy.promise import EmptyPromise
|
||||
from .instructor_dashboard import InstructorDashboardPage
|
||||
from selenium.webdriver import ActionChains
|
||||
|
||||
|
||||
PROFILE_VISIBILITY_SELECTOR = '#u-field-select-account_privacy option[value="{}"]'
|
||||
FIELD_ICONS = {
|
||||
'country': 'fa-map-marker',
|
||||
'language_proficiencies': 'fa-comment',
|
||||
}
|
||||
|
||||
|
||||
class LearnerProfilePage(FieldsMixin, PageObject):
|
||||
"""
|
||||
PageObject methods for Learning Profile Page.
|
||||
"""
|
||||
|
||||
def __init__(self, browser, username):
|
||||
"""
|
||||
Initialize the page.
|
||||
|
||||
Arguments:
|
||||
browser (Browser): The browser instance.
|
||||
username (str): Profile username.
|
||||
"""
|
||||
super(LearnerProfilePage, self).__init__(browser)
|
||||
self.username = username
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""
|
||||
Construct a URL to the page.
|
||||
"""
|
||||
return BASE_URL + "/u/" + self.username
|
||||
|
||||
def is_browser_on_page(self):
|
||||
"""
|
||||
Check if browser is showing correct page.
|
||||
"""
|
||||
return 'Learner Profile' in self.browser.title
|
||||
|
||||
@property
|
||||
def privacy(self):
|
||||
"""
|
||||
Get user profile privacy.
|
||||
|
||||
Returns:
|
||||
'all_users' or 'private'
|
||||
"""
|
||||
return 'all_users' if self.q(css=PROFILE_VISIBILITY_SELECTOR.format('all_users')).selected else 'private'
|
||||
|
||||
@privacy.setter
|
||||
def privacy(self, privacy):
|
||||
"""
|
||||
Set user profile privacy.
|
||||
|
||||
Arguments:
|
||||
privacy (str): 'all_users' or 'private'
|
||||
"""
|
||||
self.wait_for_element_visibility('select#u-field-select-account_privacy', 'Privacy dropdown is visible')
|
||||
|
||||
if privacy != self.privacy:
|
||||
self.q(css=PROFILE_VISIBILITY_SELECTOR.format(privacy)).first.click()
|
||||
EmptyPromise(lambda: privacy == self.privacy, 'Privacy is set to {}'.format(privacy)).fulfill()
|
||||
self.wait_for_ajax()
|
||||
|
||||
if privacy == 'all_users':
|
||||
self.wait_for_public_fields()
|
||||
|
||||
def field_is_visible(self, field_id):
|
||||
"""
|
||||
Check if a field with id set to `field_id` is shown.
|
||||
|
||||
Arguments:
|
||||
field_id (str): field id
|
||||
|
||||
Returns:
|
||||
True/False
|
||||
"""
|
||||
self.wait_for_ajax()
|
||||
return self.q(css='.u-field-{}'.format(field_id)).visible
|
||||
|
||||
def field_is_editable(self, field_id):
|
||||
"""
|
||||
Check if a field with id set to `field_id` is editable.
|
||||
|
||||
Arguments:
|
||||
field_id (str): field id
|
||||
|
||||
Returns:
|
||||
True/False
|
||||
"""
|
||||
self.wait_for_field(field_id)
|
||||
self.make_field_editable(field_id)
|
||||
return self.mode_for_field(field_id) == 'edit'
|
||||
|
||||
@property
|
||||
def visible_fields(self):
|
||||
"""
|
||||
Return list of visible fields.
|
||||
"""
|
||||
self.wait_for_field('username')
|
||||
|
||||
fields = ['username', 'country', 'language_proficiencies', 'bio']
|
||||
return [field for field in fields if self.field_is_visible(field)]
|
||||
|
||||
@property
|
||||
def editable_fields(self):
|
||||
"""
|
||||
Return list of editable fields currently shown on page.
|
||||
"""
|
||||
self.wait_for_ajax()
|
||||
self.wait_for_element_visibility('.u-field-username', 'username is not visible')
|
||||
|
||||
fields = ['country', 'language_proficiencies', 'bio']
|
||||
return [field for field in fields if self.field_is_editable(field)]
|
||||
|
||||
@property
|
||||
def privacy_field_visible(self):
|
||||
"""
|
||||
Check if profile visibility selector is shown or not.
|
||||
|
||||
Returns:
|
||||
True/False
|
||||
"""
|
||||
self.wait_for_ajax()
|
||||
return self.q(css='#u-field-select-account_privacy').visible
|
||||
|
||||
def field_icon_present(self, field_id):
|
||||
"""
|
||||
Check if an icon is present for a field. Only dropdown fields have icons.
|
||||
|
||||
Arguments:
|
||||
field_id (str): field id
|
||||
|
||||
Returns:
|
||||
True/False
|
||||
"""
|
||||
return self.icon_for_field(field_id, FIELD_ICONS[field_id])
|
||||
|
||||
def wait_for_public_fields(self):
|
||||
"""
|
||||
Wait for `country`, `language` and `bio` fields to be visible.
|
||||
"""
|
||||
EmptyPromise(lambda: self.field_is_visible('country'), 'Country field is visible').fulfill()
|
||||
EmptyPromise(lambda: self.field_is_visible('language_proficiencies'), 'Language field is visible').fulfill()
|
||||
EmptyPromise(lambda: self.field_is_visible('bio'), 'About Me field is visible').fulfill()
|
||||
|
||||
@property
|
||||
def profile_forced_private_message(self):
|
||||
"""
|
||||
Returns age limit message.
|
||||
"""
|
||||
self.wait_for_ajax()
|
||||
return self.q(css='#u-field-message-account_privacy').text[0]
|
||||
|
||||
@property
|
||||
def age_limit_message_present(self):
|
||||
"""
|
||||
Check if age limit message is present.
|
||||
"""
|
||||
self.wait_for_ajax()
|
||||
return self.q(css='#u-field-message-account_privacy').visible
|
||||
|
||||
@property
|
||||
def profile_has_default_image(self):
|
||||
"""
|
||||
Return bool if image field has default photo or not.
|
||||
"""
|
||||
self.wait_for_field('image')
|
||||
default_links = self.q(css='.image-frame').attrs('src')
|
||||
return 'default-profile' in default_links[0] if default_links else False
|
||||
|
||||
def mouse_hover(self, element):
|
||||
"""
|
||||
Mouse over on given element.
|
||||
"""
|
||||
mouse_hover_action = ActionChains(self.browser).move_to_element(element)
|
||||
mouse_hover_action.perform()
|
||||
|
||||
def profile_has_image_with_public_access(self):
|
||||
"""
|
||||
Check if image is present with remove/upload access.
|
||||
"""
|
||||
self.wait_for_field('image')
|
||||
|
||||
self.mouse_hover(self.browser.find_element_by_css_selector('.image-wrapper'))
|
||||
self.wait_for_element_visibility('.u-field-upload-button', "upload button is visible")
|
||||
return self.q(css='.u-field-upload-button').visible
|
||||
|
||||
def profile_has_image_with_private_access(self):
|
||||
"""
|
||||
Check if image is present with remove/upload access.
|
||||
"""
|
||||
self.wait_for_field('image')
|
||||
return self.q(css='.u-field-upload-button').visible
|
||||
|
||||
def upload_file(self, filename, wait_for_upload_button=True):
|
||||
"""
|
||||
Helper method to upload an image file.
|
||||
"""
|
||||
if wait_for_upload_button:
|
||||
self.wait_for_element_visibility('.u-field-upload-button', "upload button is visible")
|
||||
file_path = InstructorDashboardPage.get_asset_path(filename)
|
||||
|
||||
# make the elements visible.
|
||||
self.browser.execute_script('$(".u-field-upload-button").css("opacity",1);')
|
||||
self.browser.execute_script('$(".upload-button-input").css("opacity",1);')
|
||||
|
||||
self.wait_for_element_visibility('.upload-button-input', "upload button is visible")
|
||||
|
||||
self.browser.execute_script('$(".upload-submit").show();')
|
||||
|
||||
# First send_keys will initialize the jquery auto upload plugin.
|
||||
self.q(css='.upload-button-input').results[0].send_keys(file_path)
|
||||
self.q(css='.upload-submit').first.click()
|
||||
self.q(css='.upload-button-input').results[0].send_keys(file_path)
|
||||
|
||||
self.wait_for_ajax()
|
||||
|
||||
@property
|
||||
def image_upload_success(self):
|
||||
"""
|
||||
Returns the bool, if image is updated or not.
|
||||
"""
|
||||
self.wait_for_field('image')
|
||||
self.wait_for_ajax()
|
||||
|
||||
self.wait_for_element_visibility('.image-frame', "image box is visible")
|
||||
image_link = self.q(css='.image-frame').attrs('src')
|
||||
return 'default-profile' not in image_link[0]
|
||||
|
||||
@property
|
||||
def profile_image_message(self):
|
||||
"""
|
||||
Returns the text message for profile image.
|
||||
"""
|
||||
self.wait_for_field('image')
|
||||
self.wait_for_ajax()
|
||||
return self.q(css='.message-banner p').text[0]
|
||||
|
||||
def remove_profile_image(self):
|
||||
"""
|
||||
Removes the profile image.
|
||||
"""
|
||||
self.wait_for_field('image')
|
||||
self.wait_for_ajax()
|
||||
|
||||
self.wait_for_element_visibility('.image-wrapper', "remove button is visible")
|
||||
self.browser.execute_script('$(".u-field-remove-button").css("opacity",1);')
|
||||
self.mouse_hover(self.browser.find_element_by_css_selector('.image-wrapper'))
|
||||
|
||||
self.wait_for_element_visibility('.u-field-remove-button', "remove button is visible")
|
||||
self.q(css='.u-field-remove-button').first.click()
|
||||
|
||||
self.wait_for_ajax()
|
||||
self.mouse_hover(self.browser.find_element_by_css_selector('.image-wrapper'))
|
||||
self.wait_for_element_visibility('.u-field-upload-button', "upload button is visible")
|
||||
return True
|
||||
|
||||
@property
|
||||
def remove_link_present(self):
|
||||
self.wait_for_field('image')
|
||||
self.mouse_hover(self.browser.find_element_by_css_selector('.image-wrapper'))
|
||||
return self.q(css='.u-field-remove-button').visible
|
||||
@@ -19,6 +19,8 @@ from ...pages.lms.discussion import (
|
||||
DiscussionTabHomePage,
|
||||
DiscussionSortPreferencePage,
|
||||
)
|
||||
from ...pages.lms.learner_profile import LearnerProfilePage
|
||||
|
||||
from ...fixtures.course import CourseFixture, XBlockFixtureDesc
|
||||
from ...fixtures.discussion import (
|
||||
SingleThreadViewFixture,
|
||||
@@ -753,6 +755,24 @@ class DiscussionUserProfileTest(UniqueCourseTest):
|
||||
page.wait_for_ajax()
|
||||
self.assertTrue(page.is_window_on_top())
|
||||
|
||||
def test_redirects_to_learner_profile(self):
|
||||
"""
|
||||
Scenario: Verify that learner-profile link is present on forum discussions page and we can navigate to it.
|
||||
|
||||
Given that I am on discussion forum user's profile page.
|
||||
And I can see a username on left sidebar
|
||||
When I click on my username.
|
||||
Then I will be navigated to Learner Profile page.
|
||||
And I can my username on Learner Profile page
|
||||
"""
|
||||
learner_profile_page = LearnerProfilePage(self.browser, self.PROFILED_USERNAME)
|
||||
|
||||
page = self.check_pages(1)
|
||||
page.click_on_sidebar_username()
|
||||
|
||||
learner_profile_page.wait_for_page()
|
||||
self.assertTrue(learner_profile_page.field_is_visible('username'))
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class DiscussionSearchAlertTest(UniqueCourseTest):
|
||||
|
||||
@@ -277,26 +277,82 @@ class EventsTestMixin(object):
|
||||
def setUp(self):
|
||||
super(EventsTestMixin, self).setUp()
|
||||
self.event_collection = MongoClient()["test"]["events"]
|
||||
self.event_collection.drop()
|
||||
self.start_time = datetime.now()
|
||||
self.reset_event_tracking()
|
||||
|
||||
def assert_event_emitted_num_times(self, event_name, event_time, event_user_id, num_times_emitted):
|
||||
def assert_event_emitted_num_times(self, event_name, event_time, event_user_id, num_times_emitted, **kwargs):
|
||||
"""
|
||||
Tests the number of times a particular event was emitted.
|
||||
|
||||
Extra kwargs get passed to the mongo query in the form: "event.<key>: value".
|
||||
|
||||
:param event_name: Expected event name (e.g., "edx.course.enrollment.activated")
|
||||
:param event_time: Latest expected time, after which the event would fire (e.g., the beginning of the test case)
|
||||
:param event_user_id: user_id expected in the event
|
||||
:param num_times_emitted: number of times the event is expected to appear since the event_time
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.event_collection.find(
|
||||
{
|
||||
"name": event_name,
|
||||
"time": {"$gt": event_time},
|
||||
"event.user_id": int(event_user_id),
|
||||
}
|
||||
).count(), num_times_emitted
|
||||
)
|
||||
find_kwargs = {
|
||||
"name": event_name,
|
||||
"time": {"$gt": event_time},
|
||||
"event.user_id": int(event_user_id),
|
||||
}
|
||||
find_kwargs.update({"event.{}".format(key): value for key, value in kwargs.items()})
|
||||
matching_events = self.event_collection.find(find_kwargs)
|
||||
self.assertEqual(matching_events.count(), num_times_emitted, '\n'.join(str(event) for event in matching_events))
|
||||
|
||||
def reset_event_tracking(self):
|
||||
"""
|
||||
Resets all event tracking so that previously captured events are removed.
|
||||
"""
|
||||
self.event_collection.drop()
|
||||
self.start_time = datetime.now()
|
||||
|
||||
def get_matching_events(self, username, event_type):
|
||||
"""
|
||||
Returns a cursor for the matching browser events related emitted for the specified username.
|
||||
"""
|
||||
return self.event_collection.find({
|
||||
"username": username,
|
||||
"event_type": event_type,
|
||||
"time": {"$gt": self.start_time},
|
||||
})
|
||||
|
||||
def verify_events_of_type(self, username, event_type, expected_events, expected_referers=None):
|
||||
"""Verify that the expected events of a given type were logged.
|
||||
Args:
|
||||
username (str): The name of the user for which events will be tested.
|
||||
event_type (str): The type of event to be verified.
|
||||
expected_events (list): A list of dicts representing the events that should
|
||||
have been fired.
|
||||
expected_referers (list): A list of strings representing the referers for each event
|
||||
that should been fired (optional). If present, the actual referers compared
|
||||
with this list, checking that the expected_referers are the suffixes of
|
||||
actual_referers. For example, if one event is expected, specifying ["/account/settings"]
|
||||
will verify that the referer for the single event ends with "/account/settings".
|
||||
"""
|
||||
EmptyPromise(
|
||||
lambda: self.get_matching_events(username, event_type).count() >= len(expected_events),
|
||||
"Waiting for the minimum number of events of type {type} to have been recorded".format(type=event_type)
|
||||
).fulfill()
|
||||
|
||||
# Verify that the correct events were fired
|
||||
cursor = self.get_matching_events(username, event_type)
|
||||
actual_events = []
|
||||
actual_referers = []
|
||||
for __ in range(0, cursor.count()):
|
||||
emitted_data = cursor.next()
|
||||
event = emitted_data["event"]
|
||||
if emitted_data["event_source"] == "browser":
|
||||
event = json.loads(event)
|
||||
actual_events.append(event)
|
||||
actual_referers.append(emitted_data["referer"])
|
||||
self.assertEqual(expected_events, actual_events)
|
||||
if expected_referers is not None:
|
||||
self.assertEqual(len(expected_referers), len(actual_referers), "Number of expected referers is incorrect")
|
||||
for index, actual_referer in enumerate(actual_referers):
|
||||
self.assertTrue(
|
||||
actual_referer.endswith(expected_referers[index]),
|
||||
"Refer '{0}' does not have correct suffix, '{1}'.".format(actual_referer, expected_referers[index])
|
||||
)
|
||||
|
||||
|
||||
class UniqueCourseTest(WebAppTest):
|
||||
|
||||
466
common/test/acceptance/tests/lms/test_account_settings.py
Normal file
@@ -0,0 +1,466 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
End-to-end tests for the Account Settings page.
|
||||
"""
|
||||
from unittest import skip
|
||||
|
||||
from bok_choy.web_app_test import WebAppTest
|
||||
|
||||
from ...pages.lms.account_settings import AccountSettingsPage
|
||||
from ...pages.lms.auto_auth import AutoAuthPage
|
||||
from ...pages.lms.dashboard import DashboardPage
|
||||
|
||||
from ..helpers import EventsTestMixin
|
||||
|
||||
|
||||
class AccountSettingsTestMixin(EventsTestMixin, WebAppTest):
|
||||
"""
|
||||
Mixin with helper methods to test the account settings page.
|
||||
"""
|
||||
|
||||
CHANGE_INITIATED_EVENT_NAME = u"edx.user.settings.change_initiated"
|
||||
USER_SETTINGS_CHANGED_EVENT_NAME = 'edx.user.settings.changed'
|
||||
ACCOUNT_SETTINGS_REFERER = u"/account/settings"
|
||||
|
||||
def log_in_as_unique_user(self, email=None):
|
||||
"""
|
||||
Create a unique user and return the account's username and id.
|
||||
"""
|
||||
username = "test_{uuid}".format(uuid=self.unique_id[0:6])
|
||||
auto_auth_page = AutoAuthPage(self.browser, username=username, email=email).visit()
|
||||
user_id = auto_auth_page.get_user_id()
|
||||
return username, user_id
|
||||
|
||||
def assert_event_emitted_num_times(self, user_id, setting, num_times):
|
||||
"""
|
||||
Verify a particular user settings change event was emitted a certain
|
||||
number of times.
|
||||
"""
|
||||
# pylint disable=no-member
|
||||
super(AccountSettingsTestMixin, self).assert_event_emitted_num_times(
|
||||
self.USER_SETTINGS_CHANGED_EVENT_NAME, self.start_time, user_id, num_times, setting=setting
|
||||
)
|
||||
|
||||
def verify_settings_changed_events(self, username, user_id, events, table=None):
|
||||
"""
|
||||
Verify a particular set of account settings change events were fired.
|
||||
"""
|
||||
expected_referers = [self.ACCOUNT_SETTINGS_REFERER] * len(events)
|
||||
for event in events:
|
||||
event[u"user_id"] = long(user_id)
|
||||
event[u"table"] = u"auth_userprofile" if table is None else table
|
||||
event[u"truncated"] = []
|
||||
|
||||
self.verify_events_of_type(
|
||||
username, self.USER_SETTINGS_CHANGED_EVENT_NAME, events,
|
||||
expected_referers=expected_referers
|
||||
)
|
||||
|
||||
|
||||
class DashboardMenuTest(AccountSettingsTestMixin, WebAppTest):
|
||||
"""
|
||||
Tests that the dashboard menu works correctly with the account settings page.
|
||||
"""
|
||||
def test_link_on_dashboard_works(self):
|
||||
"""
|
||||
Scenario: Verify that the "Account Settings" link works from the dashboard.
|
||||
|
||||
|
||||
Given that I am a registered user
|
||||
And I visit my dashboard
|
||||
And I click on "Account Settings" in the top drop down
|
||||
Then I should see my account settings page
|
||||
"""
|
||||
self.log_in_as_unique_user()
|
||||
dashboard_page = DashboardPage(self.browser)
|
||||
dashboard_page.visit()
|
||||
dashboard_page.click_username_dropdown()
|
||||
self.assertIn('Account Settings', dashboard_page.username_dropdown_link_text)
|
||||
dashboard_page.click_account_settings_link()
|
||||
|
||||
|
||||
class AccountSettingsPageTest(AccountSettingsTestMixin, WebAppTest):
|
||||
"""
|
||||
Tests that verify behaviour of the Account Settings page.
|
||||
"""
|
||||
SUCCESS_MESSAGE = 'Your changes have been saved.'
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Initialize account and pages.
|
||||
"""
|
||||
super(AccountSettingsPageTest, self).setUp()
|
||||
self.username, self.user_id = self.log_in_as_unique_user()
|
||||
self.visit_account_settings_page()
|
||||
|
||||
def visit_account_settings_page(self):
|
||||
"""
|
||||
Visit the account settings page for the current user.
|
||||
"""
|
||||
self.account_settings_page = AccountSettingsPage(self.browser)
|
||||
self.account_settings_page.visit()
|
||||
self.account_settings_page.wait_for_ajax()
|
||||
|
||||
def test_page_view_event(self):
|
||||
"""
|
||||
Scenario: An event should be recorded when the "Account Settings"
|
||||
page is viewed.
|
||||
|
||||
Given that I am a registered user
|
||||
And I visit my account settings page
|
||||
Then a page view analytics event should be recorded
|
||||
"""
|
||||
self.verify_events_of_type(
|
||||
self.username,
|
||||
u"edx.user.settings.viewed",
|
||||
[{
|
||||
u"user_id": long(self.user_id),
|
||||
u"page": u"account",
|
||||
u"visibility": None,
|
||||
}]
|
||||
)
|
||||
|
||||
def test_all_sections_and_fields_are_present(self):
|
||||
"""
|
||||
Scenario: Verify that all sections and fields are present on the page.
|
||||
"""
|
||||
expected_sections_structure = [
|
||||
{
|
||||
'title': 'Basic Account Information (required)',
|
||||
'fields': [
|
||||
'Username',
|
||||
'Full Name',
|
||||
'Email Address',
|
||||
'Password',
|
||||
'Language',
|
||||
'Country or Region'
|
||||
]
|
||||
},
|
||||
{
|
||||
'title': 'Additional Information (optional)',
|
||||
'fields': [
|
||||
'Education Completed',
|
||||
'Gender',
|
||||
'Year of Birth',
|
||||
'Preferred Language',
|
||||
]
|
||||
},
|
||||
{
|
||||
'title': 'Connected Accounts',
|
||||
'fields': [
|
||||
'Facebook',
|
||||
'Google',
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
self.assertEqual(self.account_settings_page.sections_structure(), expected_sections_structure)
|
||||
|
||||
def _test_readonly_field(self, field_id, title, value):
|
||||
"""
|
||||
Test behavior of a readonly field.
|
||||
"""
|
||||
self.assertEqual(self.account_settings_page.title_for_field(field_id), title)
|
||||
self.assertEqual(self.account_settings_page.value_for_readonly_field(field_id), value)
|
||||
|
||||
def _test_text_field(
|
||||
self, field_id, title, initial_value, new_invalid_value, new_valid_values, success_message=SUCCESS_MESSAGE,
|
||||
assert_after_reload=True
|
||||
):
|
||||
"""
|
||||
Test behaviour of a text field.
|
||||
"""
|
||||
self.assertEqual(self.account_settings_page.title_for_field(field_id), title)
|
||||
self.assertEqual(self.account_settings_page.value_for_text_field(field_id), initial_value)
|
||||
|
||||
self.assertEqual(
|
||||
self.account_settings_page.value_for_text_field(field_id, new_invalid_value), new_invalid_value
|
||||
)
|
||||
self.account_settings_page.wait_for_indicator(field_id, 'validation-error')
|
||||
self.browser.refresh()
|
||||
self.assertNotEqual(self.account_settings_page.value_for_text_field(field_id), new_invalid_value)
|
||||
|
||||
for new_value in new_valid_values:
|
||||
self.assertEqual(self.account_settings_page.value_for_text_field(field_id, new_value), new_value)
|
||||
self.account_settings_page.wait_for_messsage(field_id, success_message)
|
||||
if assert_after_reload:
|
||||
self.browser.refresh()
|
||||
self.assertEqual(self.account_settings_page.value_for_text_field(field_id), new_value)
|
||||
|
||||
def _test_dropdown_field(
|
||||
self, field_id, title, initial_value, new_values, success_message=SUCCESS_MESSAGE, reloads_on_save=False
|
||||
):
|
||||
"""
|
||||
Test behaviour of a dropdown field.
|
||||
"""
|
||||
self.assertEqual(self.account_settings_page.title_for_field(field_id), title)
|
||||
self.assertEqual(self.account_settings_page.value_for_dropdown_field(field_id), initial_value)
|
||||
|
||||
for new_value in new_values:
|
||||
self.assertEqual(self.account_settings_page.value_for_dropdown_field(field_id, new_value), new_value)
|
||||
self.account_settings_page.wait_for_messsage(field_id, success_message)
|
||||
if reloads_on_save:
|
||||
self.account_settings_page.wait_for_loading_indicator()
|
||||
else:
|
||||
self.browser.refresh()
|
||||
self.account_settings_page.wait_for_page()
|
||||
self.assertEqual(self.account_settings_page.value_for_dropdown_field(field_id), new_value)
|
||||
|
||||
def _test_link_field(self, field_id, title, link_title, success_message):
|
||||
"""
|
||||
Test behaviour a link field.
|
||||
"""
|
||||
self.assertEqual(self.account_settings_page.title_for_field(field_id), title)
|
||||
self.assertEqual(self.account_settings_page.link_title_for_link_field(field_id), link_title)
|
||||
self.account_settings_page.click_on_link_in_link_field(field_id)
|
||||
self.account_settings_page.wait_for_messsage(field_id, success_message)
|
||||
|
||||
def test_username_field(self):
|
||||
"""
|
||||
Test behaviour of "Username" field.
|
||||
"""
|
||||
self._test_readonly_field('username', 'Username', self.username)
|
||||
|
||||
def test_full_name_field(self):
|
||||
"""
|
||||
Test behaviour of "Full Name" field.
|
||||
"""
|
||||
self._test_text_field(
|
||||
u'name',
|
||||
u'Full Name',
|
||||
self.username,
|
||||
u'@',
|
||||
[u'another name', self.username],
|
||||
)
|
||||
|
||||
self.verify_settings_changed_events(
|
||||
self.username, self.user_id,
|
||||
[
|
||||
{
|
||||
u"setting": u"name",
|
||||
u"old": self.username,
|
||||
u"new": u"another name",
|
||||
},
|
||||
{
|
||||
u"setting": u"name",
|
||||
u"old": u'another name',
|
||||
u"new": self.username,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
def test_email_field(self):
|
||||
"""
|
||||
Test behaviour of "Email" field.
|
||||
"""
|
||||
email = u"test@example.com"
|
||||
username, user_id = self.log_in_as_unique_user(email=email)
|
||||
self.visit_account_settings_page()
|
||||
self._test_text_field(
|
||||
u'email',
|
||||
u'Email Address',
|
||||
email,
|
||||
u'@',
|
||||
[u'me@here.com', u'you@there.com'],
|
||||
success_message='Click the link in the message to update your email address.',
|
||||
assert_after_reload=False
|
||||
)
|
||||
|
||||
self.verify_events_of_type(
|
||||
username,
|
||||
self.CHANGE_INITIATED_EVENT_NAME,
|
||||
[
|
||||
{
|
||||
u"user_id": long(user_id),
|
||||
u"setting": u"email",
|
||||
u"old": email,
|
||||
u"new": u'me@here.com'
|
||||
},
|
||||
{
|
||||
u"user_id": long(user_id),
|
||||
u"setting": u"email",
|
||||
u"old": email, # NOTE the first email change was never confirmed, so old has not changed.
|
||||
u"new": u'you@there.com'
|
||||
}
|
||||
],
|
||||
[self.ACCOUNT_SETTINGS_REFERER, self.ACCOUNT_SETTINGS_REFERER]
|
||||
)
|
||||
# Email is not saved until user confirms, so no events should have been
|
||||
# emitted.
|
||||
self.assert_event_emitted_num_times(user_id, 'email', 0)
|
||||
|
||||
def test_password_field(self):
|
||||
"""
|
||||
Test behaviour of "Password" field.
|
||||
"""
|
||||
self._test_link_field(
|
||||
u'password',
|
||||
u'Password',
|
||||
u'Reset Password',
|
||||
success_message='Click the link in the message to reset your password.',
|
||||
)
|
||||
|
||||
self.verify_events_of_type(
|
||||
self.username,
|
||||
self.CHANGE_INITIATED_EVENT_NAME,
|
||||
[{
|
||||
u"user_id": int(self.user_id),
|
||||
u"setting": "password",
|
||||
u"old": None,
|
||||
u"new": None
|
||||
}],
|
||||
[self.ACCOUNT_SETTINGS_REFERER]
|
||||
)
|
||||
# Like email, since the user has not confirmed their password change,
|
||||
# the field has not yet changed, so no events will have been emitted.
|
||||
self.assert_event_emitted_num_times(self.user_id, 'password', 0)
|
||||
|
||||
@skip(
|
||||
'On bokchoy test servers, language changes take a few reloads to fully realize '
|
||||
'which means we can no longer reliably match the strings in the html in other tests.'
|
||||
)
|
||||
def test_language_field(self):
|
||||
"""
|
||||
Test behaviour of "Language" field.
|
||||
"""
|
||||
self._test_dropdown_field(
|
||||
u'pref-lang',
|
||||
u'Language',
|
||||
u'English',
|
||||
[u'Dummy Language (Esperanto)', u'English'],
|
||||
reloads_on_save=True,
|
||||
)
|
||||
|
||||
def test_education_completed_field(self):
|
||||
"""
|
||||
Test behaviour of "Education Completed" field.
|
||||
"""
|
||||
self._test_dropdown_field(
|
||||
u'level_of_education',
|
||||
u'Education Completed',
|
||||
u'',
|
||||
[u'Bachelor\'s degree', u''],
|
||||
)
|
||||
self.verify_settings_changed_events(
|
||||
self.username, self.user_id,
|
||||
[
|
||||
{
|
||||
u"setting": u"level_of_education",
|
||||
u"old": None,
|
||||
u"new": u'b',
|
||||
},
|
||||
{
|
||||
u"setting": u"level_of_education",
|
||||
u"old": u'b',
|
||||
u"new": None,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
def test_gender_field(self):
|
||||
"""
|
||||
Test behaviour of "Gender" field.
|
||||
"""
|
||||
self._test_dropdown_field(
|
||||
u'gender',
|
||||
u'Gender',
|
||||
u'',
|
||||
[u'Female', u''],
|
||||
)
|
||||
self.verify_settings_changed_events(
|
||||
self.username, self.user_id,
|
||||
[
|
||||
{
|
||||
u"setting": u"gender",
|
||||
u"old": None,
|
||||
u"new": u'f',
|
||||
},
|
||||
{
|
||||
u"setting": u"gender",
|
||||
u"old": u'f',
|
||||
u"new": None,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
def test_year_of_birth_field(self):
|
||||
"""
|
||||
Test behaviour of "Year of Birth" field.
|
||||
"""
|
||||
# Note that when we clear the year_of_birth here we're firing an event.
|
||||
self.assertEqual(self.account_settings_page.value_for_dropdown_field('year_of_birth', ''), '')
|
||||
self.reset_event_tracking()
|
||||
self._test_dropdown_field(
|
||||
u'year_of_birth',
|
||||
u'Year of Birth',
|
||||
u'',
|
||||
[u'1980', u''],
|
||||
)
|
||||
self.verify_settings_changed_events(
|
||||
self.username, self.user_id,
|
||||
[
|
||||
{
|
||||
u"setting": u"year_of_birth",
|
||||
u"old": None,
|
||||
u"new": 1980L,
|
||||
},
|
||||
{
|
||||
u"setting": u"year_of_birth",
|
||||
u"old": 1980L,
|
||||
u"new": None,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
def test_country_field(self):
|
||||
"""
|
||||
Test behaviour of "Country or Region" field.
|
||||
"""
|
||||
self._test_dropdown_field(
|
||||
u'country',
|
||||
u'Country or Region',
|
||||
u'',
|
||||
[u'Pakistan', u'Palau'],
|
||||
)
|
||||
|
||||
def test_preferred_language_field(self):
|
||||
"""
|
||||
Test behaviour of "Preferred Language" field.
|
||||
"""
|
||||
self._test_dropdown_field(
|
||||
u'language_proficiencies',
|
||||
u'Preferred Language',
|
||||
u'',
|
||||
[u'Pushto', u''],
|
||||
)
|
||||
|
||||
self.verify_settings_changed_events(
|
||||
self.username, self.user_id,
|
||||
[
|
||||
{
|
||||
u"setting": u"language_proficiencies",
|
||||
u"old": [],
|
||||
u"new": [{u"code": u"ps"}],
|
||||
},
|
||||
{
|
||||
u"setting": u"language_proficiencies",
|
||||
u"old": [{u"code": u"ps"}],
|
||||
u"new": [],
|
||||
}
|
||||
],
|
||||
table=u"student_languageproficiency"
|
||||
)
|
||||
|
||||
def test_connected_accounts(self):
|
||||
"""
|
||||
Test that fields for third party auth providers exist.
|
||||
|
||||
Currently there is no way to test the whole authentication process
|
||||
because that would require accounts with the providers.
|
||||
"""
|
||||
for field_id, title, link_title in [
|
||||
['auth-facebook', 'Facebook', 'Link'],
|
||||
['auth-google', 'Google', 'Link'],
|
||||
]:
|
||||
self.assertEqual(self.account_settings_page.title_for_field(field_id), title)
|
||||
self.assertEqual(self.account_settings_page.link_title_for_link_field(field_id), link_title)
|
||||
704
common/test/acceptance/tests/lms/test_learner_profile.py
Normal file
@@ -0,0 +1,704 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
End-to-end tests for Student's Profile Page.
|
||||
"""
|
||||
from datetime import datetime
|
||||
from bok_choy.web_app_test import WebAppTest
|
||||
|
||||
from ...pages.common.logout import LogoutPage
|
||||
from ...pages.lms.account_settings import AccountSettingsPage
|
||||
from ...pages.lms.auto_auth import AutoAuthPage
|
||||
from ...pages.lms.learner_profile import LearnerProfilePage
|
||||
from ...pages.lms.dashboard import DashboardPage
|
||||
|
||||
from ..helpers import EventsTestMixin
|
||||
|
||||
|
||||
class LearnerProfileTestMixin(EventsTestMixin):
|
||||
"""
|
||||
Mixin with helper methods for testing learner profile pages.
|
||||
"""
|
||||
|
||||
PRIVACY_PUBLIC = u'all_users'
|
||||
PRIVACY_PRIVATE = u'private'
|
||||
|
||||
PUBLIC_PROFILE_FIELDS = ['username', 'country', 'language_proficiencies', 'bio']
|
||||
PRIVATE_PROFILE_FIELDS = ['username']
|
||||
|
||||
PUBLIC_PROFILE_EDITABLE_FIELDS = ['country', 'language_proficiencies', 'bio']
|
||||
|
||||
USER_SETTINGS_CHANGED_EVENT_NAME = u"edx.user.settings.changed"
|
||||
|
||||
def log_in_as_unique_user(self):
|
||||
"""
|
||||
Create a unique user and return the account's username and id.
|
||||
"""
|
||||
username = "test_{uuid}".format(uuid=self.unique_id[0:6])
|
||||
auto_auth_page = AutoAuthPage(self.browser, username=username).visit()
|
||||
user_id = auto_auth_page.get_user_id()
|
||||
return username, user_id
|
||||
|
||||
def set_public_profile_fields_data(self, profile_page):
|
||||
"""
|
||||
Fill in the public profile fields of a user.
|
||||
"""
|
||||
profile_page.value_for_dropdown_field('language_proficiencies', 'English')
|
||||
profile_page.value_for_dropdown_field('country', 'United Kingdom')
|
||||
profile_page.value_for_textarea_field('bio', 'Nothing Special')
|
||||
|
||||
def visit_profile_page(self, username, privacy=None):
|
||||
"""
|
||||
Visits a user's profile page.
|
||||
"""
|
||||
profile_page = LearnerProfilePage(self.browser, username)
|
||||
|
||||
# Change the privacy if requested by loading the page and
|
||||
# changing the drop down
|
||||
if privacy is not None:
|
||||
profile_page.visit()
|
||||
profile_page.wait_for_page()
|
||||
profile_page.privacy = privacy
|
||||
|
||||
if privacy == self.PRIVACY_PUBLIC:
|
||||
self.set_public_profile_fields_data(profile_page)
|
||||
|
||||
# Reset event tracking so that the tests only see events from
|
||||
# loading the profile page.
|
||||
self.reset_event_tracking()
|
||||
|
||||
# Load the page
|
||||
profile_page.visit()
|
||||
profile_page.wait_for_page()
|
||||
|
||||
return profile_page
|
||||
|
||||
def set_birth_year(self, birth_year):
|
||||
"""
|
||||
Set birth year for the current user to the specified value.
|
||||
"""
|
||||
account_settings_page = AccountSettingsPage(self.browser)
|
||||
account_settings_page.visit()
|
||||
account_settings_page.wait_for_page()
|
||||
self.assertEqual(
|
||||
account_settings_page.value_for_dropdown_field('year_of_birth', str(birth_year)),
|
||||
str(birth_year)
|
||||
)
|
||||
|
||||
def verify_profile_page_is_public(self, profile_page, is_editable=True):
|
||||
"""
|
||||
Verify that the profile page is currently public.
|
||||
"""
|
||||
self.assertEqual(profile_page.visible_fields, self.PUBLIC_PROFILE_FIELDS)
|
||||
if is_editable:
|
||||
self.assertTrue(profile_page.privacy_field_visible)
|
||||
self.assertEqual(profile_page.editable_fields, self.PUBLIC_PROFILE_EDITABLE_FIELDS)
|
||||
else:
|
||||
self.assertEqual(profile_page.editable_fields, [])
|
||||
|
||||
def verify_profile_page_is_private(self, profile_page, is_editable=True):
|
||||
"""
|
||||
Verify that the profile page is currently private.
|
||||
"""
|
||||
if is_editable:
|
||||
self.assertTrue(profile_page.privacy_field_visible)
|
||||
self.assertEqual(profile_page.visible_fields, self.PRIVATE_PROFILE_FIELDS)
|
||||
|
||||
def verify_profile_page_view_event(self, requesting_username, profile_user_id, visibility=None):
|
||||
"""
|
||||
Verifies that the correct view event was captured for the profile page.
|
||||
"""
|
||||
self.verify_events_of_type(
|
||||
requesting_username,
|
||||
u"edx.user.settings.viewed",
|
||||
[{
|
||||
u"user_id": int(profile_user_id),
|
||||
u"page": u"profile",
|
||||
u"visibility": unicode(visibility),
|
||||
}]
|
||||
)
|
||||
|
||||
def assert_event_emitted_num_times(self, profile_user_id, setting, num_times):
|
||||
"""
|
||||
Verify a particular user settings change event was emitted a certain
|
||||
number of times.
|
||||
"""
|
||||
# pylint disable=no-member
|
||||
super(LearnerProfileTestMixin, self).assert_event_emitted_num_times(
|
||||
self.USER_SETTINGS_CHANGED_EVENT_NAME, self.start_time, profile_user_id, num_times, setting=setting
|
||||
)
|
||||
|
||||
def verify_user_preference_changed_event(self, username, user_id, setting, old_value=None, new_value=None):
|
||||
"""
|
||||
Verifies that the correct user preference changed event was recorded.
|
||||
"""
|
||||
self.verify_events_of_type(
|
||||
username,
|
||||
self.USER_SETTINGS_CHANGED_EVENT_NAME,
|
||||
[{
|
||||
u"user_id": long(user_id),
|
||||
u"table": u"user_api_userpreference",
|
||||
u"setting": unicode(setting),
|
||||
u"old": old_value,
|
||||
u"new": new_value,
|
||||
u"truncated": [],
|
||||
}],
|
||||
expected_referers=["/u/{username}".format(username=username)],
|
||||
)
|
||||
|
||||
|
||||
class OwnLearnerProfilePageTest(LearnerProfileTestMixin, WebAppTest):
|
||||
"""
|
||||
Tests that verify a student's own profile page.
|
||||
"""
|
||||
|
||||
def verify_profile_forced_private_message(self, username, birth_year, message=None):
|
||||
"""
|
||||
Verify age limit messages for a user.
|
||||
"""
|
||||
self.set_birth_year(birth_year=birth_year if birth_year is not None else "")
|
||||
profile_page = self.visit_profile_page(username)
|
||||
self.assertTrue(profile_page.privacy_field_visible)
|
||||
self.assertEqual(profile_page.age_limit_message_present, message is not None)
|
||||
self.assertIn(message, profile_page.profile_forced_private_message)
|
||||
|
||||
def test_profile_defaults_to_public(self):
|
||||
"""
|
||||
Scenario: Verify that a new user's profile defaults to public.
|
||||
|
||||
Given that I am a new user.
|
||||
When I go to my profile page.
|
||||
Then I see that the profile visibility is set to public.
|
||||
"""
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
profile_page = self.visit_profile_page(username)
|
||||
self.verify_profile_page_is_public(profile_page)
|
||||
|
||||
def assert_default_image_has_public_access(self, profile_page):
|
||||
"""
|
||||
Assert that profile image has public access.
|
||||
"""
|
||||
self.assertTrue(profile_page.profile_has_default_image)
|
||||
self.assertTrue(profile_page.profile_has_image_with_public_access())
|
||||
|
||||
def test_make_profile_public(self):
|
||||
"""
|
||||
Scenario: Verify that the user can change their privacy.
|
||||
|
||||
Given that I am a registered user
|
||||
And I visit my private profile page
|
||||
And I set the profile visibility to public
|
||||
Then a user preference changed event should be recorded
|
||||
When I reload the page
|
||||
Then the profile visibility should be shown as public
|
||||
"""
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
profile_page = self.visit_profile_page(username, privacy=self.PRIVACY_PRIVATE)
|
||||
profile_page.privacy = self.PRIVACY_PUBLIC
|
||||
self.verify_user_preference_changed_event(
|
||||
username, user_id, "account_privacy",
|
||||
old_value=self.PRIVACY_PRIVATE, # Note: default value was public, so we first change to private
|
||||
new_value=self.PRIVACY_PUBLIC,
|
||||
)
|
||||
|
||||
# Reload the page and verify that the profile is now public
|
||||
self.browser.refresh()
|
||||
profile_page.wait_for_page()
|
||||
self.verify_profile_page_is_public(profile_page)
|
||||
|
||||
def test_make_profile_private(self):
|
||||
"""
|
||||
Scenario: Verify that the user can change their privacy.
|
||||
|
||||
Given that I am a registered user
|
||||
And I visit my public profile page
|
||||
And I set the profile visibility to private
|
||||
Then a user preference changed event should be recorded
|
||||
When I reload the page
|
||||
Then the profile visibility should be shown as private
|
||||
"""
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
profile_page = self.visit_profile_page(username, privacy=self.PRIVACY_PUBLIC)
|
||||
profile_page.privacy = self.PRIVACY_PRIVATE
|
||||
self.verify_user_preference_changed_event(
|
||||
username, user_id, "account_privacy",
|
||||
old_value=None, # Note: no old value as the default preference is public
|
||||
new_value=self.PRIVACY_PRIVATE,
|
||||
)
|
||||
|
||||
# Reload the page and verify that the profile is now private
|
||||
self.browser.refresh()
|
||||
profile_page.wait_for_page()
|
||||
self.verify_profile_page_is_private(profile_page)
|
||||
|
||||
def test_dashboard_learner_profile_link(self):
|
||||
"""
|
||||
Scenario: Verify that my profile link is present on dashboard page and we can navigate to correct page.
|
||||
|
||||
Given that I am a registered user.
|
||||
When I go to Dashboard page.
|
||||
And I click on username dropdown.
|
||||
Then I see My Profile link in the dropdown menu.
|
||||
When I click on My Profile link.
|
||||
Then I will be navigated to My Profile page.
|
||||
"""
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
dashboard_page = DashboardPage(self.browser)
|
||||
dashboard_page.visit()
|
||||
dashboard_page.click_username_dropdown()
|
||||
self.assertTrue('My Profile' in dashboard_page.username_dropdown_link_text)
|
||||
dashboard_page.click_my_profile_link()
|
||||
my_profile_page = LearnerProfilePage(self.browser, username)
|
||||
my_profile_page.wait_for_page()
|
||||
|
||||
def test_fields_on_my_private_profile(self):
|
||||
"""
|
||||
Scenario: Verify that desired fields are shown when looking at her own private profile.
|
||||
|
||||
Given that I am a registered user.
|
||||
And I visit My Profile page.
|
||||
And I set the profile visibility to private.
|
||||
And I reload the page.
|
||||
Then I should see the profile visibility selector dropdown.
|
||||
Then I see some of the profile fields are shown.
|
||||
"""
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
profile_page = self.visit_profile_page(username, privacy=self.PRIVACY_PRIVATE)
|
||||
self.verify_profile_page_is_private(profile_page)
|
||||
self.verify_profile_page_view_event(username, user_id, visibility=self.PRIVACY_PRIVATE)
|
||||
|
||||
def test_fields_on_my_public_profile(self):
|
||||
"""
|
||||
Scenario: Verify that desired fields are shown when looking at her own public profile.
|
||||
|
||||
Given that I am a registered user.
|
||||
And I visit My Profile page.
|
||||
And I set the profile visibility to public.
|
||||
And I reload the page.
|
||||
Then I should see the profile visibility selector dropdown.
|
||||
Then I see all the profile fields are shown.
|
||||
And `location`, `language` and `about me` fields are editable.
|
||||
"""
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
profile_page = self.visit_profile_page(username, privacy=self.PRIVACY_PUBLIC)
|
||||
self.verify_profile_page_is_public(profile_page)
|
||||
self.verify_profile_page_view_event(username, user_id, visibility=self.PRIVACY_PUBLIC)
|
||||
|
||||
def _test_dropdown_field(self, profile_page, field_id, new_value, displayed_value, mode):
|
||||
"""
|
||||
Test behaviour of a dropdown field.
|
||||
"""
|
||||
profile_page.value_for_dropdown_field(field_id, new_value)
|
||||
self.assertEqual(profile_page.get_non_editable_mode_value(field_id), displayed_value)
|
||||
self.assertTrue(profile_page.mode_for_field(field_id), mode)
|
||||
|
||||
self.browser.refresh()
|
||||
profile_page.wait_for_page()
|
||||
|
||||
self.assertEqual(profile_page.get_non_editable_mode_value(field_id), displayed_value)
|
||||
self.assertTrue(profile_page.mode_for_field(field_id), mode)
|
||||
|
||||
def _test_textarea_field(self, profile_page, field_id, new_value, displayed_value, mode):
|
||||
"""
|
||||
Test behaviour of a textarea field.
|
||||
"""
|
||||
profile_page.value_for_textarea_field(field_id, new_value)
|
||||
self.assertEqual(profile_page.get_non_editable_mode_value(field_id), displayed_value)
|
||||
self.assertTrue(profile_page.mode_for_field(field_id), mode)
|
||||
|
||||
self.browser.refresh()
|
||||
profile_page.wait_for_page()
|
||||
|
||||
self.assertEqual(profile_page.get_non_editable_mode_value(field_id), displayed_value)
|
||||
self.assertTrue(profile_page.mode_for_field(field_id), mode)
|
||||
|
||||
def test_country_field(self):
|
||||
"""
|
||||
Test behaviour of `Country` field.
|
||||
|
||||
Given that I am a registered user.
|
||||
And I visit My Profile page.
|
||||
And I set the profile visibility to public and set default values for public fields.
|
||||
Then I set country value to `Pakistan`.
|
||||
Then displayed country should be `Pakistan` and country field mode should be `display`
|
||||
And I reload the page.
|
||||
Then displayed country should be `Pakistan` and country field mode should be `display`
|
||||
And I make `country` field editable
|
||||
Then `country` field mode should be `edit`
|
||||
And `country` field icon should be visible.
|
||||
"""
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
profile_page = self.visit_profile_page(username, privacy=self.PRIVACY_PUBLIC)
|
||||
self._test_dropdown_field(profile_page, 'country', 'Pakistan', 'Pakistan', 'display')
|
||||
|
||||
profile_page.make_field_editable('country')
|
||||
self.assertTrue(profile_page.mode_for_field('country'), 'edit')
|
||||
|
||||
self.assertTrue(profile_page.field_icon_present('country'))
|
||||
|
||||
def test_language_field(self):
|
||||
"""
|
||||
Test behaviour of `Language` field.
|
||||
|
||||
Given that I am a registered user.
|
||||
And I visit My Profile page.
|
||||
And I set the profile visibility to public and set default values for public fields.
|
||||
Then I set language value to `Urdu`.
|
||||
Then displayed language should be `Urdu` and language field mode should be `display`
|
||||
And I reload the page.
|
||||
Then displayed language should be `Urdu` and language field mode should be `display`
|
||||
Then I set empty value for language.
|
||||
Then displayed language should be `Add language` and language field mode should be `placeholder`
|
||||
And I reload the page.
|
||||
Then displayed language should be `Add language` and language field mode should be `placeholder`
|
||||
And I make `language` field editable
|
||||
Then `language` field mode should be `edit`
|
||||
And `language` field icon should be visible.
|
||||
"""
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
profile_page = self.visit_profile_page(username, privacy=self.PRIVACY_PUBLIC)
|
||||
self._test_dropdown_field(profile_page, 'language_proficiencies', 'Urdu', 'Urdu', 'display')
|
||||
self._test_dropdown_field(profile_page, 'language_proficiencies', '', 'Add language', 'placeholder')
|
||||
|
||||
profile_page.make_field_editable('language_proficiencies')
|
||||
self.assertTrue(profile_page.mode_for_field('language_proficiencies'), 'edit')
|
||||
|
||||
self.assertTrue(profile_page.field_icon_present('language_proficiencies'))
|
||||
|
||||
def test_about_me_field(self):
|
||||
"""
|
||||
Test behaviour of `About Me` field.
|
||||
|
||||
Given that I am a registered user.
|
||||
And I visit My Profile page.
|
||||
And I set the profile visibility to public and set default values for public fields.
|
||||
Then I set about me value to `Eat Sleep Code`.
|
||||
Then displayed about me should be `Eat Sleep Code` and about me field mode should be `display`
|
||||
And I reload the page.
|
||||
Then displayed about me should be `Eat Sleep Code` and about me field mode should be `display`
|
||||
Then I set empty value for about me.
|
||||
Then displayed about me should be `Tell other edX learners a little about yourself: where you live,
|
||||
what your interests are, why you're taking courses on edX, or what you hope to learn.` and about me
|
||||
field mode should be `placeholder`
|
||||
And I reload the page.
|
||||
Then displayed about me should be `Tell other edX learners a little about yourself: where you live,
|
||||
what your interests are, why you're taking courses on edX, or what you hope to learn.` and about me
|
||||
field mode should be `placeholder`
|
||||
And I make `about me` field editable
|
||||
Then `about me` field mode should be `edit`
|
||||
"""
|
||||
placeholder_value = (
|
||||
"Tell other edX learners a little about yourself: where you live, what your interests are, "
|
||||
"why you're taking courses on edX, or what you hope to learn."
|
||||
)
|
||||
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
profile_page = self.visit_profile_page(username, privacy=self.PRIVACY_PUBLIC)
|
||||
self._test_textarea_field(profile_page, 'bio', 'Eat Sleep Code', 'Eat Sleep Code', 'display')
|
||||
self._test_textarea_field(profile_page, 'bio', '', placeholder_value, 'placeholder')
|
||||
|
||||
profile_page.make_field_editable('bio')
|
||||
self.assertTrue(profile_page.mode_for_field('bio'), 'edit')
|
||||
|
||||
def test_birth_year_not_set(self):
|
||||
"""
|
||||
Verify message if birth year is not set.
|
||||
|
||||
Given that I am a registered user.
|
||||
And birth year is not set for the user.
|
||||
And I visit my profile page.
|
||||
Then I should see a message that the profile is private until the year of birth is set.
|
||||
"""
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
message = "You must specify your birth year before you can share your full profile."
|
||||
self.verify_profile_forced_private_message(username, birth_year=None, message=message)
|
||||
self.verify_profile_page_view_event(username, user_id, visibility=self.PRIVACY_PRIVATE)
|
||||
|
||||
def test_user_is_under_age(self):
|
||||
"""
|
||||
Verify message if user is under age.
|
||||
|
||||
Given that I am a registered user.
|
||||
And birth year is set so that age is less than 13.
|
||||
And I visit my profile page.
|
||||
Then I should see a message that the profile is private as I am under thirteen.
|
||||
"""
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
under_age_birth_year = datetime.now().year - 10
|
||||
self.verify_profile_forced_private_message(
|
||||
username,
|
||||
birth_year=under_age_birth_year,
|
||||
message='You must be over 13 to share a full profile.'
|
||||
)
|
||||
self.verify_profile_page_view_event(username, user_id, visibility=self.PRIVACY_PRIVATE)
|
||||
|
||||
def test_user_can_only_see_default_image_for_private_profile(self):
|
||||
"""
|
||||
Scenario: Default profile image behaves correctly for under age user.
|
||||
|
||||
Given that I am on my profile page with private access
|
||||
And I can see default image
|
||||
When I move my cursor to the image
|
||||
Then i cannot see the upload/remove image text
|
||||
And i cannot upload/remove the image.
|
||||
"""
|
||||
year_of_birth = datetime.now().year - 5
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
profile_page = self.visit_profile_page(username, privacy=self.PRIVACY_PRIVATE)
|
||||
|
||||
self.verify_profile_forced_private_message(
|
||||
username,
|
||||
year_of_birth,
|
||||
message='You must be over 13 to share a full profile.'
|
||||
)
|
||||
self.assertTrue(profile_page.profile_has_default_image)
|
||||
self.assertFalse(profile_page.profile_has_image_with_private_access())
|
||||
|
||||
def test_user_can_see_default_image_for_public_profile(self):
|
||||
"""
|
||||
Scenario: Default profile image behaves correctly for public profile.
|
||||
|
||||
Given that I am on my profile page with public access
|
||||
And I can see default image
|
||||
When I move my cursor to the image
|
||||
Then i can see the upload/remove image text
|
||||
And i am able to upload new image
|
||||
"""
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
profile_page = self.visit_profile_page(username, privacy=self.PRIVACY_PUBLIC)
|
||||
|
||||
self.assert_default_image_has_public_access(profile_page)
|
||||
|
||||
def test_user_can_upload_the_profile_image_with_success(self):
|
||||
"""
|
||||
Scenario: Upload profile image works correctly.
|
||||
|
||||
Given that I am on my profile page with public access
|
||||
And I can see default image
|
||||
When I move my cursor to the image
|
||||
Then i can see the upload/remove image text
|
||||
When i upload new image via file uploader
|
||||
Then i can see the changed image
|
||||
And i can also see the latest image after reload.
|
||||
"""
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
profile_page = self.visit_profile_page(username, privacy=self.PRIVACY_PUBLIC)
|
||||
|
||||
self.assert_default_image_has_public_access(profile_page)
|
||||
|
||||
profile_page.upload_file(filename='image.jpg')
|
||||
self.assertTrue(profile_page.image_upload_success)
|
||||
profile_page.visit()
|
||||
self.assertTrue(profile_page.image_upload_success)
|
||||
|
||||
self.assert_event_emitted_num_times(user_id, 'profile_image_uploaded_at', 1)
|
||||
|
||||
def test_user_can_see_error_for_exceeding_max_file_size_limit(self):
|
||||
"""
|
||||
Scenario: Upload profile image does not work for > 1MB image file.
|
||||
|
||||
Given that I am on my profile page with public access
|
||||
And I can see default image
|
||||
When I move my cursor to the image
|
||||
Then i can see the upload/remove image text
|
||||
When i upload new > 1MB image via file uploader
|
||||
Then i can see the error message for file size limit
|
||||
And i can still see the default image after page reload.
|
||||
"""
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
profile_page = self.visit_profile_page(username, privacy=self.PRIVACY_PUBLIC)
|
||||
|
||||
self.assert_default_image_has_public_access(profile_page)
|
||||
|
||||
profile_page.upload_file(filename='larger_image.jpg')
|
||||
self.assertEqual(profile_page.profile_image_message, "The file must be smaller than 1 MB in size.")
|
||||
profile_page.visit()
|
||||
self.assertTrue(profile_page.profile_has_default_image)
|
||||
|
||||
self.assert_event_emitted_num_times(user_id, 'profile_image_uploaded_at', 0)
|
||||
|
||||
def test_user_can_see_error_for_file_size_below_the_min_limit(self):
|
||||
"""
|
||||
Scenario: Upload profile image does not work for < 100 Bytes image file.
|
||||
|
||||
Given that I am on my profile page with public access
|
||||
And I can see default image
|
||||
When I move my cursor to the image
|
||||
Then i can see the upload/remove image text
|
||||
When i upload new < 100 Bytes image via file uploader
|
||||
Then i can see the error message for minimum file size limit
|
||||
And i can still see the default image after page reload.
|
||||
"""
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
profile_page = self.visit_profile_page(username, privacy=self.PRIVACY_PUBLIC)
|
||||
|
||||
self.assert_default_image_has_public_access(profile_page)
|
||||
|
||||
profile_page.upload_file(filename='list-icon-visited.png')
|
||||
self.assertEqual(profile_page.profile_image_message, "The file must be at least 100 bytes in size.")
|
||||
profile_page.visit()
|
||||
self.assertTrue(profile_page.profile_has_default_image)
|
||||
|
||||
self.assert_event_emitted_num_times(user_id, 'profile_image_uploaded_at', 0)
|
||||
|
||||
def test_user_can_see_error_for_wrong_file_type(self):
|
||||
"""
|
||||
Scenario: Upload profile image does not work for wrong file types.
|
||||
|
||||
Given that I am on my profile page with public access
|
||||
And I can see default image
|
||||
When I move my cursor to the image
|
||||
Then i can see the upload/remove image text
|
||||
When i upload new csv file via file uploader
|
||||
Then i can see the error message for wrong/unsupported file type
|
||||
And i can still see the default image after page reload.
|
||||
"""
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
profile_page = self.visit_profile_page(username, privacy=self.PRIVACY_PUBLIC)
|
||||
|
||||
self.assert_default_image_has_public_access(profile_page)
|
||||
|
||||
profile_page.upload_file(filename='cohort_users_only_username.csv')
|
||||
self.assertEqual(
|
||||
profile_page.profile_image_message,
|
||||
"The file must be one of the following types: .gif, .png, .jpeg, .jpg."
|
||||
)
|
||||
profile_page.visit()
|
||||
self.assertTrue(profile_page.profile_has_default_image)
|
||||
|
||||
self.assert_event_emitted_num_times(user_id, 'profile_image_uploaded_at', 0)
|
||||
|
||||
def test_user_can_remove_profile_image(self):
|
||||
"""
|
||||
Scenario: Remove profile image works correctly.
|
||||
|
||||
Given that I am on my profile page with public access
|
||||
And I can see default image
|
||||
When I move my cursor to the image
|
||||
Then i can see the upload/remove image text
|
||||
When i click on the remove image link
|
||||
Then i can see the default image
|
||||
And i can still see the default image after page reload.
|
||||
"""
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
profile_page = self.visit_profile_page(username, privacy=self.PRIVACY_PUBLIC)
|
||||
|
||||
self.assert_default_image_has_public_access(profile_page)
|
||||
|
||||
profile_page.upload_file(filename='image.jpg')
|
||||
self.assertTrue(profile_page.image_upload_success)
|
||||
self.assertTrue(profile_page.remove_profile_image())
|
||||
self.assertTrue(profile_page.profile_has_default_image)
|
||||
profile_page.visit()
|
||||
self.assertTrue(profile_page.profile_has_default_image)
|
||||
|
||||
self.assert_event_emitted_num_times(user_id, 'profile_image_uploaded_at', 2)
|
||||
|
||||
def test_user_cannot_remove_default_image(self):
|
||||
"""
|
||||
Scenario: Remove profile image does not works for default images.
|
||||
|
||||
Given that I am on my profile page with public access
|
||||
And I can see default image
|
||||
When I move my cursor to the image
|
||||
Then i can see only the upload image text
|
||||
And i cannot see the remove image text
|
||||
"""
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
profile_page = self.visit_profile_page(username, privacy=self.PRIVACY_PUBLIC)
|
||||
|
||||
self.assert_default_image_has_public_access(profile_page)
|
||||
self.assertFalse(profile_page.remove_link_present)
|
||||
|
||||
def test_eventing_after_multiple_uploads(self):
|
||||
"""
|
||||
Scenario: An event is fired when a user with a profile image uploads another image
|
||||
|
||||
Given that I am on my profile page with public access
|
||||
And I upload a new image via file uploader
|
||||
When I upload another image via the file uploader
|
||||
Then two upload events have been emitted
|
||||
"""
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
profile_page = self.visit_profile_page(username, privacy=self.PRIVACY_PUBLIC)
|
||||
self.assert_default_image_has_public_access(profile_page)
|
||||
profile_page.upload_file(filename='image.jpg')
|
||||
self.assertTrue(profile_page.image_upload_success)
|
||||
profile_page.upload_file(filename='image.jpg', wait_for_upload_button=False)
|
||||
self.assert_event_emitted_num_times(user_id, 'profile_image_uploaded_at', 2)
|
||||
|
||||
|
||||
class DifferentUserLearnerProfilePageTest(LearnerProfileTestMixin, WebAppTest):
|
||||
"""
|
||||
Tests that verify viewing the profile page of a different user.
|
||||
"""
|
||||
def test_different_user_private_profile(self):
|
||||
"""
|
||||
Scenario: Verify that desired fields are shown when looking at a different user's private profile.
|
||||
|
||||
Given that I am a registered user.
|
||||
And I visit a different user's private profile page.
|
||||
Then I shouldn't see the profile visibility selector dropdown.
|
||||
Then I see some of the profile fields are shown.
|
||||
"""
|
||||
different_username, different_user_id = self._initialize_different_user(privacy=self.PRIVACY_PRIVATE)
|
||||
username, __ = self.log_in_as_unique_user()
|
||||
profile_page = self.visit_profile_page(different_username)
|
||||
self.verify_profile_page_is_private(profile_page, is_editable=False)
|
||||
self.verify_profile_page_view_event(username, different_user_id, visibility=self.PRIVACY_PRIVATE)
|
||||
|
||||
def test_different_user_under_age(self):
|
||||
"""
|
||||
Scenario: Verify that an under age user's profile is private to others.
|
||||
|
||||
Given that I am a registered user.
|
||||
And I visit an under age user's profile page.
|
||||
Then I shouldn't see the profile visibility selector dropdown.
|
||||
Then I see that only the private fields are shown.
|
||||
"""
|
||||
under_age_birth_year = datetime.now().year - 10
|
||||
different_username, different_user_id = self._initialize_different_user(
|
||||
privacy=self.PRIVACY_PUBLIC,
|
||||
birth_year=under_age_birth_year
|
||||
)
|
||||
username, __ = self.log_in_as_unique_user()
|
||||
profile_page = self.visit_profile_page(different_username)
|
||||
self.verify_profile_page_is_private(profile_page, is_editable=False)
|
||||
self.verify_profile_page_view_event(username, different_user_id, visibility=self.PRIVACY_PRIVATE)
|
||||
|
||||
def test_different_user_public_profile(self):
|
||||
"""
|
||||
Scenario: Verify that desired fields are shown when looking at a different user's public profile.
|
||||
|
||||
Given that I am a registered user.
|
||||
And I visit a different user's public profile page.
|
||||
Then I shouldn't see the profile visibility selector dropdown.
|
||||
Then all the profile fields are shown.
|
||||
Then I shouldn't see the profile visibility selector dropdown.
|
||||
Also `location`, `language` and `about me` fields are not editable.
|
||||
"""
|
||||
different_username, different_user_id = self._initialize_different_user(privacy=self.PRIVACY_PUBLIC)
|
||||
username, __ = self.log_in_as_unique_user()
|
||||
profile_page = self.visit_profile_page(different_username)
|
||||
profile_page.wait_for_public_fields()
|
||||
self.verify_profile_page_is_public(profile_page, is_editable=False)
|
||||
self.verify_profile_page_view_event(username, different_user_id, visibility=self.PRIVACY_PUBLIC)
|
||||
|
||||
def _initialize_different_user(self, privacy=None, birth_year=None):
|
||||
"""
|
||||
Initialize the profile page for a different test user
|
||||
"""
|
||||
username, user_id = self.log_in_as_unique_user()
|
||||
|
||||
# Set the privacy for the new user
|
||||
if privacy is None:
|
||||
privacy = self.PRIVACY_PUBLIC
|
||||
self.visit_profile_page(username, privacy=privacy)
|
||||
|
||||
# Set the user's year of birth
|
||||
if birth_year:
|
||||
self.set_birth_year(birth_year)
|
||||
|
||||
# Log the user out
|
||||
LogoutPage(self.browser).visit()
|
||||
|
||||
return username, user_id
|
||||
@@ -173,9 +173,11 @@ class RegisterFromCombinedPageTest(UniqueCourseTest):
|
||||
course_names = self.dashboard_page.wait_for_page().available_courses
|
||||
self.assertIn(self.course_info["display_name"], course_names)
|
||||
|
||||
self.assertEqual("Test User", self.dashboard_page.full_name)
|
||||
self.assertEqual(email, self.dashboard_page.email)
|
||||
self.assertEqual(username, self.dashboard_page.username)
|
||||
self.assertEqual("want to change your account settings?", self.dashboard_page.sidebar_menu_title.lower())
|
||||
self.assertEqual(
|
||||
"click the arrow next to your username above.",
|
||||
self.dashboard_page.sidebar_menu_description.lower()
|
||||
)
|
||||
|
||||
def test_register_failure(self):
|
||||
# Navigate to the registration page
|
||||
@@ -369,61 +371,6 @@ class PayAndVerifyTest(EventsTestMixin, UniqueCourseTest):
|
||||
self.assertEqual(enrollment_mode, 'verified')
|
||||
|
||||
|
||||
class LanguageTest(WebAppTest):
|
||||
"""
|
||||
Tests that the change language functionality on the dashboard works
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Initiailize dashboard page
|
||||
"""
|
||||
super(LanguageTest, self).setUp()
|
||||
self.dashboard_page = DashboardPage(self.browser)
|
||||
|
||||
self.test_new_lang = 'eo'
|
||||
# This string is unicode for "ÇÜRRÉNT ÇØÜRSÉS", which should appear in our Dummy Esperanto page
|
||||
# We store the string this way because Selenium seems to try and read in strings from
|
||||
# the HTML in this format. Ideally we could just store the raw ÇÜRRÉNT ÇØÜRSÉS string here
|
||||
self.current_courses_text = u'\xc7\xdcRR\xc9NT \xc7\xd6\xdcRS\xc9S'
|
||||
|
||||
self.username = "test"
|
||||
self.password = "testpass"
|
||||
self.email = "test@example.com"
|
||||
|
||||
def test_change_lang(self):
|
||||
AutoAuthPage(self.browser).visit()
|
||||
self.dashboard_page.visit()
|
||||
# Change language to Dummy Esperanto
|
||||
self.dashboard_page.change_language(self.test_new_lang)
|
||||
|
||||
changed_text = self.dashboard_page.current_courses_text
|
||||
|
||||
# We should see the dummy-language text on the page
|
||||
self.assertIn(self.current_courses_text, changed_text)
|
||||
|
||||
def test_language_persists(self):
|
||||
auto_auth_page = AutoAuthPage(self.browser, username=self.username, password=self.password, email=self.email)
|
||||
auto_auth_page.visit()
|
||||
|
||||
self.dashboard_page.visit()
|
||||
# Change language to Dummy Esperanto
|
||||
self.dashboard_page.change_language(self.test_new_lang)
|
||||
|
||||
# destroy session
|
||||
self.browser.delete_all_cookies()
|
||||
|
||||
# log back in
|
||||
auto_auth_page.visit()
|
||||
|
||||
self.dashboard_page.visit()
|
||||
|
||||
changed_text = self.dashboard_page.current_courses_text
|
||||
|
||||
# We should see the dummy-language text on the page
|
||||
self.assertIn(self.current_courses_text, changed_text)
|
||||
|
||||
|
||||
class CourseWikiTest(UniqueCourseTest):
|
||||
"""
|
||||
Tests that verify the course wiki.
|
||||
|
||||
BIN
common/test/data/uploads/larger_image.jpg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
common/test/data/uploads/list-icon-visited.png
Normal file
|
After Width: | Height: | Size: 99 B |
@@ -77,6 +77,9 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
|
||||
sku=uuid4().hex.decode('ascii')
|
||||
)
|
||||
|
||||
# Ignore events fired from UserFactory creation
|
||||
self.reset_tracker()
|
||||
|
||||
def test_login_required(self):
|
||||
"""
|
||||
The view should return HTTP 403 status if the user is not logged in.
|
||||
|
||||
@@ -15,7 +15,7 @@ from course_modes.models import CourseMode
|
||||
from courseware import courses
|
||||
from enrollment.api import add_enrollment
|
||||
from student.models import CourseEnrollment
|
||||
from util.authentication import SessionAuthenticationAllowInactiveUser
|
||||
from openedx.core.lib.api.authentication import SessionAuthenticationAllowInactiveUser
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -9,6 +9,7 @@ import xml.sax.saxutils as saxutils
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.context_processors import csrf
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import Http404, HttpResponseBadRequest
|
||||
from django.views.decorators.http import require_GET
|
||||
@@ -411,16 +412,18 @@ def user_profile(request, course_key, user_id):
|
||||
'annotated_content_info': _attr_safe_json(annotated_content_info),
|
||||
})
|
||||
else:
|
||||
django_user = User.objects.get(id=user_id)
|
||||
context = {
|
||||
'course': course,
|
||||
'user': request.user,
|
||||
'django_user': User.objects.get(id=user_id),
|
||||
'django_user': django_user,
|
||||
'profiled_user': profiled_user.to_dict(),
|
||||
'threads': _attr_safe_json(threads),
|
||||
'user_info': _attr_safe_json(user_info),
|
||||
'annotated_content_info': _attr_safe_json(annotated_content_info),
|
||||
'page': query_params['page'],
|
||||
'num_pages': query_params['num_pages'],
|
||||
'learner_profile_page_url': reverse('learner_profile', kwargs={'username': django_user.username})
|
||||
}
|
||||
|
||||
return render_to_response('discussion/user_profile.html', context)
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
Common utility methods and decorators for Mobile APIs.
|
||||
"""
|
||||
|
||||
|
||||
import functools
|
||||
from rest_framework import permissions
|
||||
|
||||
from util.authentication import SessionAuthenticationAllowInactiveUser, OAuth2AuthenticationAllowInactiveUser
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from courseware.courses import get_course_with_access
|
||||
from openedx.core.lib.api.authentication import (
|
||||
SessionAuthenticationAllowInactiveUser,
|
||||
OAuth2AuthenticationAllowInactiveUser,
|
||||
)
|
||||
from openedx.core.lib.api.permissions import IsUserInUrl
|
||||
|
||||
|
||||
|
||||
@@ -9,20 +9,24 @@ import json
|
||||
import mock
|
||||
import ddt
|
||||
import markupsafe
|
||||
from django.test import TestCase
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core import mail
|
||||
from django.contrib import messages
|
||||
from django.contrib.messages.middleware import MessageMiddleware
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from util.testing import UrlResetMixin
|
||||
from third_party_auth.tests.testutil import simulate_running_pipeline
|
||||
from embargo.test_utils import restrict_course
|
||||
from openedx.core.djangoapps.user_api.accounts.api import activate_account, create_account
|
||||
from openedx.core.djangoapps.user_api.accounts import EMAIL_MAX_LENGTH
|
||||
from student.tests.factories import CourseModeFactory, UserFactory
|
||||
from student_account.views import account_settings_context
|
||||
from third_party_auth.tests.testutil import simulate_running_pipeline
|
||||
from util.testing import UrlResetMixin
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from student.tests.factories import CourseModeFactory
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -499,3 +503,66 @@ class StudentAccountLoginAndRegistrationTest(UrlResetMixin, ModuleStoreTestCase)
|
||||
url=reverse("social:begin", kwargs={"backend": backend_name}),
|
||||
params=urlencode(params)
|
||||
)
|
||||
|
||||
|
||||
class AccountSettingsViewTest(TestCase):
|
||||
""" Tests for the account settings view. """
|
||||
|
||||
USERNAME = 'student'
|
||||
PASSWORD = 'password'
|
||||
FIELDS = [
|
||||
'country',
|
||||
'gender',
|
||||
'language',
|
||||
'level_of_education',
|
||||
'password',
|
||||
'year_of_birth',
|
||||
'preferred_language',
|
||||
]
|
||||
|
||||
@mock.patch("django.conf.settings.MESSAGE_STORAGE", 'django.contrib.messages.storage.cookie.CookieStorage')
|
||||
def setUp(self):
|
||||
super(AccountSettingsViewTest, self).setUp()
|
||||
self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD)
|
||||
self.client.login(username=self.USERNAME, password=self.PASSWORD)
|
||||
|
||||
self.request = RequestFactory()
|
||||
self.request.user = self.user
|
||||
|
||||
# Python-social saves auth failure notifcations in Django messages.
|
||||
# See pipeline.get_duplicate_provider() for details.
|
||||
self.request.COOKIES = {}
|
||||
MessageMiddleware().process_request(self.request)
|
||||
messages.error(self.request, 'Facebook is already in use.', extra_tags='Auth facebook')
|
||||
|
||||
def test_context(self):
|
||||
|
||||
context = account_settings_context(self.request)
|
||||
|
||||
user_accounts_api_url = reverse("accounts_api", kwargs={'username': self.user.username})
|
||||
self.assertEqual(context['user_accounts_api_url'], user_accounts_api_url)
|
||||
|
||||
user_preferences_api_url = reverse('preferences_api', kwargs={'username': self.user.username})
|
||||
self.assertEqual(context['user_preferences_api_url'], user_preferences_api_url)
|
||||
|
||||
for attribute in self.FIELDS:
|
||||
self.assertIn(attribute, context['fields'])
|
||||
|
||||
self.assertEqual(
|
||||
context['user_accounts_api_url'], reverse("accounts_api", kwargs={'username': self.user.username})
|
||||
)
|
||||
self.assertEqual(
|
||||
context['user_preferences_api_url'], reverse('preferences_api', kwargs={'username': self.user.username})
|
||||
)
|
||||
|
||||
self.assertEqual(context['duplicate_provider'].BACKEND_CLASS.name, 'facebook')
|
||||
self.assertEqual(context['auth']['providers'][0]['name'], 'Facebook')
|
||||
self.assertEqual(context['auth']['providers'][1]['name'], 'Google')
|
||||
|
||||
def test_view(self):
|
||||
|
||||
view_path = reverse('account_settings')
|
||||
response = self.client.get(path=view_path)
|
||||
|
||||
for attribute in self.FIELDS:
|
||||
self.assertIn(attribute, response.content)
|
||||
|
||||
@@ -11,3 +11,8 @@ if settings.FEATURES.get('ENABLE_COMBINED_LOGIN_REGISTRATION'):
|
||||
url(r'^register/$', 'login_and_registration_form', {'initial_mode': 'register'}, name='account_register'),
|
||||
url(r'^password$', 'password_change_request_handler', name='password_change_request'),
|
||||
)
|
||||
|
||||
urlpatterns += patterns(
|
||||
'student_account.views',
|
||||
url(r'^settings$', 'account_settings', name='account_settings'),
|
||||
)
|
||||
|
||||
@@ -5,30 +5,39 @@ import json
|
||||
from ipware.ip import get_ip
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import (
|
||||
HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
|
||||
)
|
||||
from django.shortcuts import redirect
|
||||
from django.http import HttpRequest
|
||||
from django_countries import countries
|
||||
from django.core.urlresolvers import reverse, resolve
|
||||
from django.utils.translation import ugettext as _
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from lang_pref.api import released_languages
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from microsite_configuration import microsite
|
||||
|
||||
from embargo import api as embargo_api
|
||||
import third_party_auth
|
||||
from external_auth.login_and_register import (
|
||||
login as external_auth_login,
|
||||
register as external_auth_register
|
||||
)
|
||||
from student.models import UserProfile
|
||||
from student.views import (
|
||||
signin_user as old_login_view,
|
||||
register_user as old_register_view
|
||||
)
|
||||
from student_account.helpers import auth_pipeline_urls
|
||||
import third_party_auth
|
||||
from third_party_auth import pipeline
|
||||
from util.bad_request_rate_limiter import BadRequestRateLimiter
|
||||
|
||||
from openedx.core.djangoapps.user_api.accounts.api import request_password_change
|
||||
from openedx.core.djangoapps.user_api.errors import UserNotFound
|
||||
@@ -294,3 +303,96 @@ def _external_auth_intercept(request, mode):
|
||||
return external_auth_login(request)
|
||||
elif mode == "register":
|
||||
return external_auth_register(request)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(['GET'])
|
||||
def account_settings(request):
|
||||
"""Render the current user's account settings page.
|
||||
|
||||
Args:
|
||||
request (HttpRequest)
|
||||
|
||||
Returns:
|
||||
HttpResponse: 200 if the page was sent successfully
|
||||
HttpResponse: 302 if not logged in (redirect to login page)
|
||||
HttpResponse: 405 if using an unsupported HTTP method
|
||||
|
||||
Example usage:
|
||||
|
||||
GET /account/settings
|
||||
|
||||
"""
|
||||
return render_to_response('student_account/account_settings.html', account_settings_context(request))
|
||||
|
||||
|
||||
def account_settings_context(request):
|
||||
""" Context for the account settings page.
|
||||
|
||||
Args:
|
||||
request: The request object.
|
||||
|
||||
Returns:
|
||||
dict
|
||||
|
||||
"""
|
||||
user = request.user
|
||||
|
||||
country_options = [
|
||||
(country_code, _(country_name)) # pylint: disable=translation-of-non-string
|
||||
for country_code, country_name in sorted(
|
||||
countries.countries, key=lambda(__, name): unicode(name)
|
||||
)
|
||||
]
|
||||
|
||||
year_of_birth_options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS]
|
||||
|
||||
context = {
|
||||
'auth': {},
|
||||
'duplicate_provider': None,
|
||||
'fields': {
|
||||
'country': {
|
||||
'options': country_options,
|
||||
}, 'gender': {
|
||||
'options': [(choice[0], _(choice[1])) for choice in UserProfile.GENDER_CHOICES], # pylint: disable=translation-of-non-string
|
||||
}, 'language': {
|
||||
'options': released_languages(),
|
||||
}, 'level_of_education': {
|
||||
'options': [(choice[0], _(choice[1])) for choice in UserProfile.LEVEL_OF_EDUCATION_CHOICES], # pylint: disable=translation-of-non-string
|
||||
}, 'password': {
|
||||
'url': reverse('password_reset'),
|
||||
}, 'year_of_birth': {
|
||||
'options': year_of_birth_options,
|
||||
}, 'preferred_language': {
|
||||
'options': settings.ALL_LANGUAGES,
|
||||
}
|
||||
},
|
||||
'platform_name': settings.PLATFORM_NAME,
|
||||
'user_accounts_api_url': reverse("accounts_api", kwargs={'username': user.username}),
|
||||
'user_preferences_api_url': reverse('preferences_api', kwargs={'username': user.username}),
|
||||
}
|
||||
|
||||
if third_party_auth.is_enabled():
|
||||
# If the account on the third party provider is already connected with another edX account,
|
||||
# we display a message to the user.
|
||||
context['duplicate_provider'] = pipeline.get_duplicate_provider(messages.get_messages(request))
|
||||
|
||||
auth_states = pipeline.get_provider_user_states(user)
|
||||
|
||||
context['auth']['providers'] = [{
|
||||
'name': state.provider.NAME, # The name of the provider e.g. Facebook
|
||||
'connected': state.has_account, # Whether the user's edX account is connected with the provider.
|
||||
# If the user is not connected, they should be directed to this page to authenticate
|
||||
# with the particular provider.
|
||||
'connect_url': pipeline.get_login_url(
|
||||
state.provider.NAME,
|
||||
pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS,
|
||||
# The url the user should be directed to after the auth process has completed.
|
||||
redirect_url=reverse('account_settings'),
|
||||
),
|
||||
# If the user is connected, sending a POST request to this url removes the connection
|
||||
# information for this provider from their edX account.
|
||||
'disconnect_url': pipeline.get_disconnect_url(state.provider.NAME),
|
||||
} for state in auth_states]
|
||||
|
||||
return context
|
||||
|
||||
0
lms/djangoapps/student_profile/__init__.py
Normal file
0
lms/djangoapps/student_profile/test/__init__.py
Normal file
97
lms/djangoapps/student_profile/test/test_views.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
""" Tests for student profile views. """
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
|
||||
from util.testing import UrlResetMixin
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
from student_profile.views import learner_profile_context
|
||||
|
||||
|
||||
class LearnerProfileViewTest(UrlResetMixin, TestCase):
|
||||
""" Tests for the student profile view. """
|
||||
|
||||
USERNAME = "username"
|
||||
PASSWORD = "password"
|
||||
CONTEXT_DATA = [
|
||||
'default_public_account_fields',
|
||||
'accounts_api_url',
|
||||
'preferences_api_url',
|
||||
'account_settings_page_url',
|
||||
'has_preferences_access',
|
||||
'own_profile',
|
||||
'country_options',
|
||||
'language_options',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super(LearnerProfileViewTest, self).setUp()
|
||||
self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD)
|
||||
self.client.login(username=self.USERNAME, password=self.PASSWORD)
|
||||
|
||||
def test_context(self):
|
||||
"""
|
||||
Verify learner profile page context data.
|
||||
"""
|
||||
context = learner_profile_context(self.user.username, self.USERNAME, self.user.is_staff)
|
||||
|
||||
self.assertEqual(
|
||||
context['data']['default_public_account_fields'],
|
||||
settings.ACCOUNT_VISIBILITY_CONFIGURATION['public_fields']
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
context['data']['accounts_api_url'],
|
||||
reverse("accounts_api", kwargs={'username': self.user.username})
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
context['data']['preferences_api_url'],
|
||||
reverse('preferences_api', kwargs={'username': self.user.username})
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
context['data']['profile_image_upload_url'],
|
||||
reverse("profile_image_upload", kwargs={'username': self.user.username})
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
context['data']['profile_image_remove_url'],
|
||||
reverse('profile_image_remove', kwargs={'username': self.user.username})
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
context['data']['profile_image_max_bytes'],
|
||||
settings.PROFILE_IMAGE_MAX_BYTES
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
context['data']['profile_image_min_bytes'],
|
||||
settings.PROFILE_IMAGE_MIN_BYTES
|
||||
)
|
||||
|
||||
self.assertEqual(context['data']['account_settings_page_url'], reverse('account_settings'))
|
||||
|
||||
for attribute in self.CONTEXT_DATA:
|
||||
self.assertIn(attribute, context['data'])
|
||||
|
||||
def test_view(self):
|
||||
"""
|
||||
Verify learner profile page view.
|
||||
"""
|
||||
profile_path = reverse('learner_profile', kwargs={'username': self.USERNAME})
|
||||
response = self.client.get(path=profile_path)
|
||||
|
||||
for attribute in self.CONTEXT_DATA:
|
||||
self.assertIn(attribute, response.content)
|
||||
|
||||
def test_undefined_profile_page(self):
|
||||
"""
|
||||
Verify that a 404 is returned for a non-existent profile page.
|
||||
"""
|
||||
profile_path = reverse('learner_profile', kwargs={'username': "no_such_user"})
|
||||
response = self.client.get(path=profile_path)
|
||||
self.assertEqual(404, response.status_code)
|
||||
87
lms/djangoapps/student_profile/views.py
Normal file
@@ -0,0 +1,87 @@
|
||||
""" Views for a student's profile information. """
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django_countries import countries
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponse
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from student.models import User
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(['GET'])
|
||||
def learner_profile(request, username):
|
||||
"""Render the profile page for the specified username.
|
||||
|
||||
Args:
|
||||
request (HttpRequest)
|
||||
username (str): username of user whose profile is requested.
|
||||
|
||||
Returns:
|
||||
HttpResponse: 200 if the page was sent successfully
|
||||
HttpResponse: 302 if not logged in (redirect to login page)
|
||||
HttpResponse: 404 if the specified username does not exist
|
||||
HttpResponse: 405 if using an unsupported HTTP method
|
||||
|
||||
Example usage:
|
||||
GET /account/profile
|
||||
"""
|
||||
try:
|
||||
return render_to_response(
|
||||
'student_profile/learner_profile.html',
|
||||
learner_profile_context(request.user.username, username, request.user.is_staff)
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
return HttpResponse(status=404)
|
||||
|
||||
|
||||
def learner_profile_context(logged_in_username, profile_username, user_is_staff):
|
||||
"""Context for the learner profile page.
|
||||
|
||||
Args:
|
||||
logged_in_username (str): Username of user logged In user.
|
||||
profile_username (str): username of user whose profile is requested.
|
||||
user_is_staff (bool): Logged In user has staff access.
|
||||
|
||||
Returns:
|
||||
dict
|
||||
|
||||
Raises:
|
||||
ObjectDoesNotExist: the specified profile_username does not exist.
|
||||
"""
|
||||
profile_user = User.objects.get(username=profile_username)
|
||||
|
||||
country_options = [
|
||||
(country_code, _(country_name)) # pylint: disable=translation-of-non-string
|
||||
for country_code, country_name in sorted(
|
||||
countries.countries, key=lambda(__, name): unicode(name)
|
||||
)
|
||||
]
|
||||
|
||||
context = {
|
||||
'data': {
|
||||
'profile_user_id': profile_user.id,
|
||||
'default_public_account_fields': settings.ACCOUNT_VISIBILITY_CONFIGURATION['public_fields'],
|
||||
'default_visibility': settings.ACCOUNT_VISIBILITY_CONFIGURATION['default_visibility'],
|
||||
'accounts_api_url': reverse("accounts_api", kwargs={'username': profile_username}),
|
||||
'preferences_api_url': reverse('preferences_api', kwargs={'username': profile_username}),
|
||||
'profile_image_upload_url': reverse('profile_image_upload', kwargs={'username': profile_username}),
|
||||
'profile_image_remove_url': reverse('profile_image_remove', kwargs={'username': profile_username}),
|
||||
'profile_image_max_bytes': settings.PROFILE_IMAGE_MAX_BYTES,
|
||||
'profile_image_min_bytes': settings.PROFILE_IMAGE_MIN_BYTES,
|
||||
'account_settings_page_url': reverse('account_settings'),
|
||||
'has_preferences_access': (logged_in_username == profile_username or user_is_staff),
|
||||
'own_profile': (logged_in_username == profile_username),
|
||||
'country_options': country_options,
|
||||
'language_options': settings.ALL_LANGUAGES,
|
||||
}
|
||||
}
|
||||
|
||||
return context
|
||||
@@ -131,6 +131,10 @@ if STATIC_URL_BASE:
|
||||
if not STATIC_URL.endswith("/"):
|
||||
STATIC_URL += "/"
|
||||
|
||||
# MEDIA_ROOT specifies the directory where user-uploaded files are stored.
|
||||
MEDIA_ROOT = ENV_TOKENS.get('MEDIA_ROOT', MEDIA_ROOT)
|
||||
MEDIA_URL = ENV_TOKENS.get('MEDIA_URL', MEDIA_URL)
|
||||
|
||||
PLATFORM_NAME = ENV_TOKENS.get('PLATFORM_NAME', PLATFORM_NAME)
|
||||
# For displaying on the receipt. At Stanford PLATFORM_NAME != MERCHANT_NAME, but PLATFORM_NAME is a fine default
|
||||
PLATFORM_TWITTER_ACCOUNT = ENV_TOKENS.get('PLATFORM_TWITTER_ACCOUNT', PLATFORM_TWITTER_ACCOUNT)
|
||||
@@ -594,3 +598,10 @@ if FEATURES.get('INDIVIDUAL_DUE_DATES'):
|
||||
FIELD_OVERRIDE_PROVIDERS += (
|
||||
'courseware.student_field_overrides.IndividualStudentOverrideProvider',
|
||||
)
|
||||
|
||||
# PROFILE IMAGE CONFIG
|
||||
PROFILE_IMAGE_BACKEND = ENV_TOKENS.get('PROFILE_IMAGE_BACKEND', PROFILE_IMAGE_BACKEND)
|
||||
PROFILE_IMAGE_DEFAULT_FILENAME = ENV_TOKENS.get('PROFILE_IMAGE_DEFAULT_FILENAME', PROFILE_IMAGE_DEFAULT_FILENAME)
|
||||
PROFILE_IMAGE_SECRET_KEY = AUTH_TOKENS.get('PROFILE_IMAGE_SECRET_KEY', PROFILE_IMAGE_SECRET_KEY)
|
||||
PROFILE_IMAGE_MAX_BYTES = ENV_TOKENS.get('PROFILE_IMAGE_MAX_BYTES', PROFILE_IMAGE_MAX_BYTES)
|
||||
PROFILE_IMAGE_MIN_BYTES = ENV_TOKENS.get('PROFILE_IMAGE_MIN_BYTES', PROFILE_IMAGE_MIN_BYTES)
|
||||
|
||||
@@ -49,6 +49,15 @@
|
||||
],
|
||||
"port": 27017
|
||||
},
|
||||
"TRACKING_BACKENDS": {
|
||||
"mongo": {
|
||||
"ENGINE": "track.backends.mongodb.MongoBackend",
|
||||
"OPTIONS": {
|
||||
"database": "test",
|
||||
"collection": "events"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EVENT_TRACKING_BACKENDS": {
|
||||
"mongo": {
|
||||
"ENGINE": "eventtracking.backends.mongodb.MongoBackend",
|
||||
|
||||
@@ -131,6 +131,14 @@ MOCK_SEARCH_BACKING_FILE = (
|
||||
import uuid
|
||||
SECRET_KEY = uuid.uuid4().hex
|
||||
|
||||
# Set dummy values for profile image settings.
|
||||
PROFILE_IMAGE_BACKEND = {
|
||||
'class': 'storages.backends.overwrite.OverwriteStorage',
|
||||
'options': {
|
||||
'location': os.path.join(MEDIA_ROOT, 'profile-images/'),
|
||||
'base_url': os.path.join(MEDIA_URL, 'profile-images/'),
|
||||
},
|
||||
}
|
||||
#####################################################################
|
||||
# Lastly, see if the developer has any local overrides.
|
||||
try:
|
||||
|
||||
@@ -317,9 +317,6 @@ FEATURES = {
|
||||
# Set to True to change the course sorting behavior by their start dates, latest first.
|
||||
'ENABLE_COURSE_SORTING_BY_START_DATE': True,
|
||||
|
||||
# Flag to enable new user account APIs.
|
||||
'ENABLE_USER_REST_API': False,
|
||||
|
||||
# Expose Mobile REST API. Note that if you use this, you must also set
|
||||
# ENABLE_OAUTH2_PROVIDER to True
|
||||
'ENABLE_MOBILE_REST_API': False,
|
||||
@@ -780,6 +777,10 @@ STATICFILES_DIRS = [
|
||||
|
||||
FAVICON_PATH = 'images/favicon.ico'
|
||||
|
||||
# User-uploaded content
|
||||
MEDIA_ROOT = '/edx/var/edxapp/media/'
|
||||
MEDIA_URL = '/media/'
|
||||
|
||||
# Locale/Internationalization
|
||||
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||
LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
|
||||
@@ -987,6 +988,12 @@ EDXNOTES_INTERFACE = {
|
||||
'url': 'http://localhost:8120/api/v1',
|
||||
}
|
||||
|
||||
########################## Parental controls config #######################
|
||||
|
||||
# The age at which a learner no longer requires parental consent, or None
|
||||
# if parental consent is never required.
|
||||
PARENTAL_CONSENT_AGE_LIMIT = 13
|
||||
|
||||
################################# Jasmine ##################################
|
||||
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
|
||||
|
||||
@@ -1544,7 +1551,7 @@ BULK_EMAIL_RETRY_DELAY_BETWEEN_SENDS = 0.02
|
||||
############################# Email Opt In ####################################
|
||||
|
||||
# Minimum age for organization-wide email opt in
|
||||
EMAIL_OPTIN_MINIMUM_AGE = 13
|
||||
EMAIL_OPTIN_MINIMUM_AGE = PARENTAL_CONSENT_AGE_LIMIT
|
||||
|
||||
############################## Video ##########################################
|
||||
|
||||
@@ -1918,6 +1925,8 @@ TIME_ZONE_DISPLAYED_FOR_DEADLINES = 'UTC'
|
||||
|
||||
# Source:
|
||||
# http://loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt according to http://en.wikipedia.org/wiki/ISO_639-1
|
||||
# Note that this is used as the set of choices to the `code` field of the
|
||||
# `LanguageProficiency` model.
|
||||
ALL_LANGUAGES = (
|
||||
[u"aa", u"Afar"],
|
||||
[u"ab", u"Abkhazian"],
|
||||
@@ -2216,7 +2225,7 @@ ONLOAD_BEACON_SAMPLE_RATE = 0.0
|
||||
ACCOUNT_VISIBILITY_CONFIGURATION = {
|
||||
# Default visibility level for accounts without a specified value
|
||||
# The value is one of: 'all_users', 'private'
|
||||
"default_visibility": "private",
|
||||
"default_visibility": "all_users",
|
||||
|
||||
# The list of all fields that can be shared with other users
|
||||
"shareable_fields": [
|
||||
@@ -2224,7 +2233,7 @@ ACCOUNT_VISIBILITY_CONFIGURATION = {
|
||||
'profile_image',
|
||||
'country',
|
||||
'time_zone',
|
||||
'languages',
|
||||
'language_proficiencies',
|
||||
'bio',
|
||||
],
|
||||
|
||||
@@ -2248,3 +2257,29 @@ CHECKPOINT_PATTERN = r'(?P<checkpoint_name>\w+)'
|
||||
# 'courseware.student_field_overrides.IndividualStudentOverrideProvider' to
|
||||
# this setting.
|
||||
FIELD_OVERRIDE_PROVIDERS = ()
|
||||
|
||||
# PROFILE IMAGE CONFIG
|
||||
# WARNING: Certain django storage backends do not support atomic
|
||||
# file overwrites (including the default, OverwriteStorage) - instead
|
||||
# there are separate calls to delete and then write a new file in the
|
||||
# storage backend. This introduces the risk of a race condition
|
||||
# occurring when a user uploads a new profile image to replace an
|
||||
# earlier one (the file will temporarily be deleted).
|
||||
PROFILE_IMAGE_BACKEND = {
|
||||
'class': 'storages.backends.overwrite.OverwriteStorage',
|
||||
'options': {
|
||||
'location': os.path.join(MEDIA_ROOT, 'profile-images/'),
|
||||
'base_url': os.path.join(MEDIA_URL, 'profile-images/'),
|
||||
},
|
||||
}
|
||||
PROFILE_IMAGE_DEFAULT_FILENAME = (
|
||||
'images/edx-theme/default-profile' if FEATURES['IS_EDX_DOMAIN'] else 'images/default-theme/default-profile'
|
||||
)
|
||||
PROFILE_IMAGE_DEFAULT_FILE_EXTENSION = 'png'
|
||||
# This secret key is used in generating unguessable URLs to users'
|
||||
# profile images. Once it has been set, changing it will make the
|
||||
# platform unaware of current image URLs, resulting in reverting all
|
||||
# users' profile images to the default placeholder image.
|
||||
PROFILE_IMAGE_SECRET_KEY = 'placeholder secret key'
|
||||
PROFILE_IMAGE_MAX_BYTES = 1024 * 1024
|
||||
PROFILE_IMAGE_MIN_BYTES = 100
|
||||
|
||||
@@ -74,6 +74,9 @@ FEATURES['ENABLE_COMBINED_LOGIN_REGISTRATION'] = True
|
||||
# Need wiki for courseware views to work. TODO (vshnayder): shouldn't need it.
|
||||
WIKI_ENABLED = True
|
||||
|
||||
# Enable a parental consent age limit for testing
|
||||
PARENTAL_CONSENT_AGE_LIMIT = 13
|
||||
|
||||
# Makes the tests run much faster...
|
||||
SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
|
||||
|
||||
@@ -265,7 +268,6 @@ FEATURES['ENABLE_OAUTH2_PROVIDER'] = True
|
||||
FEATURES['ENABLE_MOBILE_REST_API'] = True
|
||||
FEATURES['ENABLE_MOBILE_SOCIAL_FACEBOOK_FEATURES'] = True
|
||||
FEATURES['ENABLE_VIDEO_ABSTRACTION_LAYER_API'] = True
|
||||
FEATURES['ENABLE_USER_REST_API'] = True
|
||||
|
||||
###################### Payment ##############################3
|
||||
# Enable fake payment processing page
|
||||
@@ -473,3 +475,17 @@ FEATURES['CERTIFICATES_HTML_VIEW'] = True
|
||||
INSTALLED_APPS += ('ccx',)
|
||||
MIDDLEWARE_CLASSES += ('ccx.overrides.CcxMiddleware',)
|
||||
FEATURES['CUSTOM_COURSES_EDX'] = True
|
||||
|
||||
# Set dummy values for profile image settings.
|
||||
PROFILE_IMAGE_BACKEND = {
|
||||
'class': 'storages.backends.overwrite.OverwriteStorage',
|
||||
'options': {
|
||||
'location': MEDIA_ROOT,
|
||||
'base_url': 'http://example-storage.com/profile-images/',
|
||||
},
|
||||
}
|
||||
PROFILE_IMAGE_DEFAULT_FILENAME = 'default'
|
||||
PROFILE_IMAGE_DEFAULT_FILE_EXTENSION = 'png'
|
||||
PROFILE_IMAGE_SECRET_KEY = 'secret'
|
||||
PROFILE_IMAGE_MAX_BYTES = 1024 * 1024
|
||||
PROFILE_IMAGE_MIN_BYTES = 100
|
||||
|
||||
BIN
lms/static/images/default-theme/default-profile_120.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
lms/static/images/default-theme/default-profile_30.png
Normal file
|
After Width: | Height: | Size: 576 B |
BIN
lms/static/images/default-theme/default-profile_50.png
Normal file
|
After Width: | Height: | Size: 1023 B |
BIN
lms/static/images/default-theme/default-profile_500.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
lms/static/images/edx-theme/default-profile_120.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
lms/static/images/edx-theme/default-profile_30.png
Normal file
|
After Width: | Height: | Size: 993 B |
BIN
lms/static/images/edx-theme/default-profile_50.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
lms/static/images/edx-theme/default-profile_500.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
@@ -23,10 +23,7 @@
|
||||
* Specifically:
|
||||
* - dashboard
|
||||
* - signInUser
|
||||
* - passwordReset
|
||||
* - changeEmail
|
||||
* - changeEmailSettings
|
||||
* - changeName
|
||||
* - verifyToggleBannerFailedOff
|
||||
*/
|
||||
edx.dashboard.legacy.init = function(urls) {
|
||||
@@ -160,67 +157,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
$('#pwd_reset_button').click(function() {
|
||||
$.post(
|
||||
urls.passwordReset,
|
||||
{"email" : $('#id_email').val()},
|
||||
function() {
|
||||
$("#password_reset_complete_link").click();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#submit-lang").click(function(event) {
|
||||
event.preventDefault();
|
||||
$.post('/lang_pref/setlang/',
|
||||
{language: $('#settings-language-value').val()}
|
||||
).done(function() {
|
||||
// submit form as normal
|
||||
$('.settings-language-form').submit();
|
||||
});
|
||||
});
|
||||
|
||||
$("#change_email_form").submit(function(){
|
||||
var new_email = $('#new_email_field').val();
|
||||
var new_password = $('#new_email_password').val();
|
||||
|
||||
$.post(
|
||||
urls.changeEmail,
|
||||
{"new_email" : new_email, "password" : new_password},
|
||||
function(data) {
|
||||
if (data.success) {
|
||||
$("#change_email_title").html(gettext("Please verify your new email address"));
|
||||
$("#change_email_form").html(
|
||||
"<p>" +
|
||||
gettext("You'll receive a confirmation in your inbox. Please follow the link in the email to confirm your email address change.") +
|
||||
"</p>"
|
||||
);
|
||||
} else {
|
||||
$("#change_email_error").html(data.error).stop().css("display", "block");
|
||||
}
|
||||
}
|
||||
);
|
||||
return false;
|
||||
});
|
||||
|
||||
$("#change_name_form").submit(function(){
|
||||
var new_name = $('#new_name_field').val();
|
||||
var rationale = $('#name_rationale_field').val();
|
||||
|
||||
$.post(
|
||||
urls.changeName,
|
||||
{"new_name":new_name, "rationale":rationale},
|
||||
function(data) {
|
||||
if(data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
$("#change_name_error").html(data.error).stop().css("display", "block");
|
||||
}
|
||||
}
|
||||
);
|
||||
return false;
|
||||
});
|
||||
|
||||
$("#email_settings_form").submit(function(){
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
@@ -240,24 +176,6 @@
|
||||
return false;
|
||||
});
|
||||
|
||||
accessibleModal(
|
||||
".edit-name",
|
||||
"#apply_name_change .close-modal",
|
||||
"#apply_name_change",
|
||||
"#dashboard-main"
|
||||
);
|
||||
accessibleModal(
|
||||
".edit-email",
|
||||
"#change_email .close-modal",
|
||||
"#change_email",
|
||||
"#dashboard-main"
|
||||
);
|
||||
accessibleModal(
|
||||
"#pwd_reset_button",
|
||||
"#password_reset_complete .close-modal",
|
||||
"#password_reset_complete",
|
||||
"#dashboard-main"
|
||||
);
|
||||
|
||||
$(".action-email-settings").each(function(index){
|
||||
$(this).attr("id", "email-settings-" + index);
|
||||
|
||||
20
lms/static/js/fixtures/student_profile/student_profile.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<div class="message-banner" aria-live="polite"></div>
|
||||
<div class="wrapper-profile">
|
||||
<div class="ui-loading-indicator">
|
||||
<p>
|
||||
<span class="spin">
|
||||
<i class="icon fa fa-refresh"></i>
|
||||
</span>
|
||||
|
||||
<span class="copy">
|
||||
Loading
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="ui-loading-error is-hidden">
|
||||
<i class="fa fa-exclamation-triangle message-error" aria-hidden=true></i>
|
||||
<span class="copy">
|
||||
An error occurred. Please reload the page.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -30,6 +30,7 @@
|
||||
'backbone': 'xmodule_js/common_static/js/vendor/backbone-min',
|
||||
'backbone.associations': 'xmodule_js/common_static/js/vendor/backbone-associations-min',
|
||||
'backbone.paginator': 'xmodule_js/common_static/js/vendor/backbone.paginator.min',
|
||||
"backbone-super": "js/vendor/backbone-super",
|
||||
'tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/tinymce.full.min',
|
||||
'jquery.tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce',
|
||||
'xmodule': 'xmodule_js/src/xmodule',
|
||||
@@ -58,6 +59,7 @@
|
||||
|
||||
// Manually specify LMS files that are not converted to RequireJS
|
||||
'history': 'js/vendor/history',
|
||||
'js/mustache': 'js/mustache',
|
||||
'js/verify_student/photocapture': 'js/verify_student/photocapture',
|
||||
'js/staff_debug_actions': 'js/staff_debug_actions',
|
||||
'js/vendor/jquery.qubit': 'js/vendor/jquery.qubit',
|
||||
@@ -88,6 +90,9 @@
|
||||
'js/student_account/views/RegisterView': 'js/student_account/views/RegisterView',
|
||||
'js/student_account/views/AccessView': 'js/student_account/views/AccessView',
|
||||
'js/student_profile/profile': 'js/student_profile/profile',
|
||||
'js/student_profile/views/learner_profile_fields': 'js/student_profile/views/learner_profile_fields',
|
||||
'js/student_profile/views/learner_profile_factory': 'js/student_profile/views/learner_profile_factory',
|
||||
'js/student_profile/views/learner_profile_view': 'js/student_profile/views/learner_profile_view',
|
||||
|
||||
// edxnotes
|
||||
'annotator_1.2.9': 'xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min'
|
||||
@@ -197,6 +202,9 @@
|
||||
deps: ['backbone'],
|
||||
exports: 'Backbone.Paginator'
|
||||
},
|
||||
"backbone-super": {
|
||||
deps: ["backbone"],
|
||||
},
|
||||
'youtube': {
|
||||
exports: 'YT'
|
||||
},
|
||||
@@ -583,7 +591,14 @@
|
||||
'lms/include/js/spec/student_account/enrollment_spec.js',
|
||||
'lms/include/js/spec/student_account/emailoptin_spec.js',
|
||||
'lms/include/js/spec/student_account/shoppingcart_spec.js',
|
||||
'lms/include/js/spec/student_account/account_settings_factory_spec.js',
|
||||
'lms/include/js/spec/student_account/account_settings_fields_spec.js',
|
||||
'lms/include/js/spec/student_account/account_settings_view_spec.js',
|
||||
'lms/include/js/spec/student_profile/profile_spec.js',
|
||||
'lms/include/js/spec/views/fields_spec.js',
|
||||
'lms/include/js/spec/student_profile/learner_profile_factory_spec.js',
|
||||
'lms/include/js/spec/student_profile/learner_profile_view_spec.js',
|
||||
'lms/include/js/spec/student_profile/learner_profile_fields_spec.js',
|
||||
'lms/include/js/spec/verify_student/pay_and_verify_view_spec.js',
|
||||
'lms/include/js/spec/verify_student/webcam_photo_view_spec.js',
|
||||
'lms/include/js/spec/verify_student/image_input_spec.js',
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers',
|
||||
'js/spec/views/fields_helpers',
|
||||
'js/spec/student_account/helpers',
|
||||
'js/spec/student_account/account_settings_fields_helpers',
|
||||
'js/student_account/views/account_settings_factory',
|
||||
'js/student_account/views/account_settings_view'
|
||||
],
|
||||
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, FieldViewsSpecHelpers, Helpers,
|
||||
AccountSettingsFieldViewSpecHelpers, AccountSettingsPage) {
|
||||
'use strict';
|
||||
|
||||
describe("edx.user.AccountSettingsFactory", function () {
|
||||
|
||||
var FIELDS_DATA = {
|
||||
'country': {
|
||||
'options': Helpers.FIELD_OPTIONS
|
||||
}, 'gender': {
|
||||
'options': Helpers.FIELD_OPTIONS
|
||||
}, 'language': {
|
||||
'options': Helpers.FIELD_OPTIONS
|
||||
}, 'level_of_education': {
|
||||
'options': Helpers.FIELD_OPTIONS
|
||||
}, 'password': {
|
||||
'url': '/password_reset'
|
||||
}, 'year_of_birth': {
|
||||
'options': Helpers.FIELD_OPTIONS
|
||||
}, 'preferred_language': {
|
||||
'options': Helpers.FIELD_OPTIONS
|
||||
}
|
||||
};
|
||||
|
||||
var AUTH_DATA = {
|
||||
'providers': [
|
||||
{
|
||||
'name': "Network1",
|
||||
'connected': true,
|
||||
'connect_url': 'yetanother1.com/auth/connect',
|
||||
'disconnect_url': 'yetanother1.com/auth/disconnect'
|
||||
},
|
||||
{
|
||||
'name': "Network2",
|
||||
'connected': true,
|
||||
'connect_url': 'yetanother2.com/auth/connect',
|
||||
'disconnect_url': 'yetanother2.com/auth/disconnect'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var requests;
|
||||
|
||||
beforeEach(function () {
|
||||
setFixtures('<div class="wrapper-account-settings"></div>');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_readonly');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_dropdown');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_link');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_text');
|
||||
TemplateHelpers.installTemplate('templates/student_account/account_settings');
|
||||
});
|
||||
|
||||
it("shows loading error when UserAccountModel fails to load", function() {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var context = AccountSettingsPage(
|
||||
FIELDS_DATA, AUTH_DATA, Helpers.USER_ACCOUNTS_API_URL, Helpers.USER_PREFERENCES_API_URL
|
||||
);
|
||||
var accountSettingsView = context.accountSettingsView;
|
||||
|
||||
Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, true);
|
||||
Helpers.expectLoadingErrorIsVisible(accountSettingsView, false);
|
||||
Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView);
|
||||
|
||||
var request = requests[0];
|
||||
expect(request.method).toBe('GET');
|
||||
expect(request.url).toBe(Helpers.USER_ACCOUNTS_API_URL);
|
||||
|
||||
AjaxHelpers.respondWithError(requests, 500);
|
||||
Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, false);
|
||||
Helpers.expectLoadingErrorIsVisible(accountSettingsView, true);
|
||||
Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView);
|
||||
});
|
||||
|
||||
|
||||
it("shows loading error when UserPreferencesModel fails to load", function() {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var context = AccountSettingsPage(
|
||||
FIELDS_DATA, AUTH_DATA, Helpers.USER_ACCOUNTS_API_URL, Helpers.USER_PREFERENCES_API_URL
|
||||
);
|
||||
var accountSettingsView = context.accountSettingsView;
|
||||
|
||||
Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, true);
|
||||
Helpers.expectLoadingErrorIsVisible(accountSettingsView, false);
|
||||
Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView);
|
||||
|
||||
var request = requests[0];
|
||||
expect(request.method).toBe('GET');
|
||||
expect(request.url).toBe(Helpers.USER_ACCOUNTS_API_URL);
|
||||
|
||||
AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData());
|
||||
Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, true);
|
||||
Helpers.expectLoadingErrorIsVisible(accountSettingsView, false);
|
||||
Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView);
|
||||
|
||||
request = requests[1];
|
||||
expect(request.method).toBe('GET');
|
||||
expect(request.url).toBe(Helpers.USER_PREFERENCES_API_URL);
|
||||
|
||||
AjaxHelpers.respondWithError(requests, 500);
|
||||
Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, false);
|
||||
Helpers.expectLoadingErrorIsVisible(accountSettingsView, true);
|
||||
Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView);
|
||||
});
|
||||
|
||||
it("renders fields after the models are successfully fetched", function() {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var context = AccountSettingsPage(
|
||||
FIELDS_DATA, AUTH_DATA, Helpers.USER_ACCOUNTS_API_URL, Helpers.USER_PREFERENCES_API_URL
|
||||
);
|
||||
var accountSettingsView = context.accountSettingsView;
|
||||
|
||||
Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, true);
|
||||
Helpers.expectLoadingErrorIsVisible(accountSettingsView, false);
|
||||
Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView);
|
||||
|
||||
AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData());
|
||||
AjaxHelpers.respondWithJson(requests, Helpers.createUserPreferencesData());
|
||||
|
||||
Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, false);
|
||||
Helpers.expectLoadingErrorIsVisible(accountSettingsView, false);
|
||||
Helpers.expectSettingsSectionsAndFieldsToBeRendered(accountSettingsView);
|
||||
});
|
||||
|
||||
it("expects all fields to behave correctly", function () {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var context = AccountSettingsPage(
|
||||
FIELDS_DATA, AUTH_DATA, Helpers.USER_ACCOUNTS_API_URL, Helpers.USER_PREFERENCES_API_URL
|
||||
);
|
||||
var accountSettingsView = context.accountSettingsView;
|
||||
|
||||
AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData());
|
||||
AjaxHelpers.respondWithJson(requests, Helpers.createUserPreferencesData());
|
||||
|
||||
var sectionsData = accountSettingsView.options.sectionsData;
|
||||
|
||||
expect(sectionsData[0].fields.length).toBe(6);
|
||||
|
||||
var textFields = [sectionsData[0].fields[1], sectionsData[0].fields[2]];
|
||||
for (var i = 0; i < textFields.length ; i++) {
|
||||
|
||||
var view = textFields[i].view;
|
||||
FieldViewsSpecHelpers.verifyTextField(view, {
|
||||
title: view.options.title,
|
||||
valueAttribute: view.options.valueAttribute,
|
||||
helpMessage: view.options.helpMessage,
|
||||
validValue: 'My Name',
|
||||
invalidValue1: '',
|
||||
invalidValue2: '@',
|
||||
validationError: "Think again!"
|
||||
}, requests);
|
||||
}
|
||||
|
||||
expect(sectionsData[1].fields.length).toBe(4);
|
||||
var dropdownFields = [
|
||||
sectionsData[1].fields[0],
|
||||
sectionsData[1].fields[1],
|
||||
sectionsData[1].fields[2]
|
||||
];
|
||||
_.each(dropdownFields, function(field) {
|
||||
var view = field.view;
|
||||
FieldViewsSpecHelpers.verifyDropDownField(view, {
|
||||
title: view.options.title,
|
||||
valueAttribute: view.options.valueAttribute,
|
||||
helpMessage: '',
|
||||
validValue: Helpers.FIELD_OPTIONS[1][0],
|
||||
invalidValue1: Helpers.FIELD_OPTIONS[2][0],
|
||||
invalidValue2: Helpers.FIELD_OPTIONS[3][0],
|
||||
validationError: "Nope, this will not do!"
|
||||
}, requests);
|
||||
});
|
||||
|
||||
var section2Fields = sectionsData[2].fields;
|
||||
expect(section2Fields.length).toBe(2);
|
||||
for (var i = 0; i < section2Fields.length; i++) {
|
||||
|
||||
var view = section2Fields[i].view;
|
||||
AccountSettingsFieldViewSpecHelpers.verifyAuthField(view, view.options, requests);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers',
|
||||
'js/spec/views/fields_helpers',
|
||||
'string_utils'],
|
||||
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, FieldViewsSpecHelpers) {
|
||||
'use strict';
|
||||
|
||||
var verifyAuthField = function (view, data, requests) {
|
||||
var selector = '.u-field-value .u-field-link-title-' + view.options.valueAttribute;
|
||||
|
||||
spyOn(view, 'redirect_to');
|
||||
|
||||
FieldViewsSpecHelpers.expectTitleAndMessageToContain(view, data.title, data.helpMessage);
|
||||
expect(view.$(selector).text().trim()).toBe('Unlink');
|
||||
view.$(selector).click();
|
||||
FieldViewsSpecHelpers.expectMessageContains(view, 'Unlinking');
|
||||
AjaxHelpers.expectRequest(requests, 'POST', data.disconnectUrl);
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
|
||||
expect(view.$(selector).text().trim()).toBe('Link');
|
||||
FieldViewsSpecHelpers.expectMessageContains(view, 'Successfully unlinked.');
|
||||
|
||||
view.$(selector).click();
|
||||
FieldViewsSpecHelpers.expectMessageContains(view, 'Linking');
|
||||
expect(view.redirect_to).toHaveBeenCalledWith(data.connectUrl);
|
||||
};
|
||||
|
||||
return {
|
||||
verifyAuthField: verifyAuthField
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers',
|
||||
'js/views/fields',
|
||||
'js/spec/views/fields_helpers',
|
||||
'js/spec/student_account/account_settings_fields_helpers',
|
||||
'js/student_account/views/account_settings_fields',
|
||||
'js/student_account/models/user_account_model',
|
||||
'string_utils'],
|
||||
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, FieldViews, FieldViewsSpecHelpers,
|
||||
AccountSettingsFieldViewSpecHelpers, AccountSettingsFieldViews) {
|
||||
'use strict';
|
||||
|
||||
describe("edx.AccountSettingsFieldViews", function () {
|
||||
|
||||
var requests,
|
||||
timerCallback;
|
||||
|
||||
beforeEach(function () {
|
||||
TemplateHelpers.installTemplate('templates/fields/field_readonly');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_dropdown');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_link');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_text');
|
||||
|
||||
timerCallback = jasmine.createSpy('timerCallback');
|
||||
jasmine.Clock.useMock();
|
||||
});
|
||||
|
||||
it("sends request to reset password on clicking link in PasswordFieldView", function() {
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var fieldData = FieldViewsSpecHelpers.createFieldData(AccountSettingsFieldViews.PasswordFieldView, {
|
||||
linkHref: '/password_reset',
|
||||
emailAttribute: 'email'
|
||||
});
|
||||
|
||||
var view = new AccountSettingsFieldViews.PasswordFieldView(fieldData).render();
|
||||
view.$('.u-field-value > a').click();
|
||||
AjaxHelpers.expectRequest(requests, 'POST', '/password_reset', "email=legolas%40woodland.middlearth");
|
||||
AjaxHelpers.respondWithJson(requests, {"success": "true"});
|
||||
FieldViewsSpecHelpers.expectMessageContains(
|
||||
view,
|
||||
"We've sent a message to legolas@woodland.middlearth. " +
|
||||
"Click the link in the message to reset your password."
|
||||
);
|
||||
});
|
||||
|
||||
it("sends request to /i18n/setlang/ after changing language preference in LanguagePreferenceFieldView", function() {
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var selector = '.u-field-value > select';
|
||||
var fieldData = FieldViewsSpecHelpers.createFieldData(AccountSettingsFieldViews.DropdownFieldView, {
|
||||
valueAttribute: 'language',
|
||||
options: FieldViewsSpecHelpers.SELECT_OPTIONS
|
||||
});
|
||||
|
||||
var view = new AccountSettingsFieldViews.LanguagePreferenceFieldView(fieldData).render();
|
||||
|
||||
var data = {'language': FieldViewsSpecHelpers.SELECT_OPTIONS[2][0]};
|
||||
view.$(selector).val(data[fieldData.valueAttribute]).change();
|
||||
FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, data);
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
|
||||
AjaxHelpers.expectRequest(
|
||||
requests,
|
||||
'POST',
|
||||
'/i18n/setlang/',
|
||||
'language=' + data[fieldData.valueAttribute]
|
||||
);
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
FieldViewsSpecHelpers.expectMessageContains(view, "Your changes have been saved.");
|
||||
|
||||
data = {'language': FieldViewsSpecHelpers.SELECT_OPTIONS[1][0]};
|
||||
view.$(selector).val(data[fieldData.valueAttribute]).change();
|
||||
FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, data);
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
|
||||
AjaxHelpers.expectRequest(
|
||||
requests,
|
||||
'POST',
|
||||
'/i18n/setlang/',
|
||||
'language=' + data[fieldData.valueAttribute]
|
||||
);
|
||||
AjaxHelpers.respondWithError(requests, 500);
|
||||
FieldViewsSpecHelpers.expectMessageContains(
|
||||
view,
|
||||
"You must sign out of edX and sign back in before your language changes take effect."
|
||||
);
|
||||
});
|
||||
|
||||
it("reads and saves the value correctly for LanguageProficienciesFieldView", function() {
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var selector = '.u-field-value > select';
|
||||
var fieldData = FieldViewsSpecHelpers.createFieldData(AccountSettingsFieldViews.DropdownFieldView, {
|
||||
valueAttribute: 'language_proficiencies',
|
||||
options: FieldViewsSpecHelpers.SELECT_OPTIONS
|
||||
});
|
||||
fieldData.model.set({'language_proficiencies': [{'code': FieldViewsSpecHelpers.SELECT_OPTIONS[0][0]}]});
|
||||
|
||||
var view = new AccountSettingsFieldViews.LanguageProficienciesFieldView(fieldData).render();
|
||||
|
||||
expect(view.modelValue()).toBe(FieldViewsSpecHelpers.SELECT_OPTIONS[0][0]);
|
||||
|
||||
var data = {'language_proficiencies': [{'code': FieldViewsSpecHelpers.SELECT_OPTIONS[1][0]}]};
|
||||
view.$(selector).val(FieldViewsSpecHelpers.SELECT_OPTIONS[1][0]).change();
|
||||
FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, data);
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
});
|
||||
|
||||
it("correctly links and unlinks from AuthFieldView", function() {
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.LinkFieldView, {
|
||||
title: 'Yet another social network',
|
||||
helpMessage: '',
|
||||
valueAttribute: 'auth-yet-another',
|
||||
connected: true,
|
||||
connectUrl: 'yetanother.com/auth/connect',
|
||||
disconnectUrl: 'yetanother.com/auth/disconnect'
|
||||
});
|
||||
var view = new AccountSettingsFieldViews.AuthFieldView(fieldData).render();
|
||||
|
||||
AccountSettingsFieldViewSpecHelpers.verifyAuthField(view, fieldData, requests);
|
||||
});
|
||||
});
|
||||
});
|
||||
102
lms/static/js/spec/student_account/account_settings_view_spec.js
Normal file
@@ -0,0 +1,102 @@
|
||||
define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers',
|
||||
'js/spec/student_account/helpers',
|
||||
'js/views/fields',
|
||||
'js/student_account/models/user_account_model',
|
||||
'js/student_account/views/account_settings_view'
|
||||
],
|
||||
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, FieldViews, UserAccountModel,
|
||||
AccountSettingsView) {
|
||||
'use strict';
|
||||
|
||||
describe("edx.user.AccountSettingsView", function () {
|
||||
|
||||
var createAccountSettingsView = function () {
|
||||
|
||||
var model = new UserAccountModel();
|
||||
model.set(Helpers.createAccountSettingsData());
|
||||
|
||||
var sectionsData = [
|
||||
{
|
||||
title: "Basic Account Information",
|
||||
fields: [
|
||||
{
|
||||
view: new FieldViews.ReadonlyFieldView({
|
||||
model: model,
|
||||
title: "Username",
|
||||
valueAttribute: "username"
|
||||
})
|
||||
},
|
||||
{
|
||||
view: new FieldViews.TextFieldView({
|
||||
model: model,
|
||||
title: "Full Name",
|
||||
valueAttribute: "name"
|
||||
})
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Additional Information",
|
||||
fields: [
|
||||
{
|
||||
view: new FieldViews.DropdownFieldView({
|
||||
model: model,
|
||||
title: "Education Completed",
|
||||
valueAttribute: "level_of_education",
|
||||
options: Helpers.FIELD_OPTIONS
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
var accountSettingsView = new AccountSettingsView({
|
||||
el: $('.wrapper-account-settings'),
|
||||
model: model,
|
||||
sectionsData : sectionsData
|
||||
});
|
||||
|
||||
return accountSettingsView;
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
setFixtures('<div class="wrapper-account-settings"></div>');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_readonly');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_dropdown');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_link');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_text');
|
||||
TemplateHelpers.installTemplate('templates/student_account/account_settings');
|
||||
});
|
||||
|
||||
it("shows loading error correctly", function() {
|
||||
|
||||
var accountSettingsView = createAccountSettingsView();
|
||||
|
||||
accountSettingsView.render();
|
||||
Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, true);
|
||||
Helpers.expectLoadingErrorIsVisible(accountSettingsView, false);
|
||||
Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView);
|
||||
|
||||
accountSettingsView.showLoadingError();
|
||||
Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, false);
|
||||
Helpers.expectLoadingErrorIsVisible(accountSettingsView, true);
|
||||
Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView);
|
||||
});
|
||||
|
||||
it("renders all fields as expected", function() {
|
||||
|
||||
var accountSettingsView = createAccountSettingsView();
|
||||
|
||||
accountSettingsView.render();
|
||||
Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, true);
|
||||
Helpers.expectLoadingErrorIsVisible(accountSettingsView, false);
|
||||
Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView);
|
||||
|
||||
accountSettingsView.renderFields();
|
||||
Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, false);
|
||||
Helpers.expectLoadingErrorIsVisible(accountSettingsView, false);
|
||||
Helpers.expectSettingsSectionsAndFieldsToBeRendered(accountSettingsView);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
126
lms/static/js/spec/student_account/helpers.js
Normal file
@@ -0,0 +1,126 @@
|
||||
define(['underscore'], function(_) {
|
||||
'use strict';
|
||||
|
||||
var USER_ACCOUNTS_API_URL = '/api/user/v0/accounts/student';
|
||||
var USER_PREFERENCES_API_URL = '/api/user/v0/preferences/student';
|
||||
var IMAGE_UPLOAD_API_URL = '/api/profile_images/v0/staff/upload';
|
||||
var IMAGE_REMOVE_API_URL = '/api/profile_images/v0/staff/remove';
|
||||
|
||||
var PROFILE_IMAGE = {
|
||||
image_url_large: '/media/profile-images/image.jpg',
|
||||
has_image: true
|
||||
};
|
||||
|
||||
var DEFAULT_ACCOUNT_SETTINGS_DATA = {
|
||||
username: 'student',
|
||||
name: 'Student',
|
||||
email: 'student@edx.org',
|
||||
level_of_education: '',
|
||||
gender: '',
|
||||
year_of_birth: '3', // Note: test birth year range is a string from 0-3
|
||||
requires_parental_consent: false,
|
||||
country: '',
|
||||
language: '',
|
||||
bio: "About the student",
|
||||
language_proficiencies: [{code: '1'}],
|
||||
profile_image: PROFILE_IMAGE
|
||||
};
|
||||
|
||||
var createAccountSettingsData = function(options) {
|
||||
return _.extend(_.extend({}, DEFAULT_ACCOUNT_SETTINGS_DATA), options);
|
||||
};
|
||||
|
||||
var DEFAULT_USER_PREFERENCES_DATA = {
|
||||
'pref-lang': '2'
|
||||
};
|
||||
|
||||
var createUserPreferencesData = function(options) {
|
||||
return _.extend(_.extend({}, DEFAULT_USER_PREFERENCES_DATA), options);
|
||||
};
|
||||
|
||||
var FIELD_OPTIONS = [
|
||||
['0', 'Option 0'],
|
||||
['1', 'Option 1'],
|
||||
['2', 'Option 2'],
|
||||
['3', 'Option 3']
|
||||
];
|
||||
|
||||
var IMAGE_MAX_BYTES = 1024 * 1024;
|
||||
var IMAGE_MIN_BYTES = 100;
|
||||
|
||||
var expectLoadingIndicatorIsVisible = function (view, visible) {
|
||||
if (visible) {
|
||||
expect($('.ui-loading-indicator')).not.toHaveClass('is-hidden');
|
||||
} else {
|
||||
expect($('.ui-loading-indicator')).toHaveClass('is-hidden');
|
||||
}
|
||||
};
|
||||
|
||||
var expectLoadingErrorIsVisible = function (view, visible) {
|
||||
if (visible) {
|
||||
expect(view.$('.ui-loading-error')).not.toHaveClass('is-hidden');
|
||||
} else {
|
||||
expect(view.$('.ui-loading-error')).toHaveClass('is-hidden');
|
||||
}
|
||||
};
|
||||
|
||||
var expectElementContainsField = function(element, field) {
|
||||
var view = field.view;
|
||||
|
||||
var fieldTitle = $(element).find('.u-field-title').text().trim();
|
||||
expect(fieldTitle).toBe(view.options.title);
|
||||
|
||||
if ('fieldValue' in view) {
|
||||
expect(view.fieldValue()).toBe(view.modelValue());
|
||||
} else if (view.fieldType === 'link') {
|
||||
expect($(element).find('a').length).toBe(1);
|
||||
} else {
|
||||
throw new Error('Unexpected field type: ' + view.fieldType);
|
||||
}
|
||||
};
|
||||
|
||||
var expectSettingsSectionsButNotFieldsToBeRendered = function (accountSettingsView) {
|
||||
expectSettingsSectionsAndFieldsToBeRendered(accountSettingsView, false)
|
||||
};
|
||||
|
||||
var expectSettingsSectionsAndFieldsToBeRendered = function (accountSettingsView, fieldsAreRendered) {
|
||||
var sectionsData = accountSettingsView.options.sectionsData;
|
||||
|
||||
var sectionElements = accountSettingsView.$('.section');
|
||||
expect(sectionElements.length).toBe(sectionsData.length);
|
||||
|
||||
_.each(sectionElements, function(sectionElement, sectionIndex) {
|
||||
expect($(sectionElement).find('.section-header').text().trim()).toBe(sectionsData[sectionIndex].title);
|
||||
|
||||
var sectionFieldElements = $(sectionElement).find('.u-field');
|
||||
|
||||
if (fieldsAreRendered === false) {
|
||||
expect(sectionFieldElements.length).toBe(0);
|
||||
} else {
|
||||
expect(sectionFieldElements.length).toBe(sectionsData[sectionIndex].fields.length);
|
||||
|
||||
_.each(sectionFieldElements, function (sectionFieldElement, fieldIndex) {
|
||||
expectElementContainsField(sectionFieldElement, sectionsData[sectionIndex].fields[fieldIndex]);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
USER_ACCOUNTS_API_URL: USER_ACCOUNTS_API_URL,
|
||||
USER_PREFERENCES_API_URL: USER_PREFERENCES_API_URL,
|
||||
IMAGE_UPLOAD_API_URL: IMAGE_UPLOAD_API_URL,
|
||||
IMAGE_REMOVE_API_URL: IMAGE_REMOVE_API_URL,
|
||||
IMAGE_MAX_BYTES: IMAGE_MAX_BYTES,
|
||||
IMAGE_MIN_BYTES: IMAGE_MIN_BYTES,
|
||||
PROFILE_IMAGE: PROFILE_IMAGE,
|
||||
createAccountSettingsData: createAccountSettingsData,
|
||||
createUserPreferencesData: createUserPreferencesData,
|
||||
FIELD_OPTIONS: FIELD_OPTIONS,
|
||||
expectLoadingIndicatorIsVisible: expectLoadingIndicatorIsVisible,
|
||||
expectLoadingErrorIsVisible: expectLoadingErrorIsVisible,
|
||||
expectElementContainsField: expectElementContainsField,
|
||||
expectSettingsSectionsButNotFieldsToBeRendered: expectSettingsSectionsButNotFieldsToBeRendered,
|
||||
expectSettingsSectionsAndFieldsToBeRendered: expectSettingsSectionsAndFieldsToBeRendered,
|
||||
};
|
||||
});
|
||||
@@ -99,7 +99,7 @@ define([
|
||||
{value: "", name: "--"},
|
||||
{value: "p", name: "Doctorate"},
|
||||
{value: "m", name: "Master's or professional degree"},
|
||||
{value: "b", name: "Bachelor's degree"},
|
||||
{value: "b", name: "Bachelor's degree"}
|
||||
],
|
||||
required: false,
|
||||
instructions: 'Select your education level.',
|
||||
@@ -115,7 +115,7 @@ define([
|
||||
{value: "", name: "--"},
|
||||
{value: "m", name: "Male"},
|
||||
{value: "f", name: "Female"},
|
||||
{value: "o", name: "Other"},
|
||||
{value: "o", name: "Other"}
|
||||
],
|
||||
required: false,
|
||||
instructions: 'Select your gender.',
|
||||
@@ -131,7 +131,7 @@ define([
|
||||
{value: "", name: "--"},
|
||||
{value: 1900, name: "1900"},
|
||||
{value: 1950, name: "1950"},
|
||||
{value: 2014, name: "2014"},
|
||||
{value: 2014, name: "2014"}
|
||||
],
|
||||
required: false,
|
||||
instructions: 'Select your year of birth.',
|
||||
|
||||
113
lms/static/js/spec/student_profile/helpers.js
Normal file
@@ -0,0 +1,113 @@
|
||||
define(['underscore'], function(_) {
|
||||
'use strict';
|
||||
|
||||
var expectProfileElementContainsField = function(element, view) {
|
||||
var $element = $(element);
|
||||
var fieldTitle = $element.find('.u-field-title').text().trim();
|
||||
|
||||
if (!_.isUndefined(view.options.title)) {
|
||||
expect(fieldTitle).toBe(view.options.title);
|
||||
}
|
||||
|
||||
if ('fieldValue' in view || 'imageUrl' in view) {
|
||||
if ('imageUrl' in view) {
|
||||
expect($($element.find('.image-frame')[0]).attr('src')).toBe(view.imageUrl());
|
||||
} else if (view.fieldValue()) {
|
||||
expect(view.fieldValue()).toBe(view.modelValue());
|
||||
|
||||
} else if ('optionForValue' in view) {
|
||||
expect($($element.find('.u-field-value .u-field-value-readonly')[0]).text()).toBe(view.displayValue(view.modelValue()));
|
||||
|
||||
}else {
|
||||
expect($($element.find('.u-field-value .u-field-value-readonly')[0]).text()).toBe(view.modelValue());
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unexpected field type: ' + view.fieldType);
|
||||
}
|
||||
};
|
||||
|
||||
var expectProfilePrivacyFieldTobeRendered = function(learnerProfileView, othersProfile) {
|
||||
|
||||
var accountPrivacyElement = learnerProfileView.$('.wrapper-profile-field-account-privacy');
|
||||
var privacyFieldElement = $(accountPrivacyElement).find('.u-field');
|
||||
|
||||
if (othersProfile) {
|
||||
expect(privacyFieldElement.length).toBe(0);
|
||||
} else {
|
||||
expect(privacyFieldElement.length).toBe(1);
|
||||
expectProfileElementContainsField(privacyFieldElement, learnerProfileView.options.accountPrivacyFieldView);
|
||||
}
|
||||
};
|
||||
|
||||
var expectSectionOneTobeRendered = function(learnerProfileView) {
|
||||
|
||||
var sectionOneFieldElements = $(learnerProfileView.$('.wrapper-profile-section-one')).find('.u-field');
|
||||
|
||||
expect(sectionOneFieldElements.length).toBe(4);
|
||||
expectProfileElementContainsField(sectionOneFieldElements[0], learnerProfileView.options.profileImageFieldView);
|
||||
expectProfileElementContainsField(sectionOneFieldElements[1], learnerProfileView.options.usernameFieldView);
|
||||
|
||||
_.each(_.rest(sectionOneFieldElements, 2) , function (sectionFieldElement, fieldIndex) {
|
||||
expectProfileElementContainsField(
|
||||
sectionFieldElement,
|
||||
learnerProfileView.options.sectionOneFieldViews[fieldIndex]
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
var expectSectionTwoTobeRendered = function(learnerProfileView) {
|
||||
|
||||
var sectionTwoElement = learnerProfileView.$('.wrapper-profile-section-two');
|
||||
var sectionTwoFieldElements = $(sectionTwoElement).find('.u-field');
|
||||
|
||||
expect(sectionTwoFieldElements.length).toBe(learnerProfileView.options.sectionTwoFieldViews.length);
|
||||
|
||||
_.each(sectionTwoFieldElements, function (sectionFieldElement, fieldIndex) {
|
||||
expectProfileElementContainsField(
|
||||
sectionFieldElement,
|
||||
learnerProfileView.options.sectionTwoFieldViews[fieldIndex]
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
var expectProfileSectionsAndFieldsToBeRendered = function (learnerProfileView, othersProfile) {
|
||||
expectProfilePrivacyFieldTobeRendered(learnerProfileView, othersProfile);
|
||||
expectSectionOneTobeRendered(learnerProfileView);
|
||||
expectSectionTwoTobeRendered(learnerProfileView);
|
||||
};
|
||||
|
||||
var expectLimitedProfileSectionsAndFieldsToBeRendered = function (learnerProfileView, othersProfile) {
|
||||
expectProfilePrivacyFieldTobeRendered(learnerProfileView, othersProfile);
|
||||
|
||||
var sectionOneFieldElements = $(learnerProfileView.$('.wrapper-profile-section-one')).find('.u-field');
|
||||
|
||||
expect(sectionOneFieldElements.length).toBe(2);
|
||||
expectProfileElementContainsField(
|
||||
sectionOneFieldElements[0],
|
||||
learnerProfileView.options.profileImageFieldView
|
||||
);
|
||||
expectProfileElementContainsField(
|
||||
sectionOneFieldElements[1],
|
||||
learnerProfileView.options.usernameFieldView
|
||||
);
|
||||
|
||||
if (othersProfile) {
|
||||
expect($('.profile-private--message').text())
|
||||
.toBe('This edX learner is currently sharing a limited profile.');
|
||||
} else {
|
||||
expect($('.profile-private--message').text()).toBe('You are currently sharing a limited profile.');
|
||||
}
|
||||
};
|
||||
|
||||
var expectProfileSectionsNotToBeRendered = function(learnerProfileView) {
|
||||
expect(learnerProfileView.$('.wrapper-profile-field-account-privacy').length).toBe(0);
|
||||
expect(learnerProfileView.$('.wrapper-profile-section-one').length).toBe(0);
|
||||
expect(learnerProfileView.$('.wrapper-profile-section-two').length).toBe(0);
|
||||
};
|
||||
|
||||
return {
|
||||
expectLimitedProfileSectionsAndFieldsToBeRendered: expectLimitedProfileSectionsAndFieldsToBeRendered,
|
||||
expectProfileSectionsAndFieldsToBeRendered: expectProfileSectionsAndFieldsToBeRendered,
|
||||
expectProfileSectionsNotToBeRendered: expectProfileSectionsNotToBeRendered
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers',
|
||||
'js/spec/student_account/helpers',
|
||||
'js/spec/student_profile/helpers',
|
||||
'js/views/fields',
|
||||
'js/student_account/models/user_account_model',
|
||||
'js/student_account/models/user_preferences_model',
|
||||
'js/student_profile/views/learner_profile_view',
|
||||
'js/student_profile/views/learner_profile_fields',
|
||||
'js/student_profile/views/learner_profile_factory',
|
||||
'js/views/message_banner'
|
||||
],
|
||||
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers, FieldViews,
|
||||
UserAccountModel, UserPreferencesModel, LearnerProfileView, LearnerProfileFields, LearnerProfilePage) {
|
||||
'use strict';
|
||||
|
||||
describe("edx.user.LearnerProfileFactory", function () {
|
||||
|
||||
var requests;
|
||||
|
||||
beforeEach(function () {
|
||||
loadFixtures('js/fixtures/student_profile/student_profile.html');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_readonly');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_dropdown');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_textarea');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_image');
|
||||
TemplateHelpers.installTemplate('templates/fields/message_banner');
|
||||
TemplateHelpers.installTemplate('templates/student_profile/learner_profile');
|
||||
});
|
||||
|
||||
var createProfilePage = function(ownProfile) {
|
||||
return new LearnerProfilePage({
|
||||
'accounts_api_url': Helpers.USER_ACCOUNTS_API_URL,
|
||||
'preferences_api_url': Helpers.USER_PREFERENCES_API_URL,
|
||||
'own_profile': ownProfile,
|
||||
'account_settings_page_url': Helpers.USER_ACCOUNTS_API_URL,
|
||||
'country_options': Helpers.FIELD_OPTIONS,
|
||||
'language_options': Helpers.FIELD_OPTIONS,
|
||||
'has_preferences_access': true,
|
||||
'profile_image_max_bytes': Helpers.IMAGE_MAX_BYTES,
|
||||
'profile_image_min_bytes': Helpers.IMAGE_MIN_BYTES,
|
||||
'profile_image_upload_url': Helpers.IMAGE_UPLOAD_API_URL,
|
||||
'profile_image_remove_url': Helpers.IMAGE_REMOVE_API_URL,
|
||||
'default_visibility': 'all_users'
|
||||
});
|
||||
};
|
||||
|
||||
it("show loading error when UserAccountModel fails to load", function() {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var context = createProfilePage(true),
|
||||
learnerProfileView = context.learnerProfileView;
|
||||
|
||||
var userAccountRequest = requests[0];
|
||||
expect(userAccountRequest.method).toBe('GET');
|
||||
expect(userAccountRequest.url).toBe(Helpers.USER_ACCOUNTS_API_URL);
|
||||
|
||||
AjaxHelpers.respondWithError(requests, 500);
|
||||
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, true);
|
||||
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, false);
|
||||
LearnerProfileHelpers.expectProfileSectionsNotToBeRendered(learnerProfileView);
|
||||
});
|
||||
|
||||
it("shows loading error when UserPreferencesModel fails to load", function() {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var context = createProfilePage(true),
|
||||
learnerProfileView = context.learnerProfileView;
|
||||
|
||||
var userAccountRequest = requests[0];
|
||||
expect(userAccountRequest.method).toBe('GET');
|
||||
expect(userAccountRequest.url).toBe(Helpers.USER_ACCOUNTS_API_URL);
|
||||
|
||||
AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData());
|
||||
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
|
||||
LearnerProfileHelpers.expectProfileSectionsNotToBeRendered(learnerProfileView);
|
||||
|
||||
var userPreferencesRequest = requests[1];
|
||||
expect(userPreferencesRequest.method).toBe('GET');
|
||||
expect(userPreferencesRequest.url).toBe(Helpers.USER_PREFERENCES_API_URL);
|
||||
|
||||
AjaxHelpers.respondWithError(requests, 500);
|
||||
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, false);
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, true);
|
||||
LearnerProfileHelpers.expectProfileSectionsNotToBeRendered(learnerProfileView);
|
||||
});
|
||||
|
||||
it("renders the full profile after models are successfully fetched", function() {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var context = createProfilePage(true),
|
||||
learnerProfileView = context.learnerProfileView;
|
||||
|
||||
AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData());
|
||||
AjaxHelpers.respondWithJson(requests, Helpers.createUserPreferencesData());
|
||||
|
||||
// sets the profile for full view.
|
||||
context.accountPreferencesModel.set({account_privacy: 'all_users'});
|
||||
LearnerProfileHelpers.expectProfileSectionsAndFieldsToBeRendered(learnerProfileView, false);
|
||||
});
|
||||
|
||||
it("renders the limited profile for undefined 'year_of_birth'", function() {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var context = createProfilePage(true),
|
||||
learnerProfileView = context.learnerProfileView;
|
||||
|
||||
AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData({
|
||||
year_of_birth: '',
|
||||
requires_parental_consent: true
|
||||
}));
|
||||
AjaxHelpers.respondWithJson(requests, Helpers.createUserPreferencesData());
|
||||
|
||||
LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView);
|
||||
});
|
||||
|
||||
it("renders the limited profile for under 13 users", function() {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var context = createProfilePage(true),
|
||||
learnerProfileView = context.learnerProfileView;
|
||||
|
||||
AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData({
|
||||
year_of_birth: new Date().getFullYear() - 10,
|
||||
requires_parental_consent: true
|
||||
}));
|
||||
AjaxHelpers.respondWithJson(requests, Helpers.createUserPreferencesData());
|
||||
|
||||
LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,291 @@
|
||||
define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers',
|
||||
'js/spec/student_account/helpers',
|
||||
'js/student_account/models/user_account_model',
|
||||
'js/student_profile/views/learner_profile_fields',
|
||||
'js/views/message_banner'
|
||||
],
|
||||
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, UserAccountModel, LearnerProfileFields,
|
||||
MessageBannerView) {
|
||||
'use strict';
|
||||
|
||||
describe("edx.user.LearnerProfileFields", function () {
|
||||
|
||||
var MOCK_YEAR_OF_BIRTH = 1989;
|
||||
var MOCK_IMAGE_MAX_BYTES = 64;
|
||||
var MOCK_IMAGE_MIN_BYTES = 16;
|
||||
|
||||
var createImageView = function (options) {
|
||||
var yearOfBirth = _.isUndefined(options.yearOfBirth) ? MOCK_YEAR_OF_BIRTH : options.yearOfBirth;
|
||||
var imageMaxBytes = _.isUndefined(options.imageMaxBytes) ? MOCK_IMAGE_MAX_BYTES : options.imageMaxBytes;
|
||||
var imageMinBytes = _.isUndefined(options.imageMinBytes) ? MOCK_IMAGE_MIN_BYTES : options.imageMinBytes;
|
||||
|
||||
var imageData = {
|
||||
image_url_large: '/media/profile-images/default.jpg',
|
||||
has_image: options.hasImage ? true : false
|
||||
};
|
||||
|
||||
var accountSettingsModel = new UserAccountModel();
|
||||
accountSettingsModel.set({'profile_image': imageData});
|
||||
accountSettingsModel.set({'year_of_birth': yearOfBirth});
|
||||
accountSettingsModel.set({'requires_parental_consent': _.isEmpty(yearOfBirth) ? true : false});
|
||||
|
||||
accountSettingsModel.url = Helpers.USER_ACCOUNTS_API_URL;
|
||||
|
||||
var messageView = new MessageBannerView({
|
||||
el: $('.message-banner')
|
||||
});
|
||||
|
||||
return new LearnerProfileFields.ProfileImageFieldView({
|
||||
model: accountSettingsModel,
|
||||
valueAttribute: "profile_image",
|
||||
editable: options.ownProfile,
|
||||
messageView: messageView,
|
||||
imageMaxBytes: imageMaxBytes,
|
||||
imageMinBytes: imageMinBytes,
|
||||
imageUploadUrl: Helpers.IMAGE_UPLOAD_API_URL,
|
||||
imageRemoveUrl: Helpers.IMAGE_REMOVE_API_URL
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
loadFixtures('js/fixtures/student_profile/student_profile.html');
|
||||
TemplateHelpers.installTemplate('templates/student_profile/learner_profile');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_image');
|
||||
TemplateHelpers.installTemplate("templates/fields/message_banner");
|
||||
});
|
||||
|
||||
var createFakeImageFile = function (size) {
|
||||
var fileFakeData = 'i63ljc6giwoskyb9x5sw0169bdcmcxr3cdz8boqv0lik971972cmd6yknvcxr5sw0nvc169bdcmcxsdf';
|
||||
return new Blob(
|
||||
[ fileFakeData.substr(0, size) ],
|
||||
{ type: 'image/jpg' }
|
||||
);
|
||||
};
|
||||
|
||||
var initializeUploader = function (view) {
|
||||
view.$('.upload-button-input').fileupload({
|
||||
url: Helpers.IMAGE_UPLOAD_API_URL,
|
||||
type: 'POST',
|
||||
add: view.fileSelected,
|
||||
done: view.imageChangeSucceeded,
|
||||
fail: view.imageChangeFailed
|
||||
});
|
||||
};
|
||||
|
||||
describe("ProfileImageFieldView", function () {
|
||||
|
||||
var verifyImageUploadButtonMessage = function (view, inProgress) {
|
||||
var iconName = inProgress ? 'fa-spinner' : 'fa-camera';
|
||||
var message = inProgress ? view.titleUploading : view.uploadButtonTitle();
|
||||
expect(view.$('.upload-button-icon i').attr('class')).toContain(iconName);
|
||||
expect(view.$('.upload-button-title').text().trim()).toBe(message);
|
||||
};
|
||||
|
||||
var verifyImageRemoveButtonMessage = function (view, inProgress) {
|
||||
var iconName = inProgress ? 'fa-spinner' : 'fa-remove';
|
||||
var message = inProgress ? view.titleRemoving : view.removeButtonTitle();
|
||||
expect(view.$('.remove-button-icon i').attr('class')).toContain(iconName);
|
||||
expect(view.$('.remove-button-title').text().trim()).toBe(message);
|
||||
};
|
||||
|
||||
it("can upload profile image", function() {
|
||||
|
||||
var imageView = createImageView({ownProfile: true, hasImage: false});
|
||||
imageView.render();
|
||||
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
var imageName = 'profile_image.jpg';
|
||||
|
||||
initializeUploader(imageView);
|
||||
|
||||
// Remove button should not be present for default image
|
||||
expect(imageView.$('.u-field-remove-button').css('display') === 'none').toBeTruthy();
|
||||
|
||||
// For default image, image title should be `Upload an image`
|
||||
verifyImageUploadButtonMessage(imageView, false);
|
||||
|
||||
// Add image to upload queue. Validate the image size and send POST request to upload image
|
||||
imageView.$('.upload-button-input').fileupload('add', {files: [createFakeImageFile(60)]});
|
||||
|
||||
// Verify image upload progress message
|
||||
verifyImageUploadButtonMessage(imageView, true);
|
||||
|
||||
// Verify if POST request received for image upload
|
||||
AjaxHelpers.expectRequest(requests, 'POST', Helpers.IMAGE_UPLOAD_API_URL, new FormData());
|
||||
|
||||
// Send 204 NO CONTENT to confirm the image upload success
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
|
||||
// Upon successful image upload, account settings model will be fetched to
|
||||
// get the url for newly uploaded image, So we need to send the response for that GET
|
||||
var data = {profile_image: {
|
||||
image_url_large: '/media/profile-images/' + imageName,
|
||||
has_image: true
|
||||
}};
|
||||
AjaxHelpers.respondWithJson(requests, data);
|
||||
|
||||
// Verify uploaded image name
|
||||
expect(imageView.$('.image-frame').attr('src')).toContain(imageName);
|
||||
|
||||
// Remove button should be present after successful image upload
|
||||
expect(imageView.$('.u-field-remove-button').css('display') !== 'none').toBeTruthy();
|
||||
|
||||
// After image upload, image title should be `Change image`
|
||||
verifyImageUploadButtonMessage(imageView, false);
|
||||
});
|
||||
|
||||
it("can remove profile image", function() {
|
||||
|
||||
var imageView = createImageView({ownProfile: true, hasImage: false});
|
||||
imageView.render();
|
||||
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
|
||||
// Verify image remove title
|
||||
verifyImageRemoveButtonMessage(imageView, false);
|
||||
|
||||
imageView.$('.u-field-remove-button').click();
|
||||
|
||||
// Verify image remove progress message
|
||||
verifyImageRemoveButtonMessage(imageView, true);
|
||||
|
||||
// Verify if POST request received for image remove
|
||||
AjaxHelpers.expectRequest(requests, 'POST', Helpers.IMAGE_REMOVE_API_URL, null);
|
||||
|
||||
// Send 204 NO CONTENT to confirm the image removal success
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
|
||||
// Upon successful image removal, account settings model will be fetched to get default image url
|
||||
// So we need to send the response for that GET
|
||||
var data = {profile_image: {
|
||||
image_url_large: '/media/profile-images/default.jpg',
|
||||
has_image: false
|
||||
}};
|
||||
AjaxHelpers.respondWithJson(requests, data);
|
||||
|
||||
// Remove button should not be present for default image
|
||||
expect(imageView.$('.u-field-remove-button').css('display') === 'none').toBeTruthy();
|
||||
});
|
||||
|
||||
it("can't remove default profile image", function() {
|
||||
|
||||
var imageView = createImageView({ownProfile: true, hasImage: false});
|
||||
imageView.render();
|
||||
|
||||
spyOn(imageView, 'clickedRemoveButton');
|
||||
|
||||
// Remove button should not be present for default image
|
||||
expect(imageView.$('.u-field-remove-button').css('display') === 'none').toBeTruthy();
|
||||
|
||||
imageView.$('.u-field-remove-button').click();
|
||||
|
||||
// Remove button click handler should not be called
|
||||
expect(imageView.clickedRemoveButton).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("can't upload image having size greater than max size", function() {
|
||||
|
||||
var imageView = createImageView({ownProfile: true, hasImage: false});
|
||||
imageView.render();
|
||||
|
||||
initializeUploader(imageView);
|
||||
|
||||
// Add image to upload queue, this will validate the image size
|
||||
imageView.$('.upload-button-input').fileupload('add', {files: [createFakeImageFile(70)]});
|
||||
|
||||
// Verify error message
|
||||
expect($('.message-banner').text().trim())
|
||||
.toBe('The file must be smaller than 64 bytes in size.');
|
||||
});
|
||||
|
||||
it("can't upload image having size less than min size", function() {
|
||||
var imageView = createImageView({ownProfile: true, hasImage: false});
|
||||
imageView.render();
|
||||
|
||||
initializeUploader(imageView);
|
||||
|
||||
// Add image to upload queue, this will validate the image size
|
||||
imageView.$('.upload-button-input').fileupload('add', {files: [createFakeImageFile(10)]});
|
||||
|
||||
// Verify error message
|
||||
expect($('.message-banner').text().trim()).toBe('The file must be at least 16 bytes in size.');
|
||||
});
|
||||
|
||||
it("can't upload and remove image if parental consent required", function() {
|
||||
|
||||
var imageView = createImageView({ownProfile: true, hasImage: false, yearOfBirth: ''});
|
||||
imageView.render();
|
||||
|
||||
spyOn(imageView, 'clickedUploadButton');
|
||||
spyOn(imageView, 'clickedRemoveButton');
|
||||
|
||||
expect(imageView.$('.u-field-upload-button').css('display') === 'none').toBeTruthy();
|
||||
expect(imageView.$('.u-field-remove-button').css('display') === 'none').toBeTruthy();
|
||||
|
||||
imageView.$('.u-field-upload-button').click();
|
||||
imageView.$('.u-field-remove-button').click();
|
||||
|
||||
expect(imageView.clickedUploadButton).not.toHaveBeenCalled();
|
||||
expect(imageView.clickedRemoveButton).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("can't upload and remove image on others profile", function() {
|
||||
|
||||
var imageView = createImageView({ownProfile: false});
|
||||
imageView.render();
|
||||
|
||||
spyOn(imageView, 'clickedUploadButton');
|
||||
spyOn(imageView, 'clickedRemoveButton');
|
||||
|
||||
expect(imageView.$('.u-field-upload-button').css('display') === 'none').toBeTruthy();
|
||||
expect(imageView.$('.u-field-remove-button').css('display') === 'none').toBeTruthy();
|
||||
|
||||
imageView.$('.u-field-upload-button').click();
|
||||
imageView.$('.u-field-remove-button').click();
|
||||
|
||||
expect(imageView.clickedUploadButton).not.toHaveBeenCalled();
|
||||
expect(imageView.clickedRemoveButton).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows message if we try to navigate away during image upload/remove", function() {
|
||||
var imageView = createImageView({ownProfile: true, hasImage: false});
|
||||
spyOn(imageView, 'onBeforeUnload');
|
||||
imageView.render();
|
||||
|
||||
initializeUploader(imageView);
|
||||
|
||||
// Add image to upload queue, this will validate image size and send POST request to upload image
|
||||
imageView.$('.upload-button-input').fileupload('add', {files: [createFakeImageFile(60)]});
|
||||
|
||||
// Verify image upload progress message
|
||||
verifyImageUploadButtonMessage(imageView, true);
|
||||
|
||||
$(window).trigger('beforeunload');
|
||||
expect(imageView.onBeforeUnload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows error message for HTTP 500", function() {
|
||||
var imageView = createImageView({ownProfile: true, hasImage: false});
|
||||
imageView.render();
|
||||
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
|
||||
initializeUploader(imageView);
|
||||
|
||||
// Add image to upload queue. Validate the image size and send POST request to upload image
|
||||
imageView.$('.upload-button-input').fileupload('add', {files: [createFakeImageFile(60)]});
|
||||
|
||||
// Verify image upload progress message
|
||||
verifyImageUploadButtonMessage(imageView, true);
|
||||
|
||||
// Verify if POST request received for image upload
|
||||
AjaxHelpers.expectRequest(requests, 'POST', Helpers.IMAGE_UPLOAD_API_URL, new FormData());
|
||||
|
||||
// Send HTTP 500
|
||||
AjaxHelpers.respondWithError(requests);
|
||||
|
||||
expect($('.message-banner').text().trim()).toBe(imageView.errorMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
198
lms/static/js/spec/student_profile/learner_profile_view_spec.js
Normal file
@@ -0,0 +1,198 @@
|
||||
define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers',
|
||||
'js/spec/student_account/helpers',
|
||||
'js/spec/student_profile/helpers',
|
||||
'js/views/fields',
|
||||
'js/student_account/models/user_account_model',
|
||||
'js/student_account/models/user_preferences_model',
|
||||
'js/student_profile/views/learner_profile_fields',
|
||||
'js/student_profile/views/learner_profile_view',
|
||||
'js/student_account/views/account_settings_fields',
|
||||
'js/views/message_banner'
|
||||
],
|
||||
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers, FieldViews,
|
||||
UserAccountModel, AccountPreferencesModel, LearnerProfileFields, LearnerProfileView,
|
||||
AccountSettingsFieldViews, MessageBannerView) {
|
||||
'use strict';
|
||||
|
||||
describe("edx.user.LearnerProfileView", function () {
|
||||
|
||||
var createLearnerProfileView = function (ownProfile, accountPrivacy, profileIsPublic) {
|
||||
|
||||
var accountSettingsModel = new UserAccountModel();
|
||||
accountSettingsModel.set(Helpers.createAccountSettingsData());
|
||||
accountSettingsModel.set({'profile_is_public': profileIsPublic});
|
||||
accountSettingsModel.set({'profile_image': Helpers.PROFILE_IMAGE});
|
||||
|
||||
var accountPreferencesModel = new AccountPreferencesModel();
|
||||
accountPreferencesModel.set({account_privacy: accountPrivacy});
|
||||
|
||||
accountPreferencesModel.url = Helpers.USER_PREFERENCES_API_URL;
|
||||
|
||||
var editable = ownProfile ? 'toggle' : 'never';
|
||||
|
||||
var accountPrivacyFieldView = new LearnerProfileFields.AccountPrivacyFieldView({
|
||||
model: accountPreferencesModel,
|
||||
required: true,
|
||||
editable: 'always',
|
||||
showMessages: false,
|
||||
title: 'edX learners can see my:',
|
||||
valueAttribute: "account_privacy",
|
||||
options: [
|
||||
['all_users', 'Full Profile'],
|
||||
['private', 'Limited Profile']
|
||||
],
|
||||
helpMessage: '',
|
||||
accountSettingsPageUrl: '/account/settings/'
|
||||
});
|
||||
|
||||
var messageView = new MessageBannerView({
|
||||
el: $('.message-banner')
|
||||
});
|
||||
|
||||
var profileImageFieldView = new LearnerProfileFields.ProfileImageFieldView({
|
||||
model: accountSettingsModel,
|
||||
valueAttribute: "profile_image",
|
||||
editable: editable,
|
||||
messageView: messageView,
|
||||
imageMaxBytes: Helpers.IMAGE_MAX_BYTES,
|
||||
imageMinBytes: Helpers.IMAGE_MIN_BYTES,
|
||||
imageUploadUrl: Helpers.IMAGE_UPLOAD_API_URL,
|
||||
imageRemoveUrl: Helpers.IMAGE_REMOVE_API_URL
|
||||
});
|
||||
|
||||
var usernameFieldView = new FieldViews.ReadonlyFieldView({
|
||||
model: accountSettingsModel,
|
||||
valueAttribute: "username",
|
||||
helpMessage: ""
|
||||
});
|
||||
|
||||
var sectionOneFieldViews = [
|
||||
new FieldViews.DropdownFieldView({
|
||||
model: accountSettingsModel,
|
||||
required: false,
|
||||
editable: editable,
|
||||
showMessages: false,
|
||||
iconName: 'fa-map-marker',
|
||||
placeholderValue: '',
|
||||
valueAttribute: "country",
|
||||
options: Helpers.FIELD_OPTIONS,
|
||||
helpMessage: ''
|
||||
}),
|
||||
|
||||
new AccountSettingsFieldViews.LanguageProficienciesFieldView({
|
||||
model: accountSettingsModel,
|
||||
required: false,
|
||||
editable: editable,
|
||||
showMessages: false,
|
||||
iconName: 'fa-comment',
|
||||
placeholderValue: 'Add language',
|
||||
valueAttribute: "language_proficiencies",
|
||||
options: Helpers.FIELD_OPTIONS,
|
||||
helpMessage: ''
|
||||
})
|
||||
];
|
||||
|
||||
var sectionTwoFieldViews = [
|
||||
new FieldViews.TextareaFieldView({
|
||||
model: accountSettingsModel,
|
||||
editable: editable,
|
||||
showMessages: false,
|
||||
title: 'About me',
|
||||
placeholderValue: "Tell other edX learners a little about yourself: where you live, " +
|
||||
"what your interests are, why you're taking courses on edX, or what you hope to learn.",
|
||||
valueAttribute: "bio",
|
||||
helpMessage: ''
|
||||
})
|
||||
];
|
||||
|
||||
return new LearnerProfileView(
|
||||
{
|
||||
el: $('.wrapper-profile'),
|
||||
ownProfile: ownProfile,
|
||||
hasPreferencesAccess: true,
|
||||
accountSettingsModel: accountSettingsModel,
|
||||
preferencesModel: accountPreferencesModel,
|
||||
accountPrivacyFieldView: accountPrivacyFieldView,
|
||||
usernameFieldView: usernameFieldView,
|
||||
profileImageFieldView: profileImageFieldView,
|
||||
sectionOneFieldViews: sectionOneFieldViews,
|
||||
sectionTwoFieldViews: sectionTwoFieldViews
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
loadFixtures('js/fixtures/student_profile/student_profile.html');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_readonly');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_dropdown');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_textarea');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_image');
|
||||
TemplateHelpers.installTemplate('templates/fields/message_banner');
|
||||
TemplateHelpers.installTemplate('templates/student_profile/learner_profile');
|
||||
});
|
||||
|
||||
it("shows loading error correctly", function() {
|
||||
|
||||
var learnerProfileView = createLearnerProfileView(false, 'all_users');
|
||||
|
||||
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
|
||||
|
||||
learnerProfileView.render();
|
||||
learnerProfileView.showLoadingError();
|
||||
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, true);
|
||||
});
|
||||
|
||||
it("renders all fields as expected for self with full access", function() {
|
||||
|
||||
var learnerProfileView = createLearnerProfileView(true, 'all_users', true);
|
||||
|
||||
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
|
||||
|
||||
learnerProfileView.render();
|
||||
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
|
||||
LearnerProfileHelpers.expectProfileSectionsAndFieldsToBeRendered(learnerProfileView);
|
||||
});
|
||||
|
||||
it("renders all fields as expected for self with limited access", function() {
|
||||
|
||||
var learnerProfileView = createLearnerProfileView(true, 'private', false);
|
||||
|
||||
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
|
||||
|
||||
learnerProfileView.render();
|
||||
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
|
||||
LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView);
|
||||
});
|
||||
|
||||
it("renders the fields as expected for others with full access", function() {
|
||||
|
||||
var learnerProfileView = createLearnerProfileView(false, 'all_users', true);
|
||||
|
||||
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
|
||||
|
||||
learnerProfileView.render();
|
||||
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
|
||||
LearnerProfileHelpers.expectProfileSectionsAndFieldsToBeRendered(learnerProfileView, true);
|
||||
});
|
||||
|
||||
it("renders the fields as expected for others with limited access", function() {
|
||||
|
||||
var learnerProfileView = createLearnerProfileView(false, 'private', false);
|
||||
|
||||
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
|
||||
|
||||
learnerProfileView.render();
|
||||
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
|
||||
LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
233
lms/static/js/spec/views/fields_helpers.js
Normal file
@@ -0,0 +1,233 @@
|
||||
define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers',
|
||||
'js/views/fields',
|
||||
'string_utils'],
|
||||
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, FieldViews) {
|
||||
'use strict';
|
||||
|
||||
var API_URL = '/api/end_point/v1';
|
||||
|
||||
var USERNAME = 'Legolas',
|
||||
FULLNAME = 'Legolas Thranduil',
|
||||
EMAIL = 'legolas@woodland.middlearth',
|
||||
SELECT_OPTIONS = [['si', 'sindarin'], ['el', 'elvish'], ['na', 'nandor']];
|
||||
|
||||
var UserAccountModel = Backbone.Model.extend({
|
||||
idAttribute: 'username',
|
||||
defaults: {
|
||||
username: USERNAME,
|
||||
name: FULLNAME,
|
||||
email: EMAIL,
|
||||
language: SELECT_OPTIONS[0][0]
|
||||
},
|
||||
url: API_URL
|
||||
});
|
||||
|
||||
var createFieldData = function (fieldType, fieldData) {
|
||||
var data = {
|
||||
model: fieldData.model || new UserAccountModel({}),
|
||||
title: fieldData.title || 'Field Title',
|
||||
valueAttribute: fieldData.valueAttribute,
|
||||
helpMessage: fieldData.helpMessage || 'I am a field message',
|
||||
placeholderValue: fieldData.placeholderValue || 'I am a placeholder message'
|
||||
};
|
||||
|
||||
switch (fieldType) {
|
||||
case FieldViews.DropdownFieldView:
|
||||
data['required'] = fieldData.required || false;
|
||||
data['options'] = fieldData.options || SELECT_OPTIONS;
|
||||
break;
|
||||
case FieldViews.LinkFieldView:
|
||||
case FieldViews.PasswordFieldView:
|
||||
data['linkTitle'] = fieldData.linkTitle || "Link Title";
|
||||
data['linkHref'] = fieldData.linkHref || "/path/to/resource";
|
||||
data['emailAttribute'] = 'email';
|
||||
break;
|
||||
}
|
||||
|
||||
_.extend(data, fieldData);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
var createErrorMessage = function(attribute, user_message) {
|
||||
var field_errors = {};
|
||||
field_errors[attribute] = {
|
||||
"user_message": user_message
|
||||
};
|
||||
return {
|
||||
"field_errors": field_errors
|
||||
};
|
||||
};
|
||||
|
||||
var expectTitleToContain = function(view, expectedTitle) {
|
||||
expect(view.$('.u-field-title').text().trim()).toContain(expectedTitle);
|
||||
};
|
||||
|
||||
var expectMessageContains = function(view, expectedText) {
|
||||
expect(view.$('.u-field-message').html()).toContain(expectedText);
|
||||
};
|
||||
|
||||
var expectTitleAndMessageToContain = function(view, expectedTitle, expectedMessage) {
|
||||
expectTitleToContain(view, expectedTitle);
|
||||
expectMessageContains(view, expectedMessage);
|
||||
};
|
||||
|
||||
var expectAjaxRequestWithData = function(requests, data) {
|
||||
AjaxHelpers.expectJsonRequest(
|
||||
requests, 'PATCH', API_URL, data
|
||||
);
|
||||
};
|
||||
|
||||
var verifyMessageUpdates = function (view, data, timerCallback) {
|
||||
|
||||
var message = 'Here to help!';
|
||||
|
||||
view.showHelpMessage(message);
|
||||
expectMessageContains(view, message);
|
||||
|
||||
view.showHelpMessage();
|
||||
expectMessageContains(view, view.helpMessage);
|
||||
|
||||
view.showInProgressMessage();
|
||||
expectMessageContains(view, view.indicators.inProgress);
|
||||
expectMessageContains(view, view.messages.inProgress);
|
||||
|
||||
view.showSuccessMessage();
|
||||
expectMessageContains(view, view.indicators.success);
|
||||
expectMessageContains(view, view.getMessage('success'));
|
||||
|
||||
expect(timerCallback).not.toHaveBeenCalled();
|
||||
|
||||
view.showErrorMessage({
|
||||
responseText: JSON.stringify(createErrorMessage(data.valueAttribute, 'Ops, try again!.')),
|
||||
status: 400
|
||||
});
|
||||
expectMessageContains(view, view.indicators.validationError);
|
||||
|
||||
view.showErrorMessage({status: 500});
|
||||
expectMessageContains(view, view.indicators.error);
|
||||
expectMessageContains(view, view.indicators.error);
|
||||
};
|
||||
|
||||
var verifySuccessMessageReset = function (view) {
|
||||
view.showHelpMessage();
|
||||
expectMessageContains(view, view.helpMessage);
|
||||
view.showSuccessMessage();
|
||||
expectMessageContains(view, view.indicators.success);
|
||||
jasmine.Clock.tick(5000);
|
||||
// Message gets reset
|
||||
expectMessageContains(view, view.helpMessage);
|
||||
|
||||
view.showSuccessMessage();
|
||||
expectMessageContains(view, view.indicators.success);
|
||||
// But if we change the message, it should not get reset.
|
||||
view.showHelpMessage("Do not reset this!");
|
||||
jasmine.Clock.tick(5000);
|
||||
expectMessageContains(view, "Do not reset this!");
|
||||
};
|
||||
|
||||
var verifyEditableField = function (view, data, requests) {
|
||||
var request_data = {};
|
||||
var url = view.model.url;
|
||||
|
||||
if (data.editable === 'toggle') {
|
||||
expect(view.el).toHaveClass('mode-placeholder');
|
||||
expectTitleToContain(view, data.title);
|
||||
expectMessageContains(view, view.indicators.canEdit);
|
||||
view.$el.click();
|
||||
} else {
|
||||
expectTitleAndMessageToContain(view, data.title, data.helpMessage, false);
|
||||
}
|
||||
|
||||
expect(view.el).toHaveClass('mode-edit');
|
||||
expect(view.fieldValue()).not.toContain(data.validValue);
|
||||
|
||||
view.$(data.valueInputSelector).val(data.validValue).change();
|
||||
// When the value in the field is changed
|
||||
expect(view.fieldValue()).toBe(data.validValue);
|
||||
expectMessageContains(view, view.indicators.inProgress);
|
||||
expectMessageContains(view, view.messages.inProgress);
|
||||
request_data[data.valueAttribute] = data.validValue;
|
||||
AjaxHelpers.expectJsonRequest(
|
||||
requests, 'PATCH', url, request_data
|
||||
);
|
||||
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
// When server returns success.
|
||||
if (data.editable === 'toggle') {
|
||||
expect(view.el).toHaveClass('mode-display');
|
||||
view.$el.click();
|
||||
} else {
|
||||
expectMessageContains(view, view.indicators.success);
|
||||
}
|
||||
|
||||
view.$(data.valueInputSelector).val(data.invalidValue1).change();
|
||||
request_data[data.valueAttribute] = data.invalidValue1;
|
||||
AjaxHelpers.expectJsonRequest(
|
||||
requests, 'PATCH', url, request_data
|
||||
);
|
||||
AjaxHelpers.respondWithError(requests, 500);
|
||||
// When server returns a 500 error
|
||||
expectMessageContains(view, view.indicators.error);
|
||||
expectMessageContains(view, view.messages.error);
|
||||
expect(view.el).toHaveClass('mode-edit');
|
||||
|
||||
view.$(data.valueInputSelector).val(data.invalidValue2).change();
|
||||
request_data[data.valueAttribute] = data.invalidValue2;
|
||||
AjaxHelpers.expectJsonRequest(
|
||||
requests, 'PATCH', url, request_data
|
||||
);
|
||||
AjaxHelpers.respondWithError(requests, 400, createErrorMessage(data.valueAttribute, data.validationError));
|
||||
// When server returns a validation error
|
||||
expectMessageContains(view, view.indicators.validationError);
|
||||
expectMessageContains(view, data.validationError);
|
||||
expect(view.el).toHaveClass('mode-edit');
|
||||
|
||||
view.$(data.valueInputSelector).val('').change();
|
||||
// When the value in the field is changed
|
||||
expect(view.fieldValue()).toBe('');
|
||||
request_data[data.valueAttribute] = '';
|
||||
AjaxHelpers.expectJsonRequest(
|
||||
requests, 'PATCH', url, request_data
|
||||
);
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
// When server returns success.
|
||||
if (data.editable === 'toggle') {
|
||||
expect(view.el).toHaveClass('mode-placeholder');
|
||||
} else {
|
||||
expect(view.el).toHaveClass('mode-edit');
|
||||
}
|
||||
};
|
||||
|
||||
var verifyTextField = function (view, data, requests) {
|
||||
verifyEditableField(view, _.extend({
|
||||
valueSelector: '.u-field-value',
|
||||
valueInputSelector: '.u-field-value > input'
|
||||
}, data
|
||||
), requests);
|
||||
};
|
||||
|
||||
var verifyDropDownField = function (view, data, requests) {
|
||||
verifyEditableField(view, _.extend({
|
||||
valueSelector: '.u-field-value',
|
||||
valueInputSelector: '.u-field-value > select'
|
||||
}, data
|
||||
), requests);
|
||||
};
|
||||
|
||||
return {
|
||||
SELECT_OPTIONS: SELECT_OPTIONS,
|
||||
UserAccountModel: UserAccountModel,
|
||||
createFieldData: createFieldData,
|
||||
createErrorMessage: createErrorMessage,
|
||||
expectTitleToContain: expectTitleToContain,
|
||||
expectTitleAndMessageToContain: expectTitleAndMessageToContain,
|
||||
expectMessageContains: expectMessageContains,
|
||||
expectAjaxRequestWithData: expectAjaxRequestWithData,
|
||||
verifyMessageUpdates: verifyMessageUpdates,
|
||||
verifySuccessMessageReset: verifySuccessMessageReset,
|
||||
verifyEditableField: verifyEditableField,
|
||||
verifyTextField: verifyTextField,
|
||||
verifyDropDownField: verifyDropDownField
|
||||
};
|
||||
});
|
||||
301
lms/static/js/spec/views/fields_spec.js
Normal file
@@ -0,0 +1,301 @@
|
||||
define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers',
|
||||
'js/views/fields',
|
||||
'js/spec/views/fields_helpers',
|
||||
'string_utils'],
|
||||
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, FieldViews, FieldViewsSpecHelpers) {
|
||||
'use strict';
|
||||
|
||||
var USERNAME = 'Legolas',
|
||||
BIO = "My Name is Theon Greyjoy. I'm member of House Greyjoy";
|
||||
|
||||
describe("edx.FieldViews", function () {
|
||||
|
||||
var requests,
|
||||
timerCallback;
|
||||
|
||||
var fieldViewClasses = [
|
||||
FieldViews.ReadonlyFieldView,
|
||||
FieldViews.TextFieldView,
|
||||
FieldViews.DropdownFieldView,
|
||||
FieldViews.LinkFieldView,
|
||||
FieldViews.TextareaFieldView
|
||||
|
||||
];
|
||||
|
||||
beforeEach(function () {
|
||||
TemplateHelpers.installTemplate('templates/fields/field_readonly');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_dropdown');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_link');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_text');
|
||||
TemplateHelpers.installTemplate('templates/fields/field_textarea');
|
||||
|
||||
timerCallback = jasmine.createSpy('timerCallback');
|
||||
jasmine.Clock.useMock();
|
||||
});
|
||||
|
||||
it("updates messages correctly for all fields", function() {
|
||||
|
||||
for (var i = 0; i < fieldViewClasses.length; i++) {
|
||||
|
||||
var fieldViewClass = fieldViewClasses[i];
|
||||
var fieldData = FieldViewsSpecHelpers.createFieldData(fieldViewClass, {
|
||||
title: 'Username',
|
||||
valueAttribute: 'username',
|
||||
helpMessage: 'The username that you use to sign in to edX.'
|
||||
});
|
||||
|
||||
var view = new fieldViewClass(fieldData).render();
|
||||
FieldViewsSpecHelpers.verifyMessageUpdates(view, fieldData, timerCallback);
|
||||
}
|
||||
});
|
||||
|
||||
it("resets to help message some time after success message is set", function() {
|
||||
|
||||
for (var i = 0; i < fieldViewClasses.length; i++) {
|
||||
var fieldViewClass = fieldViewClasses[i];
|
||||
var fieldData = FieldViewsSpecHelpers.createFieldData(fieldViewClass, {
|
||||
title: 'Username',
|
||||
valueAttribute: 'username',
|
||||
helpMessage: 'The username that you use to sign in to edX.'
|
||||
});
|
||||
|
||||
var view = new fieldViewClass(fieldData).render();
|
||||
FieldViewsSpecHelpers.verifySuccessMessageReset(view, fieldData, timerCallback);
|
||||
}
|
||||
});
|
||||
|
||||
it("sends a PATCH request when saveAttributes is called", function() {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var fieldViewClass = FieldViews.EditableFieldView;
|
||||
var fieldData = FieldViewsSpecHelpers.createFieldData(fieldViewClass, {
|
||||
title: 'Preferred Language',
|
||||
valueAttribute: 'language',
|
||||
helpMessage: 'Your preferred language.'
|
||||
});
|
||||
|
||||
var view = new fieldViewClass(fieldData);
|
||||
view.saveAttributes(
|
||||
{'language': 'ur'},
|
||||
{'headers': {'Priority': 'Urgent'}}
|
||||
);
|
||||
|
||||
var request = requests[0];
|
||||
expect(request.method).toBe('PATCH');
|
||||
expect(request.requestHeaders['Content-Type']).toBe('application/merge-patch+json;charset=utf-8');
|
||||
expect(request.requestHeaders.Priority).toBe('Urgent');
|
||||
expect(request.requestBody).toBe('{"language":"ur"}');
|
||||
});
|
||||
|
||||
it("correctly renders and updates ReadonlyFieldView", function() {
|
||||
var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.ReadonlyFieldView, {
|
||||
title: 'Username',
|
||||
valueAttribute: 'username',
|
||||
helpMessage: 'The username that you use to sign in to edX.'
|
||||
});
|
||||
var view = new FieldViews.ReadonlyFieldView(fieldData).render();
|
||||
|
||||
FieldViewsSpecHelpers.expectTitleAndMessageToContain(view, fieldData.title, fieldData.helpMessage, false);
|
||||
expect(view.$('.u-field-value input').val().trim()).toBe(USERNAME);
|
||||
|
||||
view.model.set({'username': 'bookworm'});
|
||||
expect(view.$('.u-field-value input').val().trim()).toBe('bookworm');
|
||||
});
|
||||
|
||||
it("correctly renders, updates and persists changes to TextFieldView when editable == always", function() {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.TextFieldView, {
|
||||
title: 'Full Name',
|
||||
valueAttribute: 'name',
|
||||
helpMessage: 'How are you?'
|
||||
});
|
||||
var view = new FieldViews.TextFieldView(fieldData).render();
|
||||
|
||||
FieldViewsSpecHelpers.verifyTextField(view, {
|
||||
title: fieldData.title,
|
||||
valueAttribute: fieldData.valueAttribute,
|
||||
helpMessage: fieldData.helpMessage,
|
||||
validValue: 'My Name',
|
||||
invalidValue1: 'Your Name',
|
||||
invalidValue2: 'Her Name',
|
||||
validationError: "Think again!"
|
||||
}, requests);
|
||||
});
|
||||
|
||||
it("correctly renders and updates DropdownFieldView when editable == never", function() {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.DropdownFieldView, {
|
||||
title: 'Full Name',
|
||||
valueAttribute: 'name',
|
||||
helpMessage: 'edX full name',
|
||||
editable: 'never'
|
||||
|
||||
});
|
||||
var view = new FieldViews.DropdownFieldView(fieldData).render();
|
||||
FieldViewsSpecHelpers.expectTitleAndMessageToContain(view, fieldData.title, fieldData.helpMessage, false);
|
||||
expect(view.el).toHaveClass('mode-hidden');
|
||||
|
||||
view.model.set({'name': fieldData.options[1][0]});
|
||||
expect(view.el).toHaveClass('mode-display');
|
||||
view.$el.click();
|
||||
expect(view.el).toHaveClass('mode-display');
|
||||
});
|
||||
|
||||
it("correctly renders, updates and persists changes to DropdownFieldView when editable == always", function() {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.DropdownFieldView, {
|
||||
title: 'Full Name',
|
||||
valueAttribute: 'name',
|
||||
helpMessage: 'edX full name'
|
||||
});
|
||||
var view = new FieldViews.DropdownFieldView(fieldData).render();
|
||||
|
||||
FieldViewsSpecHelpers.verifyDropDownField(view, {
|
||||
title: fieldData.title,
|
||||
valueAttribute: fieldData.valueAttribute,
|
||||
helpMessage: fieldData.helpMessage,
|
||||
validValue: FieldViewsSpecHelpers.SELECT_OPTIONS[0][0],
|
||||
invalidValue1: FieldViewsSpecHelpers.SELECT_OPTIONS[1][0],
|
||||
invalidValue2: FieldViewsSpecHelpers.SELECT_OPTIONS[2][0],
|
||||
validationError: "Nope, this will not do!"
|
||||
}, requests);
|
||||
});
|
||||
|
||||
it("correctly renders, updates and persists changes to DropdownFieldView when editable == toggle", function() {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.DropdownFieldView, {
|
||||
title: 'Full Name',
|
||||
valueAttribute: 'name',
|
||||
helpMessage: 'edX full name',
|
||||
editable: 'toggle'
|
||||
});
|
||||
var view = new FieldViews.DropdownFieldView(fieldData).render();
|
||||
|
||||
FieldViewsSpecHelpers.verifyDropDownField(view, {
|
||||
title: fieldData.title,
|
||||
valueAttribute: fieldData.valueAttribute,
|
||||
helpMessage: fieldData.helpMessage,
|
||||
editable: 'toggle',
|
||||
validValue: FieldViewsSpecHelpers.SELECT_OPTIONS[0][0],
|
||||
invalidValue1: FieldViewsSpecHelpers.SELECT_OPTIONS[1][0],
|
||||
invalidValue2: FieldViewsSpecHelpers.SELECT_OPTIONS[2][0],
|
||||
validationError: "Nope, this will not do!"
|
||||
}, requests);
|
||||
});
|
||||
|
||||
it("only shows empty option in DropdownFieldView if required is false or model value is not set", function() {
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var editableOptions = ['toggle', 'always'];
|
||||
_.each(editableOptions, function(editable) {
|
||||
var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.DropdownFieldView, {
|
||||
title: 'Drop Down Field',
|
||||
valueAttribute: 'drop-down',
|
||||
helpMessage: 'edX drop down',
|
||||
editable: editable,
|
||||
required:true
|
||||
});
|
||||
var view = new FieldViews.DropdownFieldView(fieldData).render();
|
||||
|
||||
expect(view.modelValueIsSet()).toBe(false);
|
||||
expect(view.displayValue()).toBe('');
|
||||
|
||||
if(editable === 'toggle') { view.showEditMode(true); }
|
||||
view.$('.u-field-value > select').val(FieldViewsSpecHelpers.SELECT_OPTIONS[0]).change();
|
||||
expect(view.fieldValue()).toBe(FieldViewsSpecHelpers.SELECT_OPTIONS[0][0]);
|
||||
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
if(editable === 'toggle') { view.showEditMode(true); }
|
||||
// When server returns success, there should no longer be an empty option.
|
||||
expect($(view.$('.u-field-value option')[0]).val()).toBe('si');
|
||||
});
|
||||
});
|
||||
|
||||
it("correctly renders and updates TextAreaFieldView when editable == never", function() {
|
||||
var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.TextareaFieldView, {
|
||||
title: 'About me',
|
||||
valueAttribute: 'bio',
|
||||
helpMessage: 'Wicked is good',
|
||||
placeholderValue: "Tell other edX learners a little about yourself: where you live, " +
|
||||
"what your interests are, why you’re taking courses on edX, or what you hope to learn.",
|
||||
editable: 'never'
|
||||
});
|
||||
|
||||
// set bio to empty to see the placeholder.
|
||||
fieldData.model.set({bio: ''});
|
||||
var view = new FieldViews.TextareaFieldView(fieldData).render();
|
||||
FieldViewsSpecHelpers.expectTitleAndMessageToContain(view, fieldData.title, fieldData.helpMessage, false);
|
||||
expect(view.el).toHaveClass('mode-hidden');
|
||||
expect(view.$('.u-field-value .u-field-value-readonly').text()).toBe(fieldData.placeholderValue);
|
||||
|
||||
var bio = 'Too much to tell!';
|
||||
view.model.set({'bio': bio});
|
||||
expect(view.el).toHaveClass('mode-display');
|
||||
expect(view.$('.u-field-value .u-field-value-readonly').text()).toBe(bio);
|
||||
view.$el.click();
|
||||
expect(view.el).toHaveClass('mode-display');
|
||||
});
|
||||
|
||||
it("correctly renders, updates and persists changes to TextAreaFieldView when editable == toggle", function() {
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
var valueInputSelector = '.u-field-value > textarea';
|
||||
var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.TextareaFieldView, {
|
||||
title: 'About me',
|
||||
valueAttribute: 'bio',
|
||||
helpMessage: 'Wicked is good',
|
||||
placeholderValue: "Tell other edX learners a little about yourself: where you live, " +
|
||||
"what your interests are, why you’re taking courses on edX, or what you hope to learn.",
|
||||
editable: 'toggle'
|
||||
|
||||
});
|
||||
fieldData.model.set({'bio': ''});
|
||||
|
||||
var view = new FieldViews.TextareaFieldView(fieldData).render();
|
||||
|
||||
FieldViewsSpecHelpers.expectTitleToContain(view, fieldData.title);
|
||||
FieldViewsSpecHelpers.expectMessageContains(view, view.indicators.canEdit);
|
||||
expect(view.el).toHaveClass('mode-placeholder');
|
||||
expect(view.$('.u-field-value .u-field-value-readonly').text()).toBe(fieldData.placeholderValue);
|
||||
|
||||
view.$('.wrapper-u-field').click();
|
||||
expect(view.el).toHaveClass('mode-edit');
|
||||
view.$(valueInputSelector).val(BIO).focusout();
|
||||
expect(view.fieldValue()).toBe(BIO);
|
||||
AjaxHelpers.expectJsonRequest(
|
||||
requests, 'PATCH', view.model.url, {'bio': BIO}
|
||||
);
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
expect(view.el).toHaveClass('mode-display');
|
||||
|
||||
view.$('.wrapper-u-field').click();
|
||||
view.$(valueInputSelector).val('').focusout();
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
expect(view.el).toHaveClass('mode-placeholder');
|
||||
expect(view.$('.u-field-value .u-field-value-readonly').text()).toBe(fieldData.placeholderValue);
|
||||
});
|
||||
|
||||
it("correctly renders LinkFieldView", function() {
|
||||
var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.LinkFieldView, {
|
||||
title: 'Title',
|
||||
linkTitle: 'Link title',
|
||||
helpMessage: 'Click the link.',
|
||||
valueAttribute: 'password-reset'
|
||||
});
|
||||
var view = new FieldViews.LinkFieldView(fieldData).render();
|
||||
|
||||
FieldViewsSpecHelpers.expectTitleAndMessageToContain(view, fieldData.title, fieldData.helpMessage, false);
|
||||
expect(view.$('.u-field-value > a .u-field-link-title-' + view.options.valueAttribute).text().trim()).toBe(fieldData.linkTitle);
|
||||
});
|
||||
});
|
||||
});
|
||||
27
lms/static/js/spec/views/message_banner_spec.js
Normal file
@@ -0,0 +1,27 @@
|
||||
define(['backbone', 'jquery', 'underscore', 'js/views/message_banner'
|
||||
],
|
||||
function (Backbone, $, _, MessageBannerView) {
|
||||
'use strict';
|
||||
|
||||
describe("MessageBannerView", function () {
|
||||
|
||||
beforeEach(function () {
|
||||
setFixtures('<div class="message-banner"></div>');
|
||||
TemplateHelpers.installTemplate("templates/fields/message_banner");
|
||||
});
|
||||
|
||||
it('renders message correctly', function() {
|
||||
var messageSelector = '.message-banner';
|
||||
var messageView = new MessageBannerView({
|
||||
el: $(messageSelector)
|
||||
});
|
||||
|
||||
messageView.showMessage('I am message view');
|
||||
// Verify error message
|
||||
expect($(messageSelector).text().trim()).toBe('I am message view');
|
||||
|
||||
messageView.hideMessage();
|
||||
expect($(messageSelector).text().trim()).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
59
lms/static/js/student_account/models/user_account_model.js
Normal file
@@ -0,0 +1,59 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'gettext', 'underscore', 'backbone'
|
||||
], function (gettext, _, Backbone) {
|
||||
|
||||
var UserAccountModel = Backbone.Model.extend({
|
||||
idAttribute: 'username',
|
||||
defaults: {
|
||||
username: '',
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
language: null,
|
||||
country: null,
|
||||
date_joined: "",
|
||||
gender: null,
|
||||
goals: "",
|
||||
level_of_education: null,
|
||||
mailing_address: "",
|
||||
year_of_birth: null,
|
||||
bio: null,
|
||||
language_proficiencies: [],
|
||||
requires_parental_consent: true,
|
||||
profile_image: null,
|
||||
default_public_account_fields: []
|
||||
},
|
||||
|
||||
parse : function(response) {
|
||||
if (_.isNull(response)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Currently when a non-staff user A access user B's profile, the only way to tell whether user B's
|
||||
// profile is public is to check if the api has returned fields other than the default public fields
|
||||
// specified in settings.ACCOUNT_VISIBILITY_CONFIGURATION.
|
||||
var profileIsPublic = _.size(_.difference(_.keys(response), this.get('default_public_account_fields'))) > 0;
|
||||
this.set({'profile_is_public': profileIsPublic}, { silent: true });
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
hasProfileImage: function () {
|
||||
var profile_image = this.get('profile_image');
|
||||
return (_.isObject(profile_image) && profile_image['has_image'] === true);
|
||||
},
|
||||
|
||||
profileImageUrl: function () {
|
||||
return this.get('profile_image')['image_url_large'];
|
||||
},
|
||||
|
||||
isAboveMinimumAge: function() {
|
||||
var isBirthDefined = !(_.isUndefined(this.get('year_of_birth')) || _.isNull(this.get('year_of_birth')));
|
||||
return isBirthDefined && !(this.get("requires_parental_consent"));
|
||||
}
|
||||
});
|
||||
return UserAccountModel;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -0,0 +1,16 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'gettext', 'underscore', 'backbone'
|
||||
], function (gettext, _, Backbone) {
|
||||
|
||||
var UserPreferencesModel = Backbone.Model.extend({
|
||||
idAttribute: 'account_privacy',
|
||||
defaults: {
|
||||
account_privacy: 'private'
|
||||
}
|
||||
});
|
||||
|
||||
return UserPreferencesModel;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
190
lms/static/js/student_account/views/account_settings_factory.js
Normal file
@@ -0,0 +1,190 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'gettext', 'jquery', 'underscore', 'backbone', 'logger',
|
||||
'js/views/fields',
|
||||
'js/student_account/models/user_account_model',
|
||||
'js/student_account/models/user_preferences_model',
|
||||
'js/student_account/views/account_settings_fields',
|
||||
'js/student_account/views/account_settings_view'
|
||||
], function (gettext, $, _, Backbone, Logger, FieldViews, UserAccountModel, UserPreferencesModel,
|
||||
AccountSettingsFieldViews, AccountSettingsView) {
|
||||
|
||||
return function (fieldsData, authData, userAccountsApiUrl, userPreferencesApiUrl, accountUserId) {
|
||||
|
||||
var accountSettingsElement = $('.wrapper-account-settings');
|
||||
|
||||
var userAccountModel = new UserAccountModel();
|
||||
userAccountModel.url = userAccountsApiUrl;
|
||||
|
||||
var userPreferencesModel = new UserPreferencesModel();
|
||||
userPreferencesModel.url = userPreferencesApiUrl;
|
||||
|
||||
var sectionsData = [
|
||||
{
|
||||
title: gettext('Basic Account Information (required)'),
|
||||
fields: [
|
||||
{
|
||||
view: new FieldViews.ReadonlyFieldView({
|
||||
model: userAccountModel,
|
||||
title: gettext('Username'),
|
||||
valueAttribute: 'username',
|
||||
helpMessage: gettext('The name that identifies you on the edX site. You cannot change your username.')
|
||||
})
|
||||
},
|
||||
{
|
||||
view: new FieldViews.TextFieldView({
|
||||
model: userAccountModel,
|
||||
title: gettext('Full Name'),
|
||||
valueAttribute: 'name',
|
||||
helpMessage: gettext('The name that appears on your edX certificates. Other learners never see your full name.')
|
||||
})
|
||||
},
|
||||
{
|
||||
view: new AccountSettingsFieldViews.EmailFieldView({
|
||||
model: userAccountModel,
|
||||
title: gettext('Email Address'),
|
||||
valueAttribute: 'email',
|
||||
helpMessage: gettext('The email address you use to sign in to edX. Communications from edX and your courses are sent to this address.')
|
||||
})
|
||||
},
|
||||
{
|
||||
view: new AccountSettingsFieldViews.PasswordFieldView({
|
||||
model: userAccountModel,
|
||||
title: gettext('Password'),
|
||||
screenReaderTitle: gettext('Reset your Password'),
|
||||
valueAttribute: 'password',
|
||||
emailAttribute: 'email',
|
||||
linkTitle: gettext('Reset Password'),
|
||||
linkHref: fieldsData.password.url,
|
||||
helpMessage: gettext('When you click "Reset Password", a message will be sent to your email address. Click the link in the message to reset your password.')
|
||||
})
|
||||
},
|
||||
{
|
||||
view: new AccountSettingsFieldViews.LanguagePreferenceFieldView({
|
||||
model: userPreferencesModel,
|
||||
title: gettext('Language'),
|
||||
valueAttribute: 'pref-lang',
|
||||
required: true,
|
||||
refreshPageOnSave: true,
|
||||
helpMessage:
|
||||
gettext('The language used for the edX site. The site is currently available in a limited number of languages.'),
|
||||
options: fieldsData.language.options
|
||||
})
|
||||
},
|
||||
{
|
||||
view: new FieldViews.DropdownFieldView({
|
||||
model: userAccountModel,
|
||||
required: true,
|
||||
title: gettext('Country or Region'),
|
||||
valueAttribute: 'country',
|
||||
options: fieldsData['country']['options']
|
||||
})
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: gettext('Additional Information (optional)'),
|
||||
fields: [
|
||||
{
|
||||
view: new FieldViews.DropdownFieldView({
|
||||
model: userAccountModel,
|
||||
title: gettext('Education Completed'),
|
||||
valueAttribute: 'level_of_education',
|
||||
options: fieldsData.level_of_education.options
|
||||
})
|
||||
},
|
||||
{
|
||||
view: new FieldViews.DropdownFieldView({
|
||||
model: userAccountModel,
|
||||
title: gettext('Gender'),
|
||||
valueAttribute: 'gender',
|
||||
options: fieldsData.gender.options
|
||||
})
|
||||
},
|
||||
{
|
||||
view: new FieldViews.DropdownFieldView({
|
||||
model: userAccountModel,
|
||||
title: gettext('Year of Birth'),
|
||||
valueAttribute: 'year_of_birth',
|
||||
options: fieldsData['year_of_birth']['options']
|
||||
})
|
||||
},
|
||||
{
|
||||
view: new AccountSettingsFieldViews.LanguageProficienciesFieldView({
|
||||
model: userAccountModel,
|
||||
title: gettext('Preferred Language'),
|
||||
valueAttribute: 'language_proficiencies',
|
||||
options: fieldsData.preferred_language.options
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
if (_.isArray(authData.providers)) {
|
||||
var accountsSectionData = {
|
||||
title: gettext('Connected Accounts'),
|
||||
fields: _.map(authData.providers, function(provider) {
|
||||
return {
|
||||
'view': new AccountSettingsFieldViews.AuthFieldView({
|
||||
title: provider.name,
|
||||
screenReaderTitle: interpolate_text(
|
||||
gettext("Connect your {accountName} account"), {accountName: provider['name']}
|
||||
),
|
||||
valueAttribute: 'auth-' + provider.name.toLowerCase(),
|
||||
helpMessage: '',
|
||||
connected: provider.connected,
|
||||
connectUrl: provider.connect_url,
|
||||
disconnectUrl: provider.disconnect_url
|
||||
})
|
||||
};
|
||||
})
|
||||
};
|
||||
sectionsData.push(accountsSectionData);
|
||||
}
|
||||
|
||||
var accountSettingsView = new AccountSettingsView({
|
||||
model: userAccountModel,
|
||||
accountUserId: accountUserId,
|
||||
el: accountSettingsElement,
|
||||
sectionsData: sectionsData
|
||||
});
|
||||
|
||||
accountSettingsView.render();
|
||||
|
||||
var showLoadingError = function () {
|
||||
accountSettingsView.showLoadingError();
|
||||
};
|
||||
|
||||
var showAccountFields = function () {
|
||||
// Record that the account settings page was viewed.
|
||||
Logger.log('edx.user.settings.viewed', {
|
||||
page: "account",
|
||||
visibility: null,
|
||||
user_id: accountUserId
|
||||
});
|
||||
|
||||
// Render the fields
|
||||
accountSettingsView.renderFields();
|
||||
};
|
||||
|
||||
userAccountModel.fetch({
|
||||
success: function () {
|
||||
// Fetch the user preferences model
|
||||
userPreferencesModel.fetch({
|
||||
success: showAccountFields,
|
||||
error: showLoadingError
|
||||
});
|
||||
},
|
||||
error: showLoadingError
|
||||
});
|
||||
|
||||
return {
|
||||
userAccountModel: userAccountModel,
|
||||
userPreferencesModel: userPreferencesModel,
|
||||
accountSettingsView: accountSettingsView
|
||||
};
|
||||
};
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
183
lms/static/js/student_account/views/account_settings_fields.js
Normal file
@@ -0,0 +1,183 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'gettext', 'jquery', 'underscore', 'backbone', 'js/mustache', 'js/views/fields'
|
||||
], function (gettext, $, _, Backbone, RequireMustache, FieldViews) {
|
||||
|
||||
var AccountSettingsFieldViews = {};
|
||||
|
||||
AccountSettingsFieldViews.EmailFieldView = FieldViews.TextFieldView.extend({
|
||||
|
||||
successMessage: function() {
|
||||
return this.indicators.success + interpolate_text(
|
||||
gettext(
|
||||
'We\'ve sent a confirmation message to {new_email_address}. ' +
|
||||
'Click the link in the message to update your email address.'
|
||||
),
|
||||
{'new_email_address': this.fieldValue()}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
AccountSettingsFieldViews.LanguagePreferenceFieldView = FieldViews.DropdownFieldView.extend({
|
||||
|
||||
saveSucceeded: function () {
|
||||
var data = {
|
||||
'language': this.modelValue()
|
||||
};
|
||||
|
||||
var view = this;
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/i18n/setlang/',
|
||||
data: data,
|
||||
dataType: 'html',
|
||||
success: function () {
|
||||
view.showSuccessMessage();
|
||||
},
|
||||
error: function () {
|
||||
view.showNotificationMessage(
|
||||
view.indicators.error +
|
||||
gettext(
|
||||
'You must sign out of edX and sign back in before your language ' +
|
||||
'changes take effect.'
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
AccountSettingsFieldViews.PasswordFieldView = FieldViews.LinkFieldView.extend({
|
||||
|
||||
initialize: function (options) {
|
||||
this._super(options);
|
||||
_.bindAll(this, 'resetPassword');
|
||||
},
|
||||
|
||||
linkClicked: function (event) {
|
||||
event.preventDefault();
|
||||
this.resetPassword(event);
|
||||
},
|
||||
|
||||
resetPassword: function () {
|
||||
var data = {};
|
||||
data[this.options.emailAttribute] = this.model.get(this.options.emailAttribute);
|
||||
|
||||
var view = this;
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: view.options.linkHref,
|
||||
data: data,
|
||||
success: function () {
|
||||
view.showSuccessMessage();
|
||||
},
|
||||
error: function (xhr) {
|
||||
view.showErrorMessage(xhr);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
successMessage: function () {
|
||||
return this.indicators.success + interpolate_text(
|
||||
gettext(
|
||||
'We\'ve sent a message to {email_address}. ' +
|
||||
'Click the link in the message to reset your password.'
|
||||
),
|
||||
{'email_address': this.model.get(this.options.emailAttribute)}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
AccountSettingsFieldViews.LanguageProficienciesFieldView = FieldViews.DropdownFieldView.extend({
|
||||
|
||||
modelValue: function () {
|
||||
var modelValue = this.model.get(this.options.valueAttribute);
|
||||
if (_.isArray(modelValue) && modelValue.length > 0) {
|
||||
return modelValue[0].code;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
saveValue: function () {
|
||||
var attributes = {},
|
||||
value = this.fieldValue() ? [{'code': this.fieldValue()}] : [];
|
||||
attributes[this.options.valueAttribute] = value;
|
||||
this.saveAttributes(attributes);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
AccountSettingsFieldViews.AuthFieldView = FieldViews.LinkFieldView.extend({
|
||||
|
||||
initialize: function (options) {
|
||||
this._super(options);
|
||||
_.bindAll(this, 'redirect_to', 'disconnect', 'successMessage', 'inProgressMessage');
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(this.template({
|
||||
id: this.options.valueAttribute,
|
||||
title: this.options.title,
|
||||
screenReaderTitle: this.options.screenReaderTitle,
|
||||
linkTitle: this.options.connected ? gettext('Unlink') : gettext('Link'),
|
||||
linkHref: '',
|
||||
message: this.helpMessage
|
||||
}));
|
||||
return this;
|
||||
},
|
||||
|
||||
linkClicked: function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.showInProgressMessage();
|
||||
|
||||
if (this.options.connected) {
|
||||
this.disconnect();
|
||||
} else {
|
||||
// Direct the user to the providers site to start the authentication process.
|
||||
// See python-social-auth docs for more information.
|
||||
this.redirect_to(this.options.connectUrl);
|
||||
}
|
||||
},
|
||||
|
||||
redirect_to: function (url) {
|
||||
window.location.href = url;
|
||||
},
|
||||
|
||||
disconnect: function () {
|
||||
var data = {};
|
||||
|
||||
// Disconnects the provider from the user's edX account.
|
||||
// See python-social-auth docs for more information.
|
||||
var view = this;
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: this.options.disconnectUrl,
|
||||
data: data,
|
||||
dataType: 'html',
|
||||
success: function () {
|
||||
view.options.connected = false;
|
||||
view.render();
|
||||
view.showSuccessMessage();
|
||||
},
|
||||
error: function (xhr) {
|
||||
view.showErrorMessage(xhr);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
inProgressMessage: function() {
|
||||
return this.indicators.inProgress + (this.options.connected ? gettext('Unlinking') : gettext('Linking'));
|
||||
},
|
||||
|
||||
successMessage: function() {
|
||||
return this.indicators.success + gettext('Successfully unlinked.');
|
||||
}
|
||||
});
|
||||
|
||||
return AccountSettingsFieldViews;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
41
lms/static/js/student_account/views/account_settings_view.js
Normal file
@@ -0,0 +1,41 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'gettext', 'jquery', 'underscore', 'backbone'
|
||||
], function (gettext, $, _, Backbone) {
|
||||
|
||||
var AccountSettingsView = Backbone.View.extend({
|
||||
|
||||
initialize: function () {
|
||||
this.template = _.template($('#account_settings-tpl').text());
|
||||
_.bindAll(this, 'render', 'renderFields', 'showLoadingError');
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(this.template({
|
||||
sections: this.options.sectionsData
|
||||
}));
|
||||
return this;
|
||||
},
|
||||
|
||||
renderFields: function () {
|
||||
this.$('.ui-loading-indicator').addClass('is-hidden');
|
||||
|
||||
var view = this;
|
||||
_.each(this.$('.account-settings-section-body'), function (sectionEl, index) {
|
||||
_.each(view.options.sectionsData[index].fields, function (field) {
|
||||
$(sectionEl).append(field.view.render().el);
|
||||
});
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
showLoadingError: function () {
|
||||
this.$('.ui-loading-indicator').addClass('is-hidden');
|
||||
this.$('.ui-loading-error').removeClass('is-hidden');
|
||||
}
|
||||
});
|
||||
|
||||
return AccountSettingsView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
174
lms/static/js/student_profile/views/learner_profile_factory.js
Normal file
@@ -0,0 +1,174 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'gettext', 'jquery', 'underscore', 'backbone', 'logger',
|
||||
'js/student_account/models/user_account_model',
|
||||
'js/student_account/models/user_preferences_model',
|
||||
'js/views/fields',
|
||||
'js/student_profile/views/learner_profile_fields',
|
||||
'js/student_profile/views/learner_profile_view',
|
||||
'js/student_account/views/account_settings_fields',
|
||||
'js/views/message_banner'
|
||||
], function (gettext, $, _, Backbone, Logger, AccountSettingsModel, AccountPreferencesModel, FieldsView,
|
||||
LearnerProfileFieldsView, LearnerProfileView, AccountSettingsFieldViews, MessageBannerView) {
|
||||
|
||||
return function (options) {
|
||||
|
||||
var learnerProfileElement = $('.wrapper-profile');
|
||||
var defaultVisibility = options.default_visibility;
|
||||
var AccountPreferencesModelWithDefaults = AccountPreferencesModel.extend({
|
||||
defaults: {
|
||||
account_privacy: defaultVisibility
|
||||
}
|
||||
});
|
||||
var accountPreferencesModel = new AccountPreferencesModelWithDefaults();
|
||||
accountPreferencesModel.url = options.preferences_api_url;
|
||||
|
||||
var accountSettingsModel = new AccountSettingsModel({
|
||||
'default_public_account_fields': options.default_public_account_fields
|
||||
});
|
||||
accountSettingsModel.url = options.accounts_api_url;
|
||||
|
||||
var editable = options.own_profile ? 'toggle' : 'never';
|
||||
|
||||
var messageView = new MessageBannerView({
|
||||
el: $('.message-banner')
|
||||
});
|
||||
|
||||
var accountPrivacyFieldView = new LearnerProfileFieldsView.AccountPrivacyFieldView({
|
||||
model: accountPreferencesModel,
|
||||
required: true,
|
||||
editable: 'always',
|
||||
showMessages: false,
|
||||
title: gettext('edX learners can see my:'),
|
||||
valueAttribute: "account_privacy",
|
||||
options: [
|
||||
['private', gettext('Limited Profile')],
|
||||
['all_users', gettext('Full Profile')]
|
||||
],
|
||||
helpMessage: '',
|
||||
accountSettingsPageUrl: options.account_settings_page_url
|
||||
});
|
||||
|
||||
var profileImageFieldView = new LearnerProfileFieldsView.ProfileImageFieldView({
|
||||
model: accountSettingsModel,
|
||||
valueAttribute: "profile_image",
|
||||
editable: editable === 'toggle',
|
||||
messageView: messageView,
|
||||
imageMaxBytes: options['profile_image_max_bytes'],
|
||||
imageMinBytes: options['profile_image_min_bytes'],
|
||||
imageUploadUrl: options['profile_image_upload_url'],
|
||||
imageRemoveUrl: options['profile_image_remove_url']
|
||||
});
|
||||
|
||||
var usernameFieldView = new FieldsView.ReadonlyFieldView({
|
||||
model: accountSettingsModel,
|
||||
valueAttribute: "username",
|
||||
helpMessage: ""
|
||||
});
|
||||
|
||||
var sectionOneFieldViews = [
|
||||
new FieldsView.DropdownFieldView({
|
||||
model: accountSettingsModel,
|
||||
screenReaderTitle: gettext('Location'),
|
||||
required: true,
|
||||
editable: editable,
|
||||
showMessages: false,
|
||||
iconName: 'fa-map-marker',
|
||||
placeholderValue: '',
|
||||
valueAttribute: "country",
|
||||
options: options.country_options,
|
||||
helpMessage: ''
|
||||
}),
|
||||
new AccountSettingsFieldViews.LanguageProficienciesFieldView({
|
||||
model: accountSettingsModel,
|
||||
screenReaderTitle: gettext('Preferred Language'),
|
||||
required: false,
|
||||
editable: editable,
|
||||
showMessages: false,
|
||||
iconName: 'fa-comment',
|
||||
placeholderValue: gettext('Add language'),
|
||||
valueAttribute: "language_proficiencies",
|
||||
options: options.language_options,
|
||||
helpMessage: ''
|
||||
})
|
||||
];
|
||||
|
||||
var sectionTwoFieldViews = [
|
||||
new FieldsView.TextareaFieldView({
|
||||
model: accountSettingsModel,
|
||||
editable: editable,
|
||||
showMessages: false,
|
||||
title: gettext('About me'),
|
||||
placeholderValue: gettext("Tell other edX learners a little about yourself: where you live, what your interests are, why you're taking courses on edX, or what you hope to learn."),
|
||||
valueAttribute: "bio",
|
||||
helpMessage: ''
|
||||
})
|
||||
];
|
||||
|
||||
var learnerProfileView = new LearnerProfileView({
|
||||
el: learnerProfileElement,
|
||||
ownProfile: options.own_profile,
|
||||
has_preferences_access: options.has_preferences_access,
|
||||
accountSettingsModel: accountSettingsModel,
|
||||
preferencesModel: accountPreferencesModel,
|
||||
accountPrivacyFieldView: accountPrivacyFieldView,
|
||||
profileImageFieldView: profileImageFieldView,
|
||||
usernameFieldView: usernameFieldView,
|
||||
sectionOneFieldViews: sectionOneFieldViews,
|
||||
sectionTwoFieldViews: sectionTwoFieldViews
|
||||
});
|
||||
|
||||
var showLoadingError = function () {
|
||||
learnerProfileView.showLoadingError();
|
||||
};
|
||||
|
||||
var getProfileVisibility = function() {
|
||||
if (options.has_preferences_access) {
|
||||
return accountPreferencesModel.get('account_privacy');
|
||||
} else {
|
||||
return accountSettingsModel.get('profile_is_public') ? 'all_users' : 'private';
|
||||
}
|
||||
};
|
||||
|
||||
var showLearnerProfileView = function() {
|
||||
// Record that the profile page was viewed
|
||||
Logger.log('edx.user.settings.viewed', {
|
||||
page: "profile",
|
||||
visibility: getProfileVisibility(),
|
||||
user_id: options.profile_user_id
|
||||
});
|
||||
|
||||
// Render the view for the first time
|
||||
learnerProfileView.render();
|
||||
};
|
||||
|
||||
accountSettingsModel.fetch({
|
||||
success: function () {
|
||||
// Fetch the preferences model if the user has access
|
||||
if (options.has_preferences_access) {
|
||||
accountPreferencesModel.fetch({
|
||||
success: function() {
|
||||
if (accountSettingsModel.get('requires_parental_consent')) {
|
||||
accountPreferencesModel.set('account_privacy', 'private');
|
||||
}
|
||||
showLearnerProfileView();
|
||||
},
|
||||
error: showLoadingError
|
||||
});
|
||||
}
|
||||
else {
|
||||
showLearnerProfileView();
|
||||
}
|
||||
},
|
||||
error: showLoadingError
|
||||
});
|
||||
|
||||
return {
|
||||
accountSettingsModel: accountSettingsModel,
|
||||
accountPreferencesModel: accountPreferencesModel,
|
||||
learnerProfileView: learnerProfileView
|
||||
};
|
||||
};
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
111
lms/static/js/student_profile/views/learner_profile_fields.js
Normal file
@@ -0,0 +1,111 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'gettext', 'jquery', 'underscore', 'backbone', 'js/views/fields', 'backbone-super'
|
||||
], function (gettext, $, _, Backbone, FieldViews) {
|
||||
|
||||
var LearnerProfileFieldViews = {};
|
||||
|
||||
LearnerProfileFieldViews.AccountPrivacyFieldView = FieldViews.DropdownFieldView.extend({
|
||||
|
||||
render: function () {
|
||||
this._super();
|
||||
this.showNotificationMessage();
|
||||
this.updateFieldValue();
|
||||
return this;
|
||||
},
|
||||
|
||||
showNotificationMessage: function () {
|
||||
var accountSettingsLink = '<a href="' + this.options.accountSettingsPageUrl + '">' + gettext('Account Settings page.') + '</a>';
|
||||
if (this.profileIsPrivate) {
|
||||
this._super(interpolate_text(
|
||||
gettext("You must specify your birth year before you can share your full profile. To specify your birth year, go to the {account_settings_page_link}"),
|
||||
{'account_settings_page_link': accountSettingsLink}
|
||||
));
|
||||
} else if (this.requiresParentalConsent) {
|
||||
this._super(interpolate_text(
|
||||
gettext('You must be over 13 to share a full profile. If you are over 13, make sure that you have specified a birth year on the {account_settings_page_link}'),
|
||||
{'account_settings_page_link': accountSettingsLink}
|
||||
));
|
||||
}
|
||||
else {
|
||||
this._super('');
|
||||
}
|
||||
return this._super();
|
||||
},
|
||||
|
||||
updateFieldValue: function() {
|
||||
if (!this.isAboveMinimumAge) {
|
||||
this.$('.u-field-value select').val('private');
|
||||
this.disableField(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
LearnerProfileFieldViews.ProfileImageFieldView = FieldViews.ImageFieldView.extend({
|
||||
|
||||
imageUrl: function () {
|
||||
return this.model.profileImageUrl();
|
||||
},
|
||||
|
||||
imageAltText: function () {
|
||||
return interpolate_text(
|
||||
gettext("Profile image for {username}"), {username: this.model.get('username')}
|
||||
);
|
||||
},
|
||||
|
||||
imageChangeSucceeded: function (e, data) {
|
||||
var view = this;
|
||||
// Update model to get the latest urls of profile image.
|
||||
this.model.fetch().done(function () {
|
||||
view.setCurrentStatus('');
|
||||
}).fail(function () {
|
||||
view.showErrorMessage(view.errorMessage);
|
||||
});
|
||||
},
|
||||
|
||||
imageChangeFailed: function (e, data) {
|
||||
this.setCurrentStatus('');
|
||||
this.showImageChangeFailedMessage(data.jqXHR.status, data.jqXHR.responseText);
|
||||
this.render();
|
||||
},
|
||||
|
||||
showImageChangeFailedMessage: function (status, responseText) {
|
||||
if (_.contains([400, 404], status)) {
|
||||
try {
|
||||
var errors = JSON.parse(responseText);
|
||||
this.showErrorMessage(errors.user_message);
|
||||
} catch (error) {
|
||||
this.showErrorMessage(this.errorMessage);
|
||||
}
|
||||
} else {
|
||||
this.showErrorMessage(this.errorMessage);
|
||||
}
|
||||
},
|
||||
|
||||
showErrorMessage: function (message) {
|
||||
this.options.messageView.showMessage(message);
|
||||
},
|
||||
|
||||
isEditingAllowed: function () {
|
||||
return this.model.isAboveMinimumAge();
|
||||
},
|
||||
|
||||
isShowingPlaceholder: function () {
|
||||
return !this.model.hasProfileImage();
|
||||
},
|
||||
|
||||
clickedRemoveButton: function (e, data) {
|
||||
this.options.messageView.hideMessage();
|
||||
this._super(e, data);
|
||||
},
|
||||
|
||||
fileSelected: function (e, data) {
|
||||
this.options.messageView.hideMessage();
|
||||
this._super(e, data);
|
||||
}
|
||||
});
|
||||
|
||||
return LearnerProfileFieldViews;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
72
lms/static/js/student_profile/views/learner_profile_view.js
Normal file
@@ -0,0 +1,72 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'gettext', 'jquery', 'underscore', 'backbone'
|
||||
], function (gettext, $, _, Backbone) {
|
||||
|
||||
var LearnerProfileView = Backbone.View.extend({
|
||||
|
||||
initialize: function () {
|
||||
this.template = _.template($('#learner_profile-tpl').text());
|
||||
_.bindAll(this, 'showFullProfile', 'render', 'renderFields', 'showLoadingError');
|
||||
this.listenTo(this.options.preferencesModel, "change:" + 'account_privacy', this.render);
|
||||
},
|
||||
|
||||
showFullProfile: function () {
|
||||
var isAboveMinimumAge = this.options.accountSettingsModel.isAboveMinimumAge();
|
||||
if (this.options.ownProfile) {
|
||||
return isAboveMinimumAge && this.options.preferencesModel.get('account_privacy') === 'all_users';
|
||||
} else {
|
||||
return this.options.accountSettingsModel.get('profile_is_public');
|
||||
}
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(this.template({
|
||||
username: this.options.accountSettingsModel.get('username'),
|
||||
ownProfile: this.options.ownProfile,
|
||||
showFullProfile: this.showFullProfile()
|
||||
}));
|
||||
this.renderFields();
|
||||
return this;
|
||||
},
|
||||
|
||||
renderFields: function() {
|
||||
var view = this;
|
||||
|
||||
if (this.options.ownProfile) {
|
||||
var fieldView = this.options.accountPrivacyFieldView,
|
||||
settings = this.options.accountSettingsModel;
|
||||
fieldView.profileIsPrivate = !settings.get('year_of_birth');
|
||||
fieldView.requiresParentalConsent = settings.get('requires_parental_consent');
|
||||
fieldView.isAboveMinimumAge = settings.isAboveMinimumAge();
|
||||
fieldView.undelegateEvents();
|
||||
this.$('.wrapper-profile-field-account-privacy').append(fieldView.render().el);
|
||||
fieldView.delegateEvents();
|
||||
}
|
||||
|
||||
this.$('.profile-section-one-fields').append(this.options.usernameFieldView.render().el);
|
||||
|
||||
var imageView = this.options.profileImageFieldView;
|
||||
this.$('.profile-image-field').append(imageView.render().el);
|
||||
|
||||
if (this.showFullProfile()) {
|
||||
_.each(this.options.sectionOneFieldViews, function (fieldView) {
|
||||
view.$('.profile-section-one-fields').append(fieldView.render().el);
|
||||
});
|
||||
|
||||
_.each(this.options.sectionTwoFieldViews, function (fieldView) {
|
||||
view.$('.profile-section-two-fields').append(fieldView.render().el);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
showLoadingError: function () {
|
||||
this.$('.ui-loading-indicator').addClass('is-hidden');
|
||||
this.$('.ui-loading-error').removeClass('is-hidden');
|
||||
}
|
||||
});
|
||||
|
||||
return LearnerProfileView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
114
lms/static/js/vendor/backbone-super.js
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
// https://github.com/lukasolson/backbone-super
|
||||
// MIT License
|
||||
|
||||
// This is a plugin, constructed from parts of Backbone.js and John Resig's inheritance script.
|
||||
// (See http://backbonejs.org, http://ejohn.org/blog/simple-javascript-inheritance/)
|
||||
// No credit goes to me as I did absolutely nothing except patch these two together.
|
||||
(function(root, factory) {
|
||||
|
||||
// Set up Backbone appropriately for the environment. Start with AMD.
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define(['underscore', 'backbone'], function(_, Backbone) {
|
||||
// Export global even in AMD case in case this script is loaded with
|
||||
// others that may still expect a global Backbone.
|
||||
factory( _, Backbone);
|
||||
});
|
||||
|
||||
// Next for Node.js or CommonJS.
|
||||
} else if (typeof exports !== 'undefined' && typeof require === 'function') {
|
||||
var _ = require('underscore'),
|
||||
Backbone = require('backbone');
|
||||
factory(_, Backbone);
|
||||
|
||||
// Finally, as a browser global.
|
||||
} else {
|
||||
factory(root._, root.Backbone);
|
||||
}
|
||||
|
||||
}(this, function factory(_, Backbone) {
|
||||
Backbone.Model.extend = Backbone.Collection.extend = Backbone.Router.extend = Backbone.View.extend = function(protoProps, classProps) {
|
||||
var child = inherits(this, protoProps, classProps);
|
||||
child.extend = this.extend;
|
||||
return child;
|
||||
};
|
||||
var unImplementedSuper = function(method){throw "Super does not implement this method: " + method;};
|
||||
|
||||
var fnTest = /\b_super\b/;
|
||||
|
||||
var makeWrapper = function(parentProto, name, fn) {
|
||||
var wrapper = function() {
|
||||
var tmp = this._super;
|
||||
|
||||
// Add a new ._super() method that is the same method
|
||||
// but on the super-class
|
||||
this._super = parentProto[name] || unImplementedSuper(name);
|
||||
|
||||
// The method only need to be bound temporarily, so we
|
||||
// remove it when we're done executing
|
||||
var ret;
|
||||
try {
|
||||
ret = fn.apply(this, arguments);
|
||||
} finally {
|
||||
this._super = tmp;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
//we must move properties from old function to new
|
||||
for (var prop in fn) {
|
||||
wrapper[prop] = fn[prop];
|
||||
delete fn[prop];
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
var ctor = function(){}, inherits = function(parent, protoProps, staticProps) {
|
||||
var child, parentProto = parent.prototype;
|
||||
|
||||
// The constructor function for the new subclass is either defined by you
|
||||
// (the "constructor" property in your `extend` definition), or defaulted
|
||||
// by us to simply call the parent's constructor.
|
||||
if (protoProps && protoProps.hasOwnProperty('constructor')) {
|
||||
child = protoProps.constructor;
|
||||
} else {
|
||||
child = function(){ return parent.apply(this, arguments); };
|
||||
}
|
||||
|
||||
// Inherit class (static) properties from parent.
|
||||
_.extend(child, parent, staticProps);
|
||||
|
||||
// Set the prototype chain to inherit from `parent`, without calling
|
||||
// `parent`'s constructor function.
|
||||
ctor.prototype = parentProto;
|
||||
child.prototype = new ctor();
|
||||
|
||||
// Add prototype properties (instance properties) to the subclass,
|
||||
// if supplied.
|
||||
if (protoProps) {
|
||||
_.extend(child.prototype, protoProps);
|
||||
|
||||
// Copy the properties over onto the new prototype
|
||||
for (var name in protoProps) {
|
||||
// Check if we're overwriting an existing function
|
||||
if (typeof protoProps[name] == "function" && fnTest.test(protoProps[name])) {
|
||||
child.prototype[name] = makeWrapper(parentProto, name, protoProps[name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add static properties to the constructor function, if supplied.
|
||||
if (staticProps) _.extend(child, staticProps);
|
||||
|
||||
// Correctly set child's `prototype.constructor`.
|
||||
child.prototype.constructor = child;
|
||||
|
||||
// Set a convenience property in case the parent's prototype is needed later.
|
||||
child.__super__ = parentProto;
|
||||
|
||||
return child;
|
||||
};
|
||||
|
||||
return inherits;
|
||||
}));
|
||||
|
||||
733
lms/static/js/views/fields.js
Normal file
@@ -0,0 +1,733 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'gettext', 'jquery', 'underscore', 'backbone', 'js/mustache', 'backbone-super'
|
||||
], function (gettext, $, _, Backbone, RequireMustache) {
|
||||
|
||||
var Mustache = window.Mustache || RequireMustache;
|
||||
|
||||
var messageRevertDelay = 4000;
|
||||
var FieldViews = {};
|
||||
|
||||
FieldViews.FieldView = Backbone.View.extend({
|
||||
|
||||
fieldType: 'generic',
|
||||
|
||||
className: function () {
|
||||
return 'u-field' + ' u-field-' + this.fieldType + ' u-field-' + this.options.valueAttribute;
|
||||
},
|
||||
|
||||
tagName: 'div',
|
||||
|
||||
indicators: {
|
||||
'canEdit': '<i class="icon fa fa-pencil message-can-edit" aria-hidden="true"></i><span class="sr">' + gettext("Editable") + '</span>',
|
||||
'error': '<i class="fa fa-exclamation-triangle message-error" aria-hidden="true"></i><span class="sr">' + gettext("Error") + '</span>',
|
||||
'validationError': '<i class="fa fa-exclamation-triangle message-validation-error" aria-hidden="true"></i><span class="sr">' + gettext("Validation Error") + '</span>',
|
||||
'inProgress': '<i class="fa fa-spinner fa-pulse message-in-progress" aria-hidden="true"></i><span class="sr">' + gettext("In Progress") + '</span>',
|
||||
'success': '<i class="fa fa-check message-success" aria-hidden="true"></i><span class="sr">' + gettext("Success") + '</span>',
|
||||
'plus': '<i class="fa fa-plus placeholder" aria-hidden="true"></i><span class="sr">' + gettext("Placeholder")+ '</span>'
|
||||
},
|
||||
|
||||
messages: {
|
||||
'canEdit': '',
|
||||
'error': gettext('An error occurred. Please try again.'),
|
||||
'validationError': '',
|
||||
'inProgress': gettext('Saving'),
|
||||
'success': gettext('Your changes have been saved.')
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
|
||||
this.template = _.template($(this.templateSelector).text());
|
||||
|
||||
this.helpMessage = this.options.helpMessage || '';
|
||||
this.showMessages = _.isUndefined(this.options.showMessages) ? true : this.options.showMessages;
|
||||
|
||||
_.bindAll(this, 'modelValue', 'modelValueIsSet', 'showNotificationMessage','getNotificationMessage',
|
||||
'getMessage', 'title', 'showHelpMessage', 'showInProgressMessage', 'showSuccessMessage',
|
||||
'showErrorMessage'
|
||||
);
|
||||
},
|
||||
|
||||
modelValue: function () {
|
||||
return this.model.get(this.options.valueAttribute);
|
||||
},
|
||||
|
||||
modelValueIsSet: function() {
|
||||
return (this.modelValue() === true);
|
||||
},
|
||||
|
||||
title: function (text) {
|
||||
return this.$('.u-field-title').html(text);
|
||||
},
|
||||
|
||||
getMessage: function(message_status) {
|
||||
if ((message_status + 'Message') in this) {
|
||||
return this[message_status + 'Message'].call(this);
|
||||
} else if (this.showMessages) {
|
||||
return this.indicators[message_status] + this.messages[message_status];
|
||||
}
|
||||
return this.indicators[message_status];
|
||||
},
|
||||
|
||||
showHelpMessage: function (message) {
|
||||
if (_.isUndefined(message) || _.isNull(message)) {
|
||||
message = this.helpMessage;
|
||||
}
|
||||
this.$('.u-field-message-notification').html('');
|
||||
this.$('.u-field-message-help').html(message);
|
||||
},
|
||||
|
||||
getNotificationMessage: function() {
|
||||
return this.$('.u-field-message-notification').html();
|
||||
},
|
||||
|
||||
showNotificationMessage: function(message) {
|
||||
this.$('.u-field-message-help').html('');
|
||||
this.$('.u-field-message-notification').html(message);
|
||||
},
|
||||
|
||||
showCanEditMessage: function(show) {
|
||||
if (!_.isUndefined(show) && show) {
|
||||
this.showNotificationMessage(this.getMessage('canEdit'));
|
||||
} else {
|
||||
this.showNotificationMessage('');
|
||||
}
|
||||
},
|
||||
|
||||
showInProgressMessage: function () {
|
||||
this.showNotificationMessage(this.getMessage('inProgress'));
|
||||
},
|
||||
|
||||
showSuccessMessage: function () {
|
||||
var successMessage = this.getMessage('success');
|
||||
this.showNotificationMessage(successMessage);
|
||||
|
||||
if (this.options.refreshPageOnSave) {
|
||||
document.location.reload();
|
||||
}
|
||||
|
||||
var view = this;
|
||||
|
||||
var context = Date.now();
|
||||
this.lastSuccessMessageContext = context;
|
||||
|
||||
setTimeout(function () {
|
||||
if ((context === view.lastSuccessMessageContext) && (view.getNotificationMessage() === successMessage)) {
|
||||
view.showHelpMessage();
|
||||
}
|
||||
}, messageRevertDelay);
|
||||
},
|
||||
|
||||
showErrorMessage: function (xhr) {
|
||||
if (xhr.status === 400) {
|
||||
try {
|
||||
var errors = JSON.parse(xhr.responseText),
|
||||
validationErrorMessage = Mustache.escapeHtml(
|
||||
errors.field_errors[this.options.valueAttribute].user_message
|
||||
),
|
||||
message = this.indicators.validationError + validationErrorMessage;
|
||||
this.showNotificationMessage(message);
|
||||
} catch (error) {
|
||||
this.showNotificationMessage(this.getMessage('error'));
|
||||
}
|
||||
} else {
|
||||
this.showNotificationMessage(this.getMessage('error'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
FieldViews.EditableFieldView = FieldViews.FieldView.extend({
|
||||
|
||||
initialize: function (options) {
|
||||
_.bindAll(this, 'saveAttributes', 'saveSucceeded', 'showDisplayMode', 'showEditMode',
|
||||
'startEditing', 'finishEditing'
|
||||
);
|
||||
this._super(options);
|
||||
|
||||
this.editable = _.isUndefined(this.options.editable) ? 'always': this.options.editable;
|
||||
this.$el.addClass('editable-' + this.editable);
|
||||
|
||||
if (this.editable === 'always') {
|
||||
this.showEditMode(false);
|
||||
} else {
|
||||
this.showDisplayMode(false);
|
||||
}
|
||||
},
|
||||
|
||||
saveAttributes: function (attributes, options) {
|
||||
var view = this;
|
||||
var defaultOptions = {
|
||||
contentType: 'application/merge-patch+json',
|
||||
patch: true,
|
||||
wait: true,
|
||||
success: function () {
|
||||
view.saveSucceeded();
|
||||
},
|
||||
error: function (model, xhr) {
|
||||
view.showErrorMessage(xhr);
|
||||
}
|
||||
};
|
||||
this.showInProgressMessage();
|
||||
this.model.save(attributes, _.extend(defaultOptions, options));
|
||||
},
|
||||
|
||||
saveSucceeded: function () {
|
||||
this.showSuccessMessage();
|
||||
},
|
||||
|
||||
showDisplayMode: function(render) {
|
||||
this.mode = 'display';
|
||||
if (render) { this.render(); }
|
||||
|
||||
this.$el.removeClass('mode-edit');
|
||||
|
||||
this.$el.toggleClass('mode-hidden', (this.editable === 'never' && !this.modelValueIsSet()));
|
||||
this.$el.toggleClass('mode-placeholder', (this.editable === 'toggle' && !this.modelValueIsSet()));
|
||||
this.$el.toggleClass('mode-display', (this.modelValueIsSet()));
|
||||
},
|
||||
|
||||
showEditMode: function(render) {
|
||||
this.mode = 'edit';
|
||||
if (render) { this.render(); }
|
||||
|
||||
this.$el.removeClass('mode-hidden');
|
||||
this.$el.removeClass('mode-placeholder');
|
||||
this.$el.removeClass('mode-display');
|
||||
|
||||
this.$el.addClass('mode-edit');
|
||||
},
|
||||
|
||||
startEditing: function () {
|
||||
if (this.editable === 'toggle' && this.mode !== 'edit') {
|
||||
this.showEditMode(true);
|
||||
}
|
||||
},
|
||||
|
||||
finishEditing: function() {
|
||||
if (this.fieldValue() !== this.modelValue()) {
|
||||
this.saveValue();
|
||||
} else {
|
||||
if (this.editable === 'always') {
|
||||
this.showEditMode(true);
|
||||
} else {
|
||||
this.showDisplayMode(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
FieldViews.ReadonlyFieldView = FieldViews.FieldView.extend({
|
||||
|
||||
fieldType: 'readonly',
|
||||
|
||||
templateSelector: '#field_readonly-tpl',
|
||||
|
||||
initialize: function (options) {
|
||||
this._super(options);
|
||||
_.bindAll(this, 'render', 'fieldValue', 'updateValueInField');
|
||||
this.listenTo(this.model, "change:" + this.options.valueAttribute, this.updateValueInField);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(this.template({
|
||||
id: this.options.valueAttribute,
|
||||
title: this.options.title,
|
||||
value: this.modelValue(),
|
||||
message: this.helpMessage
|
||||
}));
|
||||
this.delegateEvents();
|
||||
return this;
|
||||
},
|
||||
|
||||
fieldValue: function () {
|
||||
return this.$('.u-field-value input').val();
|
||||
},
|
||||
|
||||
updateValueInField: function () {
|
||||
this.$('.u-field-value input').val(Mustache.escapeHtml(this.modelValue()));
|
||||
}
|
||||
});
|
||||
|
||||
FieldViews.TextFieldView = FieldViews.EditableFieldView.extend({
|
||||
|
||||
fieldType: 'text',
|
||||
|
||||
templateSelector: '#field_text-tpl',
|
||||
|
||||
events: {
|
||||
'change input': 'saveValue'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
this._super(options);
|
||||
_.bindAll(this, 'render', 'fieldValue', 'updateValueInField', 'saveValue');
|
||||
this.listenTo(this.model, "change:" + this.options.valueAttribute, this.updateValueInField);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(this.template({
|
||||
id: this.options.valueAttribute,
|
||||
title: this.options.title,
|
||||
value: this.modelValue(),
|
||||
message: this.helpMessage
|
||||
}));
|
||||
this.delegateEvents();
|
||||
return this;
|
||||
},
|
||||
|
||||
fieldValue: function () {
|
||||
return this.$('.u-field-value input').val();
|
||||
},
|
||||
|
||||
updateValueInField: function () {
|
||||
var value = (_.isUndefined(this.modelValue()) || _.isNull(this.modelValue())) ? '' : this.modelValue();
|
||||
this.$('.u-field-value input').val(Mustache.escapeHtml(value));
|
||||
},
|
||||
|
||||
saveValue: function () {
|
||||
var attributes = {};
|
||||
attributes[this.options.valueAttribute] = this.fieldValue();
|
||||
this.saveAttributes(attributes);
|
||||
}
|
||||
});
|
||||
|
||||
FieldViews.DropdownFieldView = FieldViews.EditableFieldView.extend({
|
||||
|
||||
fieldType: 'dropdown',
|
||||
|
||||
templateSelector: '#field_dropdown-tpl',
|
||||
|
||||
events: {
|
||||
'click': 'startEditing',
|
||||
'change select': 'finishEditing',
|
||||
'focusout select': 'finishEditing'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
_.bindAll(this, 'render', 'optionForValue', 'fieldValue', 'displayValue', 'updateValueInField', 'saveValue');
|
||||
this._super(options);
|
||||
|
||||
this.listenTo(this.model, "change:" + this.options.valueAttribute, this.updateValueInField);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(this.template({
|
||||
id: this.options.valueAttribute,
|
||||
mode: this.mode,
|
||||
title: this.options.title,
|
||||
screenReaderTitle: this.options.screenReaderTitle || this.options.title,
|
||||
iconName: this.options.iconName,
|
||||
showBlankOption: (!this.options.required || !this.modelValueIsSet()),
|
||||
selectOptions: this.options.options,
|
||||
message: this.helpMessage
|
||||
}));
|
||||
this.delegateEvents();
|
||||
this.updateValueInField();
|
||||
|
||||
if (this.editable === 'toggle') {
|
||||
this.showCanEditMessage(this.mode === 'display');
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
modelValueIsSet: function() {
|
||||
var value = this.modelValue();
|
||||
if (_.isUndefined(value) || _.isNull(value) || value === '') {
|
||||
return false;
|
||||
} else {
|
||||
return !(_.isUndefined(this.optionForValue(value)));
|
||||
}
|
||||
},
|
||||
|
||||
optionForValue: function(value) {
|
||||
return _.find(this.options.options, function(option) { return option[0] === value; });
|
||||
},
|
||||
|
||||
fieldValue: function () {
|
||||
return this.$('.u-field-value select').val();
|
||||
},
|
||||
|
||||
displayValue: function (value) {
|
||||
if (value) {
|
||||
var option = this.optionForValue(value);
|
||||
return (option ? option[1] : '');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
updateValueInField: function () {
|
||||
if (this.mode === 'display') {
|
||||
var value = this.displayValue(this.modelValue() || '');
|
||||
if (this.modelValueIsSet() === false) {
|
||||
value = this.options.placeholderValue || '';
|
||||
}
|
||||
this.$('.u-field-value').attr('aria-label', this.options.title);
|
||||
this.$('.u-field-value-readonly').html(Mustache.escapeHtml(value));
|
||||
this.showDisplayMode(false);
|
||||
} else {
|
||||
this.$('.u-field-value select').val(this.modelValue() || '');
|
||||
}
|
||||
},
|
||||
|
||||
saveValue: function () {
|
||||
var attributes = {};
|
||||
attributes[this.options.valueAttribute] = this.fieldValue();
|
||||
this.saveAttributes(attributes);
|
||||
},
|
||||
|
||||
showEditMode: function(render) {
|
||||
this._super(render);
|
||||
if (this.editable === 'toggle') {
|
||||
this.$('.u-field-value select').focus();
|
||||
}
|
||||
},
|
||||
|
||||
saveSucceeded: function() {
|
||||
if (this.editable === 'toggle') {
|
||||
this.showDisplayMode(true);
|
||||
} else {
|
||||
this.showEditMode(true);
|
||||
}
|
||||
this._super();
|
||||
},
|
||||
|
||||
disableField: function(disable) {
|
||||
this.$('.u-field-value select').prop('disabled', disable);
|
||||
}
|
||||
});
|
||||
|
||||
FieldViews.TextareaFieldView = FieldViews.EditableFieldView.extend({
|
||||
|
||||
fieldType: 'textarea',
|
||||
|
||||
templateSelector: '#field_textarea-tpl',
|
||||
|
||||
events: {
|
||||
'click .wrapper-u-field': 'startEditing',
|
||||
'click .u-field-placeholder': 'startEditing',
|
||||
'focusout textarea': 'finishEditing',
|
||||
'change textarea': 'adjustTextareaHeight',
|
||||
'keyup textarea': 'adjustTextareaHeight',
|
||||
'keydown textarea': 'adjustTextareaHeight',
|
||||
'paste textarea': 'adjustTextareaHeight',
|
||||
'cut textarea': 'adjustTextareaHeight'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
_.bindAll(this, 'render', 'adjustTextareaHeight', 'fieldValue', 'saveValue', 'updateView');
|
||||
this._super(options);
|
||||
this.listenTo(this.model, "change:" + this.options.valueAttribute, this.updateView);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var value = this.modelValue();
|
||||
if (this.mode === 'display') {
|
||||
value = value || this.options.placeholderValue;
|
||||
}
|
||||
this.$el.html(this.template({
|
||||
id: this.options.valueAttribute,
|
||||
screenReaderTitle: this.options.screenReaderTitle || this.options.title,
|
||||
mode: this.mode,
|
||||
value: value,
|
||||
message: this.helpMessage,
|
||||
placeholderValue: this.options.placeholderValue
|
||||
}));
|
||||
this.delegateEvents();
|
||||
this.title((this.modelValue() || this.mode === 'edit') ? this.options.title : this.indicators['plus'] + this.options.title);
|
||||
|
||||
if (this.editable === 'toggle') {
|
||||
this.showCanEditMessage(this.mode === 'display');
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
adjustTextareaHeight: function() {
|
||||
var textarea = this.$('textarea');
|
||||
textarea.css('height', 'auto').css('height', textarea.prop('scrollHeight') + 10);
|
||||
},
|
||||
|
||||
modelValue: function() {
|
||||
var value = this._super();
|
||||
return value ? $.trim(value) : '';
|
||||
},
|
||||
|
||||
fieldValue: function () {
|
||||
return this.$('.u-field-value textarea').val();
|
||||
},
|
||||
|
||||
saveValue: function () {
|
||||
var attributes = {};
|
||||
attributes[this.options.valueAttribute] = this.fieldValue();
|
||||
this.saveAttributes(attributes);
|
||||
},
|
||||
|
||||
updateView: function () {
|
||||
if (this.mode !== 'edit') {
|
||||
this.showDisplayMode(true);
|
||||
}
|
||||
},
|
||||
|
||||
modelValueIsSet: function() {
|
||||
return !(this.modelValue() === '');
|
||||
},
|
||||
|
||||
showEditMode: function(render) {
|
||||
this._super(render);
|
||||
this.adjustTextareaHeight();
|
||||
this.$('.u-field-value textarea').focus();
|
||||
},
|
||||
|
||||
saveSucceeded: function() {
|
||||
this._super();
|
||||
if (this.editable === 'toggle') {
|
||||
this.showDisplayMode(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
FieldViews.LinkFieldView = FieldViews.FieldView.extend({
|
||||
|
||||
fieldType: 'link',
|
||||
|
||||
templateSelector: '#field_link-tpl',
|
||||
|
||||
events: {
|
||||
'click a': 'linkClicked'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
this._super(options);
|
||||
_.bindAll(this, 'render', 'linkClicked');
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(this.template({
|
||||
id: this.options.valueAttribute,
|
||||
title: this.options.title,
|
||||
screenReaderTitle: this.options.screenReaderTitle || this.options.title,
|
||||
linkTitle: this.options.linkTitle,
|
||||
linkHref: this.options.linkHref,
|
||||
message: this.helpMessage
|
||||
}));
|
||||
this.delegateEvents();
|
||||
return this;
|
||||
},
|
||||
|
||||
linkClicked: function (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
FieldViews.ImageFieldView = FieldViews.FieldView.extend({
|
||||
|
||||
fieldType: 'image',
|
||||
|
||||
templateSelector: '#field_image-tpl',
|
||||
uploadButtonSelector: '.upload-button-input',
|
||||
|
||||
titleAdd: gettext("Upload an image"),
|
||||
titleEdit: gettext("Change image"),
|
||||
titleRemove: gettext("Remove"),
|
||||
|
||||
titleUploading: gettext("Uploading"),
|
||||
titleRemoving: gettext("Removing"),
|
||||
|
||||
titleImageAlt: '',
|
||||
|
||||
iconUpload: '<i class="icon fa fa-camera" aria-hidden="true"></i>',
|
||||
iconRemove: '<i class="icon fa fa-remove" aria-hidden="true"></i>',
|
||||
iconProgress: '<i class="icon fa fa-spinner fa-pulse fa-spin" aria-hidden="true"></i>',
|
||||
|
||||
errorMessage: gettext("An error has occurred. Refresh the page, and then try again."),
|
||||
|
||||
events: {
|
||||
'click .u-field-upload-button': 'clickedUploadButton',
|
||||
'click .u-field-remove-button': 'clickedRemoveButton',
|
||||
'click .upload-submit': 'clickedUploadButton'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
this._super(options);
|
||||
_.bindAll(this, 'render', 'imageChangeSucceeded', 'imageChangeFailed', 'fileSelected',
|
||||
'watchForPageUnload', 'onBeforeUnload');
|
||||
this.listenTo(this.model, "change:" + this.options.valueAttribute, this.render);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(this.template({
|
||||
id: this.options.valueAttribute,
|
||||
imageUrl: _.result(this, 'imageUrl'),
|
||||
imageAltText: _.result(this, 'imageAltText'),
|
||||
uploadButtonIcon: _.result(this, 'iconUpload'),
|
||||
uploadButtonTitle: _.result(this, 'uploadButtonTitle'),
|
||||
removeButtonIcon: _.result(this, 'iconRemove'),
|
||||
removeButtonTitle: _.result(this, 'removeButtonTitle')
|
||||
}));
|
||||
this.delegateEvents();
|
||||
this.updateButtonsVisibility();
|
||||
this.watchForPageUnload();
|
||||
return this;
|
||||
},
|
||||
|
||||
showErrorMessage: function (message) {
|
||||
return message;
|
||||
},
|
||||
|
||||
imageUrl: function () {
|
||||
return '';
|
||||
},
|
||||
|
||||
uploadButtonTitle: function () {
|
||||
if (this.isShowingPlaceholder()) {
|
||||
return _.result(this, 'titleAdd');
|
||||
} else {
|
||||
return _.result(this, 'titleEdit');
|
||||
}
|
||||
},
|
||||
|
||||
removeButtonTitle: function () {
|
||||
return this.titleRemove;
|
||||
},
|
||||
|
||||
isEditingAllowed: function () {
|
||||
return true;
|
||||
},
|
||||
|
||||
isShowingPlaceholder: function () {
|
||||
return false;
|
||||
},
|
||||
|
||||
setUploadButtonVisibility: function (state) {
|
||||
this.$('.u-field-upload-button').css('display', state);
|
||||
},
|
||||
|
||||
setRemoveButtonVisibility: function (state) {
|
||||
this.$('.u-field-remove-button').css('display', state);
|
||||
},
|
||||
|
||||
updateButtonsVisibility: function () {
|
||||
if (!this.isEditingAllowed() || !this.options.editable) {
|
||||
this.setUploadButtonVisibility('none');
|
||||
}
|
||||
|
||||
if (this.isShowingPlaceholder() || !this.options.editable) {
|
||||
this.setRemoveButtonVisibility('none');
|
||||
}
|
||||
},
|
||||
|
||||
clickedUploadButton: function () {
|
||||
$(this.uploadButtonSelector).fileupload({
|
||||
url: this.options.imageUploadUrl,
|
||||
type: 'POST',
|
||||
add: this.fileSelected,
|
||||
done: this.imageChangeSucceeded,
|
||||
fail: this.imageChangeFailed
|
||||
});
|
||||
},
|
||||
|
||||
clickedRemoveButton: function () {
|
||||
var view = this;
|
||||
this.setCurrentStatus('removing');
|
||||
this.setUploadButtonVisibility('none');
|
||||
this.showRemovalInProgressMessage();
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: this.options.imageRemoveUrl
|
||||
}).done(function () {
|
||||
view.imageChangeSucceeded();
|
||||
}).fail(function (jqXHR) {
|
||||
view.showImageChangeFailedMessage(jqXHR.status, jqXHR.responseText);
|
||||
});
|
||||
},
|
||||
|
||||
imageChangeSucceeded: function () {
|
||||
this.render();
|
||||
},
|
||||
|
||||
imageChangeFailed: function (e, data) {
|
||||
},
|
||||
|
||||
showImageChangeFailedMessage: function (status, responseText) {
|
||||
},
|
||||
|
||||
fileSelected: function (e, data) {
|
||||
if (_.isUndefined(data.files[0].size) || this.validateImageSize(data.files[0].size)) {
|
||||
data.formData = {file: data.files[0]};
|
||||
this.setCurrentStatus('uploading');
|
||||
this.setRemoveButtonVisibility('none');
|
||||
this.showUploadInProgressMessage();
|
||||
data.submit();
|
||||
}
|
||||
},
|
||||
|
||||
validateImageSize: function (imageBytes) {
|
||||
var humanReadableSize;
|
||||
if (imageBytes < this.options.imageMinBytes) {
|
||||
humanReadableSize = this.bytesToHumanReadable(this.options.imageMinBytes);
|
||||
this.showErrorMessage(
|
||||
interpolate_text(
|
||||
gettext("The file must be at least {size} in size."), {size: humanReadableSize}
|
||||
)
|
||||
);
|
||||
return false;
|
||||
} else if (imageBytes > this.options.imageMaxBytes) {
|
||||
humanReadableSize = this.bytesToHumanReadable(this.options.imageMaxBytes);
|
||||
this.showErrorMessage(
|
||||
interpolate_text(
|
||||
gettext("The file must be smaller than {size} in size."), {size: humanReadableSize}
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
showUploadInProgressMessage: function () {
|
||||
this.$('.u-field-upload-button').css('opacity', 1);
|
||||
this.$('.upload-button-icon').html(this.iconProgress);
|
||||
this.$('.upload-button-title').html(this.titleUploading);
|
||||
},
|
||||
|
||||
showRemovalInProgressMessage: function () {
|
||||
this.$('.u-field-remove-button').css('opacity', 1);
|
||||
this.$('.remove-button-icon').html(this.iconProgress);
|
||||
this.$('.remove-button-title').html(this.titleRemoving);
|
||||
},
|
||||
|
||||
setCurrentStatus: function (status) {
|
||||
this.$('.image-wrapper').attr('data-status', status);
|
||||
},
|
||||
|
||||
getCurrentStatus: function () {
|
||||
return this.$('.image-wrapper').attr('data-status');
|
||||
},
|
||||
|
||||
watchForPageUnload: function () {
|
||||
$(window).on('beforeunload', this.onBeforeUnload);
|
||||
},
|
||||
|
||||
onBeforeUnload: function () {
|
||||
var status = this.getCurrentStatus();
|
||||
if (status === 'uploading') {
|
||||
return gettext("Upload is in progress. To avoid errors, stay on this page until the process is complete.");
|
||||
} else if (status === 'removing') {
|
||||
return gettext("Removal is in progress. To avoid errors, stay on this page until the process is complete.");
|
||||
}
|
||||
},
|
||||
|
||||
bytesToHumanReadable: function (size) {
|
||||
var units = [gettext('bytes'), gettext('KB'), gettext('MB')];
|
||||
var i = 0;
|
||||
while(size >= 1024) {
|
||||
size /= 1024;
|
||||
++i;
|
||||
}
|
||||
return size.toFixed(1)*1 + ' ' + units[i];
|
||||
}
|
||||
});
|
||||
|
||||
return FieldViews;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
37
lms/static/js/views/message_banner.js
Normal file
@@ -0,0 +1,37 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'gettext', 'jquery', 'underscore', 'backbone'
|
||||
], function (gettext, $, _, Backbone) {
|
||||
|
||||
var MessageBannerView = Backbone.View.extend({
|
||||
|
||||
initialize: function () {
|
||||
this.template = _.template($('#message_banner-tpl').text());
|
||||
},
|
||||
|
||||
render: function () {
|
||||
if (_.isUndefined(this.message) || _.isNull(this.message)) {
|
||||
this.$el.html('');
|
||||
} else {
|
||||
this.$el.html(this.template({
|
||||
message: this.message
|
||||
}));
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
showMessage: function (message) {
|
||||
this.message = message;
|
||||
this.render();
|
||||
},
|
||||
|
||||
hideMessage: function () {
|
||||
this.message = null;
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
|
||||
return MessageBannerView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -83,6 +83,7 @@ fixture_paths:
|
||||
- templates/instructor/instructor_dashboard_2
|
||||
- templates/dashboard
|
||||
- templates/edxnotes
|
||||
- templates/fields
|
||||
- templates/student_account
|
||||
- templates/student_profile
|
||||
- templates/verify_student
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"annotator_1.2.9": "js/vendor/edxnotes/annotator-full.min",
|
||||
"date": "js/vendor/date",
|
||||
"backbone": "js/vendor/backbone-min",
|
||||
"backbone-super": "js/vendor/backbone-super",
|
||||
"underscore.string": "js/vendor/underscore.string.min",
|
||||
// Files needed by OVA
|
||||
"annotator": "js/vendor/ova/annotator-full",
|
||||
@@ -87,6 +88,9 @@
|
||||
deps: ["underscore", "jquery"],
|
||||
exports: "Backbone"
|
||||
},
|
||||
"backbone-super": {
|
||||
deps: ["backbone"],
|
||||
},
|
||||
"logger": {
|
||||
exports: "Logger"
|
||||
},
|
||||
|
||||
@@ -28,14 +28,9 @@
|
||||
@include animation(rotateCW $tmg-s1 linear infinite);
|
||||
}
|
||||
|
||||
.ui-loading {
|
||||
.ui-loading-base {
|
||||
@include animation(fadeIn $tmg-f2 linear 1);
|
||||
@extend %ui-well;
|
||||
@extend %t-copy-base;
|
||||
opacity: .6;
|
||||
background-color: $white;
|
||||
padding: ($baseline*1.5) $baseline;
|
||||
text-align: center;
|
||||
|
||||
.spin {
|
||||
@extend %anim-rotateCW;
|
||||
@@ -46,3 +41,12 @@
|
||||
padding-left: ($baseline/4);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-loading {
|
||||
@extend .ui-loading-base;
|
||||
@extend %ui-well;
|
||||
opacity: 0.6;
|
||||
background-color: $white;
|
||||
padding: ($baseline*1.5) $baseline;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
@import 'elements/system-feedback';
|
||||
|
||||
// base - specific views
|
||||
@import "views/account-settings";
|
||||
@import "views/learner-profile";
|
||||
@import 'views/verification';
|
||||
@import 'views/shoppingcart';
|
||||
@import 'views/login-register';
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
@import 'elements/system-feedback';
|
||||
|
||||
// base - specific views
|
||||
@import "views/account-settings";
|
||||
@import "views/learner-profile";
|
||||
@import 'views/login-register';
|
||||
@import 'views/verification';
|
||||
@import 'views/decoupled-verification';
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
@import 'elements/controls';
|
||||
|
||||
// shared - course
|
||||
@import 'shared/fields';
|
||||
@import 'shared/forms';
|
||||
@import 'shared/footer';
|
||||
@import 'shared/header';
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
@import 'elements/controls';
|
||||
|
||||
// shared - course
|
||||
@import 'shared/fields';
|
||||
@import 'shared/forms';
|
||||
@import 'shared/footer';
|
||||
@import 'shared/header';
|
||||
|
||||
@@ -16,33 +16,13 @@
|
||||
padding: ($baseline*2) 0 0 0;
|
||||
|
||||
.profile-sidebar {
|
||||
@include float(right);
|
||||
@include margin-left(flex-gutter());
|
||||
width: flex-grid(3);
|
||||
background: transparent;
|
||||
|
||||
.profile {
|
||||
@include box-sizing(border-box);
|
||||
border: 1px solid $border-color-2;
|
||||
border-radius: ($baseline/4) ($baseline/4) 0 0;
|
||||
width: flex-grid(12);
|
||||
|
||||
.username-header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.username-label {
|
||||
@extend %t-title7;
|
||||
@extend %t-ultrastrong;
|
||||
@extend %cont-truncated;
|
||||
text-align: center;
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: ($baseline*0.75) $baseline;
|
||||
color: $base-font-color;
|
||||
text-transform: none;
|
||||
}
|
||||
}
|
||||
@include float(right);
|
||||
margin-top: ($baseline*2);
|
||||
width: flex-grid(3);
|
||||
box-shadow: 0 0 1px $shadow-l1;
|
||||
border: 1px solid $border-color-2;
|
||||
border-radius: 3px;
|
||||
|
||||
.user-info {
|
||||
@include clearfix();
|
||||
@@ -51,12 +31,8 @@
|
||||
@include box-sizing(border-box);
|
||||
@include clearfix();
|
||||
margin: 0;
|
||||
border: 1px solid $border-color-2;
|
||||
border-top: none;
|
||||
border-radius: 0 0 ($baseline/4) ($baseline/4);
|
||||
padding: $baseline;
|
||||
width: flex-grid(12);
|
||||
background: $white;
|
||||
|
||||
li {
|
||||
@include clearfix();
|
||||
@@ -79,19 +55,22 @@
|
||||
|
||||
span {
|
||||
display: block;
|
||||
margin-bottom: ($baseline/4);
|
||||
}
|
||||
|
||||
span.title {
|
||||
color: $gray;
|
||||
font-family: $sans-serif;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
@extend %t-copy-sub1;
|
||||
@extend %t-strong;
|
||||
|
||||
a {
|
||||
text-transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
span.copy {
|
||||
@extend %t-copy-sub1;
|
||||
}
|
||||
|
||||
span.data {
|
||||
color: $base-font-color;
|
||||
font-weight: 600;
|
||||
@@ -104,40 +83,6 @@
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
.auth-provider {
|
||||
width: flex-grid(12);
|
||||
display: block;
|
||||
margin-top: ($baseline/4);
|
||||
|
||||
.status {
|
||||
width: flex-grid(1);
|
||||
display: inline-block;
|
||||
color: $gray-l2;
|
||||
|
||||
.fa-link {
|
||||
color: $base-font-color;
|
||||
}
|
||||
|
||||
.copy {
|
||||
@extend %text-sr;
|
||||
}
|
||||
}
|
||||
|
||||
.provider {
|
||||
width: flex-grid(9);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.control {
|
||||
width: flex-grid(2);
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
|
||||
a:link, a:visited {
|
||||
@extend %t-copy-sub2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +92,17 @@
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.heads-up {
|
||||
.title {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.copy {
|
||||
@extend %t-copy-sub2;
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reverify-status-list {
|
||||
|
||||
123
lms/static/sass/shared/_fields.scss
Normal file
@@ -0,0 +1,123 @@
|
||||
// lms - shared - fields
|
||||
// ====================
|
||||
|
||||
|
||||
.u-field {
|
||||
padding: $baseline 0;
|
||||
border-bottom: 1px solid $gray-l5;
|
||||
border: 1px dashed transparent;
|
||||
|
||||
&.mode-placeholder {
|
||||
border: 2px dashed transparent;
|
||||
border-radius: 3px;
|
||||
|
||||
span {
|
||||
color: $gray-l1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 2px dashed $link-color;
|
||||
|
||||
span {
|
||||
color: $link-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.editable-toggle.mode-display:hover {
|
||||
background-color: $m-blue-l4;
|
||||
border-radius: 3px;
|
||||
|
||||
.message-can-edit {
|
||||
display: inline-block;
|
||||
color: $link-color;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.mode-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
i {
|
||||
color: $gray-l2;
|
||||
vertical-align:text-bottom;
|
||||
@include margin-right(5px);
|
||||
}
|
||||
|
||||
.message-can-edit {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.message-error {
|
||||
color: $alert-color;
|
||||
}
|
||||
|
||||
.message-validation-error {
|
||||
color: $warning-color;
|
||||
}
|
||||
|
||||
.message-in-progress {
|
||||
color: $gray-d2;
|
||||
}
|
||||
|
||||
.message-success {
|
||||
color: $success-color;
|
||||
}
|
||||
}
|
||||
|
||||
.u-field-readonly {
|
||||
input[type="text"],
|
||||
input[type="text"]:focus {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.u-field-icon {
|
||||
width: $baseline;
|
||||
color: $gray-l2;
|
||||
}
|
||||
|
||||
.u-field-title {
|
||||
width: flex-grid(3, 12);
|
||||
display: inline-block;
|
||||
color: $gray;
|
||||
vertical-align: top;
|
||||
margin-bottom: 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
label, span {
|
||||
@include margin-left($baseline/2);
|
||||
}
|
||||
}
|
||||
|
||||
.u-field-value {
|
||||
width: flex-grid(3, 12);
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
|
||||
select, input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.u-field-message {
|
||||
@extend %t-copy-sub1;
|
||||
@include padding-left($baseline/2);
|
||||
width: flex-grid(6, 12);
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
color: $gray-l1;
|
||||
|
||||
i {
|
||||
@include margin-right($baseline/4);
|
||||
}
|
||||
|
||||
.u-field-message-help,
|
||||
.u-field-message-notification {
|
||||
color: $gray-l1;
|
||||
}
|
||||
}
|
||||
80
lms/static/sass/views/_account-settings.scss
Normal file
@@ -0,0 +1,80 @@
|
||||
// lms - application - account settings
|
||||
// ====================
|
||||
|
||||
// Table of Contents
|
||||
// * +Container - Account Settings
|
||||
// * +Main - Header
|
||||
// * +Settings Section
|
||||
|
||||
|
||||
// +Container - Account Settings
|
||||
.wrapper-account-settings {
|
||||
@extend .container;
|
||||
padding-top: ($baseline*2);
|
||||
|
||||
.account-settings-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ui-loading-indicator,
|
||||
.ui-loading-error {
|
||||
@extend .ui-loading-base;
|
||||
// center horizontally
|
||||
@include margin-left(auto);
|
||||
@include margin-right(auto);
|
||||
padding: ($baseline*3);
|
||||
|
||||
text-align: center;
|
||||
|
||||
.message-error {
|
||||
color: $alert-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// +Main - Header
|
||||
.wrapper-account-settings {
|
||||
|
||||
.wrapper-header {
|
||||
|
||||
.header-title {
|
||||
@extend %t-title4;
|
||||
margin-bottom: ($baseline/2);
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
color: $gray-l2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// +Settings Section
|
||||
.account-settings-sections {
|
||||
|
||||
.section-header {
|
||||
@extend %t-title6;
|
||||
@extend %t-strong;
|
||||
padding-bottom: ($baseline/2);
|
||||
border-bottom: 1px solid $gray-l4;
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: $white;
|
||||
padding: $baseline;
|
||||
margin-top: $baseline;
|
||||
border: 1px solid $gray-l4;
|
||||
box-shadow: 0 0 1px 1px $shadow-l2;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
a span {
|
||||
color: $link-color;
|
||||
}
|
||||
|
||||
a span {
|
||||
&:hover, &:focus {
|
||||
color: $pink;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
272
lms/static/sass/views/_learner-profile.scss
Normal file
@@ -0,0 +1,272 @@
|
||||
// lms - application - learner profile
|
||||
// ====================
|
||||
|
||||
// Table of Contents
|
||||
// * +Container - Learner Profile
|
||||
// * +Main - Header
|
||||
// * +Settings Section
|
||||
|
||||
.view-profile {
|
||||
$profile-image-dimension: 120px;
|
||||
|
||||
.content-wrapper {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.ui-loading-indicator {
|
||||
@extend .ui-loading-base;
|
||||
padding-bottom: $baseline;
|
||||
|
||||
// center horizontally
|
||||
@include margin-left(auto);
|
||||
@include margin-right(auto);
|
||||
width: ($baseline*5);
|
||||
}
|
||||
|
||||
.profile-image-field {
|
||||
@include float(left);
|
||||
|
||||
button {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.u-field-image {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
width: $profile-image-dimension;
|
||||
position: relative;
|
||||
|
||||
.image-frame {
|
||||
position: relative;
|
||||
width: $profile-image-dimension;
|
||||
height: $profile-image-dimension;
|
||||
border-radius: ($baseline/4);
|
||||
}
|
||||
|
||||
.u-field-upload-button {
|
||||
width: $profile-image-dimension;
|
||||
height: $profile-image-dimension;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
@include transition(all $tmg-f1 ease-in-out 0s);
|
||||
|
||||
i {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-button-icon, .upload-button-title {
|
||||
text-align: center;
|
||||
transform: translateY(35px);
|
||||
-webkit-transform: translateY(35px);
|
||||
display: block;
|
||||
color: $white;
|
||||
margin-bottom: ($baseline/4);
|
||||
line-height: 1.3em;
|
||||
}
|
||||
|
||||
.upload-button-input {
|
||||
width: $profile-image-dimension;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@include left(0);
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.u-field-remove-button {
|
||||
width: $profile-image-dimension;
|
||||
height: $baseline;
|
||||
opacity: 0;
|
||||
position: relative;
|
||||
margin-top: 2px;
|
||||
text-align: center;
|
||||
|
||||
&:focus, &:active {
|
||||
box-shadow: none;
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.u-field-upload-button, .u-field-remove-button {
|
||||
opacity: 1;
|
||||
background-color: $shadow-d2;
|
||||
border-radius: ($baseline/4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-profile {
|
||||
min-height: 200px;
|
||||
|
||||
.ui-loading-indicator {
|
||||
margin-top: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-self {
|
||||
.wrapper-profile-field-account-privacy {
|
||||
@include clearfix();
|
||||
@include box-sizing(border-box);
|
||||
margin: 0 auto 0;
|
||||
padding: ($baseline*0.75) 0;
|
||||
width: 100%;
|
||||
background-color: $gray-l3;
|
||||
|
||||
.u-field-account_privacy {
|
||||
@extend .container;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 0 ($baseline*1.5);
|
||||
}
|
||||
|
||||
.u-field-title {
|
||||
width: auto;
|
||||
color: $base-font-color;
|
||||
font-weight: $font-bold;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.u-field-value {
|
||||
width: auto;
|
||||
@include margin-left($baseline/2);
|
||||
}
|
||||
|
||||
.u-field-message {
|
||||
@include float(left);
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
color: $base-font-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-profile-sections {
|
||||
@extend .container;
|
||||
padding: 0 ($baseline*1.5);
|
||||
}
|
||||
|
||||
.wrapper-profile-section-one {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
margin-top: ($baseline*1.5);
|
||||
}
|
||||
|
||||
.profile-section-one-fields {
|
||||
@include float(left);
|
||||
width: flex-grid(4, 12);
|
||||
@include margin-left($baseline*1.5);
|
||||
|
||||
.u-field {
|
||||
margin-bottom: ($baseline/4);
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
@include padding-left(3px);
|
||||
}
|
||||
|
||||
.u-field-username {
|
||||
margin-bottom: ($baseline/2);
|
||||
|
||||
input[type="text"] {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.u-field-value {
|
||||
width: 350px;
|
||||
@extend %t-title4;
|
||||
}
|
||||
}
|
||||
|
||||
.u-field-title {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.u-field-value {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
.u-field-message {
|
||||
@include float(right);
|
||||
width: 20px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-profile-section-two {
|
||||
width: flex-grid(8, 12);
|
||||
margin-top: ($baseline*1.5);
|
||||
}
|
||||
|
||||
.profile-section-two-fields {
|
||||
|
||||
.u-field-textarea {
|
||||
margin-bottom: ($baseline/2);
|
||||
padding: ($baseline/4) ($baseline/2) ($baseline/2);
|
||||
}
|
||||
|
||||
.u-field-title {
|
||||
font-size: 1.1em;
|
||||
@extend %t-weight4;
|
||||
margin-bottom: ($baseline/4);
|
||||
}
|
||||
|
||||
.u-field-value {
|
||||
width: 100%;
|
||||
white-space: pre-line;
|
||||
line-height: 1.5em;
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.u-field-message {
|
||||
@include float(right);
|
||||
width: auto;
|
||||
padding-top: ($baseline/4);
|
||||
}
|
||||
|
||||
.u-field.mode-placeholder {
|
||||
padding: $baseline;
|
||||
border: 2px dashed $gray-l3;
|
||||
i {
|
||||
font-size: 12px;
|
||||
@include padding-right(5px);
|
||||
vertical-align: middle;
|
||||
color: $gray;
|
||||
}
|
||||
.u-field-title {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.u-field-value {
|
||||
text-align: center;
|
||||
line-height: 1.5em;
|
||||
@extend %t-copy-sub1;
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
|
||||
.u-field.mode-placeholder:hover {
|
||||
border: 2px dashed $link-color;
|
||||
.u-field-title,
|
||||
i {
|
||||
color: $link-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,10 +36,7 @@
|
||||
edx.dashboard.legacy.init({
|
||||
dashboard: "${reverse('dashboard')}",
|
||||
signInUser: "${reverse('signin_user')}",
|
||||
passwordReset: "${reverse('password_reset')}",
|
||||
changeEmail: "${reverse('change_email')}",
|
||||
changeEmailSettings: "${reverse('change_email_settings')}",
|
||||
changeName: "${reverse('change_name')}",
|
||||
verifyToggleBannerFailedOff: "${reverse('verify_student_toggle_failed_banner_off')}",
|
||||
});
|
||||
});
|
||||
@@ -60,12 +57,6 @@
|
||||
</section>
|
||||
%endif
|
||||
|
||||
% if duplicate_provider:
|
||||
<section class="dashboard-banner third-party-auth">
|
||||
<%include file='dashboard/_dashboard_third_party_error.html' />
|
||||
</section>
|
||||
% endif
|
||||
|
||||
%if enrollment_message:
|
||||
<section class="dashboard-banner">
|
||||
${enrollment_message}
|
||||
@@ -78,7 +69,8 @@
|
||||
<header class="wrapper-header-courses">
|
||||
<h2 class="header-courses">${_("Current Courses")}</h2>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
% if len(course_enrollment_pairs) > 0:
|
||||
<ul class="listing-courses">
|
||||
<% share_settings = settings.FEATURES.get('DASHBOARD_SHARE_SETTINGS', {}) %>
|
||||
@@ -133,75 +125,12 @@
|
||||
% endif
|
||||
</section>
|
||||
<section class="profile-sidebar" role="region" aria-label="User info">
|
||||
<header class="profile">
|
||||
<h2 class="username-header"><span class="sr">${_("Username")}: </span><span class="username-label">${ user.username }</span></h2>
|
||||
</header>
|
||||
<section class="user-info">
|
||||
<ul>
|
||||
<li class="info--username">
|
||||
<span class="title">${_("Full Name")} (<a href="#apply_name_change" rel="leanModal" class="edit-name">${_("edit")}</a>)</span> <span class="data">${ user.profile.name | h }</span>
|
||||
<li class="heads-up">
|
||||
<span class="title">${_("Want to change your account settings?")}</span>
|
||||
<span class="copy">${_("Click the arrow next to your username above.")}</span>
|
||||
</li>
|
||||
<li class="info--email">
|
||||
<span class="title">${_("Email")}
|
||||
% if external_auth_map is None or 'shib' not in external_auth_map.external_domain:
|
||||
(<a href="#change_email" rel="leanModal" class="edit-email">${_("edit")}</a>)
|
||||
% endif
|
||||
</span> <span class="data">${ user.email | h }</span>
|
||||
</li>
|
||||
|
||||
%if len(language_options) > 1:
|
||||
<%include file='dashboard/_dashboard_info_language.html' />
|
||||
%endif
|
||||
|
||||
% if third_party_auth.is_enabled():
|
||||
<li class="controls--account">
|
||||
<span class="title">
|
||||
## Translators: this section lists all the third-party authentication providers (for example, Google and LinkedIn) the user can link with or unlink from their edX account.
|
||||
${_("Connected Accounts")}
|
||||
</span>
|
||||
|
||||
<span class="data">
|
||||
<span class="third-party-auth">
|
||||
|
||||
% for state in provider_user_states:
|
||||
<div class="auth-provider">
|
||||
|
||||
<div class="status">
|
||||
% if state.has_account:
|
||||
<i class="icon fa fa-link"></i> <span class="copy">${_("Linked")}</span>
|
||||
% else:
|
||||
<i class="icon fa fa-unlink"></i><span class="copy">${_("Not Linked")}</span>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
<span class="provider">${state.provider.NAME}</span>
|
||||
<span class="control">
|
||||
% if state.has_account:
|
||||
<form
|
||||
action="${pipeline.get_disconnect_url(state.provider.NAME)}"
|
||||
method="post"
|
||||
name="${state.get_unlink_form_name()}">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}">
|
||||
|
||||
<a href="#" onclick="document.${state.get_unlink_form_name()}.submit()">
|
||||
## Translators: clicking on this removes the link between a user's edX account and their account with an external authentication provider (like Google or LinkedIn).
|
||||
${_("Unlink")}
|
||||
</a>
|
||||
</form>
|
||||
% else:
|
||||
<a href="${pipeline.get_login_url(state.provider.NAME, pipeline.AUTH_ENTRY_DASHBOARD, redirect_url='/')}">
|
||||
## Translators: clicking on this creates a link between a user's edX account and their account with an external authentication provider (like Google or LinkedIn).
|
||||
${_("Link")}
|
||||
</a>
|
||||
% endif
|
||||
</span>
|
||||
</div>
|
||||
% endfor
|
||||
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
% endif
|
||||
|
||||
% if len(order_history_list):
|
||||
<li class="order-history">
|
||||
@@ -212,16 +141,6 @@
|
||||
</li>
|
||||
% endif
|
||||
|
||||
% if external_auth_map is None or 'shib' not in external_auth_map.external_domain:
|
||||
<li class="controls--account">
|
||||
<span class="title"><a href="#password_reset_complete" rel="leanModal" id="pwd_reset_button">${_("Reset Password")}</a></span>
|
||||
<form id="password_reset_form" method="post" data-remote="true" action="${reverse('password_reset')}">
|
||||
<input id="id_email" type="hidden" name="email" maxlength="75" value="${user.email}" />
|
||||
<!-- <input type="submit" id="pwd_reset_button" value="${_("Reset Password")}" /> -->
|
||||
</form>
|
||||
</li>
|
||||
% endif
|
||||
|
||||
<%include file='dashboard/_dashboard_status_verification.html' />
|
||||
|
||||
<%include file='dashboard/_dashboard_reverification_sidebar.html' />
|
||||
@@ -262,123 +181,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="password_reset_complete" class="modal" aria-hidden="true">
|
||||
<div class="inner-wrapper" role="dialog" aria-labelledby="password-reset-email">
|
||||
<button class="close-modal">
|
||||
<i class="icon fa fa-remove"></i>
|
||||
<span class="sr">
|
||||
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
|
||||
${_("Close")}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<header>
|
||||
<h2 id="password-reset-email">
|
||||
${_("Password Reset Email Sent")}
|
||||
<span class="sr">,
|
||||
## Translators: this text gives status on if the modal interface (a menu or piece of UI that takes the full focus of the screen) is open or not
|
||||
${_("window open")}
|
||||
</span>
|
||||
</h2>
|
||||
<hr/>
|
||||
</header>
|
||||
<div>
|
||||
<form> <!-- Here for styling reasons -->
|
||||
<section>
|
||||
<p>${_("An email has been sent to {email}. Follow the link in the email to change your password.").format(email=user.email)}</p>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="change_email" class="modal" aria-hidden="true">
|
||||
<div class="inner-wrapper" role="dialog" aria-labelledby="change_email_title">
|
||||
<button class="close-modal">
|
||||
<i class="icon fa fa-remove"></i>
|
||||
<span class="sr">
|
||||
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
|
||||
${_("Close")}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<header>
|
||||
<h2>
|
||||
<span id="change_email_title">${_("Change Email")}</span>
|
||||
<span class="sr">,
|
||||
## Translators: this text gives status on if the modal interface (a menu or piece of UI that takes the full focus of the screen) is open or not
|
||||
${_("window open")}
|
||||
</span>
|
||||
</h2>
|
||||
<hr/>
|
||||
</header>
|
||||
<div id="change_email_body">
|
||||
<form id="change_email_form">
|
||||
<div id="change_email_error" class="modal-form-error"> </div>
|
||||
<div class="input-group">
|
||||
<label>${_("Please enter your new email address:")}
|
||||
<input id="new_email_field" type="email" value="" />
|
||||
</label>
|
||||
<label>${_("Please confirm your password:")}
|
||||
<input id="new_email_password" value="" type="password" />
|
||||
</label>
|
||||
</div>
|
||||
<section>
|
||||
<p>${_("We will send a confirmation to both {email} and your new email address as part of the process.").format(email=user.email)}</p>
|
||||
</section>
|
||||
<div class="submit">
|
||||
<input type="submit" id="submit_email_change" value="${_("Change Email")}"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%include file='modal/_modal-settings-language.html' />
|
||||
|
||||
<section id="apply_name_change" class="modal" aria-hidden="true">
|
||||
<div class="inner-wrapper" role="dialog" aria-labelledby="change-name-title">
|
||||
<button class="close-modal">
|
||||
<i class="icon fa fa-remove"></i>
|
||||
<span class="sr">
|
||||
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
|
||||
${_("Close")}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<header>
|
||||
<h2 id="change-name-title">
|
||||
${_("Change your name")}
|
||||
<span class="sr">,
|
||||
## Translators: this text gives status on if the modal interface (a menu or piece of UI that takes the full focus of the screen) is open or not
|
||||
${_("window open")}
|
||||
</span>
|
||||
</h2>
|
||||
<hr/>
|
||||
</header>
|
||||
<div id="change_name_body">
|
||||
<form id="change_name_form">
|
||||
<div id="change_name_error" class="modal-form-error"> </div>
|
||||
## Translators: note that {platform} {cert_name_short} will look something like: "edX certificate". Please do not change the order of these placeholders.
|
||||
<p>${_("To uphold the credibility of your {platform} {cert_name_short}, all name changes will be recorded.").format(platform=settings.PLATFORM_NAME, cert_name_short=cert_name_short)}</p>
|
||||
<br/>
|
||||
<div class="input-group">
|
||||
## Translators: note that {platform} {cert_name_short} will look something like: "edX certificate". Please do not change the order of these placeholders.
|
||||
<label>${_("Enter your desired full name, as it will appear on your {platform} {cert_name_short}:").format(platform=settings.PLATFORM_NAME, cert_name_short=cert_name_short)}
|
||||
<input id="new_name_field" value="" type="text" />
|
||||
</label>
|
||||
<label>${_("Reason for name change:")}
|
||||
<textarea id="name_rationale_field" value=""></textarea>
|
||||
</label>
|
||||
</div>
|
||||
<div class="submit">
|
||||
<input type="submit" id="submit" value="${_("Change My Name")}">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="unenroll-modal" class="modal unenroll-modal" aria-hidden="true">
|
||||
<div class="inner-wrapper" role="dialog" aria-labelledby="unenrollment-modal-title">
|
||||
<button class="close-modal">
|
||||
|
||||