Skip to content

Commit

Permalink
Update or create
Browse files Browse the repository at this point in the history
  • Loading branch information
gregorjerse committed Nov 14, 2023
1 parent 5ca7be8 commit 826e143
Show file tree
Hide file tree
Showing 5 changed files with 359 additions and 56 deletions.
19 changes: 13 additions & 6 deletions resolwe/flow/models/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@

from .base import AuditModel

VALIDATOR_LENGTH = 128
NAME_LENGTH = 128
LABEL_LENGTH = 128
DESCRIPTION_LENGTH = 256


class HandleMissingAnnotations(Enum):
"""How to handle missing annotations."""
Expand Down Expand Up @@ -217,10 +222,10 @@ class AnnotationGroup(models.Model):
"""Group of annotation fields."""

#: the name of the annotation group
name = models.CharField(max_length=128)
name = models.CharField(max_length=NAME_LENGTH)

#: the label of the annotation group
label = models.CharField(max_length=128)
label = models.CharField(max_length=LABEL_LENGTH)

#: the sorting order among annotation groups
sort_order = models.PositiveSmallIntegerField()
Expand All @@ -239,13 +244,13 @@ class AnnotationField(models.Model):
"""Annotation field."""

#: the name of the annotation fields
name = models.CharField(max_length=128)
name = models.CharField(max_length=NAME_LENGTH)

#: user visible field name
label = models.CharField(max_length=128)
label = models.CharField(max_length=LABEL_LENGTH)

#: user visible field description
description = models.CharField(max_length=256)
description = models.CharField(max_length=DESCRIPTION_LENGTH)

#: the type of the annotation field
type = models.CharField(max_length=16)
Expand All @@ -259,7 +264,9 @@ class AnnotationField(models.Model):
sort_order = models.PositiveSmallIntegerField()

#: optional regular expression for validation
validator_regex = models.CharField(max_length=128, null=True, blank=True)
validator_regex = models.CharField(
max_length=VALIDATOR_LENGTH, null=True, blank=True
)

#: optional map of valid values to labels
vocabulary = models.JSONField(null=True, blank=True)
Expand Down
8 changes: 8 additions & 0 deletions resolwe/flow/serializers/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from rest_framework import serializers
from rest_framework.fields import empty

