4

I'm using Django Viewflow like a flow engine without gui. Can anyone post an example for creating a process and flow management programmatically? I do not understand how to manage the flow completely from django code (eg from a test class) without a frontend. Do I need to create a flow instance first? How do I know which task I must perform and how do I performs it? I need to use only viewflow without a gui

Thanks in advance!

MyApp/models.py

class MedicalParameters(models.Model):
    # medical parameters
    pas = models.IntegerField(verbose_name="Pressione Sistolica")
    pad = models.IntegerField(verbose_name="Pressione Diastolica")
    fc = models.IntegerField(verbose_name="Frequenza Cardiaca")

class Triage(models.Model):
    date = models.DateTimeField(auto_now=True)
    patient_id = models.CharField(max_length=20)
    first_name = models.CharField(max_length=150)
    last_name = models.CharField(max_length=150)
    birth_date = models.DateField(auto_now=False)
    sex = models.CharField(max_length=1, choices=SEX, default='M')

    # Medical Parameters
    parameters = models.ForeignKey(MedicalParameters, blank=True, null=True,
                                   on_delete=models.PROTECT)



class TriageProcess(Process):
    triage = models.ForeignKey(Triage, blank=True, null=True, on_delete=models.CASCADE)

    class Meta:
        verbose_name_plural = 'Triage process'

MyApp/flow.py

class TriageFlow(Flow):
    process_class = TriageProcess

    process_title = 'Processo di Triage'
    process_description = 'Processo di Triage'

    summary_template = """
            Triage di {{ process.triage.first_name }} {{ process.triage.last_name }}
            """

    start = (
        flow.Start(
            views.StartView,
            task_title="Nuovo Triage",
            task_description="Inizia Triege"
        ).Permission(
            auto_create=True
        ).Next(this.register_measures)
    )

    register_measures = (
        flow.View(
            views.MeasuresView,
            # fields=["parameters"],
            task_description="Acquisisci Misure",
            task_title='Misure da Multiparametrico'
        )
            .Assign(lambda act: act.process.created_by)
            .Next(this.choose_capitolo)
    )

MyApp/view.py

class StartView(StartFlowMixin, generic.UpdateView):
    form_class = TriageForm

    layout = Layout(
        Row('patient_id'),
        Fieldset('Patient Details',
                 Row('first_name', 'last_name', 'birth_date'),
                 Row('sex',
                     # 'age'
                     )
                 )
    )

    def get_object(self):
        return self.activation.process.triage

    def activation_done(self, form):
        triage = form.save()
        self.activation.process.triage = triage
        self.activation.process.triage.color = COLOR_VALUE.BIANCO
        super(StartView, self).activation_done(form)

        # super(StartView, self).activation_done(form)



class MeasuresView(FlowMixin, generic.UpdateView):
    form_class = MedicalParametersForm
    layout = Layout(
        Fieldset('Temperatura ( C )',
                 Row('temp')),
        Fieldset('Pressione',
                 Row('pas'),
                 Row('pad')),
        Fieldset('Frequenza',
                 Row('fc'),
                 Row('fr'),
                 Row('fio2')),
        Fieldset("Analisi Cliniche",
                 Row('so2'),
                 Row('ph')),
        Fieldset('Emogas',
                 Row('pao2'),
                 Row('paco2'),
                 Row('hco3')),
        Fieldset("Indici",
                 Row('gcs')
                 # Row('shock')
                 ))

    def get_object(self):
        return self.activation.process.triage.parameters

    def activation_done(self, form):
        _measures = form.save()
        self.activation.process.triage.parameters = _measures
        if not self.activation.process.triage.parameters.fc is None \
                and not self.activation.process.triage.parameters.pas is None:
            self.activation.process.triage.parameters.shock = self.activation.process.triage.parameters.fc / self.activation.process.triage.parameters.pas
            self.activation.process.triage.parameters.save()
        color = _measures.calculate_color()
        self.activation.process.triage.color = color
        self.activation.process.triage.rivalutazione = None

        self.activation.process.triage.save()
        super(MeasuresView, self).activation_done(form)
kmmbvnr
  • 5,863
  • 4
  • 35
  • 44

3 Answers3

3

Testing views as part of the flow limits how much you can do in the tests. For example it becomes cumbersome to add testing of templates and template variables within the same flow for a particular view.

If you were to do thorough testing. Your tests would explode in size to an undesirable level.

To go around the fact that each view requires that the previous task is completed. You can use factory boy to create the particular flow task associated with the view. And use the post generation hook to run the necessary activations that will imply that you can call the view like other normal django views in the test.

flows.py

from viewflow import flow
from viewflow.base import Flow, this

from .views import SampleCreateView, SampleUpdateViewOne, SampleUpdateViewTwo

