Commit f03841a6 by Andy Armstrong

Add client-side events for viewing pages

TNL-1504
parent c3a39934
......@@ -277,8 +277,7 @@ 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):
"""
......@@ -298,6 +297,43 @@ class EventsTestMixin(object):
).count(), num_times_emitted
)
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, event_type):
"""
Returns a cursor for the matching browser events.
"""
return self.event_collection.find({
"event_type": event_type,
"time": {"$gt": self.start_time},
})
def verify_events_of_type(self, event_type, expected_events):
"""Verify that the expected events of a given type were logged.
Args:
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.
"""
EmptyPromise(
lambda: self.get_matching_events(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(event_type)
actual_events = []
for i in range(0, cursor.count()):
raw_event = cursor.next()
actual_events.append(json.loads(raw_event["event"]))
self.assertEqual(expected_events, actual_events)
class UniqueCourseTest(WebAppTest):
"""
......
......@@ -10,13 +10,14 @@ 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 AccountSettingsPageTest(WebAppTest):
class AccountSettingsTestMixin(EventsTestMixin, WebAppTest):
"""
Tests that verify behaviour of the Account Settings page.
Mixin with helper methods to test the account settings page.
"""
SUCCESS_MESSAGE = 'Your changes have been saved.'
USERNAME = "test"
PASSWORD = "testpass"
EMAIL = "test@example.com"
......@@ -25,17 +26,26 @@ class AccountSettingsPageTest(WebAppTest):
"""
Initialize account and pages.
"""
super(AccountSettingsPageTest, self).setUp()
super(AccountSettingsTestMixin, self).setUp()
AutoAuthPage(self.browser, username=self.USERNAME, password=self.PASSWORD, email=self.EMAIL).visit()
self.user_id = AutoAuthPage(
self.browser, username=self.USERNAME, password=self.PASSWORD, email=self.EMAIL
).visit().get_user_id()
self.account_settings_page = AccountSettingsPage(self.browser)
self.account_settings_page.visit()
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 account settings link is present in dashboard
page and we can use it to navigate to the page.
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
"""
dashboard_page = DashboardPage(self.browser)
dashboard_page.visit()
......@@ -43,6 +53,41 @@ class AccountSettingsPageTest(WebAppTest):
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()
# Visit the account settings page for the current user.
self.account_settings_page = AccountSettingsPage(self.browser)
self.account_settings_page.visit()
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(
u"edx.user.settings.viewed",
[{
u"user_id": int(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.
......@@ -245,7 +290,7 @@ class AccountSettingsPageTest(WebAppTest):
[u'Pakistan', u''],
)
def test_prefered_language_field(self):
def test_preferred_language_field(self):
"""
Test behaviour of "Preferred Language" field.
"""
......
......@@ -67,3 +67,11 @@ class LearnerProfileViewTest(UrlResetMixin, TestCase):
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)
""" 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
@login_required
@require_http_methods(['GET'])
def learner_profile(request, username):
"""
Render the students profile page.
"""Render the profile page for the specified username.
Args:
request (HttpRequest)
......@@ -23,20 +25,23 @@ def learner_profile(request, username):
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
"""
return render_to_response(
'student_profile/learner_profile.html',
learner_profile_context(request.user.username, username, request.user.is_staff)
)
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.
"""Context for the learner profile page.
Args:
logged_in_username (str): Username of user logged In user.
......@@ -45,7 +50,11 @@ def learner_profile_context(logged_in_username, profile_username, user_is_staff)
Returns:
dict
Raises:
ObjectDoesNotExist: the specified profile_username does not exist.
"""
profile_user = User.objects.get(username=profile_username)
language_options = [language for language in settings.ALL_LANGUAGES]
country_options = [
......@@ -57,6 +66,7 @@ def learner_profile_context(logged_in_username, profile_username, user_is_staff)
context = {
'data': {
'profile_user_id': profile_user.id,
'default_public_account_fields': settings.ACCOUNT_VISIBILITY_CONFIGURATION['public_fields'],
'accounts_api_url': reverse("accounts_api", kwargs={'username': profile_username}),
'preferences_api_url': reverse('preferences_api', kwargs={'username': profile_username}),
......
......@@ -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",
......
......@@ -93,8 +93,8 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
return new LearnerProfileView(
{
el: $('.wrapper-profile'),
own_profile: ownProfile,
has_preferences_access: true,
ownProfile: ownProfile,
hasPreferencesAccess: true,
accountSettingsModel: accountSettingsModel,
preferencesModel: accountPreferencesModel,
accountPrivacyFieldView: accountPrivacyFieldView,
......
;(function (define, undefined) {
'use strict';
define([
'gettext', 'jquery', 'underscore', 'backbone',
'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, FieldViews, UserAccountModel, UserPreferencesModel,
], function (gettext, $, _, Backbone, Logger, FieldViews, UserAccountModel, UserPreferencesModel,
AccountSettingsFieldViews, AccountSettingsView) {
return function (fieldsData, authData, userAccountsApiUrl, userPreferencesApiUrl) {
return function (fieldsData, authData, userAccountsApiUrl, userPreferencesApiUrl, accountUserId) {
var accountSettingsElement = $('.wrapper-account-settings');
......@@ -29,7 +29,7 @@
model: userAccountModel,
title: gettext('Username'),
valueAttribute: 'username',
helpMessage: 'The name that identifies you on the edX site. You cannot change your username.'
helpMessage: gettext('The name that identifies you on the edX site. You cannot change your username.')
})
},
{
......@@ -55,7 +55,7 @@
valueAttribute: 'password',
emailAttribute: 'email',
linkTitle: gettext('Reset Password'),
linkHref: fieldsData['password']['url'],
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.')
})
},
......@@ -67,7 +67,7 @@
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']
options: fieldsData.language.options
})
}
]
......@@ -80,7 +80,7 @@
model: userAccountModel,
title: gettext('Education Completed'),
valueAttribute: 'level_of_education',
options: fieldsData['level_of_education']['options']
options: fieldsData.level_of_education.options
})
},
{
......@@ -88,7 +88,7 @@
model: userAccountModel,
title: gettext('Gender'),
valueAttribute: 'gender',
options: fieldsData['gender']['options']
options: fieldsData.gender.options
})
},
{
......@@ -96,7 +96,7 @@
model: userAccountModel,
title: gettext('Year of Birth'),
valueAttribute: 'year_of_birth',
options: fieldsData['year_of_birth']['options']
options: fieldsData.year_of_birth.options
})
},
{
......@@ -104,7 +104,7 @@
model: userAccountModel,
title: gettext('Country or Region'),
valueAttribute: 'country',
options: fieldsData['country']['options']
options: fieldsData.country.options
})
},
{
......@@ -112,7 +112,7 @@
model: userAccountModel,
title: gettext('Preferred Language'),
valueAttribute: 'language_proficiencies',
options: fieldsData['preferred_language']['options']
options: fieldsData.preferred_language.options
})
}
]
......@@ -125,20 +125,22 @@
fields: _.map(authData.providers, function(provider) {
return {
'view': new AccountSettingsFieldViews.AuthFieldView({
title: provider['name'],
valueAttribute: 'auth-' + provider['name'].toLowerCase(),
title: provider.name,
valueAttribute: 'auth-' + provider.name.toLowerCase(),
helpMessage: '',
connected: provider['connected'],
connectUrl: provider['connect_url'],
disconnectUrl: provider['disconnect_url']
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
});
......@@ -149,14 +151,25 @@
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: function () {
accountSettingsView.renderFields();
},
success: showAccountFields,
error: showLoadingError
})
});
},
error: showLoadingError
});
......@@ -167,5 +180,5 @@
accountSettingsView: accountSettingsView
};
};
})
});
}).call(this, define || RequireJS.define);
......@@ -23,7 +23,7 @@
var view = this;
_.each(this.$('.account-settings-section-body'), function (sectionEl, index) {
_.each(view.options.sectionsData[index].fields, function (field, index) {
_.each(view.options.sectionsData[index].fields, function (field) {
$(sectionEl).append(field.view.render().el);
});
});
......@@ -37,5 +37,5 @@
});
return AccountSettingsView;
})
});
}).call(this, define || RequireJS.define);
;(function (define, undefined) {
'use strict';
define([
'gettext', 'jquery', 'underscore', 'backbone',
'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'
], function (gettext, $, _, Backbone, AccountSettingsModel, AccountPreferencesModel, FieldsView,
], function (gettext, $, _, Backbone, Logger, AccountSettingsModel, AccountPreferencesModel, FieldsView,
LearnerProfileFieldsView, LearnerProfileView, AccountSettingsFieldViews) {
return function (options) {
......@@ -18,14 +16,14 @@
var learnerProfileElement = $('.wrapper-profile');
var accountPreferencesModel = new AccountPreferencesModel();
accountPreferencesModel.url = options['preferences_api_url'];
accountPreferencesModel.url = options.preferences_api_url;
var accountSettingsModel = new AccountSettingsModel({
'default_public_account_fields': options['default_public_account_fields']
'default_public_account_fields': options.default_public_account_fields
});
accountSettingsModel.url = options['accounts_api_url'];
accountSettingsModel.url = options.accounts_api_url;
var editable = options['own_profile'] ? 'toggle' : 'never';
var editable = options.own_profile ? 'toggle' : 'never';
var accountPrivacyFieldView = new LearnerProfileFieldsView.AccountPrivacyFieldView({
model: accountPreferencesModel,
......@@ -39,7 +37,7 @@
['all_users', gettext('Full Profile')]
],
helpMessage: '',
accountSettingsPageUrl: options['account_settings_page_url']
accountSettingsPageUrl: options.account_settings_page_url
});
var usernameFieldView = new FieldsView.ReadonlyFieldView({
......@@ -58,7 +56,7 @@
iconName: 'fa-map-marker',
placeholderValue: gettext('Add country'),
valueAttribute: "country",
options: options['country_options'],
options: options.country_options,
helpMessage: ''
}),
new AccountSettingsFieldViews.LanguageProficienciesFieldView({
......@@ -69,7 +67,7 @@
iconName: 'fa-comment',
placeholderValue: gettext('Add language'),
valueAttribute: "language_proficiencies",
options: options['language_options'],
options: options.language_options,
helpMessage: ''
})
];
......@@ -88,8 +86,8 @@
var learnerProfileView = new LearnerProfileView({
el: learnerProfileElement,
own_profile: options['own_profile'],
has_preferences_access: options['has_preferences_access'],
ownProfile: options.own_profile,
has_preferences_access: options.has_preferences_access,
accountSettingsModel: accountSettingsModel,
preferencesModel: accountPreferencesModel,
accountPrivacyFieldView: accountPrivacyFieldView,
......@@ -102,20 +100,37 @@
learnerProfileView.showLoadingError();
};
var renderLearnerProfileView = function() {
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 () {
if (options['has_preferences_access']) {
// Fetch the preferences model if the user has access
if (options.has_preferences_access) {
accountPreferencesModel.fetch({
success: renderLearnerProfileView,
success: showLearnerProfileView,
error: showLoadingError
});
}
else {
renderLearnerProfileView();
showLearnerProfileView();
}
},
error: showLoadingError
......@@ -127,5 +142,5 @@
learnerProfileView: learnerProfileView
};
};
})
});
}).call(this, define || RequireJS.define);
......@@ -14,7 +14,7 @@
showFullProfile: function () {
var isAboveMinimumAge = this.options.accountSettingsModel.isAboveMinimumAge();
if (this.options.own_profile) {
if (this.options.ownProfile) {
return isAboveMinimumAge && this.options.preferencesModel.get('account_privacy') === 'all_users';
} else {
return this.options.accountSettingsModel.get('profile_is_public');
......@@ -25,7 +25,7 @@
this.$el.html(this.template({
username: this.options.accountSettingsModel.get('username'),
profilePhoto: 'http://www.teachthought.com/wp-content/uploads/2012/07/edX-120x120.jpg',
ownProfile: this.options.own_profile,
ownProfile: this.options.ownProfile,
showFullProfile: this.showFullProfile()
}));
this.renderFields();
......@@ -35,11 +35,12 @@
renderFields: function() {
var view = this;
if (this.options.own_profile) {
var fieldView = this.options.accountPrivacyFieldView;
fieldView.profileIsPrivate = (!this.options.accountSettingsModel.get('year_of_birth'));
fieldView.requiresParentalConsent = (this.options.accountSettingsModel.get('requires_parental_consent'));
fieldView.isAboveMinimumAge = this.options.accountSettingsModel.isAboveMinimumAge();
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();
......@@ -48,13 +49,13 @@
this.$('.profile-section-one-fields').append(this.options.usernameFieldView.render().el);
if (this.showFullProfile()) {
_.each(this.options.sectionOneFieldViews, function (fieldView, index) {
_.each(this.options.sectionOneFieldViews, function (fieldView) {
fieldView.undelegateEvents();
view.$('.profile-section-one-fields').append(fieldView.render().el);
fieldView.delegateEvents();
});
_.each(this.options.sectionTwoFieldViews, function (fieldView, index) {
_.each(this.options.sectionTwoFieldViews, function (fieldView) {
fieldView.undelegateEvents();
view.$('.profile-section-two-fields').append(fieldView.render().el);
fieldView.delegateEvents();
......@@ -69,5 +70,5 @@
});
return LearnerProfileView;
})
});
}).call(this, define || RequireJS.define);
......@@ -41,7 +41,7 @@
var authData = ${ json.dumps(auth) };
setupAccountSettingsSection(
fieldsData, authData, '${user_accounts_api_url}', '${user_preferences_api_url}'
fieldsData, authData, '${user_accounts_api_url}', '${user_preferences_api_url}', ${user.id}
);
});
}).call(this, require || RequireJS.require);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment