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

Add MVP modal for applying inheritable metadata #4664

Merged
Merged
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Update inheritable metadata to handle saving preferences on a folder …
…by folder level.
  • Loading branch information
rtibbles committed Aug 26, 2024

Verified

This commit was signed with the committer’s verified signature.
alexanderbez Aleksandr Bezobchuk
commit 4a8b4927d708644048b5e65dc06812b4b9d92969
Original file line number Diff line number Diff line change
@@ -17,12 +17,16 @@
<KCheckbox
v-for="item, key in inheritableMetadataItems"
:key="key"
:checked="checks[key]"
:label="generateLabel(key)"
@change="checks[key] = !checks[key]"
/>
</div>
<div class="divider"></div>
<KCheckbox
:label="$tr('doNotShowThisAgain')"
:checked="dontShowAgain"
@change="dontShowAgain = !dontShowAgain"
/>
<p>{{ $tr('doNotShowAgainDescription') }}</p>
</div>
@@ -32,9 +36,13 @@

<script>

import { mapActions } from 'vuex';
import isEmpty from 'lodash/isEmpty';
import isUndefined from 'lodash/isUndefined';
import { ContentNode } from 'shared/data/resources';

const inheritableFields = ['categories', 'grade_levels', 'language', 'learner_needs'];

export default {
name: 'InheritAncestorMetadataModal',
props: {
@@ -44,22 +52,39 @@
},
},
data() {
const checks = {};
for (const field of inheritableFields) {
checks[field] = true;
}
return {
categories: {},
grade_levels: {},
language: null,
learner_needs: {},
checks,
parent: null,
dontShowAgain: false,
closed: true,
};
},
computed: {
allFieldsDesignatedByParent() {
// Check if all fields that could be inherited from the parent have already been selected
// as to be inherited or not by a previous interaction with the modal.
return Boolean(
this.parent &&
this.parent?.extra_fields?.inherit_metadata &&
Object.keys(this.inheritableMetadataItems).every(
field => !isUndefined(this.parent.extra_fields.inherit_metadata[field])
)
);
},
active() {
return (
this.contentNode !== null &&
this.contentNode.parent &&
(!isEmpty(this.categories) ||
!isEmpty(this.grade_levels) ||
this.language ||
!isEmpty(this.learner_needs))
!this.allFieldsDesignatedByParent &&
!this.closed
);
},
inheritableMetadataItems() {
@@ -106,10 +131,22 @@

return returnValue;
},
fieldsToInherit() {
return Object.keys(this.inheritableMetadataItems).filter(field => this.checks[field]);
},
},
created() {
if (this.contentNode && this.contentNode.parent) {
ContentNode.getAncestors(this.contentNode.parent).then(ancestors => {
this.parent = ancestors[ancestors.length - 1];
for (const field of inheritableFields) {
if (
this.parent.extra_fields.inherit_metadata &&
this.parent.extra_fields.inherit_metadata[field]
) {
this.checks[field] = this.parent.extra_fields.inherit_metadata[field];
}
}
this.categories = ancestors.reduce((acc, ancestor) => {
const returnValue = {
...acc,
@@ -138,13 +175,61 @@
...ancestor.learner_needs,
};
}, {});
this.$nextTick(() => {
if (this.allFieldsDesignatedByParent) {
// If all fields have been designated by the parent, automatically continue
this.handleContinue();
} else {
// Wait for the data to be updated before showing the dialog
this.closed = false;
}
});
});
}
},
methods: {
...mapActions('contentNode', ['updateContentNode']),
storePreferences() {
// When the user asks to not show this dialog again, store the preferences
// so we can use this information in the future to apply metadata automatically
if (!this.parent) {
// Shouldn't get to this point if there is no parent
// but just in case, return
return;
}
const inherit_metadata = {
...(this.parent?.extra_fields.inherit_metadata || {}),
};
for (const field of inheritableFields) {
if (this.inheritableMetadataItems[field]) {
// Only store preferences for fields that have been shown to the user as inheritable
inherit_metadata[field] = this.checks[field];
}
}
this.updateContentNode({
id: this.parent.id,
extra_fields: {
inherit_metadata,
},
});
},
handleContinue() {
// TO DO apply metadata to the selected resources, or alternatively, just emit with event
this.$emit('handleContinue');
const payload = {};
for (const field of this.fieldsToInherit) {
if (this.inheritableMetadataItems[field] instanceof Object) {
payload[field] = {
...this.contentNode[field],
...this.inheritableMetadataItems[field],
};
} else {
payload[field] = this.inheritableMetadataItems[field];
}
}
this.$emit('inherit', payload);
if (this.dontShowAgain) {
this.storePreferences();
}
this.closed = true;
},
generateLabel(item) {
// TO DO generate label with all of the metadata le-consts, etc.
Original file line number Diff line number Diff line change
@@ -322,6 +322,9 @@ function generateContentNodeData({
if (extra_fields.suggested_duration_type) {
contentNodeData.extra_fields.suggested_duration_type = extra_fields.suggested_duration_type;
}
if (extra_fields.inherit_metadata) {
contentNodeData.extra_fields.inherit_metadata = extra_fields.inherit_metadata;
}
}
if (prerequisite !== NOVALUE) {
contentNodeData.prerequisite = prerequisite;
@@ -356,6 +359,14 @@ export function updateContentNode(context, { id, ...payload } = {}) {
};
}

if (contentNodeData.extra_fields.inherit_metadata) {
// Don't set inherit_metadata on non-topic nodes
// as they cannot have children to bequeath metadata to
if (node.kind !== ContentKindsNames.TOPIC) {
delete contentNodeData.extra_fields.inherit_metadata;
}
}

contentNodeData = {
...contentNodeData,
extra_fields: {
21 changes: 16 additions & 5 deletions contentcuration/contentcuration/tests/viewsets/test_contentnode.py
Original file line number Diff line number Diff line change
@@ -28,8 +28,8 @@
from contentcuration.tests.viewsets.base import generate_create_event
from contentcuration.tests.viewsets.base import generate_delete_event
from contentcuration.tests.viewsets.base import generate_publish_channel_event
from contentcuration.tests.viewsets.base import generate_update_event
from contentcuration.tests.viewsets.base import generate_update_descendants_event
from contentcuration.tests.viewsets.base import generate_update_event
from contentcuration.tests.viewsets.base import SyncTestMixin
from contentcuration.utils.db_tools import TreeBuilder
from contentcuration.viewsets.channel import _unpublished_changes_query
@@ -584,7 +584,7 @@ def test_update_descendants_contentnode(self):
descendants.exclude(kind_id=content_kinds.TOPIC).update(extra_fields={})

new_language = "es"

response = self.sync_changes(
[generate_update_descendants_event(root_node.id, {"language": new_language}, channel_id=self.channel.id)],
)
@@ -595,12 +595,12 @@ def test_update_descendants_contentnode(self):
language = models.ContentNode.objects.get(id=descendant.id).language
language = str(language)
self.assertEqual(language, new_language)

def test_cannot_update_descendants_when_updating_non_topic_node(self):
root_node = testdata.tree()
video_node = root_node.get_descendants().filter(kind_id=content_kinds.VIDEO).first()
new_language = "pt"

response = self.sync_changes(
[generate_update_descendants_event(video_node.id, {"language": new_language}, channel_id=self.channel.id)],
)
@@ -609,7 +609,7 @@ def test_cannot_update_descendants_when_updating_non_topic_node(self):
self.assertNotEqual(
models.ContentNode.objects.get(id=video_node.id).language, new_language
)

def test_update_contentnode_exercise_mastery_model(self):
metadata = self.contentnode_db_metadata
metadata["kind_id"] = content_kinds.EXERCISE
@@ -1060,6 +1060,17 @@ def test_update_contentnode_suggested_duration(self):
models.ContentNode.objects.get(id=contentnode.id).suggested_duration, new_suggested_duration
)

def test_update_contentnode_extra_fields_inherited_metadata(self):
contentnode = models.ContentNode.objects.create(**self.contentnode_db_metadata)

response = self.sync_changes(
[generate_update_event(contentnode.id, CONTENTNODE, {"extra_fields.inherited_metadata.categories": True}, channel_id=self.channel.id)],
)
self.assertEqual(response.status_code, 200, response.content)
self.assertTrue(
models.ContentNode.objects.get(id=contentnode.id).extra_fields["inherited_metadata"]["categories"]
)

def test_update_contentnode_tags_dont_duplicate(self):
contentnode = models.ContentNode.objects.create(**self.contentnode_db_metadata)
tag = "howzat!"
15 changes: 12 additions & 3 deletions contentcuration/contentcuration/viewsets/contentnode.py
Original file line number Diff line number Diff line change
@@ -57,8 +57,8 @@
from contentcuration.tasks import calculate_resource_size_task
from contentcuration.utils.nodes import calculate_resource_size
from contentcuration.utils.nodes import migrate_extra_fields
from contentcuration.utils.pagination import ValuesViewsetCursorPagination
from contentcuration.utils.nodes import validate_and_conform_to_schema_threshold_none
from contentcuration.utils.pagination import ValuesViewsetCursorPagination
from contentcuration.viewsets.base import BulkListSerializer
from contentcuration.viewsets.base import BulkModelSerializer
from contentcuration.viewsets.base import BulkUpdateMixin
@@ -292,10 +292,18 @@ class ExtraFieldsOptionsSerializer(JSONFieldDictSerializer):
completion_criteria = CompletionCriteriaSerializer(required=False)


class InheritedMetadataSerializer(JSONFieldDictSerializer):
categories = BooleanField(required=False)
language = BooleanField(required=False)
grade_levels = BooleanField(required=False)
learner_needs = BooleanField(required=False)


class ExtraFieldsSerializer(JSONFieldDictSerializer):
randomize = BooleanField()
options = ExtraFieldsOptionsSerializer(required=False)
suggested_duration_type = ChoiceField(choices=[completion_criteria.TIME, completion_criteria.APPROX_TIME], allow_null=True, required=False)
inherited_metadata = InheritedMetadataSerializer(required=False)

def update(self, instance, validated_data):
instance = migrate_extra_fields(instance)
@@ -1073,9 +1081,10 @@ def update_descendants(self, pk, mods):

descendantsIds = root.get_descendants(include_self=True).values_list("id", flat=True)

changes = [{ "key": descendantId, "mods": mods } for descendantId in descendantsIds]
changes = [{"key": descendantId, "mods": mods} for descendantId in descendantsIds]

return self.update_from_changes(changes) # Bulk update
# Bulk update
return self.update_from_changes(changes)

def update_descendants_from_changes(self, changes):
errors = []