Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@ repos:
- id: ruff
types_or: [python, pyi, jupyter, toml]
args: [--fix, --exit-non-zero-on-fix]
exclude: ^.*uv\.lock$
# Formatter
- id: ruff-format
types_or: [python, pyi, jupyter, toml]
exclude: ^.*uv\.lock$

- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.398
Expand Down
4 changes: 2 additions & 2 deletions apps/project/custom_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,14 @@ class CustomOptionDefaults:
"icon": IconEnum.CLOSE_OUTLINE,
"value": 0,
"description": "the shape does not outline any feature in the image",
"icon_color": "#D32F2F",
"icon_color": "transparent",
},
{
"title": "Multiple Features",
"icon": IconEnum.CHECKMARK_OUTLINE,
"value": 2,
"description": "the shape outlines multiple features in the image",
"icon_color": "#388E3C",
"icon_color": "#ffff00",
},
]

Expand Down
1 change: 1 addition & 0 deletions apps/tutorial/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class Meta: # type: ignore[reportIncompatibleVariableOverride]

client_id = factory.LazyFunction(lambda: str(ULID()))
reference = 1
task_partition_index = factory.Sequence(lambda n: n)
project_type_specifics = factory.LazyAttribute(lambda _: {})


Expand Down
4 changes: 4 additions & 0 deletions apps/tutorial/graphql/inputs/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from .project_types.compare import CompareTutorialTaskPropertyInput
from .project_types.completeness import CompletenessTutorialTaskPropertyInput
from .project_types.find import FindTutorialTaskPropertyInput
from .project_types.locate import LocateTutorialTaskPropertyInput
from .project_types.street import StreetTutorialTaskPropertyInput
from .project_types.validate import ValidateTutorialTaskPropertyInput
from .project_types.validate_image import ValidateImageTutorialTaskPropertyInput
Expand All @@ -36,19 +37,22 @@ class TutorialTaskProjectTypeSpecificInput:
validate_image: ValidateImageTutorialTaskPropertyInput | None = strawberry.UNSET
completeness: CompletenessTutorialTaskPropertyInput | None = strawberry.UNSET
street: StreetTutorialTaskPropertyInput | None = strawberry.UNSET
locate: LocateTutorialTaskPropertyInput | None = strawberry.UNSET


@strawberry_django.input(TutorialTask)
class TutorialTaskCreateInput(UserResourceCreateInputMixin):
# NOTE: scenario_id will be referenced from parent
reference: strawberry.auto
task_partition_index: strawberry.auto
project_type_specifics: TutorialTaskProjectTypeSpecificInput


@strawberry_django.partial(TutorialTask)
class TutorialTaskUpdateInput(UserResourceUpdateInputMixin):
# NOTE: scenario_id will be referenced from parent
reference: strawberry.auto
task_partition_index: strawberry.auto
project_type_specifics: TutorialTaskProjectTypeSpecificInput


Expand Down
1 change: 1 addition & 0 deletions apps/tutorial/graphql/types/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class TutorialTaskType(UserResourceTypeMixin):
id: strawberry.ID
scenario_id: strawberry.ID
reference: strawberry.auto
task_partition_index: strawberry.auto

@strawberry_django.field(
only=["project_type_specifics"],
Expand Down
18 changes: 18 additions & 0 deletions apps/tutorial/migrations/0003_tutorialtask_task_partition_index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.5 on 2026-02-26 10:28

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('tutorial', '0002_initial'),
]

operations = [
migrations.AddField(
model_name='tutorialtask',
name='task_partition_index',
field=models.PositiveSmallIntegerField(blank=True, null=True),
),
]
2 changes: 2 additions & 0 deletions apps/tutorial/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ class TutorialTask(UserResource):

reference = models.PositiveSmallIntegerField[int, int]()

task_partition_index = models.PositiveSmallIntegerField[int, int](null=True, blank=True)

project_type_specifics = models.JSONField()

# Type hints
Expand Down
44 changes: 44 additions & 0 deletions apps/tutorial/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class Meta: # type: ignore[reportIncompatibleVariableOverride]
fields = (
"id",
"reference",
"task_partition_index",
"project_type_specifics",
)

Expand Down Expand Up @@ -94,9 +95,52 @@ def _validate_project_type_specifics(self, attrs: dict[str, typing.Any]):

attrs["project_type_specifics"] = project_type_specifics

def _validate_task_partition_index(self, attrs: dict[str, typing.Any]) -> None:
tutorial = self.context.get("tutorial")
if tutorial is None:
return

scenario = self.context.get("scenario")
task_partition_index = attrs.get(
"task_partition_index",
getattr(self.instance, "task_partition_index", None),
)

# NOTE: task_partition_index can be None, but if provided
# it should not be the same as any other task in the same scenario.
if task_partition_index is None:
return

project_type = tutorial.project.project_type
if project_type is None:
raise serializers.ValidationError(
{
"project_type": gettext("Project type is required."),
},
)

# Only enforce uniqueness for LOCATE projects
if project_type != ProjectTypeEnum.LOCATE:
return

task_qs = TutorialTask.objects.filter(scenario=scenario)

if self.instance is not None:
task_qs = task_qs.exclude(pk=self.instance.pk)

if task_qs.filter(task_partition_index=task_partition_index).exists():
raise serializers.ValidationError(
{
"task_partition_index": gettext(
"Task partition index should be unique among tasks of the same scenario.",
),
},
)

@typing.override
def validate(self, attrs: dict[str, typing.Any]):
self._validate_project_type_specifics(attrs)
self._validate_task_partition_index(attrs)
return super().validate(attrs)

@typing.override
Expand Down
Loading
Loading