Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PHRAS-3768_feedback-report-per-record #4421

Merged
merged 10 commits into from
Nov 30, 2023
2 changes: 2 additions & 0 deletions bin/console
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ use Alchemy\Phrasea\Command\Task\TaskState;
use Alchemy\Phrasea\Command\Task\TaskStop;
use Alchemy\Phrasea\Command\Thesaurus\FindConceptsCommand;
use Alchemy\Phrasea\Command\Thesaurus\Translator\TranslateCommand;
use Alchemy\Phrasea\Command\Feedback\Report\FeedbackReportCommand;
use Alchemy\Phrasea\Command\UpgradeDBDatas;
use Alchemy\Phrasea\Command\User\UserApplicationsCommand;
use Alchemy\Phrasea\Command\User\UserCreateCommand;
Expand Down Expand Up @@ -180,6 +181,7 @@ $cli->command(new QueryParseCommand());
$cli->command(new QuerySampleCommand());
$cli->command(new FindConceptsCommand());
$cli->command(new TranslateCommand());
$cli->command(new FeedbackReportCommand());

$cli->command(new WorkerExecuteCommand());
$cli->command(new WorkerHeartbeatCommand());
Expand Down
24 changes: 24 additions & 0 deletions config/configuration.sample.yml
Original file line number Diff line number Diff line change
Expand Up @@ -417,3 +417,27 @@ order-manager:
download-hd:
expiration-days: null
expiration-override: false

feedback-report:
enabled: false
actions:
action_unvoted:
# if any participant has not voted, set the "incomplete" icon
status_bit: 8
value: '{% if vote.votes_unvoted > 0 %} 1 {% else %} 0 {% endif %}'

# because records not involved in a vote should not display a red or green flag, we need 2 sb
action_red:
# if _any_ vote is "no", set the red flag
status_bit: 9
value: '{% if vote.votes_no > 0 %} 1 {% else %} 0 {% endif %}'
action_green:
# if _all_ votes are "yes" (=no vote is "no"), set the green flag
status_bit: 10
value: '{% if vote.votes_no == 0 %} 1 {% else %} 0 {% endif %}'

action_log:
metadata: 'Validations'
method: "prepend"
delimiter: "\n"
value: 'Vote initated on {{ vote.created }} by {{ initiator ? initiator.getEmail() : "?" }} expired {{ vote.expired }} : {{ vote.voters_count }} participants, {{ vote.votes_unvoted }} unvoted, {{ vote.votes_no }} "no", {{ vote.votes_yes}} "yes".'
29 changes: 29 additions & 0 deletions lib/Alchemy/Phrasea/Command/Feedback/Report/Action.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Alchemy\Phrasea\Command\Feedback\Report;

use Twig_Environment;
use Twig_Template;

