mirror of
https://github.com/OPSnet/Gazelle.git
synced 2026-01-16 18:04:34 -05:00
allow bonus accrual by category
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace Gazelle\Manager;
|
||||
|
||||
use Gazelle\Enum\UserStatus;
|
||||
|
||||
class Bonus extends \Gazelle\Base {
|
||||
final public const CACHE_OPEN_POOL = 'bonus_pool'; // also defined in \Gazelle\Bonus
|
||||
final protected const CACHE_ITEM = 'bonus_item';
|
||||
@@ -45,11 +47,11 @@ class Bonus extends \Gazelle\Base {
|
||||
}
|
||||
|
||||
public function flushPriceCache(): void {
|
||||
$this->items = [];
|
||||
unset($this->items);
|
||||
self::$cache->delete_value(self::CACHE_ITEM);
|
||||
}
|
||||
|
||||
public function getOpenPool(): array {
|
||||
public function openPoolList(): array {
|
||||
$key = self::CACHE_OPEN_POOL;
|
||||
$pool = self::$cache->get_value($key);
|
||||
if ($pool === false) {
|
||||
@@ -84,13 +86,14 @@ class Bonus extends \Gazelle\Base {
|
||||
self::$db->prepared_query("
|
||||
SELECT um.ID
|
||||
FROM users_main um
|
||||
AND um.Enabled = '1'
|
||||
AND NOT EXISTS (
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM user_has_attr uha
|
||||
INNER JOIN user_attr ua ON (ua.ID = uha.UserAttrID AND ua.Name IN ('disable-bonus-points', 'no-fl-gifts'))
|
||||
WHERE uha.UserID = um.ID
|
||||
)
|
||||
");
|
||||
AND um.Enabled = ?
|
||||
", UserStatus::enabled->value
|
||||
);
|
||||
return $this->addMultiPoints($points, self::$db->collect('ID'));
|
||||
}
|
||||
|
||||
@@ -99,14 +102,14 @@ class Bonus extends \Gazelle\Base {
|
||||
SELECT um.ID
|
||||
FROM users_main um
|
||||
INNER JOIN user_last_access ula ON (ula.user_id = um.ID)
|
||||
AND um.Enabled = '1'
|
||||
AND NOT EXISTS (
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM user_has_attr uha
|
||||
INNER JOIN user_attr ua ON (ua.ID = uha.UserAttrID AND ua.Name IN ('disable-bonus-points', 'no-fl-gifts'))
|
||||
WHERE uha.UserID = um.ID
|
||||
)
|
||||
AND um.Enabled = ?
|
||||
AND ula.last_access >= ?
|
||||
", $since
|
||||
", UserStatus::enabled->value, $since
|
||||
);
|
||||
return $this->addMultiPoints($points, self::$db->collect('ID'));
|
||||
}
|
||||
@@ -116,14 +119,14 @@ class Bonus extends \Gazelle\Base {
|
||||
SELECT DISTINCT um.ID
|
||||
FROM users_main um
|
||||
INNER JOIN torrents t ON (t.UserID = um.ID)
|
||||
AND um.Enabled = '1'
|
||||
AND NOT EXISTS (
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM user_has_attr uha
|
||||
INNER JOIN user_attr ua ON (ua.ID = uha.UserAttrID AND ua.Name IN ('disable-bonus-points', 'no-fl-gifts'))
|
||||
WHERE uha.UserID = um.ID
|
||||
)
|
||||
AND um.Enabled = ?
|
||||
AND t.created >= ?
|
||||
", $since
|
||||
", UserStatus::enabled->value, $since
|
||||
);
|
||||
return $this->addMultiPoints($points, self::$db->collect('ID'));
|
||||
}
|
||||
@@ -133,24 +136,26 @@ class Bonus extends \Gazelle\Base {
|
||||
SELECT DISTINCT um.ID
|
||||
FROM users_main um
|
||||
INNER JOIN xbt_files_users xfu ON (xfu.uid = um.ID)
|
||||
AND um.Enabled = '1'
|
||||
AND NOT EXISTS (
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM user_has_attr uha
|
||||
INNER JOIN user_attr ua ON (ua.ID = uha.UserAttrID AND ua.Name IN ('disable-bonus-points', 'no-fl-gifts'))
|
||||
WHERE uha.UserID = um.ID
|
||||
)
|
||||
AND xfu.active = 1 and xfu.remaining = 0 and xfu.connectable = 1 and timespent > 0
|
||||
");
|
||||
AND xfu.remaining = 0
|
||||
AND xfu.active = 1
|
||||
AND um.Enabled = ?
|
||||
", UserStatus::enabled->value
|
||||
);
|
||||
return $this->addMultiPoints($points, self::$db->collect('ID'));
|
||||
}
|
||||
|
||||
public function givePoints(\Gazelle\Task|null $task = null): int {
|
||||
//------------------------ Update Bonus Points -------------------------//
|
||||
// calculation:
|
||||
// Size * (0.0754 + (0.1207 * ln(1 + seedtime)/ (seeders ^ 0.55)))
|
||||
// Size (convert from bytes to GB) is in torrents
|
||||
// Seedtime (convert from hours to days) is in xbt_files_history
|
||||
// Seeders is in torrents_leech_stats
|
||||
// size (convert from bytes to GB) is in torrents
|
||||
// seedtime (convert from hours to days) is in xbt_files_history
|
||||
// seeders is in torrents_leech_stats
|
||||
// bonus_scale how the torrent size is adjusted according to category
|
||||
|
||||
self::$db->dropTemporaryTable("bonus_update");
|
||||
self::$db->prepared_query("
|
||||
@@ -165,23 +170,29 @@ class Bonus extends \Gazelle\Base {
|
||||
self::$db->prepared_query("
|
||||
INSERT INTO bonus_update (user_id, delta)
|
||||
SELECT xfu.uid,
|
||||
sum(bonus_accrual(t.Size, xfh.seedtime, tls.Seeders))
|
||||
FROM xbt_files_users AS xfu
|
||||
INNER JOIN xbt_files_history AS xfh USING (uid, fid)
|
||||
INNER JOIN users_main AS um ON (um.ID = xfu.uid)
|
||||
INNER JOIN torrents AS t ON (t.ID = xfu.fid)
|
||||
INNER JOIN torrents_leech_stats AS tls ON (tls.TorrentID = t.ID)
|
||||
WHERE xfu.active = 1
|
||||
AND xfu.remaining = 0
|
||||
AND xfu.mtime > unix_timestamp(now() - INTERVAL 1 HOUR)
|
||||
AND um.Enabled = '1'
|
||||
AND NOT EXISTS (
|
||||
sum(category_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale))
|
||||
FROM (
|
||||
SELECT DISTINCT uid, fid
|
||||
FROM xbt_files_users
|
||||
WHERE remaining = 0
|
||||
AND active = 1
|
||||
AND mtime > unix_timestamp(now() - INTERVAL 1 HOUR)
|
||||
) xfu
|
||||
INNER JOIN xbt_files_history xfh USING (uid, fid)
|
||||
INNER JOIN users_main um ON (um.ID = xfu.uid)
|
||||
INNER JOIN torrents t ON (t.ID = xfu.fid)
|
||||
INNER JOIN torrents_leech_stats tls ON (tls.TorrentID = t.ID)
|
||||
INNER JOIN torrents_group tg ON (tg.ID = t.GroupID)
|
||||
INNER JOIN category c ON (c.category_id = tg.CategoryID)
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM user_has_attr uha
|
||||
INNER JOIN user_attr ua ON (ua.ID = uha.UserAttrID AND ua.Name IN ('disable-bonus-points'))
|
||||
WHERE uha.UserID = um.ID
|
||||
)
|
||||
AND um.Enabled = ?
|
||||
GROUP BY xfu.uid
|
||||
");
|
||||
", UserStatus::enabled->value
|
||||
);
|
||||
self::$db->prepared_query("
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
|
||||
");
|
||||
@@ -189,7 +200,7 @@ class Bonus extends \Gazelle\Base {
|
||||
|
||||
self::$db->prepared_query("
|
||||
INSERT INTO user_bonus
|
||||
(user_id, points)
|
||||
(user_id, points)
|
||||
SELECT bu.user_id, bu.delta
|
||||
FROM bonus_update bu
|
||||
ON DUPLICATE KEY UPDATE points = points + bu.delta
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Gazelle\User;
|
||||
|
||||
use Gazelle\BonusPool;
|
||||
use Gazelle\Util\SortableTableHeader;
|
||||
|
||||
/**
|
||||
* Note: there is no userHasItem() method to check if a user has bought a
|
||||
@@ -119,6 +120,21 @@ class Bonus extends \Gazelle\BaseUser {
|
||||
return $summary;
|
||||
}
|
||||
|
||||
public function heading(): SortableTableHeader {
|
||||
return new SortableTableHeader('hourlypoints', [
|
||||
'title' => ['dbColumn' => 'title', 'defaultSort' => 'asc', 'text' => 'Title'],
|
||||
'size' => ['dbColumn' => 'size', 'defaultSort' => 'desc', 'text' => 'Size'],
|
||||
'seeders' => ['dbColumn' => 'seeders', 'defaultSort' => 'desc', 'text' => 'Seeders'],
|
||||
'seedtime' => ['dbColumn' => 'seed_time', 'defaultSort' => 'desc', 'text' => 'Duration'],
|
||||
'hourlypoints' => ['dbColumn' => 'hourly_points', 'defaultSort' => 'desc', 'text' => 'BP/hour'],
|
||||
'dailypoints' => ['dbColumn' => 'daily_points', 'defaultSort' => 'desc', 'text' => 'BP/day'],
|
||||
'weeklypoints' => ['dbColumn' => 'weekly_points', 'defaultSort' => 'desc', 'text' => 'BP/week'],
|
||||
'monthlypoints' => ['dbColumn' => 'monthly_points', 'defaultSort' => 'desc', 'text' => 'BP/month'],
|
||||
'yearlypoints' => ['dbColumn' => 'yearly_points', 'defaultSort' => 'desc', 'text' => 'BP/year'],
|
||||
'pointspergb' => ['dbColumn' => 'points_per_gb', 'defaultSort' => 'desc', 'text' => 'BP/GB/year'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function history(int $limit, int $offset): array {
|
||||
$page = $offset / $limit;
|
||||
$key = sprintf(self::CACHE_HISTORY, $this->user->id, $page);
|
||||
@@ -494,15 +510,17 @@ class Bonus extends \Gazelle\BaseUser {
|
||||
|
||||
public function hourlyRate(): float {
|
||||
return (float)self::$db->scalar("
|
||||
SELECT sum(bonus_accrual(t.Size, xfh.seedtime, tls.Seeders))
|
||||
SELECT sum(category_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale))
|
||||
FROM (
|
||||
SELECT DISTINCT uid,fid
|
||||
SELECT DISTINCT uid, fid
|
||||
FROM xbt_files_users
|
||||
WHERE active = 1 AND remaining = 0 AND mtime > unix_timestamp(NOW() - INTERVAL 1 HOUR) AND uid = ?
|
||||
) AS xfu
|
||||
INNER JOIN xbt_files_history AS xfh USING (uid, fid)
|
||||
INNER JOIN torrents AS t ON (t.ID = xfu.fid)
|
||||
INNER JOIN xbt_files_history xfh USING (uid, fid)
|
||||
INNER JOIN torrents t ON (t.ID = xfu.fid)
|
||||
INNER JOIN torrents_leech_stats tls ON (tls.TorrentID = t.ID)
|
||||
INNER JOIN torrents_group tg ON (tg.ID = t.GroupID)
|
||||
INNER JOIN category c ON (c.category_id = tg.CategoryID)
|
||||
WHERE xfu.uid = ?
|
||||
", $this->user->id, $this->user->id
|
||||
);
|
||||
@@ -510,29 +528,31 @@ class Bonus extends \Gazelle\BaseUser {
|
||||
|
||||
public function userTotals(): array {
|
||||
$stats = self::$db->rowAssoc("
|
||||
SELECT count(*) AS total_torrents,
|
||||
coalesce(sum(t.Size), 0) AS total_size,
|
||||
coalesce(sum(bonus_accrual(t.Size, xfh.seedtime, tls.Seeders)), 0) AS hourly_points,
|
||||
coalesce(sum(bonus_accrual(t.Size, xfh.seedtime + (24 * 1), tls.Seeders)), 0) * (24 * 1) AS daily_points,
|
||||
coalesce(sum(bonus_accrual(t.Size, xfh.seedtime + (24 * 7), tls.Seeders)), 0) * (24 * 7) AS weekly_points,
|
||||
coalesce(sum(bonus_accrual(t.Size, xfh.seedtime + (24 * 365.256363004/12), tls.Seeders)), 0) * (24 * 365.256363004/12) AS monthly_points,
|
||||
coalesce(sum(bonus_accrual(t.Size, xfh.seedtime + (24 * 365.256363004), tls.Seeders)), 0) * (24 * 365.256363004) AS yearly_points,
|
||||
SELECT count(*) AS total_torrents,
|
||||
coalesce(sum(t.Size), 0) AS total_size,
|
||||
coalesce(sum(category_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale)), 0) AS hourly_points,
|
||||
coalesce(sum(future_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale, 1)), 0) AS daily_points,
|
||||
coalesce(sum(future_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale, 7)), 0) AS weekly_points,
|
||||
coalesce(sum(future_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale, 365.25636 / 12)), 0) AS monthly_points,
|
||||
coalesce(sum(future_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale, 365.25636)), 0) AS yearly_points,
|
||||
if (coalesce(sum(t.Size), 0) = 0,
|
||||
0,
|
||||
sum(bonus_accrual(t.Size, xfh.seedtime + (24 * 365.256363004), tls.Seeders)) * (24 * 365.256363004)
|
||||
/ (sum(t.Size) / (1024*1024*1024))
|
||||
sum(future_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale, 365.25636))
|
||||
/ (sum(t.Size) / (1024*1024*1024))
|
||||
) AS points_per_gb
|
||||
FROM (
|
||||
SELECT DISTINCT uid, fid
|
||||
FROM xbt_files_users
|
||||
WHERE active = 1
|
||||
WHERE active = 1
|
||||
AND remaining = 0
|
||||
AND mtime > unix_timestamp(NOW() - INTERVAL 1 HOUR)
|
||||
AND uid = ?
|
||||
AND mtime > unix_timestamp(NOW() - INTERVAL 1 HOUR)
|
||||
AND uid = ?
|
||||
) AS xfu
|
||||
INNER JOIN xbt_files_history AS xfh USING (uid, fid)
|
||||
INNER JOIN torrents AS t ON (t.ID = xfu.fid)
|
||||
INNER JOIN xbt_files_history xfh USING (uid, fid)
|
||||
INNER JOIN torrents t ON (t.ID = xfu.fid)
|
||||
INNER JOIN torrents_leech_stats tls ON (tls.TorrentID = t.ID)
|
||||
INNER JOIN torrents_group tg ON (tg.ID = t.GroupID)
|
||||
INNER JOIN category c ON (c.category_id = tg.CategoryID)
|
||||
WHERE xfu.uid = ?
|
||||
", $this->user->id, $this->user->id
|
||||
);
|
||||
@@ -540,37 +560,52 @@ class Bonus extends \Gazelle\BaseUser {
|
||||
return $stats;
|
||||
}
|
||||
|
||||
public function seedList(string $orderBy, string $orderWay, int $limit, int $offset): array {
|
||||
public function seedList(
|
||||
int $limit,
|
||||
int $offset,
|
||||
\Gazelle\Manager\Torrent $torMan = new \Gazelle\Manager\Torrent(),
|
||||
): array {
|
||||
$heading = $this->heading();
|
||||
self::$db->prepared_query("
|
||||
SELECT
|
||||
t.ID,
|
||||
tg.Name AS title,
|
||||
t.Size AS size,
|
||||
GREATEST(tls.Seeders, 1) AS seeders,
|
||||
xfh.seedtime AS seed_time,
|
||||
bonus_accrual(t.Size, xfh.seedtime, tls.Seeders) AS hourly_points,
|
||||
bonus_accrual(t.Size, xfh.seedtime + (24 * 1), tls.Seeders) * (24 * 1) AS daily_points,
|
||||
bonus_accrual(t.Size, xfh.seedtime + (24 * 7), tls.Seeders) * (24 * 7) AS weekly_points,
|
||||
bonus_accrual(t.Size, xfh.seedtime + (24 * 365.256363004/12), tls.Seeders) * (24 * 365.256363004/12) AS monthly_points,
|
||||
bonus_accrual(t.Size, xfh.seedtime + (24 * 365.256363004), tls.Seeders) * (24 * 365.256363004) AS yearly_points,
|
||||
bonus_accrual(t.Size, xfh.seedtime + (24 * 365.256363004), tls.Seeders) * (24 * 365.256363004)
|
||||
category_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale) AS hourly_points,
|
||||
future_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale, 1) AS daily_points,
|
||||
future_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale, 7) AS weekly_points,
|
||||
future_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale, 365.25636 / 12) AS monthly_points,
|
||||
future_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale, 365.25636) AS yearly_points,
|
||||
future_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale, 365.25636)
|
||||
/ (t.Size / (1024*1024*1024)) AS points_per_gb
|
||||
FROM (
|
||||
SELECT DISTINCT uid,fid FROM xbt_files_users WHERE active=1 AND remaining=0 AND mtime > unix_timestamp(NOW() - INTERVAL 1 HOUR) AND uid = ?
|
||||
SELECT DISTINCT uid, fid
|
||||
FROM xbt_files_users
|
||||
WHERE active = 1
|
||||
AND remaining = 0
|
||||
AND mtime > unix_timestamp(NOW() - INTERVAL 1 HOUR)
|
||||
AND uid = ?
|
||||
) AS xfu
|
||||
INNER JOIN xbt_files_history AS xfh USING (uid, fid)
|
||||
INNER JOIN torrents AS t ON (t.ID = xfu.fid)
|
||||
INNER JOIN torrents_leech_stats AS tls ON (tls.TorrentID = t.ID)
|
||||
INNER JOIN xbt_files_history xfh USING (uid, fid)
|
||||
INNER JOIN torrents t ON (t.ID = xfu.fid)
|
||||
INNER JOIN torrents_leech_stats tls ON (tls.TorrentID = t.ID)
|
||||
INNER JOIN torrents_group tg ON (tg.ID = t.GroupID)
|
||||
INNER JOIN category c ON (c.category_id = tg.CategoryID)
|
||||
WHERE
|
||||
xfu.uid = ?
|
||||
ORDER BY $orderBy $orderWay
|
||||
ORDER BY {$heading->orderBy()} {$heading->dir()}
|
||||
LIMIT ?
|
||||
OFFSET ?
|
||||
", $this->user->id, $this->user->id, $limit, $offset
|
||||
);
|
||||
$list = [];
|
||||
foreach (self::$db->to_array('ID', MYSQLI_ASSOC) as $r) {
|
||||
$r['torrent'] = new \Gazelle\Torrent($r['ID']);
|
||||
$list[] = $r;
|
||||
if ($r['ID']) {
|
||||
$r['torrent'] = $torMan->findById((int)$r['ID']);
|
||||
$list[] = $r;
|
||||
}
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
@@ -218,7 +218,11 @@ class Time {
|
||||
return '0s';
|
||||
}
|
||||
|
||||
$interval = [($seconds % 60) . 's'];
|
||||
$interval = [];
|
||||
$remainder = $seconds % 60;
|
||||
if ($remainder) {
|
||||
$interval[] = "{$remainder}s";
|
||||
}
|
||||
$minutes = (int)floor($seconds / 60);
|
||||
|
||||
if ($minutes >= 60) {
|
||||
@@ -254,7 +258,16 @@ class Time {
|
||||
$interval[] = "{$day}d";
|
||||
}
|
||||
if ($week) {
|
||||
$interval[] = "{$week}w";
|
||||
if ($week < 52) {
|
||||
$interval[] = "{$week}w";
|
||||
} else {
|
||||
$year = (int)floor($week / 52);
|
||||
$week = $week % 52;
|
||||
if ($week) {
|
||||
$interval[] = "{$week}w";
|
||||
}
|
||||
$interval[] = "{$year}y";
|
||||
}
|
||||
}
|
||||
return implode('', array_slice(array_reverse($interval), 0, 2));
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ From: Spine
|
||||
To: Developers
|
||||
Date: 2024-12-07
|
||||
Subject: Orpheus Development Papers #1 - Mysql Roles
|
||||
Version: 2
|
||||
Version: 3
|
||||
|
||||
The default Gazelle installation defines a single Mysql role (with full
|
||||
privileges) and used for everything: the website, Ocelot and Sphinx. If any
|
||||
@@ -61,6 +61,8 @@ GRANT CREATE TEMPORARY TABLES, DELETE, INSERT, SELECT, UPDATE ON `gazelle`.* TO
|
||||
GRANT DROP ON `gazelle`.`drives` TO 'www'@'localhost';
|
||||
GRANT EXECUTE ON FUNCTION `gazelle`.`binomial_ci` TO 'www'@'localhost';
|
||||
GRANT EXECUTE ON FUNCTION `gazelle`.`bonus_accrual` TO 'www'@'localhost';
|
||||
GRANT EXECUTE ON FUNCTION `gazelle`.`category_bonus_accrual` TO 'www'@'localhost';
|
||||
GRANT EXECUTE ON FUNCTION `gazelle`.`future_bonus_accrual` TO 'www'@'localhost';
|
||||
GRANT SELECT ON `sys`.`schema_unused_indexes` TO 'www'@'localhost';
|
||||
GRANT SELECT ON `performance_schema`.`table_io_waits_summary_by_index_usage` TO 'www'@'localhost';
|
||||
GRANT SELECT ON `performance_schema`.`table_io_waits_summary_by_table` TO 'www'@'localhost';
|
||||
|
||||
@@ -27,13 +27,11 @@ docker exec -i $MYSQL_CONTAINER sh -c "exec mysql -uroot -p'$PASSWORD'" < all-da
|
||||
docker exec -it $MYSQL_CONTAINER mysql_upgrade -u root -p$PASSWORD
|
||||
|
||||
# Some custom functions may need to be recreated (the site will error on load)
|
||||
git grep CREATE FUNCTION misc/phinx/db/migrations
|
||||
misc/phinx/db/migrations/20200320183228_bonus_accrual_function.php
|
||||
git grep -l 'CREATE FUNCTION' misc/my-migrations
|
||||
misc/my-migrations/20180104060449_tables.php
|
||||
misc/my-migrations/20200320183228_bonus_accrual_function.php
|
||||
|
||||
Execute in the mysql client:
|
||||
CREATE FUNCTION bonus_accrual(Size bigint, Seedtime float, Seeders integer)
|
||||
RETURNS float DETERMINISTIC NO SQL
|
||||
RETURN Size / pow(1024, 3) * (0.0433 + (0.07 * ln(1 + Seedtime/24)) / pow(greatest(Seeders, 1), 0.35));
|
||||
Execute the function definition statements in the mysql client.
|
||||
|
||||
memcache
|
||||
--------
|
||||
|
||||
@@ -68,12 +68,15 @@ GRANT SELECT ON performance_schema.table_io_waits_summary_by_table TO 'ro_$MYSQL
|
||||
GRANT SELECT ON sys.schema_redundant_indexes TO 'ro_$MYSQL_USER'@'%';
|
||||
GRANT SELECT ON sys.schema_unused_indexes TO 'ro_$MYSQL_USER'@'%';
|
||||
GRANT SELECT ON sys.x\$schema_flattened_keys TO 'ro_$MYSQL_USER'@'%';
|
||||
CREATE FUNCTION IF NOT EXISTS bonus_accrual(Size bigint, Seedtime float, Seeders integer)
|
||||
RETURNS float DETERMINISTIC NO SQL
|
||||
RETURN Size / pow(1024, 3) * (0.0433 + (0.07 * ln(1 + Seedtime/24)) / pow(greatest(Seeders, 1), 0.35));
|
||||
CREATE FUNCTION IF NOT EXISTS binomial_ci(p int, n int)
|
||||
RETURNS float DETERMINISTIC
|
||||
RETURN IF(n = 0,0.0,((p + 1.35336) / n - 1.6452 * SQRT((p * (n-p)) / n + 0.67668) / n) / (1 + 2.7067 / n));
|
||||
CREATE FUNCTION IF NOT EXISTS bonus_accrual(Size bigint, Seedtime float, Seeders integer)
|
||||
RETURNS float DETERMINISTIC NO SQL
|
||||
RETURN Size / pow(1024, 3) * (0.0433 + (0.07 * ln(1 + Seedtime/24)) / pow(greatest(Seeders, 1), 0.35));
|
||||
CREATE FUNCTION IF NOT EXISTS category_bonus_accrual(size bigint, seedtime float, seeders integer, scale float)
|
||||
RETURNS float DETERMINISTIC NO SQL
|
||||
RETURN (size / scale) / pow(1024, 3) * (0.0433 + (0.07 * ln(1 + seedtime/24)) / pow(greatest(seeders, 1), 0.35));
|
||||
EOF
|
||||
) | mysql -u root -p"$MYSQL_ROOT_PASSWORD" || exit 1
|
||||
fi
|
||||
|
||||
44
misc/my-migrations/20250822000000_category_bonus_scale.php
Normal file
44
misc/my-migrations/20250822000000_category_bonus_scale.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CategoryBonusScale extends AbstractMigration {
|
||||
public function up(): void {
|
||||
$this->table('category')
|
||||
->addColumn('bonus_scale', 'float', ['default' => '1.0'])
|
||||
->save();
|
||||
$this->execute('
|
||||
CREATE FUNCTION IF NOT EXISTS category_bonus_accrual(size bigint, seedtime float, seeders integer, scale float)
|
||||
RETURNS float DETERMINISTIC NO SQL
|
||||
RETURN (size / scale) / pow(1024, 3) * (0.0433 + (0.07 * ln(1 + seedtime/24)) / pow(greatest(seeders, 1), 0.35))
|
||||
');
|
||||
// 0.0433 * 24 = 1.0392, 0.07 * 24 = 1.68
|
||||
$this->execute('
|
||||
CREATE FUNCTION IF NOT EXISTS future_bonus_accrual(size bigint, seedtime float, seeders integer, scale float, days float)
|
||||
RETURNS float DETERMINISTIC NO SQL
|
||||
RETURN (size / scale) / (1024*1024*1024)
|
||||
* (
|
||||
1.0392 * days
|
||||
+ 1.68 * (
|
||||
(seedtime / 24 + days + 1) * (ln(seedtime / 24 + days + 1) - 1)
|
||||
- (seedtime / 24 + 1) * (ln(seedtime / 24 + 1) - 1)
|
||||
)
|
||||
/ pow(greatest(seeders, 1), 0.35)
|
||||
);
|
||||
');
|
||||
}
|
||||
|
||||
public function down(): void {
|
||||
$this->table('category')
|
||||
->removeColumn('bonus_scale')
|
||||
->save();
|
||||
$this->execute('
|
||||
DROP FUNCTION IF EXISTS category_bonus_accrual
|
||||
');
|
||||
$this->execute('
|
||||
DROP FUNCTION IF EXISTS future_bonus_accrual
|
||||
');
|
||||
}
|
||||
}
|
||||
37
misc/pg-migrations/20250822000000_category_bonus_scale.php
Normal file
37
misc/pg-migrations/20250822000000_category_bonus_scale.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols
|
||||
require_once __DIR__ . '/../../lib/config.php';
|
||||
// phpcs:enable PSR1.Files.SideEffects.FoundWithSymbols
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CategoryBonusScale extends AbstractMigration {
|
||||
public function up(): void {
|
||||
$this->query("
|
||||
drop foreign table if exists relay.category
|
||||
");
|
||||
$this->query("
|
||||
import foreign schema " . MYSQL_DB
|
||||
. " limit to (category) from server relayer into relay;
|
||||
");
|
||||
$this->table('category')
|
||||
->addColumn('bonus_scale', 'float', ['default' => '1.0'])
|
||||
->save();
|
||||
}
|
||||
|
||||
public function down(): void {
|
||||
$this->query("
|
||||
drop foreign table if exists relay.category
|
||||
");
|
||||
$this->query("
|
||||
import foreign schema " . MYSQL_DB
|
||||
. " limit to (category) from server relayer into relay;
|
||||
");
|
||||
$this->table('category')
|
||||
->removeColumn('bonus_scale')
|
||||
->save();
|
||||
}
|
||||
}
|
||||
@@ -6,35 +6,16 @@ declare(strict_types=1);
|
||||
|
||||
namespace Gazelle;
|
||||
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$limit = TORRENTS_PER_PAGE;
|
||||
$offset = TORRENTS_PER_PAGE * ($page - 1);
|
||||
|
||||
$heading = new Util\SortableTableHeader('hourlypoints', [
|
||||
'size' => ['dbColumn' => 'size', 'defaultSort' => 'desc', 'text' => 'Size'],
|
||||
'seeders' => ['dbColumn' => 'seeders', 'defaultSort' => 'desc', 'text' => 'Seeders'],
|
||||
'seedtime' => ['dbColumn' => 'seed_time', 'defaultSort' => 'desc', 'text' => 'Duration'],
|
||||
'hourlypoints' => ['dbColumn' => 'hourly_points', 'defaultSort' => 'desc', 'text' => 'BP/hour'],
|
||||
'dailypoints' => ['dbColumn' => 'daily_points', 'defaultSort' => 'desc', 'text' => 'BP/day'],
|
||||
'weeklypoints' => ['dbColumn' => 'weekly_points', 'defaultSort' => 'desc', 'text' => 'BP/week'],
|
||||
'monthlypoints' => ['dbColumn' => 'monthly_points', 'defaultSort' => 'desc', 'text' => 'BP/month'],
|
||||
'yearlypoints' => ['dbColumn' => 'yearly_points', 'defaultSort' => 'desc', 'text' => 'BP/year'],
|
||||
'pointspergb' => ['dbColumn' => 'points_per_gb', 'defaultSort' => 'desc', 'text' => 'BP/GB/year'],
|
||||
]);
|
||||
|
||||
$userMan = new Manager\User();
|
||||
if (empty($_GET['userid'])) {
|
||||
$user = $Viewer;
|
||||
$ownProfile = true;
|
||||
} else {
|
||||
if (!$Viewer->permitted('admin_bp_history')) {
|
||||
Error403::error();
|
||||
}
|
||||
$user = $userMan->findById((int)($_GET['userid'] ?? 0));
|
||||
$user = new Manager\User()->findById((int)($_GET['userid'] ?? 0));
|
||||
if (is_null($user)) {
|
||||
Error404::error();
|
||||
}
|
||||
$ownProfile = false;
|
||||
}
|
||||
|
||||
$bonus = new User\Bonus($user);
|
||||
@@ -43,10 +24,9 @@ $paginator = new Util\Paginator(TORRENTS_PER_PAGE, (int)($_GET['page'] ?? 1));
|
||||
$paginator->setTotal($total['total_torrents']);
|
||||
|
||||
echo $Twig->render('user/bonus.twig', [
|
||||
'heading' => $heading,
|
||||
'list' => $bonus->seedList($heading->orderBy(), $heading->dir(), $paginator->limit(), $paginator->offset()),
|
||||
'heading' => $bonus->heading(),
|
||||
'list' => $bonus->seedList($paginator->limit(), $paginator->offset()),
|
||||
'paginator' => $paginator,
|
||||
'title' => $ownProfile ? 'Your Bonus Points Rate' : ($user->username() . "'s Bonus Point Rate"),
|
||||
'total' => $total,
|
||||
'user' => $user,
|
||||
'viewer' => $Viewer,
|
||||
|
||||
@@ -38,7 +38,7 @@ echo $Twig->render('bonus/store.twig', [
|
||||
'bonus' => $bonus,
|
||||
'discount' => $bonusMan->discount(),
|
||||
'donate' => $donate,
|
||||
'pool' => $bonusMan->getOpenPool(),
|
||||
'pool' => $bonusMan->openPoolList(),
|
||||
'purchase' => $purchase,
|
||||
'viewer' => $Viewer,
|
||||
]);
|
||||
|
||||
@@ -12,39 +12,39 @@
|
||||
<div class="thin">
|
||||
{% if paginator.offset == 0 %}
|
||||
<div class="pad box">
|
||||
{% if pool_total %}
|
||||
{% for p in pool_summary %}
|
||||
{% if pool_total %}
|
||||
{% for p in pool_summary %}
|
||||
<p>
|
||||
{%- if self %}You{% else %}{{ user.username }}{% endif %} spent
|
||||
{% if self %}You{% else %}{{ user.username }}{% endif %} spent
|
||||
{{ p.total|number_format }} bonus points to donate to the {{ p.name }}
|
||||
{% if date(now) > date(p.until_date) %}ended {% else %}ending in {% endif -%}
|
||||
{% if date(now) > date(p.until_date) %}ended {% else %}ending in {% endif -%}
|
||||
{{- p.until_date|time_diff }}.
|
||||
</p>
|
||||
{% endfor %}
|
||||
{% endif -%}
|
||||
{%- if summary.total -%}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if summary.total %}
|
||||
<p>
|
||||
{%- if self %}You{% else %}{{ user.id|user_url }}{% endif %} spent
|
||||
{% if pool_total %} a further {% endif -%} {{ summary.total|number_format }}
|
||||
{% if self %}You{% else %}{{ user.id|user_url }}{% endif %} spent
|
||||
{% if pool_total %} a further {% endif -%} {{ summary.total|number_format }}
|
||||
bonus points to purchase {{ summary.nr|number_format }} item{{ summary.nr|plural }}.
|
||||
</p>
|
||||
{% endif %}
|
||||
{%- if pool_total and summary.total %}
|
||||
{% endif %}
|
||||
{% if pool_total and summary.total %}
|
||||
<p>That makes a grand total of {{ (pool_total + summary.total)|number_format }} points,
|
||||
{%- set total = pool_total + summary.total -%}
|
||||
{%- if total > 500000 %} very
|
||||
{%- elseif total > 1000000 %} very, very
|
||||
{%- elseif total > 5000000 %} extremely
|
||||
{%- elseif total > 10000000 %} exceptionally
|
||||
{%- endif %} well done!</p>
|
||||
{%- endif %}
|
||||
{% set total = pool_total + summary.total -%}
|
||||
{% if total > 500000 %} very
|
||||
{% elseif total > 1000000 %} very, very
|
||||
{% elseif total > 5000000 %} extremely
|
||||
{% elseif total > 10000000 %} exceptionally
|
||||
{% endif %} well done!</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not history %}
|
||||
<h3>No purchase history</h3>
|
||||
{% else %}
|
||||
{% if paginator.offset == 0 %}
|
||||
{% if paginator.offset == 0 %}
|
||||
<br />
|
||||
<h3>Item summary</h3>
|
||||
<table>
|
||||
@@ -53,17 +53,17 @@
|
||||
<td style="text-align: right; width: 100px">Total</td>
|
||||
<td style="text-align: right; width: 100px">Cost</td>
|
||||
</tr>
|
||||
{% set total_item = 0 %}
|
||||
{% set total_cost = 0 %}
|
||||
{% for i in item %}
|
||||
{% set total_item = 0 %}
|
||||
{% set total_cost = 0 %}
|
||||
{% for i in item %}
|
||||
<tr class="row{{ cycle(['a', 'b'], loop.index0) }}">
|
||||
{% set total_item = total_item + i.total %}
|
||||
{% set total_cost = total_cost + i.cost %}
|
||||
{% set total_item = total_item + i.total %}
|
||||
{% set total_cost = total_cost + i.cost %}
|
||||
<td>{{ i.title }}</td>
|
||||
<td style="text-align: right;">{{ i.total|number_format }}</td>
|
||||
<td style="text-align: right;">{{ i.cost|number_format }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<tr style="border-top: #333333 solid 1px;">
|
||||
<td><i>Total</i></td>
|
||||
<td style="text-align: right;">{{ total_item|number_format }}</td>
|
||||
@@ -71,10 +71,10 @@
|
||||
</tr>
|
||||
</table>
|
||||
<br />
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<h3>Purchase Details</h3>
|
||||
{{ paginator.linkbox|raw }}
|
||||
{{ paginator.linkbox|raw }}
|
||||
<table>
|
||||
<tr class="colhead">
|
||||
<td>Item</td>
|
||||
@@ -82,14 +82,14 @@
|
||||
<td style="width:180px">Purchase Date</td>
|
||||
<td>For</td>
|
||||
</tr>
|
||||
{% for h in history %}
|
||||
{% for h in history %}
|
||||
<tr class="row{{ cycle(['a', 'b'], loop.index0) }}">
|
||||
<td>{{ h.Title }}</td>
|
||||
<td style="text-align: right;">{{ h.Price|number_format }}</td>
|
||||
<td>{{ h.PurchaseDate|time_diff }}</td>
|
||||
<td>{% if h.OtherUserID > 0 %}{{ h.OtherUserID|user_url }}{% else %} {% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{{ paginator.linkbox|raw }}
|
||||
{% endif %}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
{% set title %}
|
||||
{% if user.id == viewer.id %}
|
||||
Your Bonus Points Rates
|
||||
{% else %}
|
||||
{{ user.username }}'s Bonus Point Rates
|
||||
{% endif %}
|
||||
{% endset %}
|
||||
{{ header(title) }}
|
||||
<div class="header">
|
||||
<h2>{{ title }}</h2>
|
||||
@@ -42,16 +49,16 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="colhead">
|
||||
<td>Torrent</td>
|
||||
<td class="nobr">{{ heading.emit('title')|raw }}</td>
|
||||
<td class="nobr number_column">{{ heading.emit('size')|raw }}</td>
|
||||
<td class="nobr">{{ heading.emit('seeders')|raw }}</td>
|
||||
<td class="nobr">{{ heading.emit('seedtime')|raw }}</td>
|
||||
<td class="nobr">{{ heading.emit('hourlypoints')|raw }}</td>
|
||||
<td class="nobr">{{ heading.emit('dailypoints')|raw }}</td>
|
||||
<td class="nobr">{{ heading.emit('weeklypoints')|raw }}</td>
|
||||
<td class="nobr">{{ heading.emit('monthlypoints')|raw }}</td>
|
||||
<td class="nobr">{{ heading.emit('yearlypoints')|raw }}</td>
|
||||
<td class="nobr">{{ heading.emit('pointspergb')|raw }}</td>
|
||||
<td class="nobr number_column">{{ heading.emit('seeders')|raw }}</td>
|
||||
<td class="nobr number_column">{{ heading.emit('seedtime')|raw }}</td>
|
||||
<td class="nobr number_column">{{ heading.emit('hourlypoints')|raw }}</td>
|
||||
<td class="nobr number_column">{{ heading.emit('dailypoints')|raw }}</td>
|
||||
<td class="nobr number_column">{{ heading.emit('weeklypoints')|raw }}</td>
|
||||
<td class="nobr number_column">{{ heading.emit('monthlypoints')|raw }}</td>
|
||||
<td class="nobr number_column">{{ heading.emit('yearlypoints')|raw }}</td>
|
||||
<td class="nobr number_column">{{ heading.emit('pointspergb')|raw }}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -70,7 +77,7 @@
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="9" style="text-align:center;">No torrents being seeded currently</td>
|
||||
<td colspan="10" style="text-align:center;">No torrents being seeded currently</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -132,6 +132,7 @@ class Helper {
|
||||
array $tagName,
|
||||
int $releaseType = 1
|
||||
): \Gazelle\TGroup {
|
||||
$user->requestContext()->setViewer($user);
|
||||
$tgroup = new \Gazelle\Manager\TGroup()->create(
|
||||
categoryId: (int)new Category()->findIdByName('Music'),
|
||||
releaseType: $releaseType,
|
||||
|
||||
@@ -16,8 +16,10 @@ class BonusTest extends TestCase {
|
||||
Helper::removeTGroup($tgroup, current($this->userList));
|
||||
}
|
||||
}
|
||||
foreach ($this->userList as $user) {
|
||||
$user->remove();
|
||||
if (isset($this->userList)) {
|
||||
foreach ($this->userList as $user) {
|
||||
$user->remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +52,10 @@ class BonusTest extends TestCase {
|
||||
$giver->setPoints($startingPoints);
|
||||
$this->assertEquals($startingPoints, $giver->user()->bonusPointsTotal(), 'bonus-set-points');
|
||||
|
||||
$itemList = new Manager\Bonus()->itemList();
|
||||
$manager = new Manager\Bonus();
|
||||
$manager->flushPriceCache();
|
||||
|
||||
$itemList = $manager->itemList();
|
||||
$this->assertArrayHasKey('token-1', $itemList, 'item-token-1');
|
||||
$token = $giver->item('token-1');
|
||||
$this->assertArrayHasKey('Price', $token, 'item-price-1');
|
||||
@@ -134,9 +139,100 @@ class BonusTest extends TestCase {
|
||||
$this->assertTrue($giver->removePoints(1.125), 'bonus-taketh-away');
|
||||
}
|
||||
|
||||
public function testBonusPool(): void {
|
||||
global $Cache;
|
||||
$Cache->delete_value("bonus_pool");
|
||||
$manager = new Manager\Bonus();
|
||||
$this->assertEquals(
|
||||
[],
|
||||
$manager->openPoolList(),
|
||||
'bonus-open-pool',
|
||||
);
|
||||
}
|
||||
|
||||
public function testAddPoints(): void {
|
||||
$this->userList = [
|
||||
Helper::makeUser('bonusadd.' . randomString(6), 'bonus', enable: false),
|
||||
Helper::makeUser('bonusadd.' . randomString(6), 'bonus', enable: true),
|
||||
];
|
||||
// back to the future
|
||||
DB::DB()->prepared_query("
|
||||
INSERT INTO user_last_access (user_id, last_access) values (?, ?)
|
||||
", $this->userList[1]->id, date('Y-m-d H:i:s', time() + 10)
|
||||
);
|
||||
|
||||
$manager = new Manager\Bonus();
|
||||
$this->assertEquals(
|
||||
1,
|
||||
// but not too far
|
||||
$manager->addActivePoints(23456, date('Y-m-d H:i:s', time() + 5)),
|
||||
'bonus-add-active',
|
||||
);
|
||||
$this->assertEquals(
|
||||
0,
|
||||
$manager->addMultiPoints(789, []),
|
||||
'bonus-add-no-multi-points',
|
||||
);
|
||||
$this->assertEquals(
|
||||
2,
|
||||
$manager->addMultiPoints(
|
||||
12345,
|
||||
array_map(fn ($u) => $u->id, $this->userList),
|
||||
),
|
||||
'bonus-add-multi-points',
|
||||
);
|
||||
$this->assertEquals(
|
||||
0,
|
||||
$manager->addUploadPoints(369, date('Y-m-d H:i:s', time() + 1)),
|
||||
'bonus-add-upload-points',
|
||||
);
|
||||
$this->assertEquals(
|
||||
12345,
|
||||
$this->userList[0]->flush()->bonusPointsTotal(),
|
||||
'bonus-added-user-0',
|
||||
);
|
||||
|
||||
$this->tgroupList[] = Helper::makeTGroupMusic(
|
||||
name: 'bonus add ' . randomString(10),
|
||||
artistName: [[ARTIST_MAIN], ['phpunit bonus add ' . randomString(12)]],
|
||||
tagName: ['hard.bop'],
|
||||
user: $this->userList[1],
|
||||
);
|
||||
$torrent = Helper::makeTorrentMusic(
|
||||
tgroup: $this->tgroupList[0],
|
||||
user: $this->userList[1],
|
||||
title: 'phpunit bonus add ' . randomString(10),
|
||||
);
|
||||
Helper::generateTorrentSeed($torrent, $this->userList[1]);
|
||||
$this->assertGreaterThan(
|
||||
0,
|
||||
$manager->addSeedPoints(24680),
|
||||
'bonus-add-seed-points',
|
||||
);
|
||||
$this->assertEquals(
|
||||
12345 + 23456 + 24680,
|
||||
$this->userList[1]->flush()->bonusPointsTotal(),
|
||||
'bonus-added-user-1',
|
||||
);
|
||||
|
||||
$this->userList[1]->toggleAttr('no-fl-gifts', true);
|
||||
$this->assertGreaterThan(
|
||||
0,
|
||||
$manager->addGlobalPoints(7531),
|
||||
'bonus-add-global-points',
|
||||
);
|
||||
$this->assertEquals(
|
||||
12345 + 23456 + 24680,
|
||||
$this->userList[1]->flush()->bonusPointsTotal(),
|
||||
'bonus-added-no-global',
|
||||
);
|
||||
foreach ($this->userList as $u) {
|
||||
new User\Bonus($u)->setPoints(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
public function testUploadReward(): void {
|
||||
$this->userList[] = Helper::makeUser('bonusup.' . randomString(6), 'bonus');
|
||||
$this->userList[0]->requestContext()->setViewer($this->userList[0]);
|
||||
$this->tgroupList[] = Helper::makeTGroupMusic(
|
||||
name: 'bonus ' . randomString(10),
|
||||
artistName: [[ARTIST_MAIN], ['phpunit bonus ' . randomString(12)]],
|
||||
|
||||
@@ -262,8 +262,9 @@ class RequestTest extends TestCase {
|
||||
// race condition between requests.LastPostTime and requests.created that would be
|
||||
// difficult to remove without adding a lot of complications to the code.
|
||||
$this->assertFalse($this->request->hasNewVote(), 'request-no-new-vote');
|
||||
Helper::sleepTick(); // to ensure lastVoteDate() > created()
|
||||
|
||||
// add some bounty
|
||||
Helper::sleepTick(); // to ensure lastVoteDate() > created()
|
||||
$this->assertTrue($this->request->vote($user, $bounty), 'request-more-bounty');
|
||||
$this->assertTrue($this->request->flush()->hasNewVote(), 'request-has-new-vote');
|
||||
$this->assertEquals(2, $this->request->userVotedTotal(), 'request-total-voted');
|
||||
|
||||
@@ -78,4 +78,34 @@ class TimeTest extends TestCase {
|
||||
'time-convert-span'
|
||||
);
|
||||
}
|
||||
|
||||
public static function providerSeconds(): array {
|
||||
$hour = 3600;
|
||||
$day = $hour * 24;
|
||||
$week = $day * 7;
|
||||
$year = $week * 52;
|
||||
return [
|
||||
[ -1, '0s'],
|
||||
[ 0, '0s'],
|
||||
[ 1, '1s'],
|
||||
[ 1, '1s'],
|
||||
[ 60, '1m'],
|
||||
[ 119, '1m59s'],
|
||||
[ 3599, '59m59s'],
|
||||
[ $hour * 19, '19h'],
|
||||
[ $hour + 62, '1h1m'],
|
||||
[ $day * 4, '4d'],
|
||||
[ $week + 61, '1w1m'],
|
||||
[$year + $hour * 3, '1y3h'],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('providerSeconds')]
|
||||
public function testConvertSeconds(int $seconds, string $expected): void {
|
||||
$this->assertEquals(
|
||||
$expected,
|
||||
Time::convertSeconds($seconds),
|
||||
"time-seconds-$seconds"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user