IA-4776: Implemented data models for validation workflow#2750
IA-4776: Implemented data models for validation workflow#2750
Conversation
| # 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") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I've never used GenericForeignKey, so I don't have any strong opinion (maybe I'll have one after I start using it? 😅)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
iaso/models/risp/__init__.py
Outdated
| "RISPNode", | ||
| "RISPNodeInstance", | ||
| "RISPTransition", | ||
| "RISPTransitionInstance", | ||
| "RISPWorkflow", | ||
| "RISPWorkflowInstance", |
There was a problem hiding this comment.
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.
iaso/models/risp/run_instances.py
Outdated
| from ..common import CreatedAndUpdatedModel | ||
|
|
||
|
|
||
| class RISPWorkflowInstance(CreatedAndUpdatedModel): |
There was a problem hiding this comment.
Instance is also the name for Form's submissions. I think it's confusing to use it here.
Maybe ObjectValidationWorkflow ?
| """ | ||
| Represents a running instance of the workflow | ||
| """ |
There was a problem hiding this comment.
This is the same description as above
iaso/models/risp/run_instances.py
Outdated
| content_object = GenericForeignKey("content_type", "object_id") | ||
|
|
||
|
|
||
| class RISPNodeInstance(CreatedAndUpdatedModel): |
There was a problem hiding this comment.
Same remark about Instance.
Maybe ObjectValidationWorkflowNode
iaso/models/risp/templates.py
Outdated
| slug = AutoSlugField(populate_from="name", unique=True) | ||
| description = models.TextField(blank=True, max_length=1024) | ||
|
|
||
| # is_active = models.BooleanField(default=True) => SoftDeletableModel ? needed ? |
There was a problem hiding this comment.
I would go with SoftDeletableModel as this is what is done everywhere else in the code base.
iaso/models/risp/templates.py
Outdated
|
|
||
| # is_active = models.BooleanField(default=True) => SoftDeletableModel ? needed ? | ||
|
|
||
| owner = models.ForeignKey(get_user_model(), null=True, blank=True, on_delete=models.SET_NULL) |
There was a problem hiding this comment.
I'm not sure if this is useful. Probably a created_by and a updated_by fields make more sense to me
There was a problem hiding this comment.
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
iaso/models/risp/templates.py
Outdated
| from iaso.models.common import CreatedAndUpdatedModel | ||
|
|
||
|
|
||
| class RISPWorkflow(CreatedAndUpdatedModel): |
There was a problem hiding this comment.
I would name it ValidationWorkflow
iaso/models/risp/templates.py
Outdated
| ct = ContentType.objects.get_for_model(entity) | ||
| return self.allowed_content_types.filter(id=ct.id).exists() | ||
|
|
||
| class RISPNode(CreatedAndUpdatedModel): |
There was a problem hiding this comment.
I would name it ValidationWorkflowNode
iaso/models/risp/templates.py
Outdated
| return f"{self.workflow_id} - {self.name}" | ||
|
|
||
|
|
||
| class RISPTransition(CreatedAndUpdatedModel): |
There was a problem hiding this comment.
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.
| dependencies = [ | ||
| migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
| ("contenttypes", "0002_remove_content_type_name"), | ||
| ("iaso", "0364_alter_metricvalue_metric_type"), |
There was a problem hiding this comment.
I merged a PR with another migration yesterday, you should rebase develop and redo this migration file 😅
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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 ? |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
Interesting! Does this mean that slug will be automatically updated when name changes or it's just set once on creation?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
So, this means that reusing UserRole sounds like the best plan to you?
There was a problem hiding this comment.
Don't have any strong opinion on this tbh 😄
| from .templates import ValidationNode, ValidationWorkflow | ||
|
|
||
|
|
||
| class ValidationWorkflowInstance(CreatedAndUpdatedModel): |
There was a problem hiding this comment.
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 🤓)
There was a problem hiding this comment.
Understood, I renamed most of the models to have a more meaningful business logic, also tried to avoid using instance
| ) | ||
|
|
||
|
|
||
| class ValidationTransitionInstance(CreatedAndUpdatedModel): |
There was a problem hiding this comment.
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
iaso/models/risp/templates.py
Outdated
|
|
||
| # is_active = models.BooleanField(default=True) => SoftDeletableModel ? needed ? | ||
|
|
||
| owner = models.ForeignKey(get_user_model(), null=True, blank=True, on_delete=models.SET_NULL) |
There was a problem hiding this comment.
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
iaso/models/risp/templates.py
Outdated
| """ | ||
| 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) |
There was a problem hiding this comment.
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
| # 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) |
There was a problem hiding this comment.
I would expect the workflow to know where we're currently at
| # 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() | ||
| ) |
There was a problem hiding this comment.
I'm not sure why there's no object created here
There was a problem hiding this comment.
I don't understand the name of the file
| CANCELLED = "cancelled", _("Cancelled") | ||
|
|
||
|
|
||
| class ValidationWorkflowObject(CreatedAndUpdatedModel): |
There was a problem hiding this comment.
I'm quite sure this should be a SoftDeletableModel
There was a problem hiding this comment.
As probably all the models that are in this PR
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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() | ||
| # |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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"]) |
There was a problem hiding this comment.
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"
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.
Notes
Still WIP