class Action
{
/** @var Twig_Template[] */
private $template = null;

/**
* @var array
*/
private $action_conf;

public function __construct(Twig_Environment $twig, array $action_conf)
{
$this->action_conf = $action_conf;
$this->template = $twig->createTemplate($action_conf['value']);
}

public function getValue(array $context)
{
return $this->template->render($context);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Alchemy\Phrasea\Command\Feedback\Report;


interface ActionInterface
{
function addAction(array &$actions, array $context);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Alchemy\Phrasea\Command\Feedback\Report;

use Exception;

class ConfigurationException extends Exception
{

}
234 changes: 234 additions & 0 deletions lib/Alchemy/Phrasea/Command/Feedback/Report/FeedbackReportCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
<?php

namespace Alchemy\Phrasea\Command\Feedback\Report;


use Alchemy\Phrasea\Command\Command as phrCommand;
use Alchemy\Phrasea\Core\Configuration\PropertyAccess;
use Alchemy\Phrasea\Model\Repositories\UserRepository;
use appbox;
use Doctrine\DBAL\DBALException;
use Exception;
use PDO;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
*
* @license http://opensource.org/licenses/gpl-3.0 GPLv3
* @link www.phraseanet.com
*/
class FeedbackReportCommand extends phrCommand
{
/** @var InputInterface $input */
private $input;
/** @var OutputInterface $output */
private $output;

/** @var GlobalConfiguration */
private $config;

public function configure()
{
$this->setName('feedback:report')
->setDescription('Report ended feedback results (votes) on records (set status-bits)')
->addOption('report', null, InputOption::VALUE_REQUIRED, "Report output format (all|condensed)", "all")
->addOption('dry', null, InputOption::VALUE_NONE, "list translations but don't apply.", null)
->setHelp("")
;
}

/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
* @throws DBALException
* @throws \Throwable
*/
protected function doExecute(InputInterface $input, OutputInterface $output)
{
// add cool styles
$style = new OutputFormatterStyle('black', 'yellow'); // , array('bold'));
$output->getFormatter()->setStyle('warning', $style);

$this->input = $input;
$this->output = $output;

// config must be ok
//
try {
$this->config = new GlobalConfiguration(
$this->getConf(),
$this->container['twig'],
$this->container['phraseanet.appbox'],
$input->getOption('dry'),
$input->getOption('report')
);
}
catch(Exception $e) {
$output->writeln(sprintf("<error>missing or bad configuration: %s</error>", $e->getMessage()));

return -1;
}

if(!$this->config->isEnabled()) {
$output->writeln(sprintf("<info>configuration is not enabled</info>"));

return 0;
}


$appbox = $this->getAppBox();

$sql_update = "UPDATE `BasketElements` SET `vote_expired` = :expired WHERE `id` = :id";
$stmt_update = $appbox->get_connection()->prepare($sql_update);

$sql_select = "SELECT * FROM (
SELECT q1.*,
COUNT(bp.id) AS `voters_count`,
SUM(IF(ISNULL(`agreement`), 1 , 0)) AS `votes_unvoted`,
SUM(IF((`agreement`=0), 1, 0)) AS `votes_no`,
SUM(IF((`agreement`=1), 1, 0)) AS `votes_yes`
FROM (
SELECT SUBSTRING_INDEX(GROUP_CONCAT(b.`id` ORDER BY `vote_expires` DESC), ',', 1) AS `basket_id`,
b.`vote_created` AS `created`, b.`vote_initiator_id`,
MAX(b.`vote_expires`) AS `expired`, be.`id` AS `be_id`, be.`vote_expired` AS `be_vote_expired`,
be.`sbas_id`, be.`record_id`, CONCAT(be.`sbas_id`, '_', be.`record_id`) AS `sbid_rid`
FROM `BasketElements` AS be INNER JOIN `Baskets` AS b ON b.`id`=be.`basket_id`
WHERE b.`vote_expires` < NOW()
GROUP BY `sbid_rid`
) AS q1
INNER JOIN `BasketParticipants` AS bp ON bp.`basket_id`=q1.`basket_id`
LEFT JOIN `BasketElementVotes` AS bv ON bv.`participant_id`=bp.`id` AND bv.`basket_element_id`=`be_id`
GROUP BY q1.`sbid_rid`
HAVING ISNULL(`be_vote_expired`) OR `expired` > `be_vote_expired`
) AS q2 ORDER BY basket_id, record_id";

$last_basket_id = null;
$condensed = null;
$vote_initiator = null;
$stmt_select = $appbox->get_connection()->query($sql_select);
while ($row = $stmt_select->fetch(PDO::FETCH_ASSOC)) {
if($row['basket_id'] !== $last_basket_id) {
$this->outputCondensed($condensed);
$condensed = [
'voters_count' => $row['voters_count'],
'records_count' => 0,
'votes_unvoted' => 0,
'votes_no' => 0,
'votes_yes' => 0,
];

$vote_initiator = $this->findUser($row['vote_initiator_id']);

$this->output->writeln(sprintf("basket: %s, initated on %s by %s (%s), expired %s",
$last_basket_id = $row['basket_id'],
$row['created'],
$row['vote_initiator_id'],
$vote_initiator ? $vote_initiator->getEmail() : "<error>unknown</error>",
$row['expired'])
);
}
if($this->config->getReportFormat() === 'all') {
$this->output->writeln(sprintf("\tdatabox: %s, record id: %s", $row['sbas_id'], $row['record_id']));
}

if( ($databox = $this->config->getDatabox($row['sbas_id'])) === null) {
$this->output->writeln(sprintf("\t\t<error>unknown databox</error> (ignored)"));
continue;
}

try {
$record = $databox->get_record($row['record_id']);
}
catch(Exception $e) {
$this->output->writeln(sprintf("\t\t<error>unknown record</error> (ignored)"));
continue;
}

$condensed['records_count']++;
foreach(['votes_unvoted', 'votes_no', 'votes_yes'] as $k) {
if($this->config->getReportFormat() !== 'condensed') {
$this->output->writeln(sprintf("\t\t%s: %s", $k, $row[$k]));
}
$condensed[$k] += $row[$k];
}

$setMetasActions = [];
foreach($this->config->getActions($databox) as $action) {
$action->addAction(
$setMetasActions,
[
'initiator' => $vote_initiator,
'vote' => $row,
]
);
}

if(count($setMetasActions) > 0) {
$jsActions = json_encode($setMetasActions, JSON_PRETTY_PRINT);
if($this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE) {
$this->output->writeln(sprintf("<info>JS : %s</info>", $jsActions));
}

if(!$this->config->isDryRun()) {
$record->setMetadatasByActions(json_decode($jsActions));
}
}
if(!$this->config->isDryRun()) {
$stmt_update->execute([
':expired' => $row['expired'],
':id' => $row['be_id']
]);
}
}
$this->outputCondensed($condensed);
$stmt_select->closeCursor();

return 0;
}

/**
* @return appbox
*/
private function getAppBox(): appbox
{
return $this->container['phraseanet.appbox'];
}

/**
* @return PropertyAccess
*/
protected function getConf()
{
return $this->container['conf'];
}

private function findUser($user_id)
{
/** @var UserRepository $repo */
$repo = $this->container['repo.users'];
try {
return $repo->find($user_id);
}
catch (Exception $e) {
return null;
}
}

/**
* @param array|null $condensed
* @return void
*/
private function outputCondensed($condensed)
{
if($condensed !== null && $this->config->getReportFormat() === 'condensed') {
foreach($condensed as $k => $v) {
$this->output->writeln(sprintf("\t%s: %s", $k, $v));
}
}
}

}
Loading