Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • arkindex/backend
1 result
Show changes
Commits on Source (2)
......@@ -605,6 +605,7 @@ class DockerWorkerVersionSerializer(serializers.ModelSerializer):
)
gpu_usage = EnumField(FeatureUsage, required=False, default=FeatureUsage.Disabled)
model_usage = EnumField(FeatureUsage, required=False, default=FeatureUsage.Disabled)
configuration = serializers.DictField(required=False, default={})
class Meta:
model = WorkerVersion
......@@ -630,6 +631,20 @@ class DockerWorkerVersionSerializer(serializers.ModelSerializer):
}
}
def validate_configuration(self, configuration):
errors = defaultdict(list)
user_configuration = configuration.get("user_configuration")
if not user_configuration:
return configuration
field = serializers.DictField(child=UserConfigurationFieldSerializer())
try:
field.to_internal_value(user_configuration)
except ValidationError as e:
errors["user_configuration"].append(e.detail)
if errors:
raise ValidationError(errors)
return configuration
@transaction.atomic
def create(self, validated_data):
"""
......
......@@ -6,6 +6,7 @@ from rest_framework import status
from arkindex.ponos.models import Farm
from arkindex.process.models import FeatureUsage, GitRefType, Process, ProcessMode, Repository, Worker, WorkerType
from arkindex.project.tests import FixtureAPITestCase
from arkindex.training.models import Model
from arkindex.users.models import Role, Scope
......@@ -128,6 +129,7 @@ class TestDockerWorkerVersion(FixtureAPITestCase):
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertDictEqual(response.json(), {
"configuration": ['Expected a dictionary of items but got type "str".'],
"docker_image_iid": ["Not a valid string."],
"gpu_usage": ["Value is not of type FeatureUsage"],
"model_usage": ["Value is not of type FeatureUsage"],
......@@ -546,3 +548,209 @@ class TestDockerWorkerVersion(FixtureAPITestCase):
self.assertListEqual(list(new_repo.memberships.values_list("user", "level")), [
(self.user.id, Role.Admin.value)
])
# Test user configuration
def test_create_version_valid_user_configuration(self):
test_model = Model.objects.create(
name="Generic model",
public=False,
)
self.client.force_login(self.user)
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"demo_integer": {"title": "Demo Integer", "type": "int", "required": True, "default": 1},
"demo_boolean": {"title": "Demo Boolean", "type": "bool", "required": False, "default": True},
"demo_dict": {"title": "Demo Dict", "type": "dict", "required": True, "default": {"a": "b", "c": "d"}},
"demo_choice": {"title": "Decisions", "type": "enum", "required": True, "default": 1, "choices": [1, 2, 3]},
"demo_list": {"title": "Demo List", "type": "list", "required": True, "subtype": "int", "default": [1, 2, 3, 4]},
"boolean_list": {"title": "It's a list of booleans", "type": "list", "required": False, "subtype": "bool", "default": [True, False, False]},
"demo_model": {"title": "Model for training", "type": "model", "required": True},
"other_model": {"title": "Model the second", "type": "model", "default": str(test_model.id)}
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.json()["configuration"], {
"user_configuration": {
"demo_integer": {
"title": "Demo Integer",
"type": "int",
"required": True,
"default": 1
},
"demo_boolean": {
"title": "Demo Boolean",
"type": "bool",
"required": False,
"default": True
},
"demo_dict": {
"title": "Demo Dict",
"type": "dict",
"required": True,
"default": {"a": "b", "c": "d"}
},
"demo_choice": {
"title": "Decisions",
"type": "enum",
"required": True,
"default": 1,
"choices": [1, 2, 3]
},
"demo_list": {
"title": "Demo List",
"type": "list",
"subtype": "int",
"required": True,
"default": [1, 2, 3, 4]
},
"boolean_list": {
"title": "It's a list of booleans",
"type": "list",
"subtype": "bool",
"required": False,
"default": [True, False, False]
},
"demo_model": {
"title": "Model for training",
"type": "model",
"required": True
},
"other_model": {
"title": "Model the second",
"type": "model",
"default": str(test_model.id)
}
}
})
def test_create_invalid_user_configuration(self):
cases = [
(
"non",
['Expected a dictionary of items but got type "str".']
),
(
{"demo_list": {"title": "Demo List", "type": "list", "required": True, "default": [1, 2, 3, 4]}},
{"demo_list": {
"subtype": ['The "subtype" field must be set for "list" type properties.']
}}
),
(
{"demo_list": {"title": "Demo List", "type": "list", "required": True, "subtype": "dict", "default": [1, 2, 3, 4]}},
{"demo_list": {
"subtype": ["Subtype can only be int, float, bool or string."]
}}
),
(
{"demo_list": {"title": "Demo List", "type": "list", "required": True, "subtype": "int", "default": [1, 2, "three", 4]}},
{"demo_list": {
"default": ["All items in the default value must be of type int."]
}}
),
(
{"demo_choice": {"title": "Decisions", "type": "enum", "required": True, "default": 1, "choices": "eeee"}},
{"demo_choice": {
"choices": ['Expected a list of items but got type "str".']
}}
),
(
{"secrets": ["aaaaaa"]},
{"secrets": {"__all__": ["User configuration field definitions should be of type dict, not list."]}}
),
(
{"something": {"title": "some thing", "type": "uh oh", "required": 2}},
{"something": {
"required": ["Must be a valid boolean."],
"type": ["Value is not of type UserConfigurationFieldType"]
}}
),
(
{"something": {"title": "some thing", "type": "int", "required": False, "choices": [1, 2, 3]}},
{"something": {
"choices": ['The "choices" field can only be set for an "enum" type property.']
}}
),
(
{"demo_integer": {"type": "int", "required": True, "default": 1}},
{"demo_integer": {"title": ["This field is required."]}}
),
(
{"demo_integer": {"title": "an integer", "type": "int", "required": True, "default": 1, "some_key": "oh no"}},
{"demo_integer": {
"some_key": ["Configurable properties can only be defined using the following keys: title, type, required, default, subtype, choices."]
}}
),
(
{"param": {"title": "Model to train", "type": "model", "default": "12341234-1234-1234-1234-123412341234"}},
{"param": {"default": ["Model 12341234-1234-1234-1234-123412341234 not found."]}}
)
]
self.client.force_login(self.user)
for user_configuration, error in cases:
with self.subTest(user_configuration=user_configuration, error=error):
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": user_configuration
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {
"configuration": {
"user_configuration": [error]
}
})
def test_create_version_invalid_user_configuration_default_value(self):
self.client.force_login(self.user)
cases = [
({"type": "int", "default": False}, "int"),
({"type": "int", "default": True}, "int"),
({"type": "float", "default": False}, "float"),
({"type": "float", "default": True}, "float"),
({"type": "bool", "default": 0}, "bool"),
({"type": "bool", "default": 1}, "bool"),
({"type": "string", "default": 1}, "string"),
({"type": "dict", "default": ["a", "b"]}, "dict"),
({"type": "model", "default": "gigi hadid"}, "model"),
({"type": "model", "default": False}, "model"),
({"type": "list", "subtype": "int", "default": 12}, "list")
]
for params, expected in cases:
with self.subTest(**params):
response = self.client.post(
reverse("api:version-from-docker"),
data={
"docker_image_iid": "a_docker_image",
"repository_url": self.repo.url,
"revision_hash": "new_revision_hash",
"worker_slug": self.worker.slug,
"configuration": {
"user_configuration": {
"param": {"title": "param", **params}
}
},
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {"configuration": {"user_configuration": [{"param": {"default": [f"This is not a valid value for a field of type {expected}."]}}]}})
......@@ -1552,7 +1552,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
},
})
def test_create_version_empty_configuration(self):
def test_create_version_configuration_wrong_type(self):
"""
Configuration body must be an object
"""
......
......@@ -140,8 +140,10 @@ class PasswordResetConfirmSerializer(serializers.Serializer):
def save(self):
user = self.validated_data["user"]
if not user or not self.validated_data["valid_token"]:
if not user:
return
if not self.validated_data["valid_token"]:
raise serializers.ValidationError({"token": "This password reset link has expired. Please generate a new one using the 'Forgot your password?' link on the Login page."})
user.set_password(self.validated_data["password"])
user.save()
......
......@@ -75,7 +75,7 @@ class TestPasswordReset(FixtureAPITestCase):
},
format="json",
)
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
self.user.refresh_from_db()
self.assertFalse(self.user.check_password("S3cr37Pa$$w0rd"))
self.assertEqual(token_gen_mock.check_token.call_count, 1)
......