Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix bulk descendants boolean maps #4751

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from 'shared/data/constants';
import { ContentNode } from 'shared/data/resources';
import { ContentKindsNames } from 'shared/leUtils/ContentKinds';
import { findLicense } from 'shared/utils/helpers';
import { findLicense, getMergedMapFields } from 'shared/utils/helpers';
import { RolesNames } from 'shared/leUtils/Roles';
import { isNodeComplete } from 'shared/utils/validation';
import * as publicApi from 'shared/data/public';
Expand Down Expand Up @@ -339,16 +339,6 @@ function generateContentNodeData({
return contentNodeData;
}

const mapFields = [
'accessibility_labels',
'grade_levels',
'learner_needs',
'categories',
'learning_activities',
'resource_types',
'tags',
];

export function updateContentNode(context, { id, mergeMapFields, ...payload } = {}) {
if (!id) {
throw ReferenceError('id must be defined to update a contentNode');
Expand Down Expand Up @@ -387,38 +377,10 @@ export function updateContentNode(context, { id, mergeMapFields, ...payload } =
}

if (mergeMapFields) {
for (const mapField of mapFields) {
if (contentNodeData[mapField]) {
if (mapField === 'categories') {
// Reduce categories to the minimal set
const existingCategories = Object.keys(node.categories || {});
const newCategories = Object.keys(contentNodeData.categories);
const newMap = {};
for (const category of existingCategories) {
// If any of the new categories are more specific than the existing category,
// omit this.
if (!newCategories.some(newCategory => newCategory.startsWith(category))) {
newMap[category] = true;
}
}
for (const category of newCategories) {
if (
!existingCategories.some(
existingCategory =>
existingCategory.startsWith(category) && category !== existingCategory
)
) {
newMap[category] = true;
}
}
} else {
contentNodeData[mapField] = {
...node[mapField],
...contentNodeData[mapField],
};
}
}
}
contentNodeData = {
...contentNodeData,
...getMergedMapFields(node, contentNodeData),
};
}

const newNode = {
Expand Down Expand Up @@ -463,11 +425,12 @@ export function updateContentNodeDescendants(context, { id, ...payload } = {}) {
const contentNodeData = generateContentNodeData(payload);

const descendants = context.getters.getContentNodeDescendants(id);
const contentNodeIds = [id, ...descendants.map(node => node.id)];
const contentNodes = [node, ...descendants];

const contentNodesData = contentNodeIds.map(contentNodeId => ({
id: contentNodeId,
const contentNodesData = contentNodes.map(contentNode => ({
id: contentNode.id,
...contentNodeData,
...getMergedMapFields(contentNode, contentNodeData),
}));

context.commit('ADD_CONTENTNODES', contentNodesData);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,12 +228,7 @@ class ReturnedChanges extends ChangeDispatcher {
}

return transaction(change, TABLE_NAMES.CONTENTNODE, async () => {
const ids = await resource.getLoadedDescendantsIds(change.key);
return db
.table(TABLE_NAMES.CONTENTNODE)
.where(':id')
.anyOf(ids)
.modify(obj => applyMods(obj, change.mods));
return resource.applyChangesToLoadedDescendants(change.key, change.mods);
});
}
}
Expand Down
31 changes: 0 additions & 31 deletions contentcuration/contentcuration/frontend/shared/data/changes.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,6 @@ import {
COPYING_STATUS,
TASK_ID,
} from 'shared/data/constants';
import {
Categories,
ContentLevels,
ResourcesNeededTypes,
ResourcesNeededOptions,
} from 'shared/constants';
import { INDEXEDDB_RESOURCES } from 'shared/data/registry';

/**
Expand Down Expand Up @@ -489,38 +483,13 @@ export class UpdatedDescendantsChange extends Change {
this.validateObj(changes, 'changes');
changes = omitIgnoredSubFields(changes);
this.mods = changes;
this.setModsDeletedProperties();
this.setChannelAndUserId(oldObj);
}

get changed() {
return !isEmpty(this.mods);
}

/**
* To ensure that the mods properties that are multi valued have set
* to true just the options that are present in the mods object. All
* other options are set to null.
*/
setModsDeletedProperties() {
if (!this.mods) return;

const multiValueProperties = {
categories: Object.values(Categories),
learner_needs: ResourcesNeededOptions.map(option => ResourcesNeededTypes[option]),
grade_levels: Object.values(ContentLevels),
};
Object.entries(multiValueProperties).forEach(([key, values]) => {
if (this.mods[key]) {
values.forEach(value => {
if (!this.mods[key][value]) {
this.mods[key][value] = null;
}
});
}
});
}

saveChange() {
if (!this.changed) {
return Promise.resolve(null);
Expand Down
53 changes: 39 additions & 14 deletions contentcuration/contentcuration/frontend/shared/data/resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { currentLanguage } from 'shared/i18n';
import client, { paramsSerializer } from 'shared/client';
import { DELAYED_VALIDATION, fileErrors, NEW_OBJECT } from 'shared/constants';
import { ContentKindsNames } from 'shared/leUtils/ContentKinds';
import { getMergedMapFields } from 'shared/utils/helpers';

// Number of seconds after which data is considered stale.
const REFRESH_INTERVAL = 5;
Expand Down Expand Up @@ -257,6 +258,10 @@ class IndexedDBResource {
inheritedChanges.push(
...parentChanges.map(change => ({
...change,
mods: {
...change.mods,
...getMergedMapFields(item, change.mods),
},
key: item.id,
type: CHANGE_TYPES.UPDATED,
}))
Expand All @@ -267,14 +272,24 @@ class IndexedDBResource {
return inheritedChanges;
}

mergeDescendantsChanges(changes, inheritedChanges) {
mergeDescendantsChanges(changes, inheritedChanges, itemData) {
if (inheritedChanges.length) {
changes.push(...inheritedChanges);
changes = sortBy(changes, 'rev');
}
changes
.filter(change => change.type === CHANGE_TYPES.UPDATED_DESCENDANTS)
.forEach(change => (change.type = CHANGE_TYPES.UPDATED));
.forEach(change => {
change.type = CHANGE_TYPES.UPDATED;
const item = itemData.find(i => i.id === change.key);
if (!item) {
return;
}
change.mods = {
...change.mods,
...getMergedMapFields(item, change.mods),
};
});

return changes;
}
Expand All @@ -296,7 +311,7 @@ class IndexedDBResource {

return Promise.all([changesPromise, inheritedChangesPromise, currentPromise]).then(
([changes, inheritedChanges, currents]) => {
changes = this.mergeDescendantsChanges(changes, inheritedChanges);
changes = this.mergeDescendantsChanges(changes, inheritedChanges, itemData);
changes = mergeAllChanges(changes, true);
const collectedChanges = collectChanges(changes)[this.tableName] || {};
for (const changeType of Object.keys(collectedChanges)) {
Expand Down Expand Up @@ -1882,20 +1897,35 @@ export const ContentNode = new TreeResource({
* @returns {Promise<string[]>}
*
*/
async getLoadedDescendantsIds(id) {
async getLoadedDescendants(id) {
const [node] = await this.table.where({ id }).toArray();
if (!node) {
return [];
}
const children = await this.table.where({ parent: id }).toArray();
if (!children.length) {
return [id];
return [node];
}
const descendants = await Promise.all(
children.map(child => {
if (child.kind === ContentKindsNames.TOPIC) {
return this.getLoadedDescendantsIds(child.id);
return this.getLoadedDescendants(child.id);
}
return child.id;
return child;
})
);
return [node].concat(flatMap(descendants, d => d));
},
async applyChangesToLoadedDescendants(id, changes) {
const descendants = await this.getLoadedDescendants(id);
return Promise.all(
descendants.map(descendant => {
return this.table.update(descendant.id, {
...changes,
...getMergedMapFields(descendant, changes),
});
})
);
return [id].concat(flatMap(descendants, d => d));
},
/**
* Update a node and all its descendants that are already loaded in IndexedDB
Expand All @@ -1907,12 +1937,7 @@ export const ContentNode = new TreeResource({
return this.transaction({ mode: 'rw' }, CHANGES_TABLE, async () => {
changes = this._cleanNew(changes);

// Update node descendants that are already loaded
const ids = await this.getLoadedDescendantsIds(id);
await this.table
.where('id')
.anyOf(...ids)
.modify(changes);
await this.applyChangesToLoadedDescendants(id, changes);

return this._updateDescendantsChange(id, changes);
});
Expand Down
48 changes: 48 additions & 0 deletions contentcuration/contentcuration/frontend/shared/utils/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -575,3 +575,51 @@ export function hasMultipleFieldValues(array, field) {
}
return false;
}

export const mapFields = [
'accessibility_labels',
'grade_levels',
'learner_needs',
'categories',
'learning_activities',
'resource_types',
'tags',
];

export function getMergedMapFields(node, contentNodeData) {
const mergedMapFields = {};
for (const mapField of mapFields) {
if (contentNodeData[mapField]) {
if (mapField === 'categories') {
// Reduce categories to the minimal set
const existingCategories = Object.keys(node.categories || {});
const newCategories = Object.keys(contentNodeData.categories);
const newMap = {};
for (const category of existingCategories) {
// If any of the new categories are more specific than the existing category,
// omit this.
if (!newCategories.some(newCategory => newCategory.startsWith(category))) {
newMap[category] = true;
}
}
for (const category of newCategories) {
if (
!existingCategories.some(
existingCategory =>
existingCategory.startsWith(category) && category !== existingCategory
)
) {
newMap[category] = true;
}
}
mergedMapFields[mapField] = newMap;
} else {
mergedMapFields[mapField] = {
...node[mapField],
...contentNodeData[mapField],
};
}
}
}
return mergedMapFields;
}
Original file line number Diff line number Diff line change
Expand Up @@ -580,8 +580,6 @@ def test_update_descendants_contentnode(self):
root_node = testdata.tree(parent=self.channel.main_tree)

descendants = root_node.get_descendants(include_self=True)
# Fix undefined extra_fields
descendants.exclude(kind_id=content_kinds.TOPIC).update(extra_fields={})

new_language = "es"

Expand Down