WIP: general: run workflow as after job (#81260) #1365

Draft
fpeters wants to merge 1 commits from wip/81260-workflow-in-progress into main
9 changed files with 183 additions and 39 deletions

View File

@ -386,7 +386,11 @@ class FormFillPage(PublicFormFillPage):
self.clean_submission_context()
filled.refresh_from_storage()
filled.record_workflow_event('backoffice-created')
url = filled.perform_workflow()
if get_publisher().has_site_option('perform-workflow-as-job'):
url = None
filled.perform_workflow_as_job()
else:
url = filled.perform_workflow()
return self.redirect_after_submitted(url, filled)
def redirect_after_submitted(self, url, filled):

View File

@ -27,11 +27,12 @@ import urllib.parse
import unidecode
from django.utils.html import strip_tags
from django.utils.timezone import localtime, make_naive
from quixote import get_publisher, get_request, get_session
from quixote import get_publisher, get_request, get_response, get_session
from quixote.errors import RequestError
from quixote.html import htmltext
from quixote.http_request import Upload
from wcs.qommon.afterjobs import AfterJob
from wcs.sql_criterias import And, Contains, Equal, Intersects, Null, StrictNotEqual
from .qommon import _, misc
@ -306,6 +307,7 @@ class FormData(StorableObject):
geolocations = None
statistics_data = None
relations_data = None
workflow_processing_afterjob_id = None
_formdef = None
@ -731,6 +733,13 @@ class FormData(StorableObject):
with push_perform_workflow(self):
return perform_items(wf_status.items, self)
def perform_workflow_as_job(self):
job = PerformWorkflowJob(label=_('Processing'), formdata=self)
job.store()
self.workflow_processing_afterjob_id = job.id
self.store()
get_response().add_after_job(job)
def perform_global_action(self, action_id, user):
from wcs.workflows import perform_items, push_perform_workflow
@ -2078,6 +2087,33 @@ class FormData(StorableObject):
raise AttributeError(attr)
class PerformWorkflowJob(AfterJob):
url = None
def __init__(self, formdata, **kwargs):
formdef = formdata._formdef
super().__init__(
formdef_class=formdef.__class__,
formdef_id=formdef.id,
formdata_id=formdata.id,
session_user_id=get_session().get_user_id() if get_session() else None,
**kwargs,
)
def execute(self):
formdef = self.kwargs['formdef_class'].get(self.kwargs['formdef_id'])
formdata = formdef.data_class().get(self.kwargs['formdata_id'])
if self.kwargs['user_id']:
user = get_publisher().user_class.get(self.kwargs['user_id'], ignore_errors=True)
with get_publisher().substitutions.freeze():
get_publisher().substitutions.feed(user)
try:
self.url = formdata.perform_workflow()
finally:
formdata.workflow_processing_afterjob_id = None
formdata.store()
class FormDetails:
# lazy object compatibility to keep form_details as part of static variables while
# generating it only if/when accessed.

View File

@ -33,6 +33,7 @@ from wcs.api_utils import get_query_flag, get_user_from_api_query_string, is_url
from wcs.blocks import BlockSubWidget, BlockWidget
from wcs.fields import FileField
from wcs.qommon.admin.texts import TextsDirectory
from wcs.qommon.afterjobs import AfterJob
from wcs.qommon.upload_storage import get_storage_object
from wcs.utils import record_timings
from wcs.wf.editable import EditableWorkflowStatusItem
@ -161,7 +162,16 @@ class FormTemplateMixin:
class FormStatusPage(Directory, FormTemplateMixin):
_q_exports_orig = ['', 'download', 'json', 'action', 'live', 'tempfile', 'tsupdate']
_q_exports_orig = [
'',
'download',
'json',
'action',
'live',
'tempfile',
'tsupdate',
('check-workflow-progress', 'check_workflow_progress'),
]
_q_extra_exports = []
form_page_class = None
@ -254,6 +264,16 @@ class FormStatusPage(Directory, FormTemplateMixin):
get_response().set_content_type('application/json')
return json.dumps({'ts': str(self.filled.last_update_time.timestamp())})
def check_workflow_progress(self):
self.check_auth()
get_request().ignore_session = True
get_response().set_content_type('application/json')
try:
afterjob = AfterJob.get(get_request().form.get('id') or '-')
except KeyError:
return json.dumps({'err': 1})
return json.dumps({'err': 0, 'status': afterjob.status, 'url': afterjob.url})
def tempfile(self):
# allow for file uploaded via a file widget in a workflow form
# to be downloaded back from widget
@ -676,6 +696,13 @@ class FormStatusPage(Directory, FormTemplateMixin):
r += htmltext(bottom_workflow_messages)
if self.filled.workflow_processing_afterjob_id:
form = None
locked = True
r += htmltext('<div class="infonotice"><p>')
r += str(_('Currently processing.'))
r += htmltext('</p>')
locked = False
if form:
all_visitors = get_session().get_object_visitors(self.filled)
@ -755,6 +782,9 @@ class FormStatusPage(Directory, FormTemplateMixin):
},
)
if isinstance(next_url, AfterJob):
return
if next_url:
return next_url
if form.has_errors():

