Skip to content

Comments

IA-4776: Implemented data models for validation workflow#2750

Draft
hugo093 wants to merge 6 commits intodevelopfrom
IA-4776-Data-model-of-workflow
Draft

IA-4776: Implemented data models for validation workflow#2750
hugo093 wants to merge 6 commits intodevelopfrom
IA-4776-Data-model-of-workflow

Conversation

@hugo093
Copy link
Contributor

@hugo093 hugo093 commented Feb 17, 2026

What problem is this PR solving?

Data modelling for the new validation workflow

Related JIRA tickets

IA-4776

Changes

Explain the changes that were made.

  • Added a base abstract CreatedAndUpdatedModel
  • Added django-autoslug
  • Created the main validation workflow models

Notes

Still WIP

# we attach here any object, can be a form submission, an orgunit, whatever.
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.CharField(max_length=500, blank=True)
content_object = GenericForeignKey("content_type", "object_id")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, I am not a fan of GenericForeignKey, it lowers the performances and, AFAIK, doesn't give proper related fields on the other model.
I don't think we are going to have many (10+) different kinds of objects we want to validate so I would use nullable ForeignKeys instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never used GenericForeignKey, so I don't have any strong opinion (maybe I'll have one after I start using it? 😅)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit of a pain to use since it adds a level of indirection to foreign keys (you have to figure out what the models pointed at is before joining). It's neat as a concept, but I've found them difficult to optimize and handle.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, it's a good concept when you really don't know how many content types will be linked and/or there will be many content type. Otherwise adding nullable FKs is more than enough

Comment on lines 5 to 10
"RISPNode",
"RISPNodeInstance",
"RISPTransition",
"RISPTransitionInstance",
"RISPWorkflow",
"RISPWorkflowInstance",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RISP is the client's account name. I don't think we should prefix any of the classes with it as this is going to be used by all clients.

from ..common import CreatedAndUpdatedModel


class RISPWorkflowInstance(CreatedAndUpdatedModel):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instance is also the name for Form's submissions. I think it's confusing to use it here.

Maybe ObjectValidationWorkflow ?

Comment on lines 30 to 32
"""
Represents a running instance of the workflow
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same description as above

content_object = GenericForeignKey("content_type", "object_id")


class RISPNodeInstance(CreatedAndUpdatedModel):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same remark about Instance.

Maybe ObjectValidationWorkflowNode

slug = AutoSlugField(populate_from="name", unique=True)
description = models.TextField(blank=True, max_length=1024)

# is_active = models.BooleanField(default=True) => SoftDeletableModel ? needed ?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would go with SoftDeletableModel as this is what is done everywhere else in the code base.


# is_active = models.BooleanField(default=True) => SoftDeletableModel ? needed ?

owner = models.ForeignKey(get_user_model(), null=True, blank=True, on_delete=models.SET_NULL)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is useful. Probably a created_by and a updated_by fields make more sense to me

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree, created_by and updated_by are the names we usually use in IASO whenever we need to store some kind of author/owner

from iaso.models.common import CreatedAndUpdatedModel


class RISPWorkflow(CreatedAndUpdatedModel):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would name it ValidationWorkflow

ct = ContentType.objects.get_for_model(entity)
return self.allowed_content_types.filter(id=ct.id).exists()

class RISPNode(CreatedAndUpdatedModel):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would name it ValidationWorkflowNode

return f"{self.workflow_id} - {self.name}"


class RISPTransition(CreatedAndUpdatedModel):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would name it ValidationWorkflowTransition.

Even though, I'm not sure I understand the role of this class. I think this class (and the corresponding "ObjectValidationWorkflowTransition") should be merged directly into the ValidationWorkflowNode class. To me, it adds unnecessary complexity but I might be missing something.

Copy link
Member

@tdethier tdethier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some comments here and there.

Overall, it's good, and there are some nice ideas, but I think I'm missing something on the "instance" stuff. Maybe I misunderstood something.

We can discuss that later if you want

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("contenttypes", "0002_remove_content_type_name"),
("iaso", "0364_alter_metricvalue_metric_type"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I merged a PR with another migration yesterday, you should rebase develop and redo this migration file 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do at the end , thanks :)

# we attach here any object, can be a form submission, an orgunit, whatever.
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.CharField(max_length=500, blank=True)
content_object = GenericForeignKey("content_type", "object_id")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never used GenericForeignKey, so I don't have any strong opinion (maybe I'll have one after I start using it? 😅)

slug = AutoSlugField(populate_from="name", unique=True)
description = models.TextField(blank=True, max_length=1024)

# is_active = models.BooleanField(default=True) => SoftDeletableModel ? needed ?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I can see workflows being deleted by mistake, I would use SoftDeletableModel

class ValidationNode(CreatedAndUpdatedModel):
workflow = models.ForeignKey(ValidationWorkflow, on_delete=models.CASCADE, related_name="nodes")
name = models.CharField(max_length=256)
slug = AutoSlugField(populate_from="name")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting! Does this mean that slug will be automatically updated when name changes or it's just set once on creation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be yes, depends how you configure it. I've added some tests in models so you have an example. Otherwise the doc is quite simple and explicit

from_node = models.ForeignKey(ValidationNode, on_delete=models.CASCADE, related_name="from_node")
to_node = models.ForeignKey(ValidationNode, on_delete=models.CASCADE, related_name="to_node")

roles_required = models.ManyToManyField(UserRole, blank=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, this means that reusing UserRole sounds like the best plan to you?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't have any strong opinion on this tbh 😄

from .templates import ValidationNode, ValidationWorkflow


class ValidationWorkflowInstance(CreatedAndUpdatedModel):
Copy link
Member

@tdethier tdethier Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's annoying to have Instance in the name of these models since it already means something else in IASO and is confusing.
Maybe we should remove Instance from these names (or maybe it's time to finally rename Instance to Submission 🤓)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood, I renamed most of the models to have a more meaningful business logic, also tried to avoid using instance

)


class ValidationTransitionInstance(CreatedAndUpdatedModel):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Benjamin, I'm not sure about this class. I understand the ones in templates.py, but I'm not sure about this and ValidationNodeInstance.

Maybe there's something that I'm missing


# is_active = models.BooleanField(default=True) => SoftDeletableModel ? needed ?

owner = models.ForeignKey(get_user_model(), null=True, blank=True, on_delete=models.SET_NULL)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree, created_by and updated_by are the names we usually use in IASO whenever we need to store some kind of author/owner

"""
The main workflow object that is a template, a static definition of a workflow
"""
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In IASO, UUIDs are usually generated by the mobile app and stored in parallel to a standard int ID that's used mostly for the backend.

When there's no object creation on mobile, we usually don't add UUIDs

Comment on lines +94 to +97
# get the next step pending => how? => need to find a better way ?
step_object = ValidationStepObject.objects.filter(step_template=self.step).first()

WorkflowEngine.complete_step(step_object, True, self.other_user)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect the workflow to know where we're currently at

Comment on lines +139 to +144
# we check the ending state object has not been created
self.assertFalse(
ValidationStateObject.objects.filter(
workflow_object=workflow_object, state_template=self.end_state
).exists()
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why there's no object created here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the name of the file

CANCELLED = "cancelled", _("Cancelled")


class ValidationWorkflowObject(CreatedAndUpdatedModel):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm quite sure this should be a SoftDeletableModel

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As probably all the models that are in this PR

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, since this is not something that users will be allowed to delete, it does make sense that it's not soft deletable


roles_required = models.ManyToManyField(UserRole, blank=True) # todo : define if we want roles or permissions ?

priority = models.PositiveIntegerField(default=0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why we need this and where this notion comes from

ACTIVE = "active", _("Active")
APPROVED = "approved", _("Approved")
REJECTED = "rejected", _("Rejected")
CANCELLED = "cancelled", _("Cancelled")
Copy link
Member

@madewulf madewulf Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does it mean for a validation state to be cancelled, approved or rejected?
It feels like my states have states ;-)


state_template = state_object.state_template
# transitions = node_object.outgoing_transition_objects.all()
#
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My impression is that commented code should not be here (but I know it's a draft, so, this will probably be removed)


# Create ValidationStepObject for all steps outgoing from this state
with transaction.atomic():
ValidationStepObject.objects.bulk_create(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it a bit wasteful to create in advance all the possible transitions. Why not just create the transition that really happened and compute the possibilities on the fly when presenting to the users (instead of loading that from te database). It will also lends itsefl better to have workflows that are changed on the fly. (If you remove a step in a workflow in the current implementation, you have to remove all the tentative steps in the workflow objects if you want to allow users to adapt)

# START node auto-completes
if state_template.state_type == ValidationStateTemplateType.START:
state_object.status = ValidationStateObjectStatus.COMPLETED
state_object.save(update_fields=["status"])
Copy link
Member

@madewulf madewulf Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't totally see the sense of having states to be completed. I would say that the state of a workflow object is just the last state it was in, all previous states are in the past and consequently "completed"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants