[statistics] add statistics page
This commit is contained in:
parent
b59fe4c694
commit
8af33ded18
5 changed files with 267 additions and 0 deletions
21
ext/statistics/info.php
Normal file
21
ext/statistics/info.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
class StatisticsInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "statistics";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Statistics";
|
||||
public array $authors = ["Discomrade" => ""];
|
||||
public string $license = self::LICENSE_WTFPL;
|
||||
public ExtensionVisibility $visibility = ExtensionVisibility::ADMIN;
|
||||
public string $description = "Displays a user statistics page, similar to booru.org. Read the documentation before enabling.";
|
||||
public ?string $documentation =
|
||||
"This will display certain user statistics, depending on which extensions are enabled. The taggers statistic relies on the Tag History extension, so it will only count from when that extension was enabled.\n
|
||||
Assuming the extension is enabled, statistics are shown for user uploads, comments, tags (requires Tag History), notes, sources (requires Source History), favorites and forum posts.\n
|
||||
Tags statistics count both removing and adding tags, so changing 'tag_me' to 'tagme' counts as both a deletion and an addition, 2 tag edits. This is different to how booru.org calculates their tag statistics (which seems to only count the number of changes submitted).";
|
||||
}
|
148
ext/statistics/main.php
Normal file
148
ext/statistics/main.php
Normal file
|
@ -0,0 +1,148 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
class Statistics extends Extension
|
||||
{
|
||||
/** @var StatisticsTheme */
|
||||
protected Themelet $theme;
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event): void
|
||||
{
|
||||
global $config, $page;
|
||||
if ($event->page_matches("stats") || $event->page_matches("stats/100")) {
|
||||
$base_href = get_base_href();
|
||||
$sitename = $config->get_string(SetupConfig::TITLE);
|
||||
$theme_name = $config->get_string(SetupConfig::THEME);
|
||||
$anon_id = $config->get_int("anon_id");
|
||||
|
||||
$limit = 10;
|
||||
if ($event->page_matches("stats/100")) {
|
||||
$limit = 100;
|
||||
}
|
||||
|
||||
if (Extension::is_enabled(TagHistoryInfo::KEY)) {
|
||||
$tag_tally = $this->get_tag_stats($anon_id);
|
||||
arsort($tag_tally, $flags = SORT_NUMERIC);
|
||||
$tag_table = $this->theme->build_table($tag_tally, "Taggers", "Top $limit taggers", $limit);
|
||||
} else {
|
||||
$tag_table = null;
|
||||
}
|
||||
|
||||
$upload_tally = [];
|
||||
foreach ($this->get_upload_stats($anon_id) as $name) {
|
||||
array_key_exists($name, $upload_tally) ? $upload_tally[$name] += 1 : $upload_tally[$name] = 1;
|
||||
|
||||
}
|
||||
arsort($upload_tally, $flags = SORT_NUMERIC);
|
||||
$upload_table = $this->theme->build_table($upload_tally, "Uploaders", "Top $limit uploaders", $limit);
|
||||
|
||||
if (Extension::is_enabled(CommentListInfo::KEY)) {
|
||||
$comment_tally = [];
|
||||
foreach ($this->get_comment_stats($anon_id) as $name) {
|
||||
array_key_exists($name, $comment_tally) ? $comment_tally[$name] += 1 : $comment_tally[$name] = 1;
|
||||
|
||||
}
|
||||
arsort($comment_tally, $flags = SORT_NUMERIC);
|
||||
$comment_table = $this->theme->build_table($comment_tally, "Commenters", "Top $limit commenters", $limit);
|
||||
} else {
|
||||
$comment_table = null;
|
||||
}
|
||||
|
||||
if (Extension::is_enabled(FavoritesInfo::KEY)) {
|
||||
$favorite_tally = [];
|
||||
foreach ($this->get_favorite_stats($anon_id) as $name) {
|
||||
array_key_exists($name, $favorite_tally) ? $favorite_tally[$name] += 1 : $favorite_tally[$name] = 1;
|
||||
|
||||
}
|
||||
arsort($favorite_tally, $flags = SORT_NUMERIC);
|
||||
$favorite_table = $this->theme->build_table($favorite_tally, "Favoriters", "Top $limit favoriters", $limit);
|
||||
} else {
|
||||
$favorite_table = null;
|
||||
}
|
||||
|
||||
$this->theme->display_page($page, $limit, $tag_table, $upload_table, $comment_table, $favorite_table);
|
||||
}
|
||||
}
|
||||
|
||||
public function onSetupBuilding(SetupBuildingEvent $event): void
|
||||
{
|
||||
$sb = $event->panel->create_new_block("Stats Page");
|
||||
$sb->add_longtext_option("stats_text", "<br>Page Text:<br>");
|
||||
}
|
||||
|
||||
public function onPageNavBuilding(PageNavBuildingEvent $event): void
|
||||
{
|
||||
$event->add_nav_link("stats", new Link('stats'), "Stats");
|
||||
}
|
||||
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void
|
||||
{
|
||||
if ($event->parent == "stats") {
|
||||
$event->add_nav_link("stats_100", new Link('stats/100'), "Top 100");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, int>
|
||||
*/
|
||||
private function get_tag_stats(int $anon_id): array
|
||||
{
|
||||
global $database;
|
||||
// Returns the username and tags from each tag history entry. Excludes Anonymous
|
||||
$tag_stats = $database->get_all("SELECT users.name,tag_histories.tags,tag_histories.image_id FROM tag_histories INNER JOIN users ON users.id = tag_histories.user_id WHERE tag_histories.user_id <> $anon_id;");
|
||||
|
||||
// Group tag history entries by image id
|
||||
$tag_histories = [];
|
||||
foreach ($tag_stats as $ts) {
|
||||
$tag_history = ['name' => $ts['name'], 'tags' => $ts['tags']];
|
||||
$id = $ts['image_id'];
|
||||
array_key_exists($id, $tag_histories) ? array_push($tag_histories[$id], $tag_history) : $tag_histories[$id] = [$tag_history];
|
||||
}
|
||||
// Count changes made in each tag history and tally tags for users
|
||||
$tag_tally = [];
|
||||
foreach ($tag_histories as $i => $image) {
|
||||
$first = true;
|
||||
$prev = [];
|
||||
foreach ($image as $change) {
|
||||
$curr = explode(' ', $change['tags']);
|
||||
$name = (string)$change['name'];
|
||||
$tag_tally[$name] += count(array_diff($curr, $prev));
|
||||
$prev = $curr;
|
||||
}
|
||||
}
|
||||
return $tag_tally;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
private function get_upload_stats(int $anon_id): array
|
||||
{
|
||||
global $database;
|
||||
// Returns the username of each post, as an array. Excludes Anonymous
|
||||
return $database->get_col("SELECT users.name FROM images INNER JOIN users ON users.id = images.owner_id WHERE images.owner_id <> $anon_id;");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
private function get_comment_stats(int $anon_id): array
|
||||
{
|
||||
global $database;
|
||||
// Returns the username of each comment, as an array. Excludes Anonymous
|
||||
return $database->get_col("SELECT users.name FROM comments INNER JOIN users ON users.id = comments.owner_id WHERE comments.owner_id <> $anon_id;");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
private function get_favorite_stats(int $anon_id): array
|
||||
{
|
||||
global $database;
|
||||
// Returns the username of each favorite, as an array. Excludes Anonymous
|
||||
return $database->get_col("SELECT users.name FROM user_favorites INNER JOIN users ON users.id = user_favorites.user_id WHERE user_favorites.user_id <> $anon_id;");
|
||||
}
|
||||
}
|
2
ext/statistics/style.css
Normal file
2
ext/statistics/style.css
Normal file
|
@ -0,0 +1,2 @@
|
|||
.stats-table{display:inline-table;width:auto;}
|
||||
.stats-container{display:inline-block;margin:10px;}
|
20
ext/statistics/test.php
Normal file
20
ext/statistics/test.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
class StatisticsTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testStatisticsPage(): void
|
||||
{
|
||||
$page = $this->get_page('stats');
|
||||
$this->assert_title("Stats");
|
||||
}
|
||||
|
||||
public function testTop100StatisticsPage(): void
|
||||
{
|
||||
$page = $this->get_page('stats/100');
|
||||
$this->assert_title("Stats");
|
||||
}
|
||||
}
|
76
ext/statistics/theme.php
Normal file
76
ext/statistics/theme.php
Normal file
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use MicroHTML\HTMLElement;
|
||||
|
||||
use function MicroHTML\{TABLE,THEAD,TBODY,TR,TH,TD};
|
||||
use function MicroHTML\A;
|
||||
use function MicroHTML\B;
|
||||
use function MicroHTML\P;
|
||||
use function MicroHTML\DIV;
|
||||
use function MicroHTML\emptyHTML;
|
||||
use function MicroHTML\rawHTML;
|
||||
|
||||
class StatisticsTheme extends Themelet
|
||||
{
|
||||
public function display_page(Page $page, int $limit, ?HTMLElement $tag_table, ?HTMLElement $upload_table, ?HTMLElement $comment_table, ?HTMLElement $favorite_table): void
|
||||
{
|
||||
$html = emptyHTML(
|
||||
$tag_table,
|
||||
$upload_table,
|
||||
$comment_table,
|
||||
$favorite_table,
|
||||
);
|
||||
|
||||
$page->set_title(html_escape("Stats"));
|
||||
$page->set_heading(html_escape("Stats - Top $limit"));
|
||||
$page->add_block(new NavBlock());
|
||||
$page->add_block(new Block("Stats", $html, "main", 20));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int> $data
|
||||
*/
|
||||
public function build_table(array $data, string $id, string $title, ?int $limit = 10): HTMLElement
|
||||
{
|
||||
$rows = emptyHTML();
|
||||
$n = 1;
|
||||
foreach ($data as $user => $value) {
|
||||
$rows->appendChild(
|
||||
TR(
|
||||
TD([], $n),
|
||||
TD([], $value),
|
||||
TD([], $user)
|
||||
)
|
||||
);
|
||||
$n++;
|
||||
if ($n > $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$table = TABLE(
|
||||
["class" => "zebra stats-table"],
|
||||
THEAD(
|
||||
TR(
|
||||
TH(
|
||||
["colspan" => 3],
|
||||
B($title)
|
||||
)
|
||||
),
|
||||
TR(
|
||||
TH([], "Place"),
|
||||
TH([], "Amount"),
|
||||
TH([], "User")
|
||||
)
|
||||
),
|
||||
TBODY($rows)
|
||||
);
|
||||
return DIV(
|
||||
["id" => "table$id", "class" => "stats-container"],
|
||||
$table
|
||||
);
|
||||
}
|
||||
}
|
Reference in a new issue