Version 5.0.13

This commit is contained in:
Neo committed 2025-12-19 19:04:57 -08:00
1 parent b89858b2ff
commit 5efe744c5f
1407 files changed
+69690 -30177

No files matched your search

+160 -17
View File
@@ -21,7 +21,9 @@ use IPS\Db;
use IPS\Helpers\Chart;
use IPS\Helpers\Form;
use IPS\Helpers\Form\Date;
use IPS\Helpers\Form\Select;
use IPS\Helpers\Form\Text;
use IPS\Helpers\Form\YesNo;
use IPS\Http\Url;
use IPS\IPS;
use IPS\Member;
@@ -30,12 +32,19 @@ use IPS\Request;
use IPS\Session;
use IPS\Theme;
use Throwable;
use UnderflowException;
use function array_merge;
use function defined;
use function explode;
use function file_get_contents;
use function in_array;
use function is_array;
use function is_numeric;
use function is_string;
use function iterator_to_array;
use function json_encode;
use function mb_substr;
use function var_export;
use const IPS\TEMP_DIRECTORY;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
@@ -183,6 +192,13 @@ abstract class Dynamic extends Chart
* @brief Prefix passed to tables
*/
public string $tableLangPrefix = '';
/**
* When downloading chart collections from the saved reports page, this indicates the type that the chart should load as by default
*
* @var string|null
*/
public static null|string $mergedChartType = null;
/**
* Constructor
@@ -200,12 +216,38 @@ abstract class Dynamic extends Chart
*/
public function __construct( Url $url, string $title='', array $options=array(), string $defaultType='AreaChart', string $defaultTimescale='monthly', array $defaultTimes=array( 'start' => 0, 'end' => 0 ), string $identifier='', DateTime $minimumDate=null )
{
if ( isset( static::$mergedChartType ) )
{
$defaultType = static::$mergedChartType;
}
/* If we are deleting a chart, just do that now and redirect */
if( isset( Request::i()->deleteChart ) )
{
Session::i()->csrfCheck();
Db::i()->delete( 'core_saved_charts', array( 'chart_id=? and chart_member=?', Request::i()->deleteChart, Member::loggedIn()->member_id ) );
Output::i()->redirect( Request::i()->url()->stripQueryString( array( 'deleteChart', 'chartId', 'csrfKey' ) ), 'chart_deleted' );
$where = array( 'chart_id=? and (chart_member IS NULL OR chart_member=0 OR chart_member=?)', Request::i()->deleteChart, Member::loggedIn()->member_id );
try
{
$reportID = Db::i()->select( "chart_report_id", "core_saved_charts", $where )->first();
}
catch ( UnderflowException )
{
$reportID = null;
}
Db::i()->delete( 'core_saved_charts', $where );
$url = Request::i()->url()->stripQueryString( array( 'deleteChart', 'chartId', 'csrfKey', 'wasConfirmed' ) );
if ( isset( $reportID ) )
{
$url = $url->setQueryString( "report_id", $reportID );
}
if ( Request::i()->do === 'getChart' )
{
$url = $url->stripQueryString([ 'do' ]);
}
Output::i()->redirect( $url, 'chart_deleted' );
}
if ( !isset( $options['chartArea'] ) )
@@ -351,7 +393,7 @@ abstract class Dynamic extends Chart
}
/* If we have requested a saved chart, load its filters */
if( isset( Request::i()->chartId ) AND Request::i()->chartId != '_default' AND !isset( Request::i()->filters ) )
if( isset( Request::i()->chartId ) AND is_numeric( Request::i()->chartId ) AND !isset( Request::i()->filters ) )
{
foreach( $this->loadAvailableChartTabs() as $chart )
{
@@ -414,7 +456,7 @@ abstract class Dynamic extends Chart
try
{
/* Generate a form so we can save our filters as a new saved chart */
$this->form = new Form;
$this->form = new Form( 'form_' . $this->identifier );
$this->form->class = 'ipsForm--vertical ipsForm--chart-custom-filters';
if ( $this->customFiltersForm )
@@ -429,18 +471,18 @@ abstract class Dynamic extends Chart
}
/* If we have a sub selector from a node form, add on the identifier and pass off to the form to show the ajax */
if ( Request::i()->_nodeSelectName )
if ( isset( Request::i()->_nodeSelectName ) and Request::i()->_nodeSelectName )
{
Request::i()->_nodeSelectName = $this->identifier . Request::i()->_nodeSelectName;
return (string) $this->getCustomFiltersForm();
}
/* We have an existing chart ID */
if( isset( Request::i()->chartId ) AND Request::i()->chartId != '_default' )
if( isset( Request::i()->chartId ) AND is_numeric( Request::i()->chartId ) )
{
$title = '';
foreach( $this->loadAvailableChartTabs() as $chart )
foreach( $this->loadSavedCharts() as $chart )
{
if( $chart['chart_id'] == Request::i()->chartId )
{
@@ -451,7 +493,7 @@ abstract class Dynamic extends Chart
$custom = array();
$chartFilters = ( isset( Request::i()->chartFilters ) and Request::i()->chartFilters ) ? Request::i()->chartFilters : array();
$this->timescale = ( isset( Request::i()->timescale ) and Request::i()->timescale[ $this->identifier ] ) ? Request::i()->timescale[ $this->identifier ] : $chart['chart_timescale'] ?? 'monthly';
$this->timescale = ( isset( Request::i()->timescale ) and in_array( Request::i()->timescale[ $this->identifier ], ['hourly', 'daily', 'monthly', 'weekly'] ) ) ? Request::i()->timescale[ $this->identifier ] : $chart['chart_timescale'] ?? 'monthly';
foreach( Request::i() as $key => $value )
@@ -483,7 +525,45 @@ abstract class Dynamic extends Chart
/* We are not viewing a saved chart */
else
{
$this->form->add( new Text( 'custom_chart_title', NULL, TRUE ) );
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'admin_stats.js', 'core' ) );
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'system/statistics.css', 'core', 'admin' ) );
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'system/reports.css', 'core', 'admin' ) );
$reports = iterator_to_array( Db::i()->select( "*", "core_reports", order: "report_title ASC" ) );
$reportOptions = [];
$defaultOption = false;
foreach( $reports as $row )
{
$defaultOption = $defaultOption ?: "report_" . $row["id"];
$reportOptions["report_" . $row['id']] = $row['report_title'];
}
$noTabs = (bool) $this->extension?->noTabs;
$this->form->add( new YesNo( "statsreports_show_all_members", $noTabs, false, ['disabled' => $noTabs] ) );
$globalTogglesOn = ['statsreports_report_select'];
if ( empty( $reports ) )
{
$globalTogglesOn[] = "statsreports_report_new_title";
}
$this->form->add( new YesNo( "statsreports_include_in_report", false, false, [ "togglesOn" => $globalTogglesOn ] ) );
// if there are saved reports, allow user to select one of them
if ( !empty( $reportOptions ) )
{
// todo an autocomplete field is probably a better UX when there could be dozens of reports saved
$this->form->add( new Select( "statsreports_report", $defaultOption, false, [
"unlimited" => false,
"unlimitedLang" => "statsreports_report_new",
"unlimitedToggles" => [
'statsreports_report_new_title'
],
"options" => $reportOptions,
], id: 'statsreports_report_select' ) );
}
$this->form->add( new Text( "statsreports_report_new_title", $this->title, null, id: "statsreports_report_new_title" ) );
$this->form->add( new Text( 'custom_chart_title', $this->title, true ) );
if( $values = $this->form->values() )
{
@@ -498,14 +578,37 @@ abstract class Dynamic extends Chart
$chartFilters = ( isset( Request::i()->chartFilters ) and Request::i()->chartFilters ) ? Request::i()->chartFilters : array();
/* Store the new chart */
$id = Db::i()->insert( 'core_saved_charts', array(
'chart_member' => Member::loggedIn()->member_id,
'chart_controller' => Request::i()->app . '_' . Request::i()->module . '_' . Request::i()->controller . ( Request::i()->tab ? '_' . Request::i()->tab : '' ),
// is it saved to a report?
$reportID = null;
if ( @$values['statsreports_include_in_report'] )
{
if ( isset( $values['statsreports_report'] ) and $values['statsreports_report'] )
{
$reportID = (int) explode( '_', $values['statsreports_report'], 2 )[1];
}
else
{
$title = $values['statsreports_report_new_title'];
$reportID = Db::i()->insert( "core_reports", [ "report_title" => $title] );
}
}
if ( $noTabs )
{
$values['statsreports_show_all_members'] = true;
}
$data = array(
'chart_member' => @$values['statsreports_show_all_members'] ? 0 : Member::loggedIn()->member_id, // we make the member "0" when the show to all members option is enabled
'chart_controller' => $this->getController(),
'chart_configuration' => json_encode( array_merge( $chartFilters, $custom ) ),
'chart_timescale' => Request::i()->timescale ?? $this->timescale ?? 'monthly',
'chart_title' => $values['custom_chart_title'],
) );
'chart_report_id' => $reportID
);
/* Store the new chart */
$id = Db::i()->insert( 'core_saved_charts', $data );
/* Set some input parameters */
$this->url = $this->url->setQueryString( 'chartId', $id );
@@ -526,6 +629,7 @@ abstract class Dynamic extends Chart
/* And then return the output we need */
Output::i()->json( array(
'reportURL' => ($data['chart_member'] and !$data['chart_report_id']) ? null : Url::internal( "app=core&module=overview&controller=mycharts&highlight={$id}" . ( $data['chart_report_id'] ? "&report_id={$data['chart_report_id']}" : "" ) ),
'tabHref' => $this->url->stripQueryString( 'filters' ),
'chartId' => $id,
'tabId' => md5( $this->url->acpQueryString() ),
@@ -576,6 +680,21 @@ abstract class Dynamic extends Chart
}
}
/**
* Get the "controller" to use when saving this chart
*
* @return string
*/
public function getController() : string
{
if ( is_string( $this->extension?->controller ) )
{
return $this->extension->controller;
}
return Request::i()->app . '_' . Request::i()->module . '_' . Request::i()->controller . ( Request::i()->tab ? '_' . Request::i()->tab : '' );
}
/**
* @brief Show filter tabs
*/
@@ -586,6 +705,8 @@ abstract class Dynamic extends Chart
*/
protected ?array $availableChartTabs = NULL;
protected ?array $savedCharts = null;
/**
* @Brief Extension
*/
@@ -617,19 +738,35 @@ abstract class Dynamic extends Chart
{
if ( $this->availableChartTabs === null )
{
if ( $this->extension !== null )
if ( $this->extension?->noTabs )
{
$this->availableChartTabs = iterator_to_array( Db::i()->select( '*', 'core_saved_charts', ['chart_member=? AND chart_controller=?', Member::loggedIn()->member_id, $this->extension->controller] ) );
$this->availableChartTabs = [];
}
else
{
$this->availableChartTabs = iterator_to_array( Db::i()->select( '*', 'core_saved_charts', ['chart_member=? AND chart_controller=?', Member::loggedIn()->member_id, Request::i()->app . '_' . Request::i()->module . '_' . Request::i()->controller . ( Request::i()->tab ? '_' . Request::i()->tab : '' )] ) );
$this->availableChartTabs = iterator_to_array( Db::i()->select( '*', 'core_saved_charts', ['chart_member=? AND chart_controller=? AND chart_report_id IS NULL', Member::loggedIn()->member_id, $this->getController()] ) );
}
}
return $this->availableChartTabs;
}
/**
* Load and return available chart tabs
*
* @return array|null
*/
protected function loadSavedCharts() : ?array
{
if ( $this->savedCharts === null )
{
$this->savedCharts = iterator_to_array( Db::i()->select( '*', 'core_saved_charts', ['(chart_member=? OR chart_member=0 OR chart_member IS NULL) AND chart_controller=?', Member::loggedIn()->member_id, $this->getController()] ) );
}
return $this->savedCharts;
}
/**
* Set extension
*
@@ -672,6 +809,12 @@ abstract class Dynamic extends Chart
*/
protected function initData(): array
{
/* The JS can set this to "undefined" so let's make sure it's a valid value */
if ( !in_array( $this->timescale, ["none", "hourly", "daily", "monthly", "weekly"] ) )
{
$this->timescale = "monthly";
}
/* Init data */
$data = array();
if ( $this->start AND $this->timescale !== 'none' )