class SampleFlow(Flow):

    start = flow.Start(SampleCreateView).Next(this.update_one)

    update_one = flow.View(SampleUpdateViewOne).Next(this.update_two)

    update_two = flow.View(SampleUpdateViewTwo).Next(this.end)

    end = flow.End()

tests/factories.py

class TaskFactory(factory.django.DjangoModelFactory):

    class Meta:
        model = Task

    process = factory.SubFactory(SampleProcessFactory)
    flow_task = SampleFlow.start
    owner = factory.SubFactory(UserFactory)
    token = 'START'

    @factory.post_generation
    def run_activations(self, create, extracted, **kwargs):
        activation = self.activate()
        if hasattr(activation, 'assign'):
            activation.assign()

tests/test_views.py

class TestSampleFlowUpdateViewTwo(TestCase):

    def setUp(self):
        self.process = SampleProcessFactory()
        self.task_owner = UserFactory()
        self.task = TaskFactory(process=self.process, 
            flow_task=SampleFlow.update_two, owner=self.task_owner)
        self.url = reverse('unittest_viewflow:sampleflow:update_two',
                       kwargs={'process_pk': self.process.pk, 'task_pk': self.task.pk})

    def test_get(self):
        self.client.force_login(self.task_owner)
        response = self.client.get(self.url)
        self.assertTrue(response.status_code, 200)

    def test_post(self):
        self.client.force_login(self.task_owner)
        data = {'_viewflow_activation-started': '1970-01-01', 'update_two': 'Update Two'}
        response = self.client.post(self.url, data=data)
        self.assertEqual(Task.objects.get(pk=self.task.pk).status, 'DONE')

For more information, you can check out this repo

unlockme
  • 3,897
  • 3
  • 28
  • 42
  • I think if you replace `self`(1st argument) in the run_activations to `task` or `obj` will make much more sense, right now `self` is a bit confused with instance method of `TaskFactory` – James Lin Jun 29 '23 at 00:08
1

To test the flow in the TestClass you can use django TestClient as usual. Just repeat the same steps as you do manually in a browser.

You can check for the example the HelloWorld demo tests - https://github.com/viewflow/cookbook/blob/master/helloworld/demo/tests.py

class Test(TestCase):
    def setUp(self):
        User.objects.create_superuser('admin', 'admin@example.com', 'password')
        self.client.login(username='admin', password='password')

    def testApproved(self):
        self.client.post(
            '/workflow/helloworld/helloworld/start/',
            {'text': 'Hello, world',
             '_viewflow_activation-started': '2000-01-01'}
        )

        self.client.post(
            '/workflow/helloworld/helloworld/1/approve/2/assign/'
        )

        self.client.post(
            '/workflow/helloworld/helloworld/1/approve/2/',
            {'approved': True,
             '_viewflow_activation-started': '2000-01-01'}
        )

        process = Process.objects.get()

        self.assertEquals('DONE', process.status)
        self.assertEquals(5, process.task_set.count())
kmmbvnr
  • 5,863
  • 4
  • 35
  • 44
  • You have to be careful with hard coding primary keys. If it so happens that you run multiple flow tests at the same time. The primary keys will be different than what you expect and your tests will fail. Either you reset sequences to ensure that the primary key is reset (which is expensive) see. https://docs.djangoproject.com/en/2.2/topics/testing/advanced/#django.test.TransactionTestCase.reset_sequences or you query for the task and process and use their pks e.g.. task_pk = Task.objects.get(flow_task=HelloWorldFlow.approve).pk. – unlockme May 24 '20 at 17:05
1

See this answer which shows how to add a programmatic start point to the Flow alongside the normal "manual" start point:

class MyRunFlow(flow.Flow):
    process_class = Run

    start = flow.Start(ProcessCreate, fields=['schedule']). \
        Permission(auto_create=True). \
        Next(this.wait_data_collect_start)
    start2 = flow.StartFunction(process_create). \
        Next(this.wait_data_collect_start)

Note the important point is that process_create has the Process object and this code must programmatically set up the same fields that the manual form submission does via the fields specification to ProcessCreate:

@flow_start_func
def process_create(activation: FuncActivation, **kwargs):
    #
    # Update the database record.
    #
    db_sch = Schedule.objects.get(id=kwargs['schedule'])
    activation.process.schedule = db_sch # <<<< Same fields as ProcessCreate
    activation.process.save()
    #
    # Go!
    #
    activation.prepare()
    with Context(propagate_exception=False):
        activation.done()
    return activation

It is useful to note that once you start a flow programatically, any non-manual tasks in the sequence are automatically performed

There is an import caveat which is not mentioned about error handling in a sequence of non-manual tasks which I describe here, and for which I give a partial answer (I don't know the full answer, which is why the question is posted!); here that is the reason for the with Context() part.

The first answer to the original thread by @kmmbvnr also contains a hint on how to subsequently programmatically manipulate tasks. So, when your flow gets to a manual task, you can assign it and so on.

Shaheed Haque
  • 644
  • 5
  • 14