Commit inicial - WordPress Análisis de Precios Unitarios

- WordPress core y plugins
- Tema Twenty Twenty-Four configurado
- Plugin allow-unfiltered-html.php simplificado
- .gitignore configurado para excluir wp-config.php y uploads

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-11-03 21:04:30 -06:00
commit a22573bf0b
24068 changed files with 4993111 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
use TVE\Reporting\Main;
use TVE\Reporting\Store;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Silence is golden!
}
/**
* Verify if we have the requirements
* @return bool
*/
function thrive_reporting_dashboard_can_run() {
return PHP_VERSION_ID >= 70000;
}
if ( thrive_reporting_dashboard_can_run() ) {
require_once( __DIR__ . '/inc/classes/class-main.php' );
Main::init();
}
function thrive_reporting_dashboard_register_event( $event_class ) {
Store::get_instance()->register_event( $event_class );
}
function thrive_reporting_dashboard_register_report_app( $app_class ) {
Store::get_instance()->register_report_app( $app_class );
}

View File

@@ -0,0 +1,203 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting\EventFields;
use TVE\Reporting\Event;
use TVE\Reporting\Logs;
use TVE\Reporting\Main;
use TVE\Reporting\Report_Type;
abstract class Event_Field {
/**
* @var mixed|null
*/
protected $value;
/**
* Return true/false if a report can be filtered by this field
*
* @return bool
*/
public static function can_filter_by(): bool {
return true;
}
/*
* Return true/false if we can group data by this field in a report
*/
public static function can_group_by(): bool {
return false;
}
/**
* Label used to display this field in reports
*
* @param $singular
*
* @return string
*/
public static function get_label( $singular = true ): string {
return $singular ? 'Item' : 'Items';
}
/**
* Query used to select this field from the db
*
* @param $db_col
*
* @return string
*/
public static function get_query_select_field( $db_col ): string {
return "`$db_col` AS " . static::key();
}
/**
* Identifier for the field
*
* @return string
*/
abstract public static function key(): string;
public function __construct( $value = null ) {
$this->value = $value;
}
public function get_value( $format = true ) {
return $format ? static::format_value( $this->value ) : $this->value;
}
/**
* Most of the time we store ID's in the db so this method will convert the ID in the title of the field
*
* @return string
*/
public function get_title(): string {
return $this->value === null ? 'Item' : get_the_title( $this->value );
}
/**
* @param $value
*
* @return mixed
* @deprecated
*/
public function format( $value ) {
return $value;
}
/**
* Format field value
*
* @param $value
*
* @return mixed
*/
public static function format_value( $value ) {
return $value;
}
/**
* In case we can filter by this field, what type of filter we can use
*
* @return string
*/
public static function get_filter_type(): string {
return 'multiple-select';
}
/**
* Get all filter options
*
* @return array
*/
public static function get_filter_options(): array {
return [];
}
/**
* Register rest route for filter values
*
* @param $report_type_route
* @param Report_Type|Event|string $report_type_class
*/
public static function register_options_route( $report_type_route, $report_type_class ) {
$route = '/' . $report_type_route . '/filter-data';
register_rest_route( Main::REST_NAMESPACE, $route . '/' . static::key(), [
[
'methods' => \WP_REST_Server::READABLE,
'callback' => static function ( $request ) use ( $report_type_class ): \WP_REST_Response {
$options = [];
$ids = $request->get_param( 'ids' );
$should_get_all = $request->get_param( 'all' );
if ( $should_get_all ) {
$options = static::get_filter_options();
} else {
$field_table_column = $report_type_class::get_field_table_col( static::key() );
$fields = Logs::get_instance()->get_fields( $report_type_class::key(), $field_table_column, empty( $ids ) ? [] : $ids );
foreach ( $fields as $field ) {
$field_instance = new static( $field['value'] );
$options[] = [
'id' => $field['value'],
'label' => $field_instance->get_title(),
];
}
}
return new \WP_REST_Response( $options );
},
'args' => [
'ids' => [
'type' => 'object',
],
'all' => [
'type' => 'integer',
],
],
'permission_callback' => [ __CLASS__, 'permission_callback' ],
],
] );
}
/**
* Who can access rest routes for event fields
*
* @return bool
*/
public static function permission_callback(): bool {
return current_user_can( TVE_DASH_CAPABILITY );
}
/**
* Provides label information for the charts.
*
* @param array $values
*
* @return array
*/
public static function get_label_structure( array $values = [] ) {
return [
'key' => static::key(),
'text' => static::get_label(),
'values' => $values,
];
}
/**
* Return true/false if this field contains an attached image.
*
* @return bool
*/
public static function has_image(): bool {
return false;
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting;
abstract class Event {
use Traits\Event;
abstract public static function key();
abstract public static function label();
/**
* Register event
*
* @return void
*/
final public static function register() {
Store::get_instance()->register_event( static::class );
}
/**
* Store fields values for the event
*
* @param $fields
*/
public function __construct( $fields = [] ) {
$registered_fields = static::get_registered_fields();
foreach ( $fields as $key => $value ) {
$db_col = static::get_field_table_col( $key );
if ( isset( $registered_fields[ $db_col ] ) ) {
/* in case we have defined a class for the field, save an instance */
$this->fields[ $db_col ] = new $registered_fields[ $db_col ]( $value );
} else {
$this->fields[ $key ] = $value;
}
}
}
}

View File

@@ -0,0 +1,166 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting;
use TVE\Reporting\EventFields\Event_Type;
use TVE\Reporting\EventFields\Item_Id;
use function register_rest_route;
abstract class Report_App {
abstract public static function key();
abstract public static function label();
/**
* Registered report types for this app
*
* @return Report_Type[]|Event[]
*/
public static function get_report_types(): array {
return [];
}
/**
* Register app in the store
*
* @return void
*/
public static function register() {
Store::get_instance()->register_report_app( static::class );
}
/**
* Method called after the app is registered
*
* @return void
*/
public static function after_register() {
add_action( 'rest_api_init', [ static::class, 'register_rest_routes' ] );
static::set_auto_remove_logs();
}
public static function set_auto_remove_logs() {
/* override when necessary - used for clearing Lesson and Course events */
}
/**
* When a post is deleted, we should also remove logs that belong to that specific post type
* Called from Apprentice for lessons and modules
*
* @param string $post_type
* @param array $reports
*
* @return void
*/
public static function remove_logs_on_post_delete( $post_type, $reports ) {
add_action( 'delete_post', static function ( $post_id ) use ( $post_type, $reports ) {
if ( get_post_type( $post_id ) === $post_type ) {
static::remove_report_logs( $post_id, $reports );
}
} );
}
/**
* @param $item_id
* @param $reports
*
* @return void
*/
public static function remove_report_logs( $item_id, $reports ) {
$event_types = array_map( static function ( $report_class ) {
return $report_class::key();
}, $reports );
Logs::get_instance()->delete( [
Event_Type::key() => $event_types,
Item_Id::key() => $item_id
] );
}
/**
* Register routes for each report type to get data
*
* @return void
*/
final public static function register_rest_routes() {
foreach ( static::get_report_types() as $report_type_class ) {
/** @var Report_Type|Event $report_type_class */
$route = static::key() . '/' . $report_type_class::key();
register_rest_route(
Main::REST_NAMESPACE,
'/' . $route,
[
[
'methods' => \WP_REST_Server::READABLE,
'callback' => static function ( \WP_REST_Request $request ) use ( $report_type_class ) {
$data_type = $request->get_param( 'report-data-type' );
$query = $request->get_param( 'query' ) ?? [];
$fn_name = "get_{$data_type}_data";
if ( method_exists( $report_type_class, $fn_name ) ) {
$data = $report_type_class::$fn_name( $query );
} else {
$data = [];
}
return new \WP_REST_Response( $data );
},
'args' => [
'query' => [
'type' => 'object',
'required' => false,
],
'report-data-type' => [
'type' => 'string',
'required' => true,
],
],
'permission_callback' => [ $report_type_class, 'permission_callback' ],
],
] );
$report_type_class::register_filter_routes( $route );
}
register_rest_route(
Main::REST_NAMESPACE,
'/' . static::key() . '/report-types',
[
[
'methods' => \WP_REST_Server::READABLE,
'callback' => function () {
return new \WP_REST_Response( array_map( static function ( $report_type ) {
return [
'key' => $report_type::key(),
'label' => $report_type::label(),
'group' => $report_type::get_group_by(),
'filters' => $report_type::get_filters(),
'display' => $report_type::get_display_types(),
];
}, static::get_report_types() ) );
},
'permission_callback' => [ static::class, 'permission_callback' ],
],
] );
}
/**
* Permission for accessing app rest routes
*
* @return bool
*/
public static function permission_callback(): bool {
return current_user_can( TVE_DASH_CAPABILITY );
}
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting;
use TVE\Reporting\Traits\Report;
abstract class Report_Type {
use Report;
abstract public static function key(): string;
abstract public static function label(): string;
public static function get_registered_fields(): array {
return [];
}
public static function get_registered_field() {
return null;
}
public static function get_filters(): array {
return [];
}
}

View File

@@ -0,0 +1,207 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting;
use TVE\Reporting\Shortcodes\Filter;
abstract class Shortcode {
/**
* Tag used for the shortcode
*
* @return string
*/
public static function get_tag() {
return '';
}
/**
* Extra class for container element
*
* @return string
*/
public static function get_element_class() {
return '';
}
/**
* Allowed attributes for the shortcode
*
* @return array
*/
public static function get_allowed_attr() {
return [
'report-app' => '',
'report-type' => '',
'report-title' => '',
'report-group-by' => '',
'report-size' => 'default',
'report-global-filter-fields' => '',
'report-filter-fields' => '',
'report-expanded-view' => '',
'report-has-export' => 0,
];
}
public static function add() {
add_shortcode( static::get_tag(), [ static::class, 'render' ] );
}
/**
* Display shortcode
*
* @param array $attr
* @param string $icon
*
* @return string
*/
public static function render( $attr, $icon = '' ) {
$attr = static::recursive_merge_atts( static::get_allowed_attr(), $attr );
/* get all filters used by the report */
$all_filter_fields = static::get_filter_fields( $attr['report-app'], $attr['report-type'] );
/* render only the filters the shortcode wants */
$filters = static::get_filters_html( $attr['report-app'], $attr['report-type'], $attr['report-filter-fields'], $all_filter_fields );
/* save allowed filter fields, so we can trigger change only when those change */
$attr['report-filter-fields'] = empty( $all_filter_fields ) ? '' : implode( ',', $all_filter_fields );
if ( $attr['report-global-filter-fields'] === 'none' || empty( $all_filter_fields ) ) {
/* when we don't listen to anything global */
$attr['report-global-filter-fields'] = '';
} elseif ( empty( $attr['report-global-filter-fields'] ) ) {
/* in case nothing was added, we listen to all available filter fields */
$attr['report-global-filter-fields'] = implode( ',', $all_filter_fields );
}
$content = sprintf( '<div class="thrive-reporting-shortcode %s" %s>%s</div>',
static::get_element_class(),
static::render_attr( $attr, 'data-' ),
$icon
);
return $filters . $content;
}
/**
* Group attr for shortcode render
*
* @param $attr
* @param $prefix
*
* @return string
*/
public static function render_attr( $attr, $prefix = '' ) {
$element_data = [];
foreach ( $attr as $key => $value ) {
if ( is_array( $value ) ) {
$value = str_replace( '"', "'", json_encode( $value ) );
}
$element_data[] = sprintf( '%s%s="%s"', $prefix, $key, $value );
}
return implode( ' ', $element_data );
}
/**
* Get shortcode filters to display
*
* @param string $app
* @param string $type
* @param array|string $used_filter_fields
* @param array $all_filter_fields
*
* @return string
*/
public static function get_filters_html( $app, $type, $used_filter_fields = 'all', $all_filter_fields = [] ) {
if ( $used_filter_fields !== 'all' ) {
$used_filter_fields = empty( $used_filter_fields ) ? [] : explode( ',', $used_filter_fields );
}
$filters = '';
if ( ! empty( $used_filter_fields ) ) {
foreach ( $all_filter_fields as $field_key ) {
if ( $used_filter_fields === 'all' || in_array( $field_key, $used_filter_fields, true ) ) {
$filters .= Filter::render( [
'report-app' => $app,
'report-type' => $type,
'field-key' => $field_key,
] );
}
}
if ( ! empty( $filters ) ) {
$filters = sprintf( '<div class="thrive-reporting-filter-wrapper" >%s </div >', $filters );
}
}
return $filters;
}
/**
* Fields used by the shortcode filters
*
* @param $app
* @param $type
*
* @return array|int[]|string[]
*/
public static function get_filter_fields( $app, $type ) {
$report_type_class = Store::get_instance()->get_report_type( $app, $type );
return $report_type_class === null ? [] : array_keys( $report_type_class::get_filters() );
}
/**
* Recursive merge for attributes used by shortcode
*
* @param $default
* @param $attr
* @param $only_default_keys
*
* @return array|mixed
*/
public static function recursive_merge_atts( $default, $attr, $only_default_keys = true ) {
$out = $only_default_keys ? [] : $attr;
foreach ( $default as $key => $value ) {
if ( isset( $attr[ $key ] ) ) {
if ( is_array( $value ) ) {
$out[ $key ] = static::recursive_merge_atts( $default[ $key ], $attr[ $key ], $only_default_keys );
} else {
$out[ $key ] = $attr[ $key ];
}
} else {
$out[ $key ] = $value;
}
}
return $out;
}
/**
* Enqueue scripts used by shortcode
*
* @return void
*/
public static function enqueue_scripts() {
tve_dash_enqueue_vue();
tve_dash_enqueue_script( 'td-highcharts', '//code.highcharts.com/highcharts.js' );
tve_dash_enqueue_script( Main::SCRIPTS_HANDLE, TVE_DASH_URL . '/assets/dist/js/reporting-charts.js', [ 'tve-dash-main-vue', 'td-highcharts' ] );
tve_dash_enqueue_style( Main::SCRIPTS_HANDLE, TVE_DASH_URL . '/assets/dist/css/reporting-charts.css' );
wp_localize_script( Main::SCRIPTS_HANDLE, 'ThriveReporting', [
'nonce' => wp_create_nonce( 'wp_rest' ),
'route' => get_rest_url( get_current_blog_id(), Main::REST_NAMESPACE ),
] );
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace TVE\Reporting\Automator;
use Thrive\Automator\Items\Action_Field;
class Event_Key extends Action_Field {
public static function get_id() {
return 'reporting-event-key';
}
public static function get_type() {
return 'input';
}
/**
* Field name
*/
public static function get_name() {
return 'Event key';
}
/**
* Field description
*/
public static function get_description() {
return 'The key of the event to register';
}
/**
* Field input placeholder
*/
public static function get_placeholder() {
return 'event-key';
}
/**
* @return string
*/
public static function get_preview_template() {
return 'Event key: $$value';
}
public static function get_validators() {
return [ 'required' ];
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace TVE\Reporting\Automator;
use Thrive\Automator\Items\Action;
use Thrive\Automator\Items\Automation_Data;
use Thrive\Automator\Items\User_Data;
use TVE\Reporting\EventFields\User_Id;
use TVE\Reporting\Traits\Event;
class Register_Event_Action extends Action {
use Event;
/**
* Get the action identifier
*
* @return string
*/
public static function get_id() {
return 'thrive/reporting-event';
}
/**
* Get the action name/label
*
* @return string
*/
public static function get_name() {
return 'Register user event';
}
/**
* Get the action description
*
* @return string
*/
public static function get_description() {
return static::get_name();
}
/**
* Get the action logo
*
* @return string
*/
public static function get_image() {
return 'tap-unlock-content';
}
/**
* Get an array of keys with the required data-objects
*
* @return array
*/
public static function get_required_data_objects() {
return [ 'user_data' ];
}
public function prepare_data( $data = array() ) {
/** @var $automation_data Automation_Data */
global $automation_data;
$automation_data->set( 'reporting_event_key', empty( $data['reporting-event-key']['value'] ) ? null : $data['reporting-event-key']['value'] );
}
public function do_action( $data ) {
/** @var $data Automation_Data */
$user_data = $data->get( User_Data::get_id() );
if ( ! empty( $user_data ) ) {
$user_id = (int) $user_data->get_value( User_Id::key() );
if ( ! empty( $user_id ) ) {
$this->fields = [
User_Id::key() => $user_id,
];
$this->log();
}
}
}
public static function key() {
/** @var $automation_data Automation_Data */
global $automation_data;
return $automation_data->get( 'reporting_event_key' );
}
public static function get_required_action_fields() {
return [ 'reporting-event-key' ];
}
}

View File

@@ -0,0 +1,60 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting;
use TVE\Reporting\Automator\Event_Key;
use TVE\Reporting\Automator\Register_Event_Action;
use TVE\Reporting\EventFields\User_Id;
class Hooks {
protected static $actions = [
'init',
'deleted_user'
/*'thrive_automator_init' - not right now */
];
public static function register() {
foreach ( static::$actions as $action ) {
if ( is_array( $action ) ) {
if ( method_exists( __CLASS__, $action[0] ) ) {
add_action( $action, [ __CLASS__, $action[0] ], $action[1] ?? 10, $action[2] ?? 1 );
}
} elseif ( is_string( $action ) && method_exists( __CLASS__, $action ) ) {
add_action( $action, [ __CLASS__, $action ] );
}
}
}
public static function init() {
do_action( 'thrive_reporting_init' );
do_action( 'thrive_reporting_register_events', Store::get_instance() );
do_action( 'thrive_reporting_register_report_apps', Store::get_instance() );
Main::add_shortcodes();
}
public static function thrive_automator_init() {
require __DIR__ . '/automator/class-register-event-action.php';
require __DIR__ . '/automator/class-event-key.php';
thrive_automator_register_action( Register_Event_Action::class );
thrive_automator_register_action_field( Event_Key::class );
}
/**
* @param $id
*
* @return void
*/
public static function deleted_user( $id ) {
Logs::get_instance()->remove_by( User_Id::key(), $id );
}
}

View File

@@ -0,0 +1,337 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting;
use TVE\Reporting\EventFields\Item_Id;
use TVE\Reporting\EventFields\Post_Id;
use TVE\Reporting\EventFields\User_Id;
class Logs {
use \TD_Singleton;
const TABLE_NAME = 'thrive_reporting_logs';
/** @var \Tve_Wpdb */
protected $db;
/**
* @var string
*/
private $select;
/**
* @var string
*/
private $where = '';
/**
* @var string
*/
private $group_by = '';
/**
* @var string
*/
private $order_by = '';
/**
* @var string
*/
private $limit = '';
/**
* @var string[]
*/
private $args = [];
/**
* @var string
*/
private $table;
public function __construct() {
$this->db = \Tve_Wpdb::instance();
$this->table = $GLOBALS['wpdb']->prefix . static::TABLE_NAME;
}
/**
* @param Event|mixed $event
*
* @return bool|int|\mysqli_result|resource|null
*/
public function insert( $event ) {
$log_data = $event->get_log_data();
return $this->db->insert( $this->table, $log_data );
}
public function update( $event, $id, $fields_to_update ) {
$log_data = $event->get_log_data( $fields_to_update );
return $this->db->update( $this->table, $log_data, [ 'id' => $id ] );
}
public function get_row() {
return $this->db->get_row( $this->prepare_query() );
}
/**
* @param $event_type
* @param $field
* @param $values
*
* @return array|object|\stdClass[]|null
*/
public function get_fields( $event_type, $field, $values = [] ) {
$this->args = [];
$this->where = "event_type='%s'";
$this->args[] = $event_type;
if ( ! empty( $values ) ) {
$this->where .= sprintf( ' AND %s IN ( %s )', $field, implode( ', ', $values ) );
}
//exp: 'SELECT DISTINCT item_id FROM wp_thrive_reporting_logs WHERE event_type = "tqb_quiz_completed"';
$query = sprintf(
"SELECT DISTINCT %s as 'value' FROM %s WHERE %s",
$field,
$this->table,
// phpcs:ignore
$this->db->prepare( $this->where, $this->args )
);
/* @codingStandardsIgnoreLine */
return $this->db->get_results( $query, ARRAY_A );
}
/**
* Remove logs
*
* @param $field_key
* @param $field_value
* @param $format
*
* @return bool|int|\mysqli_result|resource|null
*/
public function remove_by( $field_key, $field_value, $format = '%d' ) {
return $this->db->delete( $this->table, [ $field_key => $field_value ], [ $format ] );
}
/**
* @param array $where_args
*
* @return \Tve_Wpdb
*/
public function delete( array $where_args ): \Tve_Wpdb {
$conditions = [];
$values = [];
if ( isset( $where_args['event_type'] ) ) {
$conditions[] = 'event_type IN (' . implode( ',', array_fill( 0, count( $where_args['event_type'] ), "'%s'" ) ) . ')';
$values = array_values( $where_args['event_type'] );
unset( $where_args['event_type'] );
}
foreach ( $where_args as $field => $value ) {
$conditions[] = "`$field` = " . $value;
}
$conditions = implode( ' AND ', $conditions );
return $this->db->do_query( $this->db->prepare( "DELETE FROM `$this->table` WHERE $conditions", $values ) );
}
/**
* @param array $args
*
* @return $this
*/
public function set_query( array $args = [] ): Logs {
/* reset query */
$this->select = '';
$this->where = '';
$this->group_by = '';
$this->order_by = '';
if ( empty( $args['fields'] ) ) {
$this->select = '*';
} elseif ( is_string( $args['fields'] ) ) {
$this->select = $args['fields'];
} elseif ( is_array( $args['fields'] ) ) {
$this->select = implode( ', ', $args['fields'] );
}
$this->args = [];
if ( isset( $args['event_type'] ) ) {
if ( is_array( $args['event_type'] ) ) {
/* fill an array with %s for each event type */
$this->where .= 'event_type IN (' . join( ',', array_fill( 0, count( $args['event_type'] ), "'%s'" ) ) . ')';
$this->args = array_merge( $this->args, array_values( $args['event_type'] ) );
} else {
$this->where = "event_type='%s'";
$this->args[] = $args['event_type'];
}
} else {
$this->where = '1';
}
if ( ! empty( $args['filters'] ) && is_array( $args['filters'] ) ) {
foreach ( $args['filters'] as $key => $values ) {
if ( empty( $values ) ) {
continue;
}
$this->set_filter( $key, $values );
}
}
if ( ! empty( $args['group_by'] ) ) {
$group_by = is_string( $args['group_by'] ) ? $args['group_by'] : implode( ', ', $args['group_by'] );
$this->group_by = " GROUP BY $group_by";
$this->select .= ', COUNT(' . $args['count'] . ') AS count';
}
if ( empty( $args['page'] ) || empty( $args['items_per_page'] ) ) {
$this->limit = '';
} else {
$items_per_page = (int) $args['items_per_page'];
$this->limit = sprintf( ' LIMIT %d, %d', ( (int) $args['page'] - 1 ) * $items_per_page, $items_per_page );
}
if ( ! empty( $args['order_by'] ) && ! empty( $args['order_by_direction'] ) ) {
$this->order_by = ' ORDER BY ' . $args['order_by'] . ' ' . $args['order_by_direction'];
}
return $this;
}
/**
* @param $key
* @param $values
*
* @return void
*/
public function set_filter( $key, $values ) {
switch ( $key ) {
case 'created':
if ( ! empty( $values['from'] ) ) {
/* extracts the date from the full date-time - if we also need the time at some point, modify this */
$this->where .= " AND DATE(created) >= '%s'";
$this->args[] = $values['from'];
}
if ( ! empty( $values['to'] ) ) {
$this->where .= " AND DATE(created) <= '%s'";
$this->args[] = $values['to'];
}
break;
case User_Id::key():
case Post_Id::key():
case Item_Id::key():
default:
if ( is_array( $values ) ) {
$this->where .= sprintf( ' AND %s IN ( %s )', $key, implode( ', ', $values ) );
} else {
$this->where .= " AND $key='%s'";
$this->args[] = $values;
}
break;
}
}
/**
* @return array|object|\stdClass[]|null
*/
public function get_results() {
return $this->db->get_results( $this->prepare_query(), ARRAY_A );
}
public function query() {
$this->db->do_query( $this->prepare_query() );
}
public function get_one_row() {
return $this->db->get_one_row();
}
public function count_results() {
$count = $this->db->do_query( $this->prepare_query( true ) )->num_rows();
return empty( $count ) ? 0 : $count;
}
/**
* Get date format depending on the range we use
*
* @return string
*/
public function get_date_format( $min_date = 0, $max_date = 0 ) {
if ( empty( $this->min_date ) || empty( $this->max_date ) ) {
$min_max_date = $this->db->do_query( "SELECT MAX(created) AS max_date, MIN(created) AS min_date FROM $this->table" )->get_one_row();
$this->min_date = empty( $min_max_date->min_date ) ? time() : strtotime( $min_max_date->min_date );
$this->max_date = empty( $min_max_date->max_date ) ? time() : strtotime( $min_max_date->max_date );
}
$from = empty( $min_date ) ? $this->min_date : max( $this->min_date, strtotime( $min_date ) );
$to = empty( $max_date ) ? $this->max_date : min( $this->max_date, strtotime( $max_date ) );
$days = ( $to - $from ) / DAY_IN_SECONDS;
if ( $days > 30 * 12 * 10 ) {
/* display years if we have at least 10 */
$format = 'year';
} elseif ( $days > 30 * 10 ) {
/* display months if we have at least 10 */
$format = 'month';
} elseif ( $days > 7 * 10 ) {
/* display weeks if we have at least 10 */
$format = 'week';
} else {
$format = 'day';
}
return $format;
}
/**
* sum all counts from the query
*
* @return mixed|null
*/
public function sum_results_count() {
$query = $this->prepare_query();
$query = "SELECT SUM(`items`.count) AS total from ($query) as `items`";
$row = $this->db->get_row( $query, ARRAY_A );
return empty( $row ) ? null : $row['total'];
}
protected function prepare_query( $count_results = false ) {
if ( $count_results ) {
$this->order_by = '';
$this->limit = '';
}
/* @codingStandardsIgnoreLine */
$where = "WHERE $this->where $this->group_by $this->order_by $this->limit";
// phpcs:ignore
return "SELECT $this->select FROM $this->table " . $this->db->prepare( $where, $this->args );
}
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting;
class Main {
const REST_NAMESPACE = 'trd/v1';
const SCRIPTS_HANDLE = 'thrive-reporting';
public static function init() {
static::includes();
Hooks::register();
User_Events::add_hooks();
Privacy::init();
}
public static function includes() {
require_once dirname( __DIR__ ) . '/traits/trait-event.php';
require_once dirname( __DIR__ ) . '/traits/trait-report.php';
require_once __DIR__ . '/class-hooks.php';
require_once __DIR__ . '/class-logs.php';
require_once __DIR__ . '/class-store.php';
require_once __DIR__ . '/class-privacy.php';
require_once __DIR__ . '/class-user-events.php';
require_once __DIR__ . '/abstract/class-shortcode.php';
require_once __DIR__ . '/abstract/class-event.php';
require_once __DIR__ . '/abstract/class-event-field.php';
require_once __DIR__ . '/abstract/class-report-app.php';
require_once __DIR__ . '/abstract/class-report-type.php';
require_once __DIR__ . '/event-fields/class-event-type.php';
require_once __DIR__ . '/event-fields/class-created.php';
require_once __DIR__ . '/event-fields/class-item-id.php';
require_once __DIR__ . '/event-fields/class-post-id.php';
require_once __DIR__ . '/event-fields/class-user-id.php';
}
public static function add_shortcodes() {
/* life beats the movie and card depends on chart so we read them in reverse so we won't have conflicts */
foreach ( array_reverse( glob( __DIR__ . '/shortcodes/class-*.php' ) ) as $file_path ) {
require_once $file_path;
preg_match( '/shortcodes\/class-([^.]*)\.php/', $file_path, $matches );
$class_name = empty( $matches[1] ) ? null : 'TVE\Reporting\Shortcodes\\' . ucfirst( $matches[1] );
if ( class_exists( $class_name, false ) && method_exists( $class_name, 'add' ) ) {
$class_name::add();
}
}
}
}

View File

@@ -0,0 +1,124 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting;
use TVE\Reporting\EventFields\User_Id;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Silence is golden!
}
class Privacy {
public static function init() {
add_filter( 'wp_privacy_personal_data_exporters', [ __CLASS__, 'add_personal_data_exporter' ] );
add_filter( 'wp_privacy_personal_data_erasers', [ __CLASS__, 'add_personal_data_eraser' ] );
}
/**
* @param $exporters
*
* @return mixed
*/
public static function add_personal_data_exporter( $exporters ) {
$exporters[] = [
'exporter_friendly_name' => __( 'Thrive Reporting', 'thrive-dash' ),
'callback' => [ __CLASS__, 'export' ],
];
return $exporters;
}
/**
* @param $email_address
*
* @return array
*/
public static function export( $email_address ): array {
$exported_items = [];
$user = get_user_by( 'email', $email_address );
if ( $user && $user->ID ) {
$events = Logs::get_instance()->set_query( [
'filters' => [
User_Id::key() => $user->ID,
],
] )->get_results();
foreach ( $events as $event ) {
$event_data = [];
$event_instance = Store::get_instance()->event_factory( $event );
foreach ( $event_instance::get_registered_fields() as $field_key => $field_class ) {
$event_data[] = [
'name' => $field_class::get_label(),
'value' => $event_instance->get_field( $field_key )->get_title(),
];
}
$exported_items[] = [
'group_id' => 'thrive-reporting-user-privacy',
'group_label' => __( 'Thrive Reporting Event Data', 'thrive-dash' ),
'item_id' => $event_instance->get_field( 'id' ),
'data' => $event_data,
];
}
}
return [
'data' => $exported_items,
'done' => true,
];
}
/**
* @param $erasers
*
* @return mixed
*/
public static function add_personal_data_eraser( $erasers ) {
$erasers[] = [
'eraser_friendly_name' => __( 'Thrive Reporting', 'thrive-dash' ),
'callback' => [ __CLASS__, 'erase' ],
];
return $erasers;
}
/**
* @param $email_address
*
* @return array
*/
public static function erase( $email_address ): array {
$response = [
'items_removed' => false,
'items_retained' => false,
'messages' => [],
'done' => true,
];
if ( empty( $email_address ) ) {
return $response;
}
$user = get_user_by( 'email', $email_address );
$logs_instance = Logs::get_instance();
$logs_instance->remove_by( User_Id::key(), $user->ID );
$user_data = $logs_instance->set_query( [
'filters' => [
User_Id::key() => $user->ID,
],
] )->get_results();
$response['items_removed'] = empty( $user_data );
return $response;
}
}

View File

@@ -0,0 +1,100 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting;
class Store {
use \TD_Singleton;
/**
* @var array
*/
protected $registered_events = [];
/**
* @var array
*/
protected $registered_report_apps = [];
/**
* @param Event|String $event_class
*
* @return void
*/
public function register_event( $event_class ) {
$this->registered_events[ $event_class::key() ] = $event_class;
$event_class::after_register();
}
public function get_registered_events( $event_key = null ) {
return $event_key === null ? $this->registered_events : $this->registered_events[ $event_key ] ?? null;
}
/**
* @param array $event_data
*
* @return Event|null
*/
public function event_factory( array $event_data ) {
$event_instance = null;
if ( ! empty( $event_data['event_type'] ) ) {
$event_class = $this->get_registered_events( $event_data['event_type'] );
if ( $event_class ) {
$event_instance = new $event_class( $event_data );
}
}
return $event_instance;
}
public function register_report_app( $app_class ) {
$this->registered_report_apps[ $app_class::key() ] = $app_class;
$app_class::after_register();
}
/**
* @param $app_key
*
* @return Report_App[]|Report_App
*/
public function get_registered_report_apps( $app_key = null ) {
return $app_key === null ? $this->registered_report_apps : $this->registered_report_apps[ $app_key ] ?? null;
}
public function has_registered_report_app( $app_key ): bool {
return isset( $this->registered_report_apps[ $app_key ] );
}
/**
* @param $app_key
* @param $type_key
*
* @return Report_Type|null
*/
public function get_report_type( $app_key, $type_key ) {
$report_type = null;
$report_app_class = $this->get_registered_report_apps( $app_key );
if ( $report_app_class ) {
$report_types = $report_app_class::get_report_types();
foreach ( $report_types as $report_type_class ) {
if ( $report_type_class::key() === $type_key ) {
$report_type = $report_type_class;
break;
}
}
}
return $report_type;
}
}

View File

@@ -0,0 +1,132 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting;
use TVE\Reporting\Traits\Report;
class User_Events {
public static function add_hooks() {
add_action( 'rest_api_init', static function () {
register_rest_route( Main::REST_NAMESPACE, '/user-events', [
[
'methods' => \WP_REST_Server::READABLE,
'callback' => [ __CLASS__, 'get_user_events' ],
'permission_callback' => static function () {
return current_user_can( TVE_DASH_CAPABILITY );
},
'args' => [
'filters' => [
'type' => 'object',
'required' => true,
],
'page' => [
'type' => 'integer',
'required' => true,
],
'items-per-page' => [
'type' => 'integer',
'required' => true,
],
],
],
] );
} );
}
/**
* Get user data
*
* @param \WP_REST_Request $request The request data from admin.
*
* @return \WP_REST_Response
*/
public static function get_user_events( $request ): \WP_REST_Response {
$filters = $request->get_param( 'filters' );
$page = $request->get_param( 'page' ) ?? 1;
$items_per_page = $request->get_param( 'items-per-page' ) ?? 4;
return new \WP_REST_Response( static::get_events( $filters, $page, $items_per_page ) );
}
/**
* @param $filters
* @param $page
* @param $items_per_page
*
* @return array
*/
public static function get_events( $filters, $page, $items_per_page ): array {
$items = [];
$users = [];
if ( empty( $filters ) ) {
$filters = [];
}
$query = [
'filters' => [],
'items_per_page' => $items_per_page,
'page' => $page,
'order_by' => 'created',
'order_by_direction' => 'desc',
];
/** @var $all_events Event[] */
$all_events = Store::get_instance()->get_registered_events();
foreach ( $filters as $filter_key => $filter_value ) {
$db_col = $filter_key;
foreach ( $all_events as $event ) {
foreach ( $event::get_registered_fields() as $field_db_col => $field ) {
if ( $filter_key === $field_db_col || $filter_key === $field::key() ) {
$db_col = $field_db_col;
}
}
}
$query['filters'][ $db_col ] = $filter_value;
}
/* For now, we are retrieving all the events. In the future we could provide an array of event types here. */
$events = Logs::get_instance()->set_query( $query )->get_results();
$events = array_map( static function ( $event ) {
return Store::get_instance()->event_factory( $event );
}, $events );
/* in case we don't have the event class for something */
$events = array_filter( $events, 'is_object' );
foreach ( $events as $event ) {
/** @var Report|Event $event */
$event_date = $event->get_event_date();
$user = $event->get_event_user();
$user_id = $user->get_value();
$items[] = [
'user' => $user_id,
'description' => $event->get_event_description(),
'date' => $event_date,
];
if ( empty( $users[ $user_id ] ) ) {
$users[ $user_id ] = [
'name' => $user->get_title(),
'picture' => $user->get_image(),
];
}
}
return [
'items' => $items,
'users' => $users,
'number_of_items' => Logs::get_instance()->set_query( $query )->count_results(),
];
}
}

View File

@@ -0,0 +1,75 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting\EventFields;
use TVE\Reporting\Logs;
class Created extends Event_Field {
public static function key(): string {
return 'date';
}
public static function can_group_by(): bool {
return true;
}
/**
* Subtract one day from date
*
* @param string $date
* @param string $format
*
* @return string
*/
public static function minus_one_day( $date, $format = 'Y-m-d' ) {
$timestamp = strtotime( $date );
$timestamp -= DAY_IN_SECONDS;
return date( $format, $timestamp );
}
public static function get_query_select_field( $db_col = '' ): string {
global $reports_query;
switch ( $reports_query['date_format'] ) {
case 'year':
$format = 'DATE_FORMAT(`created`, "%Y")';
break;
case 'month':
$format = 'DATE_FORMAT(`created`, "%M %Y")';
break;
case 'week':
$format = 'DATE_FORMAT(`created`, "%YW%v")';
break;
case 'day':
default:
$format = 'DATE(`created`)';
break;
}
return "$format AS date";
}
public static function get_label( $singular = true ): string {
return $singular ? 'Date' : 'Dates';
}
public static function format_value( $value ) {
return strtotime( $value );
}
public function get_title(): string {
return $this->value === null ? 'Date' : static::format_value( $this->value );
}
public static function get_filter_type(): string {
return 'date-range-picker';
}
}

View File

@@ -0,0 +1,47 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting\EventFields;
use TVE\Reporting\Event;
use TVE\Reporting\Store;
class Event_Type extends Event_Field {
public static function key(): string {
return 'event_type';
}
public static function can_filter_by(): bool {
return false;
}
public static function can_group_by(): bool {
return true;
}
public static function get_label( $singular = true ): string {
return 'Event type';
}
public function get_title(): string {
/** @var Event $event */
$event = Store::get_instance()->get_registered_events( $this->value );
return $event === null ? $this->value : $event::label();
}
public static function get_filter_options(): array {
return array_map( static function ( $event ) {
/** @var Event $event */
return [
'id' => $event::key(),
'label' => $event::label(),
];
}, Store::get_instance()->get_registered_events() );
}
}

View File

@@ -0,0 +1,23 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting\EventFields;
class Item_Id extends Event_Field {
public static function key(): string {
return 'item_id';
}
public static function can_group_by(): bool {
return true;
}
public static function format_value( $value ) {
return (int) $value;
}
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting\EventFields;
class Post_Id extends Event_Field {
public static function key(): string {
return 'post_id';
}
public static function can_group_by(): bool {
return true;
}
public static function get_label( $singular = true ): string {
return $singular ? 'Post' : 'Posts';
}
public static function format_value( $value ) {
return (int) $value;
}
public static function can_filter_by(): bool {
return false;
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting\EventFields;
class User_Id extends Event_Field {
public static function key(): string {
return 'user_id';
}
public static function can_group_by(): bool {
return true;
}
public static function get_label( $singular = true ): string {
return $singular ? 'User' : 'Users';
}
public static function format_value( $value ) {
return (int) $value;
}
public function get_title(): string {
if ( $this->value === null ) {
$user_name = 'Users';
} elseif ( (int) $this->value === 0 ) {
$user_name = 'Unknown user';
} else {
$user = get_user_by( 'ID', $this->value );
$user_name = $user instanceof \WP_User ? $user->display_name : "User $this->value";
}
return $user_name;
}
/**
* @return string
*/
public function get_image(): string {
return get_avatar_url( $this->value );
}
public static function get_filter_options(): array {
return array_map( static function ( $user ) {
return [
'id' => $user->ID,
'label' => $user->data->display_name,
];
}, get_users() );
}
/**
* Return true/false if this field contains an attached image.
*
* @return bool
*/
public static function has_image(): bool {
return true;
}
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting\Shortcodes;
use TVE\Reporting\Shortcode;
class Card extends Chart {
public static function get_tag() {
return 'tve_reporting_card';
}
public static function get_element_class() {
return 'thrive-reporting-card';
}
public static function get_allowed_attr() {
return array_merge(
Shortcode::get_allowed_attr(),
[
'has-chart' => 0,
'has-date-comparison' => 0,
'report-data-type' => 'card',
'chart-config' => [
'type' => 'line',
'on-click-url' => '',
'cumulative' => 1,
],
]
);
}
}

View File

@@ -0,0 +1,59 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting\Shortcodes;
use TVE\Reporting\Shortcode;
class Chart extends Shortcode {
public static function get_tag() {
return 'tve_reporting_chart';
}
public static function get_element_class() {
return 'thrive-reporting-chart';
}
public static function get_allowed_attr() {
return array_merge(
parent::get_allowed_attr(),
[
'report-data-type' => 'chart',
'chart-config' => [
'type' => '',
'on-click-url' => '',
'stacking' => 0,
'cumulative' => 0,
'cumulative-toggle' => 0,
'has-legend' => 0,
],
]
);
}
/**
* Parse the chart config first because we want to correctly merge with default attributes
*
* @param $default
* @param $attr
* @param $only_default_keys
*
* @return array|mixed
*/
public static function recursive_merge_atts( $default, $attr, $only_default_keys = true ) {
try {
if ( isset( $attr['chart-config'] ) ) {
$attr['chart-config'] = json_decode( str_replace( "'", '"', $attr['chart-config'] ), true );
}
} catch ( \Exception $e ) {
$attr['chart-config'] = [];
}
return parent::recursive_merge_atts( $default, $attr, $only_default_keys );
}
}

View File

@@ -0,0 +1,70 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting\Shortcodes;
use TVE\Reporting\Store;
class Filter {
public static function add() {
add_shortcode( 'tve_reporting_filter', [ static::class, 'render' ] );
}
public static function get_allowed_attr() {
return [
'report-app' => '',
'report-type' => '',
'options' => '',
'filter-label' => '',
'field-key' => '',
'is-global' => 0,
'is-multiple' => 1,
'placeholder' => '',
'default-value' => '',
'persist-value' => 0,
'retrieve-all-values' => 0,
'no-options-text' => 0,
];
}
/**
* @param array $attr
*
* @return string
*/
public static function render( $attr ) {
$attr = shortcode_atts( static::get_allowed_attr(), $attr );
$report_type_class = Store::get_instance()->get_report_type( $attr['report-app'], $attr['report-type'] );
if ( empty( $attr['field-key'] ) || $report_type_class === null ) {
$content = '';
} else {
$element_data = [];
/* allowed attr from shortcode */
foreach ( static::get_allowed_attr() as $key => $default_value ) {
$element_data[ $key ] = sprintf( 'data-%s="%s"', $key, $attr[ $key ] );
}
/* attr from filter ( filter-label and filter-type ) */
foreach ( $report_type_class::get_filters()[ $attr['field-key'] ] as $key => $value ) {
if ( empty( $attr["filter-$key"] ) ) {
$element_data["filter-$key"] = sprintf( 'data-filter-%s="%s"', $key, $value );
}
}
$content = sprintf(
'<div class="thrive-reporting-filter" %s></div>',
implode( ' ', $element_data )
);
}
return $content;
}
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting\Shortcodes;
use TVE\Reporting\Shortcode;
class Table extends Shortcode {
public static function get_tag() {
return 'tve_reporting_table';
}
public static function get_element_class() {
return 'thrive-reporting-table';
}
public static function get_allowed_attr() {
return array_merge(
parent::get_allowed_attr(),
[
'report-data-type' => 'table',
'report-items-per-page' => 10,
'report-table-columns' => '',
'report-order-by' => '',
'report-order-by-direction' => '',
'report-restrict-order-by' => '',
'has-pagination' => 0,
'export-title' => '',
]
);
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting\Shortcodes;
use TVE\Reporting\Event;
use TVE\Reporting\Shortcode;
use TVE\Reporting\Store;
class Timeline extends Shortcode {
public static function get_tag() {
return 'tve_reporting_timeline';
}
public static function get_element_class() {
return 'thrive-reporting-timeline';
}
public static function get_allowed_attr() {
return array_merge(
parent::get_allowed_attr(),
[
'report-items-per-page' => 4,
'report-data-type' => '',
'has-pagination' => 0,
'report-size' => 'sm',
'user-url' => '',
]
);
}
/**
* More or less, all fields can filter user activity
*
* @param $app
* @param $type
*
* @return array|int[]|string[]
*/
public static function get_filter_fields( $app, $type ) {
$fields = [];
/** @var $all_events Event[] */
$all_events = Store::get_instance()->get_registered_events();
foreach ( $all_events as $event ) {
foreach ( $event::get_registered_fields() as $field ) {
$field_key = $field::key();
if ( ! in_array( $field_key, $fields ) ) {
$fields[] = $field_key;
}
}
}
return $fields;
}
}

View File

@@ -0,0 +1,287 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting\Traits;
use TVE\Reporting\EventFields\Event_Field;
use TVE\Reporting\EventFields\Created;
use TVE\Reporting\EventFields\Event_Type;
use TVE\Reporting\EventFields\Item_Id;
use TVE\Reporting\EventFields\Post_Id;
use TVE\Reporting\EventFields\User_Id;
use TVE\Reporting\Logs;
trait Event {
protected static $hook_name = '';
protected static $hook_priority = 10;
protected static $hook_params_number = 1;
/**
* @var Event_Field[]|mixed
*/
protected $fields = [];
/**
* Things to do after an event has been registered
*
* @return void
*/
public static function after_register() {
static::register_action();
}
/**
* Register action that will provide the data for log
*
* @return void
*/
public static function register_action() {
if ( empty( static::$hook_name ) ) {
throw new \RuntimeException( __CLASS__ . 'Please define a hook name or register an action!' );
} else {
add_action( static::$hook_name, static function ( $fields ) {
$event = new static( $fields );
$event->log();
}, static::$hook_priority, static::$hook_params_number );
}
}
public static function get_event_type_field(): string {
return Event_Type::class;
}
public static function get_created_field(): string {
return Created::class;
}
public static function get_post_id_field(): string {
return Post_Id::class;
}
public static function get_user_id_field(): string {
return User_Id::class;
}
public static function get_item_id_field(): string {
return Item_Id::class;
}
public static function get_extra_int_field_1() {
return null;
}
public static function get_extra_int_field_2() {
return null;
}
public static function get_extra_float_field() {
return null;
}
public static function get_extra_varchar_field_1() {
return null;
}
public static function get_extra_varchar_field_2() {
return null;
}
public static function get_extra_text_field_1() {
return null;
}
/**
* Return only the registered (non null) fields
*
* @return Event_Field[]
*/
final public static function get_registered_fields(): array {
$fields = [
'event_type' => static::get_event_type_field(),
'created' => static::get_created_field(),
'item_id' => static::get_item_id_field(),
'post_id' => static::get_post_id_field(),
'user_id' => static::get_user_id_field(),
'int_field_1' => static::get_extra_int_field_1(),
'int_field_2' => static::get_extra_int_field_2(),
'float_field' => static::get_extra_float_field(),
'varchar_field_1' => static::get_extra_varchar_field_1(),
'varchar_field_2' => static::get_extra_varchar_field_2(),
'text_field_1' => static::get_extra_text_field_1(),
];
return array_filter( $fields, static function ( $field ) {
return $field !== null;
} );
}
/**
* Get db table column for a specific field
*
* @param string $key
*
* @return string
*/
final public static function get_field_table_col( string $key ): string {
$db_col = $key;
foreach ( static::get_registered_fields() as $col => $field_class ) {
if ( $field_class::key() === $key ) {
$db_col = $col;
}
}
return $db_col;
}
/**
* @param $key
*
* @return Event_Field|null
*/
final public static function get_registered_field( $key ) {
$field = null;
foreach ( static::get_registered_fields() as $col => $field_class ) {
if ( $col === $key || $field_class::key() === $key ) {
$field = $field_class;
}
}
return $field;
}
/**
* @param $field_key
* @param bool $format
*
* @return mixed|null
*/
public function get_field_value( $field_key, $format = true ) {
$field_key = static::get_field_table_col( $field_key );
if ( isset( $this->fields[ $field_key ] ) ) {
if ( is_subclass_of( $this->fields[ $field_key ], Event_Field::class ) ) {
$value = $this->fields[ $field_key ]->get_value( $format );
} else {
$value = $this->fields[ $field_key ];
}
} else {
$value = null;
}
return $value;
}
/**
* @param $field_key
*
* @return mixed|Event_Field|null
*/
public function get_field( $field_key ) {
$field_key = static::get_field_table_col( $field_key );
return $this->fields[ $field_key ] ?? null;
}
/**
* @return mixed|Event_Field[]
*/
public function get_fields() {
return $this->fields;
}
/**
* Store event data in the database
*
* @return bool|int|\mysqli_result|resource|null
*/
final public function log() {
return Logs::get_instance()->insert( $this );
}
/**
* Update event data in the database
*
* @param $id
* @param $fields_to_update
*
* @return bool|int|\mysqli_result|resource|null
*/
final public function update_log( $id, $fields_to_update ) {
return Logs::get_instance()->update( $this, $id, $fields_to_update );
}
/**
* @param $query_fields
* @param $fields_to_update
*
* @return void
*/
public function upsert( $query_fields, $fields_to_update = [] ) {
$id = $this->get_entry_row( $query_fields );
if ( is_null( $id ) ) {
$this->log();
} else {
if ( empty( $fields_to_update ) ) {
$fields_to_update = $query_fields;
}
$this->update_log( $id, $fields_to_update );
}
}
/**
* @param $filter_keys
*
* @return string|null
*/
final public function get_entry_row( $filter_keys ) {
$filters = [];
foreach ( array_keys( $this::get_registered_fields() ) as $field_key ) {
if ( in_array( $field_key, $filter_keys, true ) ) {
$filters[ $field_key ] = $this->get_field_value( $field_key );
}
}
return Logs::get_instance()->set_query( [
'event_type' => static::key(),
'filters' => $filters,
] )->get_row();
}
/**
* @param $allowed_fields
*
* @return array
*/
public function get_log_data( $allowed_fields = [] ) {
$log_data = [];
if ( empty( $allowed_fields ) ) {
$allowed_fields = array_keys( $this::get_registered_fields() );
}
foreach ( array_keys( $this::get_registered_fields() ) as $field_key ) {
if ( in_array( $field_key, $allowed_fields, true ) ) {
$log_data[ $field_key ] = $this->get_field_value( $field_key );
}
}
$log_data['event_type'] = $this::key();
$log_data['created'] = gmdate( 'Y-m-d H:i:s' );
return $log_data;
}
}

View File

@@ -0,0 +1,492 @@
<?php
/**
* Thrive Themes - https://thrivethemes.com
*
* @package thrive-dashboard
*/
namespace TVE\Reporting\Traits;
use TVE\Reporting\EventFields\Created;
use TVE\Reporting\EventFields\Event_Field;
use TVE\Reporting\EventFields\Item_Id;
use TVE\Reporting\EventFields\User_Id;
use TVE\Reporting\Logs;
trait Report {
/**
* Who is allowed to use rest routes for reports
*
* @return bool
*/
public static function permission_callback(): bool {
return current_user_can( TVE_DASH_CAPABILITY );
}
/**
* ways we can display data on the reporting dashboard
*
* @return string[]
*/
public static function get_display_types(): array {
return [
'card',
'table',
'column_chart',
'line_chart',
'pie_chart',
];
}
/**
* Return an array of key-value for all the ways we can group this report
*
* @return string[]
*/
public static function get_group_by(): array {
$group_by = [];
foreach ( static::get_registered_fields() as $field ) {
/** @var $field Event_Field */
if ( $field::can_group_by() ) {
$group_by[ $field::key() ] = $field::get_label();
}
}
return $group_by;
}
/**
* Return an array of all filters supported by fields
*
* @return array
*/
public static function get_filters(): array {
$filters = [];
foreach ( static::get_registered_fields() as $field ) {
/** @var $field Event_Field */
if ( $field::can_filter_by() ) {
$filters[ $field::key() ] = [
'label' => $field::get_label(),
'type' => $field::get_filter_type(),
];
}
}
return $filters;
}
/**
* Register REST routes for each field to return its values
*
* @param $report_type_route
*/
public static function register_filter_routes( $report_type_route ) {
if ( ! method_exists( static::class, 'get_registered_fields' ) ) {
return;
}
foreach ( static::get_registered_fields() as $field ) {
/** @var $field Event_Field */
if ( $field::can_filter_by() ) {
$field::register_options_route( $report_type_route, static::class );
}
}
}
/**
* Prepare query, ensure default values, filter only allowed fields
*
* @param $query
*
* @return array
*/
protected static function parse_query( $query ): array {
global $reports_query;
$reports_query = array_merge( [
'event_type' => static::key(),
'count' => 'id',
'page' => 0,
'items_per_page' => 0,
'group_by' => [],
], $query );
if ( empty( $reports_query['date_format'] ) ) {
$reports_query['date_format'] = Logs::get_instance()->get_date_format( $query['filters']['date']['from'] ?? 0, $query['filters']['date']['to'] ?? 0 );
}
if ( is_string( $reports_query['group_by'] ) ) {
$reports_query['group_by'] = empty( $reports_query['group_by'] ) ? [] : explode( ',', $reports_query['group_by'] );
}
/* make sure we use the db key when we group/select */
$reports_query['group_by'] = array_map( static function ( $field_key ) {
if ( method_exists( static::class, 'get_registered_field' ) ) {
$field = static::get_registered_field( trim( $field_key ) );
if ( method_exists( $field, 'key' ) ) {
$field_key = static::get_registered_field( trim( $field_key ) )::key();
}
}
return $field_key;
}, $reports_query['group_by'] );
if ( ! empty( $reports_query['order_by'] ) && method_exists( static::class, 'get_field_table_col' ) ) {
$reports_query['order_by'] = static::get_field_table_col( $reports_query['order_by'] );
}
/* we only need to select the fields we group by and the timestamp */
$reports_query['fields'] = static::get_query_select_fields(
empty( $reports_query['fields'] ) ?
array_unique( array_merge( [ 'date' ], $reports_query['group_by'] ) ) :
$reports_query['fields']
);
/* the fields should be db-compatible */
if ( ! empty( $reports_query['filters'] ) && is_array( $reports_query['filters'] ) ) {
$parsed_filters = [];
$allowed_filters = static::get_filters();
foreach ( $reports_query['filters'] as $key => $value ) {
if ( method_exists( static::class, 'get_field_table_col' ) ) {
$db_col_key = static::get_field_table_col( $key );
if ( isset( $allowed_filters[ $key ] ) || isset( $allowed_filters[ $db_col_key ] ) ) {
$parsed_filters[ $db_col_key ] = $value;
}
}
}
$reports_query['filters'] = $parsed_filters;
}
return $reports_query;
}
/**
* Array of fields to select from the logs table
*
* @return string[]
*/
public static function get_query_select_fields( $fields = [] ): array {
$selected_fields = [];
if ( method_exists( static::class, 'get_registered_field' ) ) {
foreach ( $fields as $field ) {
/** @var $event_field Event_Field */
$event_field = static::get_registered_field( $field );
if ( $event_field === null ) {
/* add raw field */
$selected_fields[] = $field;
} else {
/* get select query from field class */
$selected_fields[] = $event_field::get_query_select_field( static::get_field_table_col( $event_field::key() ) );
}
}
} else {
$selected_fields = $fields;
}
return $selected_fields;
}
/**
* only count the number of items
*
* @param $query
*
* @return int
*/
public static function count_data( $query = [] ): int {
return (int) Logs::get_instance()->set_query( static::parse_query( $query ) )->count_results();
}
/**
* General function for getting data. other functions use this one as base, but can be overwritten
*
* @param $query
*
* @return array
*/
public static function get_data( $query = [] ): array {
$query = static::parse_query( $query );
$items = Logs::get_instance()->set_query( $query )->get_results();
$labels = static::get_data_labels( $items );
return [
'labels' => $labels,
'items' => $items,
];
}
/**
* Get data for chart - has an extra key for tooltip :)
*
* @param $query
*
* @return array
*/
public static function get_chart_data( $query ): array {
$data = static::get_data( $query );
$data['tooltip_text'] = static::get_tooltip_text();
return $data;
}
/**
* Get data for card. The number needs the count value
*
* @param $query
*
* @return array
*/
public static function get_card_data( $query ): array {
if ( empty( $query['has_chart'] ) ) {
$count = Logs::get_instance()->set_query( static::parse_query( $query ) )->sum_results_count();
$chart_data['count'] = $count === null ? 0 : $count;
$chart_data['no_data'] = $count === null ? 1 : 0;
} else {
$data = static::get_chart_data( $query );
$chart_data = empty( $query['has_chart'] ) ? [] : $data;
$chart_data['no_data'] = empty( $data['items'] ) ? 1 : 0;
$chart_data['count'] = array_reduce( $data['items'], static function ( $total, $item ) {
return $total + (int) $item['count'];
}, 0 );
}
return $chart_data;
}
/**
* Get table data. also return the number of items so we can paginate and link in case we need
*
* @param $query
*
* @return array
*/
public static function get_table_data( $query ): array {
/* for table display, we want data sorted by day because we retrieve all entries */
$query['date_format'] = 'day';
$data = static::get_data( $query );
if ( ! empty( $query['has_pagination'] ) ) {
$data['number_of_items'] = Logs::get_instance()->count_results();
}
$data['images'] = static::get_data_images( $data['items'] );
$data['links'] = [];
return $data;
}
/**
* @param $items
*
* @return array
*/
public static function get_data_labels( $items ): array {
$items = array_map( static function ( $item ) {
return new static( $item );
}, $items );
$labels = [];
foreach ( $items as $item ) {
foreach ( $item->get_fields() as $field_key => $field ) {
if ( is_a( $field, Event_Field::class ) ) {
$field_key = $field::key();
if ( empty( $labels[ $field_key ] ) ) {
$labels[ $field_key ] = [
'key' => $field_key,
'text' => $field::get_label(),
'values' => [],
];
}
$field_value = $field->get_value( false );
if ( ! isset( $labels[ $field_key ]['values'][ $field_value ] ) ) {
$labels[ $field_key ]['values'][ $field_value ] = $field->get_title();
}
} elseif ( $field_key === 'count' ) {
$labels['count'] = [
'key' => 'count',
'text' => __( 'Count', 'thrive-dashboard' ),
];
}
}
}
return $labels;
}
/**
* @param $items
*
* @return array
*/
public static function get_data_images( $items ): array {
$items = array_map( static function ( $item ) {
return new static( $item );
}, $items );
$images = [];
foreach ( $items as $item ) {
static::collect_field_images( $item->get_fields(), $images );
}
return $images;
}
/**
* Used by custom reports (the ones in Apprentice)
*
* @param array $items
* @param array $fields
*
* @return array
*/
public static function get_custom_data_images( array $items, array $fields ): array {
$images = [];
foreach ( $items as $item ) {
$fields = array_map( static function ( $field ) use ( $item ) {
return new $field( $item[ $field::key() ] );
}, $fields );
static::collect_field_images( $fields, $images );
}
return $images;
}
/**
* @param $fields
* @param $images
*
* @return void
*/
public static function collect_field_images( $fields, &$images ) {
foreach ( $fields as $field ) {
if ( is_a( $field, Event_Field::class ) && method_exists( $field, 'has_image' ) && $field::has_image() ) {
$field_key = $field::key();
if ( empty( $images[ $field_key ] ) ) {
$images[ $field_key ] = [];
}
$field_value = $field->get_value( false );
if ( ! isset( $images[ $field_key ][ $field_value ] ) ) {
$images[ $field_key ][ $field_value ] = $field->get_image();
}
}
}
}
/**
* Order items based on query. mostly used when we have custom data that is no retrieved from db
*
* @param array $items Items to sort
* @param array $query query that let's us know how to order and slice items
* @param array $labels labels used for comparison
*
* @return array|mixed
*/
protected static function order_items( $items = [], $query = [], $labels = [] ): array {
if ( isset( $query['order_by'] ) ) {
if ( isset( $labels[ $query['order_by'] ] ) ) {
$labels = $labels[ $query['order_by'] ]['values'];
}
usort( $items, static function ( $a, $b ) use ( $query, $labels ) {
if ( ! isset( $a[ $query['order_by'] ] ) || ! isset( $b[ $query['order_by'] ] ) ) {
return 0;
}
$a = $a[ $query['order_by'] ];
$b = $b[ $query['order_by'] ];
if ( $query['order_by'] === 'date' ) {
/* compare dates */
$a = strtotime( $a );
$b = strtotime( $b );
} elseif ( isset( $labels[ $a ] ) && isset( $labels[ $b ] ) ) {
/* usually we store ids in values, so we have to compare the labels */
$a = $labels[ $a ];
$b = $labels[ $b ];
}
return ( strtolower( $query['order_by_direction'] ) === 'desc' ? - 1 : 1 ) * ( is_numeric( $a ) && is_numeric( $b ) ? $a - $b : strcasecmp( $a, $b ) );
} );
}
return $items;
}
/**
* Slice data when we have custom queries
*
* @param $items
* @param $query
*
* @return array|mixed
*/
public static function slice_items( $items = [], $query = [] ) {
if ( ! empty( $query['items_per_page'] ) && is_numeric( $query['items_per_page'] ) ) {
$page = empty( $query['page'] ) ? 1 : (int) $query['page'];
$items = array_slice( $items, ( $page - 1 ) * $query['items_per_page'], (int) $query['items_per_page'] );
}
return $items;
}
/**
* Text that will be displayed as tooltips for points in charts
*
* @return string
*/
public static function get_tooltip_text(): string {
return static::label() . ': <strong>{number}</strong>';
}
/**
* Event description - used for user timeline
*
* @return string
*/
public function get_event_description(): string {
$item = $this->get_field( Item_Id::key() )->get_title();
return " did this event for $item.";
}
public function get_event_user() {
return $this->get_field( User_Id::key() );
}
public function get_event_date(): string {
return $this->get_field_value( Created::key(), false );
}
}