[statistics] add statistics page

This commit is contained in:
discomrade 2024-03-23 04:17:05 +00:00 committed by Shish
parent b59fe4c694
commit 8af33ded18
5 changed files with 267 additions and 0 deletions

21
ext/statistics/info.php Normal file
View 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
View 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
View 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
View 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
View 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
);
}
}