Skip to content

Commit

Permalink
Update recalc logic to be resumable
Browse files Browse the repository at this point in the history
  • Loading branch information
pupi1985 committed Aug 13, 2024
1 parent bc1a8bc commit 73a2356
Show file tree
Hide file tree
Showing 51 changed files with 2,321 additions and 1,186 deletions.
150 changes: 99 additions & 51 deletions qa-content/qa-admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,76 +19,124 @@
More about this license: http://www.question2answer.org/license.php
*/

var qa_recalc_running = 0;
const qa_recalcProcesses = new Map();

window.onbeforeunload = function(event)
{
if (qa_recalc_running > 0) {
event = event || window.event;
var message = qa_warning_recalc;
event.returnValue = message;
return message;
window.onbeforeunload = event => {
for (let [processKey, process] of qa_recalcProcesses.entries()) {
if (process.clientRunning) {
event.preventDefault();
event.returnValue = true;
}
}
};

function qa_recalc_click(state, elem, value, noteid)
/**
* @param {String} processKey
* @param {object} options - See keys and default values below
* @returns {boolean}
*/
function qa_recalc_click(processKey, options = {})
{
if (elem.qa_recalc_running) {
elem.qa_recalc_stopped = true;

options = {
forceRestart: false,
requiresServerTracking: true,
callbackStart: process => {},
callbackStop: hasFinished => {},
...options,
};

let process = qa_recalcProcesses.get(processKey) ?? {processKey: processKey};

const startButton = document.getElementById(processKey);
const continueButton = document.getElementById(processKey + '_continue');
const statusLabel = document.getElementById(processKey + '_status');

if (process.clientRunning) {
process.stopRequest = true;
} else {
elem.qa_recalc_running = true;
elem.qa_recalc_stopped = false;
qa_recalc_running++;
process = {
...process,
"startButton": startButton,
"continueButton": continueButton,
"statusLabel": statusLabel,
"clientRunning": true,
"stopRequest": false,
"options": options
};

qa_recalcProcesses.set(processKey, process);

document.getElementById(noteid).innerHTML = '';
elem.qa_original_value = elem.value;
if (value)
elem.value = value;
qa_conceal(process.continueButton);

qa_recalc_update(elem, state, noteid);
statusLabel.innerHTML = qa_langs.please_wait;
startButton.value = qa_langs.process_stop;

process.options.callbackStart(process);

qa_recalc_update(process);
}

return false;
}

function qa_recalc_update(elem, state, noteid)
function qa_recalc_update(process)
{
if (state) {
var recalcCode = elem.form.elements.code_recalc ? elem.form.elements.code_recalc.value : elem.form.elements.code.value;
qa_ajax_post(
'recalc',
{state: state, code: recalcCode},
function(lines) {
if (lines[0] == '1') {
if (lines[2])
document.getElementById(noteid).innerHTML = lines[2];

if (elem.qa_recalc_stopped)
qa_recalc_cleanup(elem);
else
qa_recalc_update(elem, lines[1], noteid);

} else if (lines[0] == '0') {
document.getElementById(noteid).innerHTML = lines[1];
qa_recalc_cleanup(elem);

} else {
const recalcCode = process.startButton.form.elements.code.value;

qa_ajax_post(
'recalc',
{
process: process.processKey,
forceRestart: process.options.forceRestart,
code: recalcCode
},
function (lines) {
const result = lines[0] ?? '0';
const message = lines[1] ?? null;
const nextProcessKey = lines[2] ?? null;
const hasFinished = (lines[3] ?? '0') === '1';

switch (result) {
case '1':
if (message !== null) {
process.statusLabel.innerHTML = message;
}

process.serverProcessPending = process.options.requiresServerTracking ? !hasFinished : false;
if (hasFinished || process.stopRequest) {
process.clientRunning = false;
qa_recalc_cleanup(process, hasFinished);
} else {
process.options.forceRestart = false;
qa_recalc_update(process);
}
break;
case '0':
process.statusLabel.innerHTML = message;
process.clientRunning = false;
qa_recalc_cleanup(process);
break;
default:
qa_ajax_error();
qa_recalc_cleanup(elem);
}
process.clientRunning = false;
qa_recalc_cleanup(process);
}
);
} else {
qa_recalc_cleanup(elem);
}
}
);
}

function qa_recalc_cleanup(elem)
function qa_recalc_cleanup(process, hasFinished = false)
{
elem.value = elem.qa_original_value;
elem.qa_recalc_running = null;
qa_recalc_running--;
process.options.callbackStop(hasFinished);

if (process.options.requiresServerTracking && process.serverProcessPending) {
process.startButton.value = qa_langs.process_restart;
process.statusLabel.innerHTML = qa_langs.process_unfinished;
qa_reveal(process.continueButton);
} else {
process.startButton.value = qa_langs.process_start;
qa_conceal(process.continueButton);
}
}

function qa_mailing_start(noteid, pauseid)
Expand Down
123 changes: 123 additions & 0 deletions qa-include/Q2A/Admin/Recalc/AbstractProcessManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
More about this license: http://www.question2answer.org/license.php
*/

abstract class Q2A_Admin_Recalc_AbstractProcessManager
{
/** @var string */
protected $stateOption;

/** @var int */
protected $currentStepIndex = 0;

/** @var array */
protected $steps;

/**
* @return array Return step and process state data
*/
public function execute($forceRestart = false)
{
$newStep = false;

try {
if ($forceRestart) {
throw new Exception('Force exception');
}

$step = $this->loadState();

if ($step->isFinished()) {
$this->currentStepIndex++;

if ($this->currentStepIndex >= count($this->steps)) {
$this->clearState();

return [
'process_finished' => true,
'message' => qa_lang('admin/process_complete'),
];
}

$newStep = true;
$step = $this->getCurrentStepInstance();
}
} catch (Exception $e) {
$step = $this->getCurrentStepInstance();
$newStep = true;
}

if ($newStep) {
$step->setup();
} else {
$step->execute();
}

$result = [
'step_state' => $step->asArray(),
'step_index' => $this->currentStepIndex,
'process_finished' => false,
];
$this->saveState($result);

$result['message'] = $step->getMessage();

return $result;
}

/**
* @throws Exception
*/
private function loadState()
{
$state = qa_opt($this->stateOption);
$state = json_decode($state, true);

if (!isset($state['step_index'])) {
throw new Exception('Nothing to load');
}

$this->currentStepIndex = $state['step_index'];

$step = $this->getCurrentStepInstance();
$step->loadFromJson($state['step_state']);

return $step;
}

private function saveState($state)
{
qa_opt($this->stateOption, json_encode($state));
}

private function clearState()
{
qa_opt($this->stateOption, '');
}

/**
* @return Q2A_Admin_Recalc_AbstractStep
*/
protected function getCurrentStepInstance()
{
// Make sure the step index to instantiate is valid
$this->currentStepIndex = min(max(0, $this->currentStepIndex), count($this->steps) - 1);

return new $this->steps[$this->currentStepIndex];
}
}
84 changes: 84 additions & 0 deletions qa-include/Q2A/Admin/Recalc/AbstractStep.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
More about this license: http://www.question2answer.org/license.php
*/

abstract class Q2A_Admin_Recalc_AbstractStep
{
/** @var int|null */
protected $processedItems = 0;

/** @var int|null */
protected $totalItems;

/** @var mixed */
protected $nextItemId = 0;

/** @var mixed */
protected $lastProcessedItemId = 0;

/** @var bool */
protected $isFinished = false;

/** @var string */
protected $messageLangId = '';

abstract public function setup();

abstract public function execute();

/**
* @return array
*/
public function asArray()
{
return [
'is_finished' => $this->isFinished,
'processed_items' => $this->processedItems,
'total_items' => $this->totalItems,
'next_item_id' => $this->nextItemId,
'last_processed_item_id' => $this->lastProcessedItemId,
];
}

public function loadFromJson($state)
{
$this->isFinished = $state['is_finished'];
$this->processedItems = $state['processed_items'];
$this->totalItems = $state['total_items'];
$this->nextItemId = $state['next_item_id'];
$this->lastProcessedItemId = $state['last_processed_item_id'];
}

/**
* @return bool
*/
public function isFinished()
{
return $this->isFinished;
}

public function getMessage()
{
require_once QA_INCLUDE_DIR . 'app/format.php';

return strtr(qa_lang($this->messageLangId), array(
'^1' => qa_format_number($this->processedItems),
'^2' => qa_format_number($this->totalItems),
));
}
}
Loading

0 comments on commit 73a2356

Please sign in to comment.