The VERIFIED_NAME_FLAG was added as part https://github.com/edx/edx-name-affirmation/pull/12, [MST-801](https://openedx.atlassian.net/browse/MST-801) in order to control the release of the Verified Name project. It was used for a phased roll out by percentage of users. The release reached a percentage of 50% before it was observed that, due to the way percentage roll out works in django-waffle, the code to create or update VerifiedName records was not working properly. The code was written such that any change to a SoftwareSecurePhotoVerification model instance sent a signal, which was received and handled by the Name Affirmation application. If the VERIFIED_NAME_FLAG was on for the requesting user, a Celery task was launched from the Name Affirmation application to perform the creation of or update to the appropriate VerifiedName model instances based on the verify_student application signal. However, we observed that when SoftwareSecurePhotoVerification records were moved into the "created" or "ready" status, a Celery task in Name Affirmation was created, but when SoftwareSecurePhotoVerification records were moved into the "submitted" status, the corresponding Celery task in Name Affirmation was not created. This caused VerifiedName records to stay in the "pending" state. The django-waffle waffle flag used by the edx-toggle library implements percentage rollout by setting a cookie in a learner's browser session to assign them to the enabled or disabled group. It turns out that the code that submits a SoftwareSecurePhotoVerification record, which moves it into the "submitted" state, happens as part of a Celery task in the verify_student application in the edx-platform. Therefore, we believe that because there is no request object in a Celery task, the edx-toggle code is defaulting to the case where there is no request object. In this case, the code checks whether the flag is enabled for everyone when determining whether the flag is enabled. Because of the percentage rollout (i.e. waffle flag not enabled for everyone), the Celery task in Name Affirmation is not created. This behavior was confirmed by logging added as part of https://github.com/edx/edx-name-affirmation/pull/62. We have determined that we do not need the waffle flag, as we are comfortable that enabling the waffle flag for everyone will fix the issue and are comfortable releasing the feature to all users. For this reason, we are removing references to the flag. [MST-1130](https://openedx.atlassian.net/browse/MST-1130)
303 lines
9.9 KiB
JavaScript
303 lines
9.9 KiB
JavaScript
import { getConfig } from '@edx/frontend-platform';
|
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
|
import pick from 'lodash.pick';
|
|
import pickBy from 'lodash.pickby';
|
|
import omit from 'lodash.omit';
|
|
import isEmpty from 'lodash.isempty';
|
|
|
|
import { handleRequestError, unpackFieldErrors } from './utils';
|
|
import { getThirdPartyAuthProviders } from '../third-party-auth';
|
|
import { postVerifiedNameConfig } from '../certificate-preference/data/service';
|
|
import { getCoachingPreferences, patchCoachingPreferences } from '../coaching/data/service';
|
|
import { getDemographics, getDemographicsOptions, patchDemographics } from '../demographics/data/service';
|
|
import { DEMOGRAPHICS_FIELDS } from '../demographics/data/utils';
|
|
|
|
const SOCIAL_PLATFORMS = [
|
|
{ id: 'twitter', key: 'social_link_twitter' },
|
|
{ id: 'facebook', key: 'social_link_facebook' },
|
|
{ id: 'linkedin', key: 'social_link_linkedin' },
|
|
];
|
|
|
|
function unpackAccountResponseData(data) {
|
|
const unpackedData = data;
|
|
|
|
// This is handled by preferences
|
|
delete unpackedData.time_zone;
|
|
|
|
SOCIAL_PLATFORMS.forEach(({ id, key }) => {
|
|
const platformData = data.social_links.find(({ platform }) => platform === id);
|
|
unpackedData[key] = typeof platformData === 'object' ? platformData.social_link : '';
|
|
});
|
|
|
|
if (Array.isArray(data.language_proficiencies)) {
|
|
if (data.language_proficiencies.length) {
|
|
unpackedData.language_proficiencies = data.language_proficiencies[0].code;
|
|
} else {
|
|
unpackedData.language_proficiencies = '';
|
|
}
|
|
}
|
|
|
|
return unpackedData;
|
|
}
|
|
|
|
function packAccountCommitData(commitData) {
|
|
const packedData = commitData;
|
|
|
|
SOCIAL_PLATFORMS.forEach(({ id, key }) => {
|
|
// Skip missing values. Empty strings are valid values and should be preserved.
|
|
if (commitData[key] === undefined) {
|
|
return;
|
|
}
|
|
|
|
packedData.social_links = [{ platform: id, social_link: commitData[key] }];
|
|
delete packedData[key];
|
|
});
|
|
|
|
if (commitData.language_proficiencies !== undefined) {
|
|
if (commitData.language_proficiencies) {
|
|
packedData.language_proficiencies = [{ code: commitData.language_proficiencies }];
|
|
} else {
|
|
// An empty string should be sent as an array.
|
|
packedData.language_proficiencies = [];
|
|
}
|
|
}
|
|
|
|
if (commitData.year_of_birth !== undefined) {
|
|
if (commitData.year_of_birth) {
|
|
packedData.year_of_birth = commitData.year_of_birth;
|
|
} else {
|
|
// An empty string should be sent as null.
|
|
packedData.year_of_birth = null;
|
|
}
|
|
}
|
|
return packedData;
|
|
}
|
|
|
|
export async function getAccount(username) {
|
|
const { data } = await getAuthenticatedHttpClient()
|
|
.get(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`);
|
|
return unpackAccountResponseData(data);
|
|
}
|
|
|
|
export async function patchAccount(username, commitValues) {
|
|
const requestConfig = {
|
|
headers: { 'Content-Type': 'application/merge-patch+json' },
|
|
};
|
|
|
|
const { data } = await getAuthenticatedHttpClient()
|
|
.patch(
|
|
`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`,
|
|
packAccountCommitData(commitValues),
|
|
requestConfig,
|
|
)
|
|
.catch((error) => {
|
|
const unpackFunction = (fieldErrors) => {
|
|
const unpackedFieldErrors = fieldErrors;
|
|
if (fieldErrors.social_links) {
|
|
SOCIAL_PLATFORMS.forEach(({ key }) => {
|
|
unpackedFieldErrors[key] = fieldErrors.social_links;
|
|
});
|
|
}
|
|
return unpackFieldErrors(unpackedFieldErrors);
|
|
};
|
|
handleRequestError(error, unpackFunction);
|
|
});
|
|
|
|
return unpackAccountResponseData(data);
|
|
}
|
|
|
|
export async function getPreferences(username) {
|
|
const { data } = await getAuthenticatedHttpClient()
|
|
.get(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`);
|
|
return data;
|
|
}
|
|
|
|
export async function patchPreferences(username, commitValues) {
|
|
const requestConfig = { headers: { 'Content-Type': 'application/merge-patch+json' } };
|
|
const requestUrl = `${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`;
|
|
|
|
// Ignore the success response, the API does not currently return any data.
|
|
await getAuthenticatedHttpClient()
|
|
.patch(requestUrl, commitValues, requestConfig).catch(handleRequestError);
|
|
|
|
return commitValues;
|
|
}
|
|
|
|
export async function getTimeZones(forCountry) {
|
|
const { data } = await getAuthenticatedHttpClient()
|
|
.get(`${getConfig().LMS_BASE_URL}/user_api/v1/preferences/time_zones/`, {
|
|
params: { country_code: forCountry },
|
|
})
|
|
.catch(handleRequestError);
|
|
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Determine if the user's profile data is managed by a third-party identity provider.
|
|
*/
|
|
export async function getProfileDataManager(username, userRoles) {
|
|
const userRoleNames = userRoles.map(role => role.split(':')[0]);
|
|
|
|
if (userRoleNames.includes('enterprise_learner')) {
|
|
const url = `${getConfig().LMS_BASE_URL}/enterprise/api/v1/enterprise-learner/?username=${username}`;
|
|
const { data } = await getAuthenticatedHttpClient().get(url).catch(handleRequestError);
|
|
|
|
if (data.results.length > 0) {
|
|
const enterprise = data.results[0] && data.results[0].enterprise_customer;
|
|
// To ensure that enterprise returned is current enterprise & it manages profile settings
|
|
if (enterprise && enterprise.sync_learner_profile_data) {
|
|
return enterprise.name;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* A function to determine if the Demographics questions should be displayed to the user. For the
|
|
* MVP release of Demographics we are limiting the Demographics question visibility only to
|
|
* MicroBachelors learners.
|
|
*/
|
|
export async function shouldDisplayDemographicsQuestions() {
|
|
const requestUrl = `${getConfig().LMS_BASE_URL}/api/demographics/v1/demographics/status/`;
|
|
let data = {};
|
|
|
|
try {
|
|
({ data } = await getAuthenticatedHttpClient().get(requestUrl));
|
|
if (data.display) {
|
|
return data.display;
|
|
}
|
|
} catch (error) {
|
|
// if there was an error then we just hide the section
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export async function getVerifiedName() {
|
|
let data;
|
|
const client = getAuthenticatedHttpClient();
|
|
try {
|
|
const requestUrl = `${getConfig().LMS_BASE_URL}/api/edx_name_affirmation/v1/verified_name`;
|
|
({ data } = await client.get(requestUrl));
|
|
} catch (error) {
|
|
return {};
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
export async function getVerifiedNameHistory() {
|
|
let data;
|
|
const client = getAuthenticatedHttpClient();
|
|
try {
|
|
const requestUrl = `${getConfig().LMS_BASE_URL}/api/edx_name_affirmation/v1/verified_name/history`;
|
|
({ data } = await client.get(requestUrl));
|
|
} catch (error) {
|
|
return {};
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
export async function postVerifiedName(data) {
|
|
const requestConfig = { headers: { Accept: 'application/json' } };
|
|
const requestUrl = `${getConfig().LMS_BASE_URL}/api/edx_name_affirmation/v1/verified_name`;
|
|
|
|
await getAuthenticatedHttpClient()
|
|
.post(requestUrl, data, requestConfig)
|
|
.catch(error => handleRequestError(error));
|
|
}
|
|
|
|
/**
|
|
* A single function to GET everything considered a setting.
|
|
* Currently encapsulates Account, Preferences, Coaching, ThirdPartyAuth, and Demographics
|
|
*/
|
|
export async function getSettings(username, userRoles, userId) {
|
|
const [
|
|
account,
|
|
preferences,
|
|
thirdPartyAuthProviders,
|
|
profileDataManager,
|
|
timeZones,
|
|
coaching,
|
|
shouldDisplayDemographicsQuestionsResponse,
|
|
demographics,
|
|
demographicsOptions,
|
|
] = await Promise.all([
|
|
getAccount(username),
|
|
getPreferences(username),
|
|
getThirdPartyAuthProviders(),
|
|
getProfileDataManager(username, userRoles),
|
|
getTimeZones(),
|
|
getConfig().COACHING_ENABLED && getCoachingPreferences(userId),
|
|
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && shouldDisplayDemographicsQuestions(),
|
|
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographics(userId),
|
|
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographicsOptions(),
|
|
]);
|
|
|
|
return {
|
|
...account,
|
|
...preferences,
|
|
thirdPartyAuthProviders,
|
|
profileDataManager,
|
|
timeZones,
|
|
coaching,
|
|
shouldDisplayDemographicsSection: shouldDisplayDemographicsQuestionsResponse,
|
|
...demographics,
|
|
demographicsOptions,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A single function to PATCH everything considered a setting.
|
|
* Currently encapsulates Account, Preferences, coaching and ThirdPartyAuth
|
|
*/
|
|
export async function patchSettings(username, commitValues, userId) {
|
|
// Note: time_zone exists in the return value from user/v1/accounts
|
|
// but it is always null and won't update. It also exists in
|
|
// user/v1/preferences where it does update. This is the one we use.
|
|
const preferenceKeys = ['time_zone'];
|
|
const coachingKeys = ['coaching'];
|
|
const demographicsKeys = DEMOGRAPHICS_FIELDS;
|
|
const certificateKeys = ['useVerifiedNameForCerts'];
|
|
const isDemographicsKey = (value, key) => key.includes('demographics');
|
|
const accountCommitValues = omit(
|
|
commitValues,
|
|
preferenceKeys,
|
|
coachingKeys,
|
|
demographicsKeys,
|
|
certificateKeys,
|
|
);
|
|
const preferenceCommitValues = pick(commitValues, preferenceKeys);
|
|
const coachingCommitValues = pick(commitValues, coachingKeys);
|
|
const demographicsCommitValues = pickBy(commitValues, isDemographicsKey);
|
|
const certCommitValues = pick(commitValues, certificateKeys);
|
|
const patchRequests = [];
|
|
|
|
if (!isEmpty(accountCommitValues)) {
|
|
patchRequests.push(patchAccount(username, accountCommitValues));
|
|
}
|
|
if (!isEmpty(preferenceCommitValues)) {
|
|
patchRequests.push(patchPreferences(username, preferenceCommitValues));
|
|
}
|
|
if (!isEmpty(coachingCommitValues)) {
|
|
patchRequests.push(patchCoachingPreferences(userId, coachingCommitValues));
|
|
}
|
|
if (!isEmpty(demographicsCommitValues)) {
|
|
patchRequests.push(patchDemographics(userId, demographicsCommitValues));
|
|
}
|
|
if (!isEmpty(certCommitValues)) {
|
|
patchRequests.push(postVerifiedNameConfig(username, certCommitValues));
|
|
}
|
|
|
|
const results = await Promise.all(patchRequests);
|
|
// Assigns in order of requests. Preference keys
|
|
// will override account keys. Notably time_zone.
|
|
const combinedResults = Object.assign({}, ...results);
|
|
return combinedResults;
|
|
}
|