View File

@ -1930,7 +1930,10 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
self.clean_submission_context()
filled.refresh_from_storage()
filled.record_workflow_event('frontoffice-created')
url = filled.perform_workflow()
if get_publisher().has_site_option('perform-workflow-as-job'):
filled.perform_workflow_as_job()
else:
url = filled.perform_workflow()
if not filled.user_id:
get_session().mark_anonymous_formdata(filled)
@ -2454,7 +2457,15 @@ class RootDirectory(AccessControlled, Directory):
class PublicFormStatusPage(FormStatusPage):
_q_exports_orig = ['', 'download', 'status', 'live', 'tempfile', 'tsupdate']
_q_exports_orig = [
'',
'download',
'status',
'live',
'tempfile',
'tsupdate',
('check-workflow-progress', 'check_workflow_progress'),
]
form_page_class = FormPage
history_templates = ['wcs/front/formdata_history.html', 'wcs/formdata_history.html']
status_templates = ['wcs/front/formdata_status.html', 'wcs/formdata_status.html']

View File

@ -148,8 +148,11 @@ class Session(QommonSession, CaptchaSession, StorableObject):
user_id = QuixoteSession.get_user(self)
return bool(user_id)
def get_user_id(self):
return super().get_user()
def get_user(self):
user_id = QuixoteSession.get_user(self)
user_id = self.get_user_id()
if user_id:
try:
user = get_publisher().user_class.get(user_id)

View File

@ -1118,3 +1118,43 @@ document.addEventListener('DOMContentLoaded', function(){
})
})
})
document.addEventListener('DOMContentLoaded', function() {
const formdata_page_div = document.getElementById('formdata-page')
if (! formdata_page_div) return
const afterjob_id = formdata_page_div.dataset.workflowProcessingAfterjobId
if (! afterjob_id) return
var wait_count = 0
var in_call = false
var wait_id = setInterval(function() {
wait_count += 1
if (! in_call && (wait_count < 20 || wait_count % 10 == 0)) {
in_call = true
fetch(`${window.location.pathname}check-workflow-progress?id=${afterjob_id}`).then((response) => {
if (! response.ok) {
in_call = false
clearInterval(wait_id)
return
}
return response.json()
}).then((json) => {
console.log('got json:', json)
if (json && json.status == 'completed') {
clearInterval(wait_id)
if (json.url) {
window.location = json.url
} else {
window.location.reload()
}
} else {
// continue
}
in_call = false
})
}
}, 200)
})

View File

