def test_has_changed_descendant__forced(self): topic_tree_node = tree() query = Metadata(topic_tree_node) self.set_tree_changed(topic_tree_node, False) topic_tree_node.refresh_from_db() results = query.annotate(**{HAS_CHANGED_DESCENDANT: HasChanged()}) serialized = RootNodeSerializer(topic_tree_node).data self.assertEqual( serialized.get('metadata').get('has_changed_descendant'), results.get(serialized.get('id')).get(HAS_CHANGED_DESCENDANT)) self.assertFalse( results.get(serialized.get('id')).get(HAS_CHANGED_DESCENDANT)) video_node = topic_tree_node.get_descendants().filter(kind=content_kinds.TOPIC).first()\ .get_descendants().first() video_node.changed = True video_node.save() topic_tree_node.refresh_from_db() results = query.annotate(**{HAS_CHANGED_DESCENDANT: HasChanged()}) serialized = RootNodeSerializer(topic_tree_node).data self.assertEqual( serialized.get('metadata').get('has_changed_descendant'), results.get(serialized.get('id')).get(HAS_CHANGED_DESCENDANT)) self.assertTrue( results.get(serialized.get('id')).get(HAS_CHANGED_DESCENDANT))
def test_resource_count(self): topic_tree_node = tree() query = Metadata(topic_tree_node) results = query.annotate(**{RESOURCE_COUNT: ResourceCount()}) serialized = ContentNodeSerializer(topic_tree_node).data self.assertEqual( serialized.get('metadata').get('resource_count'), results.get(serialized.get('id')).get(RESOURCE_COUNT)) self.assertEqual(5, results.get(serialized.get('id')).get(RESOURCE_COUNT))
def test_has_changed_descendant(self): topic_tree_node = tree() query = Metadata(topic_tree_node) results = query.annotate(**{HAS_CHANGED_DESCENDANT: HasChanged()}) serialized = RootNodeSerializer(topic_tree_node).data self.assertEqual( serialized.get('metadata').get('has_changed_descendant'), results.get(serialized.get('id')).get(HAS_CHANGED_DESCENDANT)) self.assertTrue( results.get(serialized.get('id')).get(HAS_CHANGED_DESCENDANT))
def test_max_sort_order(self): topic_tree_node = tree() query = Metadata(topic_tree_node) results = query.annotate(**{MAX_SORT_ORDER: SortOrderMax()}) serialized = RootNodeSerializer(topic_tree_node).data self.assertEqual( serialized.get('metadata').get('max_sort_order'), results.get(serialized.get('id')).get(MAX_SORT_ORDER)) self.assertEqual(2.0, results.get(serialized.get('id')).get(MAX_SORT_ORDER))
def test_descendant_count(self): topic_tree_node = tree() query = Metadata(topic_tree_node) results = query.annotate(**{DESCENDANT_COUNT: DescendantCount()}) serialized = ContentNodeSerializer(topic_tree_node).data self.assertEqual( serialized.get('metadata').get('total_count'), results.get(serialized.get('id')).get(DESCENDANT_COUNT)) self.assertEqual( 7, results.get(serialized.get('id')).get(DESCENDANT_COUNT))
def test_max_sort_order__alternate(self): topic_tree_node = tree().get_descendants().filter( kind=content_kinds.TOPIC).first() query = Metadata(topic_tree_node) results = query.annotate(**{MAX_SORT_ORDER: SortOrderMax()}) serialized = RootNodeSerializer(topic_tree_node).data self.assertEqual( serialized.get('metadata').get('max_sort_order'), results.get(serialized.get('id')).get(MAX_SORT_ORDER)) self.assertEqual(4.0, results.get(serialized.get('id')).get(MAX_SORT_ORDER))
def test_resource_size__topic(self): topic_tree_node = tree() nested_topic = topic_tree_node.get_descendants().filter( kind=content_kinds.TOPIC).first() self.create_coach_node(nested_topic) query = Metadata(topic_tree_node) results = query.annotate(**{RESOURCE_SIZE: ResourceSize()}) serialized = ContentNodeSerializer(topic_tree_node).data self.assertEqual( serialized.get('metadata').get('resource_size'), results.get(serialized.get('id')).get(RESOURCE_SIZE)) self.assertEqual(0, results.get(serialized.get('id')).get(RESOURCE_SIZE))
def test_assessment_count(self): tree() node = ContentNode.objects.get( node_id='00000000000000000000000000000005') query = Metadata(node) results = query.annotate(**{ASSESSMENT_COUNT: AssessmentCount()}) serialized = ContentNodeSerializer(node).data self.assertEqual( serialized.get('metadata').get('resource_count'), results.get(serialized.get('id')).get(ASSESSMENT_COUNT)) self.assertEqual( 3, results.get(serialized.get('id')).get(ASSESSMENT_COUNT))
def test_coach_count(self): topic_tree_node = tree() nested_topic = topic_tree_node.get_descendants().filter( kind=content_kinds.TOPIC).first() self.create_coach_node(nested_topic) query = Metadata(topic_tree_node) results = query.annotate(**{COACH_COUNT: CoachCount()}) serialized = ContentNodeSerializer(topic_tree_node).data self.assertEqual( serialized.get('metadata').get('coach_count'), results.get(serialized.get('id')).get(COACH_COUNT)) self.assertEqual(1, results.get(serialized.get('id')).get(COACH_COUNT))
class RootNodeSerializer(SimplifiedContentNodeSerializer, ContentNodeFieldMixin): channel_name = serializers.SerializerMethodField('retrieve_channel_name') metadata_query = Metadata(total_count=DescendantCount(), resource_count=ResourceCount(), max_sort_order=SortOrderMax(), resource_size=Value(0, output_field=IntegerField()), has_changed_descendant=HasChanged()) def retrieve_metadata(self, node): data = self.metadata_query.get(node.pk) data.update(self.get_creators(node.get_descendants())) return data def retrieve_channel_name(self, node): channel = node.get_channel() return channel.name if channel else None class Meta: model = ContentNode fields = ('title', 'id', 'kind', 'children', 'metadata', 'published', 'publishing', 'node_id', 'channel_name', 'prerequisite', 'is_prerequisite_of', 'ancestors', 'tree_id', 'role_visibility')
def to_representation(self, data): if self.child: query = data if not isinstance(data, QuerySet) and isinstance(data, (list, tuple)): query = ContentNode.objects.filter(pk__in=[n.pk for n in data]) # update metadata_query with queryset for all data such that it minimizes queries for attr_query in ('metadata_query', 'ancestor_query'): attr_query_val = getattr(self.child, attr_query, None) if attr_query_val: setattr(self.child, attr_query, Metadata(query.all(), **attr_query_val.annotations)) return super(CustomListSerializer, self).to_representation(data)
class ContentNodeSerializer(SimplifiedContentNodeSerializer, ContentNodeFieldMixin): ancestors = serializers.SerializerMethodField('get_node_ancestors') valid = serializers.SerializerMethodField('check_valid') associated_presets = serializers.SerializerMethodField( 'retrieve_associated_presets') original_channel = serializers.SerializerMethodField( 'retrieve_original_channel') thumbnail_src = serializers.SerializerMethodField('retrieve_thumbail_src') tags = TagSerializer(many=True, read_only=False) metadata_query = Metadata( total_count=DescendantCount(), resource_count=ResourceCount(include_self=True), assessment_count=AssessmentCount(include_self=True), max_sort_order=SortOrderMax(include_self=True), resource_size=ResourceSize(include_self=True), has_changed_descendant=HasChanged(include_self=True), coach_count=CoachCount(include_self=True), ) def retrieve_associated_presets(self, node): return list(node.get_associated_presets()) def check_valid(self, node): isoriginal = node.node_id == node.original_source_node_id if node.kind_id == content_kinds.TOPIC: return True elif isoriginal and not node.license: return False elif isoriginal and node.license.copyright_holder_required and not node.copyright_holder: return False elif isoriginal and node.license.is_custom and not node.license_description: return False elif node.kind_id == content_kinds.EXERCISE: for aitem in node.assessment_items.exclude( type=exercises.PERSEUS_QUESTION): answers = json.loads(aitem.answers) correct_answers = filter(lambda a: a['correct'], answers) if aitem.question == "" or len(answers) == 0 or len(correct_answers) == 0 or \ any(filter(lambda a: a['answer'] == "", answers)) or \ (aitem.type == exercises.SINGLE_SELECTION and len(correct_answers) > 1) or \ any(filter(lambda h: h['hint'] == "", json.loads(aitem.hints))): return False return True else: return node.files.filter(preset__supplementary=False).exists() def retrieve_metadata(self, node): data = self.metadata_query.get(node.pk) if node.kind_id == content_kinds.EXERCISE: data.update(resource_count=data.get('assessment_count')) if node.kind_id != content_kinds.TOPIC: return data if not node.parent_id: # Add extra data to root node data.update( self.get_creators(node.get_descendants(include_self=True))) return data def retrieve_original_channel(self, node): channel_id = node.original_channel_id channel = channel_id and Channel.objects.get(pk=channel_id) return { "id": channel.pk, "name": channel.name, "thumbnail_url": channel.get_thumbnail(), } if (channel and not channel.deleted) else None class Meta: list_serializer_class = CustomListSerializer model = ContentNode fields = ('title', 'changed', 'id', 'description', 'sort_order', 'author', 'copyright_holder', 'license', 'language', 'license_description', 'assessment_items', 'slideshow_slides', 'files', 'ancestors', 'modified', 'original_channel', 'kind', 'parent', 'children', 'published', 'associated_presets', 'valid', 'metadata', 'original_source_node_id', 'tags', 'extra_fields', 'prerequisite', 'is_prerequisite_of', 'node_id', 'tree_id', 'publishing', 'freeze_authoring_data', 'role_visibility', 'provider', 'aggregator', 'thumbnail_src')
class SimplifiedContentNodeSerializer(BulkSerializerMixin, serializers.ModelSerializer): id = serializers.CharField(required=False) children = serializers.PrimaryKeyRelatedField(many=True, read_only=True) prerequisite = serializers.PrimaryKeyRelatedField( many=True, queryset=ContentNode.objects.all()) is_prerequisite_of = serializers.PrimaryKeyRelatedField(many=True, read_only=True) metadata = serializers.SerializerMethodField('retrieve_metadata') ancestors = serializers.SerializerMethodField('get_node_ancestors') metadata_query = Metadata( total_count=DescendantCount(), resource_count=ResourceCount(), coach_count=CoachCount(), ) ancestor_query = Metadata(ancestors=AncestorArrayAgg()) def retrieve_metadata(self, node): return self.metadata_query.get(node.pk) def get_node_ancestors(self, node): return filter(lambda a: a, self.ancestor_query.get(node.pk).get('ancestors', [])) @staticmethod def setup_eager_loading(queryset): """ Perform necessary eager loading of data. """ queryset = queryset.prefetch_related('children').prefetch_related( 'files').prefetch_related('assessment_items') return queryset def to_internal_value(self, data): """ In order to be able to handle passing tag_name in array, we need to overwrite this method to bypass run_validation for tags """ if not isinstance(data, dict): message = self.error_messages['invalid'].format( datatype=type(data).__name__) raise ValidationError( {api_settings.NON_FIELD_ERRORS_KEY: [message]}) ret = OrderedDict() errors = OrderedDict() fields = self._writable_fields for field in fields: validate_method = getattr(self, 'validate_' + field.field_name, None) primitive_value = field.get_value(data) try: if field.field_name != 'tags': validated_value = field.run_validation(primitive_value) else: validated_value = primitive_value if validate_method is not None: validated_value = validate_method(validated_value) except ValidationError as exc: errors[field.field_name] = exc.detail except DjangoValidationError as exc: errors[field.field_name] = list(exc.messages) except SkipField: pass else: set_value(ret, field.source_attrs, validated_value) if errors: raise ValidationError(errors) return ret def create(self, validated_data): ModelClass = self.Meta.model info = model_meta.get_field_info(ModelClass) many_to_many = {} for field_name, relation_info in info.relations.items(): if relation_info.to_many and (field_name in validated_data): many_to_many[field_name] = validated_data.pop(field_name) try: instance = ModelClass.objects.create(**validated_data) except TypeError as exc: msg = ('Got a `TypeError` when calling `%s.objects.create()`. ' 'This may be because you have a writable field on the ' 'serializer class that is not a valid argument to ' '`%s.objects.create()`. You may need to make the field ' 'read-only, or override the %s.create() method to handle ' 'this correctly.\nOriginal exception text was: %s.' % (ModelClass.__name__, ModelClass.__name__, self.__class__.__name__, exc)) raise TypeError(msg) # Save many-to-many relationships after the instance is created. if self.validated_data['tags']: tag_list = [] for tag in self.validated_data['tags']: # tag_list.append(ContentTag.objects.get_or_create(tag_name=tag['tag_name'])[0]) tag_list.append( ContentTag.objects.get_or_create(tag_name=tag)[0]) setattr(instance, 'tags', tag_list) many_to_many.pop('tags') if many_to_many: for field_name, value in many_to_many.items(): setattr(instance, field_name, value) instance.save() return instance def update(self, instance, validated_data): """ Since we are not doing anything crazy about the nested writable field(tags here), so just bypass the raise_errors_on_nested_writes(). This may need to change in the future when we need to do crazy things on nested writable field. """ for attr, value in validated_data.items(): setattr(instance, attr, value) instance.save() return instance class Meta: model = ContentNode fields = ('title', 'id', 'sort_order', 'kind', 'children', 'parent', 'metadata', 'content_id', 'prerequisite', 'is_prerequisite_of', 'ancestors', 'tree_id', 'language', 'role_visibility')
def test_multiple(self): topic_tree_node1 = tree() topic_tree_node2 = tree() topic_tree1_topics = topic_tree_node1.get_descendants().filter( kind=content_kinds.TOPIC) video_node = self.create_coach_node(topic_tree1_topics.first()) exercise_node = ContentNode.objects.get( tree_id=topic_tree_node2.tree_id, node_id='00000000000000000000000000000005') self.set_tree_changed(topic_tree_node1, False) self.set_tree_changed(topic_tree_node2, False) topic_tree1_topics.last().delete() nodes = ContentNode.objects.filter(pk__in=[ topic_tree_node1.pk, topic_tree_node2.pk, video_node.pk, exercise_node.pk, ]) query = Metadata( nodes, **{ DESCENDANT_COUNT: DescendantCount(), RESOURCE_COUNT: ResourceCount(), ASSESSMENT_COUNT: AssessmentCount(), RESOURCE_SIZE: ResourceSize(), COACH_COUNT: CoachCount(), HAS_CHANGED_DESCENDANT: HasChanged(include_self=True), MAX_SORT_ORDER: SortOrderMax(), }) topic_tree1_results = query.get(topic_tree_node1.pk) topic_tree2_results = query.get(topic_tree_node2.pk) video_node_results = query.get(video_node.pk) exercise_node_results = query.get(exercise_node.pk) self.assertIsNotNone(topic_tree1_results) self.assertEqual(6, topic_tree1_results.get(DESCENDANT_COUNT)) self.assertEqual(5, topic_tree1_results.get(RESOURCE_COUNT)) self.assertEqual(0, topic_tree1_results.get(ASSESSMENT_COUNT)) self.assertEqual(0, topic_tree1_results.get(RESOURCE_SIZE)) self.assertEqual(1, topic_tree1_results.get(COACH_COUNT)) self.assertTrue(topic_tree1_results.get(HAS_CHANGED_DESCENDANT)) self.assertEqual(1, topic_tree1_results.get(MAX_SORT_ORDER)) self.assertIsNotNone(topic_tree2_results) self.assertEqual(7, topic_tree2_results.get(DESCENDANT_COUNT)) self.assertEqual(5, topic_tree2_results.get(RESOURCE_COUNT)) self.assertEqual(0, topic_tree2_results.get(ASSESSMENT_COUNT)) self.assertEqual(0, topic_tree2_results.get(RESOURCE_SIZE)) self.assertEqual(0, topic_tree2_results.get(COACH_COUNT)) self.assertFalse(topic_tree2_results.get(HAS_CHANGED_DESCENDANT)) self.assertEqual(2, topic_tree2_results.get(MAX_SORT_ORDER)) self.assertIsNotNone(video_node_results) self.assertEqual(1, video_node_results.get(DESCENDANT_COUNT)) self.assertEqual(1, video_node_results.get(RESOURCE_COUNT)) self.assertEqual(0, video_node_results.get(ASSESSMENT_COUNT)) self.assertEqual( video_node.files.aggregate(size=Sum('file_size')).get('size'), video_node_results.get(RESOURCE_SIZE)) self.assertEqual(1, video_node_results.get(COACH_COUNT)) self.assertFalse(video_node_results.get(HAS_CHANGED_DESCENDANT)) self.assertEqual(video_node.sort_order, video_node_results.get(MAX_SORT_ORDER)) self.assertIsNotNone(exercise_node_results) self.assertEqual(1, exercise_node_results.get(DESCENDANT_COUNT)) self.assertEqual(1, exercise_node_results.get(RESOURCE_COUNT)) self.assertEqual(3, exercise_node_results.get(ASSESSMENT_COUNT)) self.assertEqual(0, exercise_node_results.get(RESOURCE_SIZE)) self.assertEqual(0, exercise_node_results.get(COACH_COUNT)) self.assertFalse(exercise_node_results.get(HAS_CHANGED_DESCENDANT)) self.assertEqual(exercise_node.sort_order, exercise_node_results.get(MAX_SORT_ORDER))