from resolwe.flow.models.annotations import NAME_LENGTH as ANNOTATION_NAME_LENGTH
from resolwe.flow.models.annotations import (
AnnotationField,
AnnotationGroup,
Expand Down Expand Up @@ -80,6 +81,13 @@ class AnnotationsSerializer(serializers.Serializer):
value = serializers.JSONField()


class AnnotationsByPathSerializer(serializers.Serializer):
"""Serializer that reads annotation field and its value."""

field_path = serializers.CharField(max_length=2 * ANNOTATION_NAME_LENGTH + 1)
value = serializers.JSONField()


class AnnotationPresetSerializer(ResolweBaseSerializer):
"""Serializer for AnnotationPreset objects."""

Expand Down
226 changes: 222 additions & 4 deletions resolwe/flow/tests/test_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,10 +414,13 @@ def test_create_annotation_value(self):
self.client.force_authenticate(self.contributor)
response = self.client.post(path, values, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
expected = AnnotationValue.objects.annotate(value=F("_value__value")).values(
"entity", "field", "value"
attributes = ("entity", "field", "value")
created = AnnotationValue.objects.annotate(value=F("_value__value")).values(
*attributes
)
self.assertCountEqual(values, expected)
received = [{key: entry[key] for key in attributes} for entry in response.data]
self.assertCountEqual(received, values)
self.assertCountEqual(created, values)

# Authenticated request, no permission.
AnnotationValue.objects.all().delete()
Expand Down Expand Up @@ -451,7 +454,7 @@ def test_update_annotation_value(self):

# Authenticated request, entity should not be changed
values = {
"id": self.annotation_value1.pk,
"field": self.annotation_field1.pk,
"value": "string",
"entity": self.entity2.pk,
}
Expand All @@ -467,6 +470,95 @@ def test_update_annotation_value(self):
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(response.data, {"detail": "Not found."})

# Single / bulk update with put.
self.entity1.collection.set_permission(Permission.EDIT, self.contributor)
path = reverse("resolwe-api:annotationvalue-list")
client = APIClient()
values = {
"entity": self.entity1.pk,
"field": self.annotation_field1.pk,
"value": -1,
}

# Unauthenticated request.
response = client.put(path, values, format="json")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(
response.data,
{"detail": "You do not have permission to perform this action."},
)

# Authenticated request with validation error.
client.force_authenticate(self.contributor)
response = client.put(path, values, format="json")
self.annotation_value1.refresh_from_db()
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.data["error"],
[
"The value '-1' is not of the expected type 'str'.",
f"The value '-1' is not valid for the field {self.annotation_field1}.",
],
)
self.assertEqual(self.annotation_value1.value, "string")
self.assertEqual(AnnotationValue.objects.count(), 2)

# Authenticated request with validation error.
values["value"] = "another"
response = client.put(path, values, format="json")
self.annotation_value1.refresh_from_db()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data,
{
"label": "Another one",
"id": self.annotation_value1.pk,
"entity": self.entity1.pk,
"field": self.annotation_field1.pk,
"value": "another",
},
)
self.assertEqual(self.annotation_value1.value, "another")
self.assertEqual(self.annotation_value1.label, "Another one")
self.assertEqual(AnnotationValue.objects.count(), 2)

# Multi.
values = [
{"field": self.annotation_field2.pk, "value": 1, "entity": self.entity1.pk},
{
"field": self.annotation_field1.pk,
"value": "string",
"entity": self.entity1.pk,
},
]

response = client.put(path, values, format="json")
self.annotation_value1.refresh_from_db()
self.assertEqual(response.status_code, status.HTTP_200_OK)
created_value = AnnotationValue.objects.get(
entity=self.entity1, field=self.annotation_field2
)
expected = [
{
"label": "label string",
"id": self.annotation_value1.pk,
"entity": self.entity1.pk,
"field": self.annotation_field1.pk,
"value": "string",
},
{
"label": created_value.label,
"id": created_value.pk,
"entity": created_value.entity.pk,
"field": created_value.field.pk,
"value": created_value.value,
},
]
self.assertCountEqual(response.data, expected)
self.assertEqual(self.annotation_value1.value, "string")
self.assertEqual(self.annotation_value1.label, "label string")
self.assertEqual(AnnotationValue.objects.count(), 3)

def test_delete_annotation_value(self):
"""Test deleting annotation value objects."""
client = APIClient()
Expand Down Expand Up @@ -1256,3 +1348,129 @@ def has_value(entity, field_id, value):
self.assertEqual(self.annotation_value1.value, "bbb")
has_value(self.entity1, self.annotation_field1.pk, "bbb")
has_value(self.entity1, self.annotation_field2.pk, -1)

def test_set_values_by_path(self):
def has_value(entity, field_id, value):
self.assertEqual(
value, entity.annotations.filter(field_id=field_id).get().value
)

# Remove vocabulary to simplify testing.
self.annotation_field1.vocabulary = None
self.annotation_field1.save()
viewset = EntityViewSet.as_view(actions={"post": "set_annotations_by_path"})
request = factory.post("/", {}, format="json")

# Unauthenticated request, no permissions.
response: Response = viewset(request, pk=self.entity1.pk)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

# Request without required parameter.
request = factory.post("/", [{}], format="json")
force_authenticate(request, self.contributor)
response = viewset(request, pk=self.entity1.pk)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertDictEqual(
response.data[0],
{
"field_path": ["This field is required."],
"value": ["This field is required."],
},
)

annotations = [
{"field_path": str(self.annotation_field1), "value": "new value"},
{"field_path": str(self.annotation_field2), "value": -1},
]

# Valid request without regex validation.
request = factory.post("/", annotations, format="json")
force_authenticate(request, self.contributor)
response = viewset(request, pk=self.entity1.pk)

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(self.entity1.annotations.count(), 2)
has_value(self.entity1, self.annotation_field1.pk, "new value")
has_value(self.entity1, self.annotation_field2.pk, -1)

# Wrong type.
annotations = [{"field_path": str(self.annotation_field1), "value": 10}]
request = factory.post("/", annotations, format="json")
force_authenticate(request, self.contributor)
response = viewset(request, pk=self.entity1.pk)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(len(response.data), 1)
self.assertEqual(
response.data,
{"error": ["The value '10' is not of the expected type 'str'."]},
)

has_value(self.entity1, self.annotation_field1.pk, "new value")
has_value(self.entity1, self.annotation_field2.pk, -1)

# Wrong regex.
self.annotation_field1.validator_regex = "b+"
self.annotation_field1.save()
annotations = [{"field_path": str(self.annotation_field1), "value": "aaa"}]
request = factory.post("/", annotations, format="json")
force_authenticate(request, self.contributor)
response = viewset(request, pk=self.entity1.pk)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(len(response.data), 1)
self.assertEqual(
response.data,
{
"error": [
f"The value 'aaa' for the field '{self.annotation_field1.pk}' does not match the regex 'b+'."
],
},
)
has_value(self.entity1, self.annotation_field1.pk, "new value")
has_value(self.entity1, self.annotation_field2.pk, -1)

# Wrong regex and type.
annotations = [{"field_path": str(self.annotation_field1), "value": 10}]
request = factory.post("/", annotations, format="json")
force_authenticate(request, self.contributor)
response = viewset(request, pk=self.entity1.pk)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertCountEqual(
response.data["error"],
[
f"The value '10' for the field '{self.annotation_field1.pk}' does not match the regex 'b+'.",
"The value '10' is not of the expected type 'str'.",
],
)
has_value(self.entity1, self.annotation_field1.pk, "new value")
has_value(self.entity1, self.annotation_field2.pk, -1)

# Multiple fields validation error.
annotations = [
{"field_path": str(self.annotation_field1), "value": 10},
{"field_path": str(self.annotation_field2), "value": "string"},
]
request = factory.post("/", annotations, format="json")
force_authenticate(request, self.contributor)
response = viewset(request, pk=self.entity1.pk)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertCountEqual(
response.data["error"],
[
"The value '10' is not of the expected type 'str'.",
f"The value '10' for the field '{self.annotation_field1.pk}' does not match the regex 'b+'.",
"The value 'string' is not of the expected type 'int'.",
],
)
has_value(self.entity1, self.annotation_field1.pk, "new value")
has_value(self.entity1, self.annotation_field2.pk, -1)

# Regular request with regex validation.
annotations = [{"field_path": str(self.annotation_field1), "value": "bbb"}]
request = factory.post("/", annotations, format="json")
force_authenticate(request, self.contributor)
response = viewset(request, pk=self.entity1.pk)
self.annotation_value1.refresh_from_db()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(self.annotation_value1.value, "bbb")
has_value(self.entity1, self.annotation_field1.pk, "bbb")
has_value(self.entity1, self.annotation_field2.pk, -1)
Loading

0 comments on commit 826e143

Please sign in to comment.