@ -2289,6 +2289,7 @@ class SqlDataMixin(SqlMixin):
('auto_geoloc', 'point'),
('statistics_data', 'jsonb'),
('relations_data', 'jsonb'),
('workflow_processing_afterjob_id', 'varchar'),
]
def __init__(self, id=None):
@ -2474,6 +2475,7 @@ class SqlDataMixin(SqlMixin):
'workflow_merged_roles_dict': self.workflow_merged_roles_dict,
'statistics_data': self.statistics_data or {},
'relations_data': self.relations_data or {},
'workflow_processing_afterjob_id': self.workflow_processing_afterjob_id,
}
if self._evolution is not None and hasattr(self, '_last_update_time'):
# if evolution was loaded it may have been been modified, and last update time
@ -5148,7 +5150,7 @@ def get_period_total(
# latest migration, number + description (description is not used
# programmaticaly but will make sure git conflicts if two migrations are
# separately added with the same number)
SQL_LEVEL = (106, 'add context column to logged_errors table')
SQL_LEVEL = (107, 'add workflow_processing_afterjob_id to card/form data')
def migrate_global_views(conn, cur):
@ -5426,13 +5428,14 @@ def migrate():
continue
for formdata in formdef.data_class().select_iterator():
formdata._set_auto_fields(cur) # build digests
if sql_level < 102:
if sql_level < 107:
# 58: add workflow_merged_roles_dict as a jsonb column with
# combined formdef and formdata value.
# 69: add auto_geoloc field to form/card tables
# 80: add jsonb column to hold statistics data
# 91: add jsonb column to hold relations data
# 102: switch formdata datetime columns to timestamptz
# 107: add workflow_processing_afterjob_id to card/form data
drop_views(None, conn, cur)
for formdef in FormDef.select() + CardDef.select():
do_formdef_tables(formdef, rebuild_views=False, rebuild_global_views=False)

View File

@ -2,38 +2,52 @@
{% load i18n %}
{% block body %}
{% block session-message %}
{{session_message|safe}}
{% endblock %}
<div id="formdata-page" {% if formdata.workflow_processing_afterjob_id %}data-workflow-processing-afterjob-id="{{ formdata.workflow_processing_afterjob_id }}"{% endif %}>
{% block session-message %}
{{session_message|safe}}
{% endblock %}
{% with workflow_messages=view.workflow_messages %}
{% if workflow_messages %}
{{ workflow_messages|safe }}
{% else %}
<div id="receipt-intro">
{{ view.recorded_message|safe }}
{% if mine %}
{{ view.get_handling_role_info_text|safe }}
{% endif %}
{% if mine and formdata.formdef.enable_tracking_codes and formdata.tracking_code %}
<p id="tracking-code">
{% trans "You can get back to this page using the following tracking code:" %}
<a href="../code/{{ formdata.tracking_code }}/" data-popup>{{ formdata.tracking_code }}</a>
</p>
{% endif %}
</div>
{% if formdata.workflow_processing_afterjob_id %}
<div style="background: #eee; padding: 1em;"><div class="loader"></div><p style="text-align: center;">{% trans "Processing..." %}</p></div>
{% endif %}
{% endwith %}
{{ view.receipt|safe }}
{{ view.history|safe }}
{% with workflow_messages=view.workflow_messages %}
{% if workflow_messages %}
{{ workflow_messages|safe }}
{% else %}
<div id="receipt-intro">
{{ view.recorded_message|safe }}
{% if mine %}
{{ view.get_handling_role_info_text|safe }}
{% endif %}
{% if mine and formdata.formdef.enable_tracking_codes and formdata.tracking_code %}
<p id="tracking-code">
{% trans "You can get back to this page using the following tracking code:" %}
<a href="../code/{{ formdata.tracking_code }}/" data-popup>{{ formdata.tracking_code }}</a>
</p>
{% endif %}
</div>
{% endif %}
{% endwith %}
{% with workflow_messages=view.bottom_workflow_messages %}
{% if workflow_messages or workflow_form %}<span id="action-zone"></span>{% endif %}
{{ workflow_messages|safe }}
{% endwith %}
{% if workflow_form %}
{{ view.actions_workflow_messages|safe }}
{{ workflow_form.render|safe }}
{% endif %}
{{ view.receipt|safe }}
{% if formdata.evolution %}
{{ view.history|safe }}
{% endif %}
{% with workflow_messages=view.bottom_workflow_messages %}
{% if workflow_messages or workflow_form %}<span id="action-zone"></span>{% endif %}
{{ workflow_messages|safe }}
{% endwith %}
{% if workflow_form and not formdata.workflow_processing_afterjob_id %}
{{ view.actions_workflow_messages|safe }}
{{ workflow_form.render|safe }}
{% endif %}
{% if formdata.workflow_processing_afterjob_id %}
<div style="background: #eee; padding: 1em;"><div class="loader"></div><p style="text-align: center;">{% trans "Processing..." %}</p></div>
{% endif %}
</div>
{% endblock %}

View File

@ -2779,7 +2779,10 @@ class WorkflowStatus(SerieOfActionsMixin):
if evo.status:
filled.status = evo.status
filled.store()
return filled.perform_workflow()
if get_publisher().has_site_option('perform-workflow-as-job'):
return filled.perform_workflow_as_job()
else:
return filled.perform_workflow()
def get_subdirectories(self, formdata):
subdirectories = []