Search code examples
djangodjango-viewflow

How to customise task context (process_data cards) in django-viewflow


What I want

I'm building an approval workflow using django-viewflow and django-material.

The individual tasks are rendered as a main form with context on a very narrow column on the right-hand side.

I want to change the layout so that the task context (the detail views of all involved model instances) is better readable to the user, and also customise which fields are shown (eg. exclude a user's password hash).

Where I'm stuck

  • Is there a way to override which data is available as process_data short of overriding viewflow's get_model_display_data and include_process_data? E.g. I'd like to have the related instance's __str__() as title.
  • Does viewflow have any canonical way to provide individual detail card templates? My alternative would be to completely re-work the contents of the process_data sidebar using context['process'] as the central instance, but that would tie the templates to the data model.
  • Are django-material formsets with read-only fields and a custom layout the answer I'm after? (I will try this out as soon as I have a viewflow pro license.)

I'd be grateful on any pointers here.

What I've tried

I'm overriding/extending the viewflow templates. As per templatetag include_process_data, the template process_data.html supplies the column of model instance detail cards, fed by data from get_model_display_data.

It's e.g. easy to override process_data.html to change the cards into a MaterializeCSS collapsible list:

{% load i18n viewflow material_frontend viewflow_frontend %}

<!--
    This is template "APP_NAME/PROCESS_NAME/process_data.html" overriding "viewflow/flow/process_data.html".
    We override instead of extending as "viewflow/flow/process_data.html" has no content block.super

    Changes:

    * Collapsible list of process context instead of cards.
-->


<script type="text/javascript">
    $(document).ready(function () { $('.collapsible').collapsible(); });
</script>

<ul class="collapsible">
    {% for root, fields, root_url in process_data %}
    <li>
        <div class="collapsible-header">
            <span class="card-title">{{ root }} #{{ process.pk }}</span>
            {% if root_url and request.user.is_staff %}
            <a href="{{ root_url }}" class="card-edit" target="_blank" data-turbolinks="false" style="float:right">
                &nbsp;{% trans 'edit' %}
            </a>
            {% endif %}
        </div>

        <div class="collapsible-body process_data_content">
            <dl class="dl-horizontal">
                {% for name, value in fields %}
                <dt>{{ name }}:</dt>
                <dd>
                    {% if value is True %}{% trans 'Yes' %}{% else %}
                    {% if value is False %}{% trans 'No' %}{% else %}
                    {% if value and value.url %}<a href="{{ value.url }}" target="_blank">{{ value.name }}</a>{% else %}
                    {{ value }}{% endif %}{% endif %}{% endif %}
                </dd>
                {% endfor %}
            </dl>
        </div>

    {% if not hide_active_tasks and forloop.counter == 1 and process.active_tasks %}
    <li>
        <div class="collapsible-header">
            <span class="card-title">{% trans 'Active tasks' %}</span>
        </div>

        <div class="collapsible-body process_data_content">
            <table>
                <thead>
                    <tr>
                        <th>{% trans 'Task' %}</th>
                        <th>{% trans 'Owner' %}</th>
                    </tr>
                </thead>
                <tbody>
                    {% for task in process.active_tasks %}
                    {% if task.flow_task.task_type == 'HUMAN' or task.flow_task.task_type == 'JOB' %}
                    <tr>
                        <td>
                            <a href="{% flowurl task user=request.user %}?{{ request|query_back }}">{{ task.flow_task}}/#{{ task.pk}}</a>
                        </td>
                        <td>{{ task.owner|default:"" }}</td>
                    </tr>
                    {% endif %}
                    {% endfor %}
                </tbody>
            </table>
        </div>
    </li>
    {% endif %}

    </li>
    {% endfor %}
</ul>

Overriding the template viewflow/flow/task.html with my own APP_NAME/PROCESS_NAME/TASK_NAME.html template at the cost of including model-specific information can show e.g. customised cards of related instances over the main form. I would refactor the individual cards into their own templates.

{% extends "viewflow/flow/task.html" %}

{% block left-panel__top %}
<!--
    This is template "APP_NAME/PROCESS_NAME/TASK_NAME.html" extending "viewflow/flow/task.html".
-->
<!-- Help specific to this step goes here. -->
{% with activation.process as p %}

<div class="row">
    <h3>Nested formsets</h3>
    <p>These cards are placeholders for formsets nested within the main application form.</p>
    <p>Nested formsets and the main application form can be updated at the same time and have exactly one submission button.</p>
</div>

<div class="row">

    <!-- Organisation Questions: Answers -->
    <!-- TODO refactor to template include -->
    {% for x in p.organisationanswer_set.all %}
    <div class="col s12 m6 xl4">
      <div class="card blue-grey darken-1">
        <div class="card-content white-text">
          <span class="card-title">
            {{ x.question.question }}
          </span>
          <p><strong>Your answer:</strong> {{ oa.answer }}</p>
        </div>
        <div class="card-action">
          <a href="{{ x.absolute_admin_url }}" target="_" title="Provide an answer to this question">Provide an answer</a>
        </div>
      </div>
    </div>
    {% endfor %}

    <!-- Dataset Questions: Answers -->
    {% for x in p.datasetanswer_set.all %}
    <div class="col s12 m6 xl4">
        <div class="card blue-grey darken-1">
          <div class="card-content white-text">
            <span class="card-title">
              {{ x.question.question }}
            </span>
            <p><strong>Your answer:</strong> {{ x.answer }}</p>
          </div>
          <div class="card-action">
            <a href="{{ x.absolute_admin_url }}" target="_" title="Provide an answer to this question">Provide an answer</a>
          </div>
        </div>
    </div>
    {% endfor %}

    <!-- Approvals: Receipts - TODO show this in task after custodian approval -->
    {% for x in p.approvalreceipt_set.all %}
    <div class="col s12 m6 xl4">
        <div class="card blue-grey darken-1">
            <div class="card-content white-text">
            <span class="card-title">
                {{ x.approval }}
            </span>
            <p><strong>Your approval receipt:</strong> {{ x.receipt }}</p>
            </div>
            <div class="card-action">
            <a href="{{ x.absolute_admin_url }}" target="_" title="Provide an answer to this question">Provide an answer</a>
            </div>
        </div>
    </div>
    {% endfor %}


</div>


{% endwith %}

<div class="row">
    <h3>Main application form</h3>
    <p>These main application form would include the forms above as nested formsets.</p>
    <p>This note will disappear once this has been implemented.</p>
</div>
{% endblock %}

Solution

  • For the basic cases create a template named as [app_label]/[flow_label]/process_data.html For example check the shipment demo

    Viewflow is the thin workflow layer on top of the standard Django model-view-template pattern. Any customization practices for Django are valid for Viewflow

    For the complex cases it's better to not to tune some universal implementation, but create your own set of templates for Viewflow, ex cookbook/custom_ui