mirror of
https://github.com/OPSnet/Gazelle.git
synced 2026-01-16 18:04:34 -05:00
generate invite tree with a recursive CTE
This commit is contained in:
@@ -11,145 +11,177 @@ use Gazelle\Enum\UserAuditEvent;
|
||||
* between the ancestor inviter and the invitee).
|
||||
*/
|
||||
|
||||
class InviteTree extends \Gazelle\Base {
|
||||
class InviteTree extends \Gazelle\BaseUser {
|
||||
protected array $info;
|
||||
|
||||
public function __construct(
|
||||
protected \Gazelle\User $user,
|
||||
protected \Gazelle\Manager\User $userMan,
|
||||
) {}
|
||||
protected array $tree;
|
||||
|
||||
public function flush(): static {
|
||||
unset($this->info);
|
||||
unset($this->info, $this->tree);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function info(): array {
|
||||
protected function calculate(): void {
|
||||
$width = (int)self::$db->scalar("
|
||||
select ceil(log10(max(id))) from users_main
|
||||
");
|
||||
/* Ordinarily, list queries are usually written to return only
|
||||
* an id, and a manager is used to hydrate the object. Amongst
|
||||
* other benefits, this simplifies cache invalidation. In the
|
||||
* case of invite trees, for some origin users there can be
|
||||
* tens of thousands of results. As a consequence, this is one
|
||||
* of the few cases where the all the required fields are
|
||||
* returned by a query. The management of paranoia is
|
||||
* particularly ghastly.
|
||||
*/
|
||||
self::$db->prepared_query("
|
||||
WITH RECURSIVE r AS (
|
||||
SELECT um.inviter_user_id AS inviter_user_id,
|
||||
um.ID AS user_id,
|
||||
0 AS depth,
|
||||
cast(lpad(um.ID, ?, '0') AS char(5000)) AS path
|
||||
FROM users_main um
|
||||
WHERE um.ID = ?
|
||||
UNION ALL
|
||||
SELECT c.inviter_user_id,
|
||||
c.ID,
|
||||
depth + 1,
|
||||
concat(r.path, lpad(c.ID, ?, '0'))
|
||||
FROM r,
|
||||
users_main AS c
|
||||
WHERE r.user_id = c.inviter_user_id
|
||||
)
|
||||
SELECT r.user_id,
|
||||
r.inviter_user_id,
|
||||
um.created,
|
||||
r.path,
|
||||
ula.last_access AS last_seen,
|
||||
if(locate('s:8:\"lastseen\";', um.Paranoia) > 0, 1, 0)
|
||||
AS paranoid_last_seen,
|
||||
um.Username AS username,
|
||||
um.RequiredRatio AS required_ratio,
|
||||
if(ui.RatioWatchEnds IS NOT NULL
|
||||
AND ui.RatioWatchEnds < now()
|
||||
AND uls.Uploaded <= uls.Downloaded * um.RequiredRatio,
|
||||
1, 0) AS on_ratio_watch,
|
||||
if(um.Enabled = '2', 1, 0) AS disabled,
|
||||
p.Name AS userclass,
|
||||
p.Level AS userlevel,
|
||||
uls.Uploaded AS uploaded,
|
||||
if(locate('s:10:\"uploaded\";', um.Paranoia) > 0, 1, 0)
|
||||
AS paranoid_up,
|
||||
uls.Downloaded AS downloaded,
|
||||
if(locate('s:10:\"downloaded\";', um.Paranoia) > 0, 1, 0)
|
||||
AS paranoid_down,
|
||||
if(ul.UserID IS NULL, 0, 1) AS donor,
|
||||
r.depth AS depth
|
||||
FROM r
|
||||
INNER JOIN users_main um ON (um.ID = r.user_id)
|
||||
INNER JOIN users_info ui ON (um.ID = ui.UserID)
|
||||
INNER JOIN permissions p ON (p.ID = um.PermissionID)
|
||||
INNER JOIN users_leech_stats uls ON (uls.UserID = um.ID)
|
||||
LEFT JOIN user_last_access ula ON (ula.user_id = um.ID)
|
||||
LEFT JOIN users_levels ul ON (
|
||||
ul.UserID = um.ID
|
||||
AND ul.PermissionID = (
|
||||
SELECT ID from permissions WHERE Name = 'donor'
|
||||
)
|
||||
)
|
||||
WHERE r.user_id != ?
|
||||
ORDER BY path
|
||||
", $width, $this->id(), $width, $this->id()
|
||||
);
|
||||
$userclassMap = []; // how many people per userclass
|
||||
$userlevelMap = []; // sort userclasses by level rather than name
|
||||
$this->info = [
|
||||
'branch' => 0,
|
||||
'downloaded' => 0,
|
||||
'depth' => 0,
|
||||
'direct' => [
|
||||
'down' => 0,
|
||||
'up' => 0,
|
||||
],
|
||||
'disabled' => 0,
|
||||
'donor' => 0,
|
||||
'paranoid' => 0,
|
||||
'uploaded' => 0,
|
||||
'userclass' => [],
|
||||
];
|
||||
$this->tree = [];
|
||||
$prev_depth = 0;
|
||||
foreach (self::$db->to_array(false, MYSQLI_ASSOC, false) as $row) {
|
||||
$this->info['downloaded'] += $row['downloaded'];
|
||||
$this->info['uploaded'] += $row['uploaded'];
|
||||
if ($row['depth'] == 1) {
|
||||
$this->info['direct']['down'] += $row['downloaded'];
|
||||
$this->info['direct']['up'] += $row['uploaded'];
|
||||
}
|
||||
if ($row['depth'] > $prev_depth) {
|
||||
// we have to go deeper
|
||||
$this->info['branch']++;
|
||||
}
|
||||
if (!isset($userlevelMap[$row['userclass']])) {
|
||||
$userlevelMap[$row['userclass']] = $row['userlevel'];
|
||||
}
|
||||
if (!isset($userclassMap[$row['userclass']])) {
|
||||
$userclassMap[$row['userclass']] = 0;
|
||||
}
|
||||
$userclassMap[$row['userclass']]++;
|
||||
if (!isset($this->info['userclass'][$row['userclass']])) {
|
||||
$this->info['userclass'][$row['userclass']] = 0;
|
||||
}
|
||||
$this->info['userclass'][$row['userclass']]++;
|
||||
if ($this->info['depth'] < $row['depth']) {
|
||||
$this->info['depth'] = $row['depth'];
|
||||
}
|
||||
if ($row['disabled']) {
|
||||
$this->info['disabled']++;
|
||||
}
|
||||
if ($row['donor']) {
|
||||
$this->info['donor']++;
|
||||
}
|
||||
if ($row['paranoid_down'] || $row['paranoid_up']) {
|
||||
$this->info['paranoid']++;
|
||||
}
|
||||
$this->tree[] = $row;
|
||||
$prev_depth = $row['depth'];
|
||||
}
|
||||
uksort($userclassMap, fn($a, $b) => $userlevelMap[$a] <=> $userlevelMap[$b]);
|
||||
$this->info['userclass'] = $userclassMap;
|
||||
$this->info['total'] = count($this->tree);
|
||||
}
|
||||
|
||||
public function summary(): array {
|
||||
if (!isset($this->info)) {
|
||||
$this->info = self::$db->rowAssoc("
|
||||
SELECT
|
||||
t1.TreeID AS tree_id,
|
||||
t1.TreeLevel AS depth,
|
||||
t1.TreePosition AS position,
|
||||
(
|
||||
SELECT t2.TreePosition
|
||||
FROM invite_tree AS t2
|
||||
WHERE t2.TreeID = t1.TreeID
|
||||
AND t2.TreeLevel = t1.TreeLevel
|
||||
AND t2.TreePosition > t1.TreePosition
|
||||
ORDER BY t2.TreePosition
|
||||
LIMIT 1
|
||||
) AS max_position
|
||||
FROM invite_tree AS t1
|
||||
WHERE t1.UserID = ?
|
||||
", $this->user->id()
|
||||
) ?? [
|
||||
'tree_id' => 0,
|
||||
'depth' => null,
|
||||
'position' => null,
|
||||
'max_position' => null,
|
||||
];
|
||||
$this->calculate();
|
||||
}
|
||||
return $this->info;
|
||||
}
|
||||
|
||||
public function treeId(): int {
|
||||
return $this->info()['tree_id'];
|
||||
}
|
||||
|
||||
public function depth(): ?int {
|
||||
return $this->info()['depth'];
|
||||
}
|
||||
|
||||
public function position(): ?int {
|
||||
return $this->info()['position'];
|
||||
}
|
||||
|
||||
public function maxPosition(): ?int {
|
||||
return $this->info()['max_position'];
|
||||
public function inviteTree(): array {
|
||||
if (!isset($this->tree)) {
|
||||
$this->calculate();
|
||||
}
|
||||
return $this->tree;
|
||||
}
|
||||
|
||||
public function hasInvitees(): bool {
|
||||
return (bool)self::$db->scalar("
|
||||
SELECT 1
|
||||
FROM invite_tree
|
||||
WHERE InviterId = ?
|
||||
LIMIT 1
|
||||
", $this->user->id()
|
||||
);
|
||||
return $this->summary()['total'] > 0;
|
||||
}
|
||||
|
||||
public function inviteeList(): array {
|
||||
self::$db->prepared_query("
|
||||
SELECT UserID
|
||||
FROM invite_tree
|
||||
WHERE TreeID = ?
|
||||
AND TreeLevel > ?
|
||||
AND TreePosition > ?
|
||||
AND TreePosition < coalesce(?, 100000000)
|
||||
ORDER BY TreePosition
|
||||
", $this->treeId(), $this->depth(), $this->position(), $this->maxPosition()
|
||||
return array_map(
|
||||
fn ($u) => $u['user_id'],
|
||||
$this->inviteTree()
|
||||
);
|
||||
return self::$db->collect('UserID');
|
||||
}
|
||||
|
||||
public function add(\Gazelle\User $user): int {
|
||||
if (!$this->treeId()) {
|
||||
// Not everyone is created by the genesis user. Invite trees may be disconnected.
|
||||
self::$db->prepared_query("
|
||||
INSERT INTO invite_tree
|
||||
(UserID, TreeID)
|
||||
VALUES (?, (SELECT coalesce(max(it.TreeID), 0) + 1 FROM invite_tree AS it))
|
||||
", $this->user->id()
|
||||
);
|
||||
$this->flush();
|
||||
}
|
||||
$nextPosition = self::$db->scalar("
|
||||
SELECT TreePosition
|
||||
FROM invite_tree
|
||||
WHERE TreeID = ?
|
||||
AND TreePosition > ?
|
||||
AND TreeLevel <= ?
|
||||
ORDER BY TreePosition LIMIT 1
|
||||
", $this->treeId(), $this->position(), $this->depth()
|
||||
);
|
||||
if (!$nextPosition) {
|
||||
// Tack them on the end of the list.
|
||||
$nextPosition = self::$db->scalar("
|
||||
SELECT max(TreePosition) + 1
|
||||
FROM invite_tree
|
||||
WHERE TreeID = ?
|
||||
", $this->treeId()
|
||||
);
|
||||
} else {
|
||||
// Someone invited Alice and then Bob. Later on, Alice invites Carol,
|
||||
// so Bob and others have to "pushed down" a row so that Carol can
|
||||
// be lodged under Alice.
|
||||
self::$db->prepared_query("
|
||||
UPDATE invite_tree SET
|
||||
TreePosition = TreePosition + 1
|
||||
WHERE TreeID = ?
|
||||
AND TreePosition >= ?
|
||||
", $this->treeId(), $nextPosition
|
||||
);
|
||||
}
|
||||
self::$db->prepared_query("
|
||||
INSERT INTO invite_tree
|
||||
(UserID, InviterID, TreeID, TreePosition, TreeLevel)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
", $user->id(), $this->user->id(), $this->treeId(), $nextPosition, $this->depth() + 1
|
||||
);
|
||||
$affected = self::$db->affected_rows();
|
||||
$this->flush();
|
||||
return $affected;
|
||||
}
|
||||
|
||||
public function manipulate(
|
||||
string $comment,
|
||||
bool $doDisable,
|
||||
bool $doInvites,
|
||||
\Gazelle\Tracker $tracker,
|
||||
\Gazelle\User $admin,
|
||||
string $comment,
|
||||
bool $doDisable,
|
||||
bool $doInvites,
|
||||
\Gazelle\Tracker $tracker,
|
||||
\Gazelle\User $admin,
|
||||
\Gazelle\Manager\User $userMan,
|
||||
): string {
|
||||
if ($doDisable) {
|
||||
$message = "Banned";
|
||||
@@ -178,7 +210,7 @@ class InviteTree extends \Gazelle\Base {
|
||||
$this->user->auditTrail()->addEvent(UserAuditEvent::invite, $staffNote);
|
||||
$ban = [];
|
||||
foreach ($inviteeList as $inviteeId) {
|
||||
$invitee = $this->userMan->findById($inviteeId);
|
||||
$invitee = $userMan->findById($inviteeId);
|
||||
if (is_null($invitee)) {
|
||||
continue;
|
||||
}
|
||||
@@ -200,13 +232,13 @@ class InviteTree extends \Gazelle\Base {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!$doDisable) { // $this->userMan->disableUserList will add the staff note otherwise
|
||||
if (!$doDisable) { // $userMan->disableUserList will add the staff note otherwise
|
||||
$invitee->addStaffNote($staffNote)->modify();
|
||||
$invitee->auditTrail()->addEvent(UserAuditEvent::invite, $staffNote);
|
||||
}
|
||||
}
|
||||
if ($ban) {
|
||||
$this->userMan->disableUserList(
|
||||
$userMan->disableUserList(
|
||||
$tracker,
|
||||
$ban,
|
||||
UserAuditEvent::invite,
|
||||
@@ -216,114 +248,4 @@ class InviteTree extends \Gazelle\Base {
|
||||
}
|
||||
return $message;
|
||||
}
|
||||
|
||||
public function details(\Gazelle\User $viewer): array {
|
||||
if (!$this->treeId()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$maxDepth = $this->depth(); // The deepest level (this increases when an invitee invites someone else)
|
||||
|
||||
$args = [$this->treeId(), $this->position(), $this->depth()];
|
||||
$maxPosition = self::$db->scalar("
|
||||
SELECT TreePosition
|
||||
FROM invite_tree
|
||||
WHERE TreeID = ?
|
||||
AND TreePosition > ?
|
||||
AND TreeLevel = ?
|
||||
ORDER BY TreePosition ASC
|
||||
LIMIT 1
|
||||
", ...$args
|
||||
);
|
||||
if (is_null($maxPosition)) {
|
||||
$maxCond = '/* no max pos */';
|
||||
} else {
|
||||
$maxCond = 'AND it.TreePosition < ?';
|
||||
$args[] = $maxPosition;
|
||||
}
|
||||
self::$db->prepared_query("
|
||||
SELECT
|
||||
it.UserID,
|
||||
it.TreePosition,
|
||||
it.TreeLevel
|
||||
FROM invite_tree AS it
|
||||
WHERE it.TreeID = ?
|
||||
AND it.TreePosition > ?
|
||||
AND it.TreeLevel > ?
|
||||
$maxCond
|
||||
ORDER BY it.TreePosition
|
||||
", ...$args
|
||||
);
|
||||
$inviteeList = self::$db->to_array(false, MYSQLI_NUM, false);
|
||||
|
||||
$info = [
|
||||
'tree' => [],
|
||||
'total' => 0,
|
||||
'branch' => 0,
|
||||
'disabled' => 0,
|
||||
'donor' => 0,
|
||||
'paranoid' => 0,
|
||||
'upload_total' => 0,
|
||||
'download_total' => 0,
|
||||
'upload_top' => 0,
|
||||
'download_top' => 0,
|
||||
];
|
||||
$classSummary = [];
|
||||
foreach ($inviteeList as [$inviteeId, /* $position -- unused */, $depth]) {
|
||||
$invitee = $this->userMan->findById($inviteeId);
|
||||
if (is_null($invitee)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$info['total']++;
|
||||
$info['tree'][] = [
|
||||
'user' => $invitee,
|
||||
'depth' => $depth,
|
||||
];
|
||||
if ($invitee->isDisabled()) {
|
||||
$info['disabled']++;
|
||||
}
|
||||
if ((new Donor($invitee))->isDonor()) {
|
||||
$info['donor']++;
|
||||
}
|
||||
|
||||
$paranoid = $invitee->propertyVisibleMulti($viewer, ['uploaded', 'downloaded']) === PARANOIA_HIDE;
|
||||
if ($depth == $this->depth() + 1) {
|
||||
$info['branch']++;
|
||||
if (!$paranoid) {
|
||||
$info['upload_top'] += $invitee->uploadedSize();
|
||||
$info['download_top'] += $invitee->downloadedSize();
|
||||
}
|
||||
}
|
||||
if ($paranoid) {
|
||||
$info['paranoid']++;
|
||||
} else {
|
||||
$info['upload_total'] += $invitee->uploadedSize();
|
||||
$info['download_total'] += $invitee->downloadedSize();
|
||||
}
|
||||
|
||||
$primaryClass = $invitee->primaryClass();
|
||||
if (!isset($classSummary[$primaryClass])) {
|
||||
$classSummary[$primaryClass] = 0;
|
||||
}
|
||||
$classSummary[$primaryClass]++;
|
||||
|
||||
if ($maxDepth < $depth) {
|
||||
$maxDepth = $depth;
|
||||
}
|
||||
}
|
||||
return $info['total'] === 0
|
||||
? []
|
||||
: [ 'classes' =>
|
||||
array_merge(
|
||||
...array_map(
|
||||
fn($c) => [$this->userMan->userclassName($c) => $classSummary[$c]],
|
||||
array_keys($classSummary)
|
||||
)
|
||||
),
|
||||
'depth' => $this->depth(),
|
||||
'height' => $maxDepth - $this->depth(),
|
||||
'info' => $info,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +157,6 @@ class UserCreator extends Base {
|
||||
|
||||
if ($inviter) {
|
||||
(new Manager\InviteSource())->resolveInviteSource($this->inviteKey, $user);
|
||||
(new User\InviteTree($inviter, $manager))->add($user);
|
||||
$inviter->stats()->increment('invited_total');
|
||||
$user->externalProfile()->modifyProfile($inviterReason);
|
||||
self::$db->prepared_query("
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../lib/bootstrap.php';
|
||||
$db = Gazelle\DB::DB();
|
||||
|
||||
$db->prepared_query("
|
||||
DELETE FROM invite_tree
|
||||
");
|
||||
$invite = $db->prepared_query('
|
||||
SELECT ID, inviter_user_id
|
||||
FROM users_main
|
||||
WHERE inviter_user_id > 0
|
||||
ORDER BY UserID
|
||||
');
|
||||
$inv = [];
|
||||
while ([$invitee, $inviter] = $db->next_record()) {
|
||||
$save = $db->get_query_id();
|
||||
if (!isset($inv[$inviter])) {
|
||||
$inv[$inviter] = new Gazelle\User\InviteTree(new Gazelle\User($inviter), new Gazelle\Manager\User());
|
||||
}
|
||||
$inv[$inviter]->add($invitee);
|
||||
$db->set_query_id($save);
|
||||
}
|
||||
@@ -29,17 +29,18 @@ if (isset($_POST['id'])) {
|
||||
error(404);
|
||||
}
|
||||
|
||||
$message = (new Gazelle\User\InviteTree($user, $userMan))
|
||||
$message = (new Gazelle\User\InviteTree($user))
|
||||
->manipulate(
|
||||
$comment,
|
||||
$doDisable,
|
||||
$doInvites,
|
||||
new \Gazelle\Tracker(),
|
||||
$Viewer
|
||||
$Viewer,
|
||||
$userMan,
|
||||
);
|
||||
}
|
||||
|
||||
echo $Twig->render('user/invite-tree-bulkedit.twig', [
|
||||
'auth' => $Viewer->auth(),
|
||||
'viewer' => $Viewer,
|
||||
'message' => $message,
|
||||
]);
|
||||
|
||||
@@ -16,7 +16,7 @@ if (!isset($_GET['userid'])) {
|
||||
}
|
||||
|
||||
echo $Twig->render('user/invite-tree-page.twig', [
|
||||
...(new Gazelle\User\InviteTree($user, $userMan))->details($Viewer),
|
||||
'tree' => new Gazelle\User\InviteTree($user),
|
||||
'user' => $user,
|
||||
'viewer' => $Viewer,
|
||||
]);
|
||||
|
||||
@@ -17,9 +17,8 @@ if (is_null($user)) {
|
||||
json_die("Not found");
|
||||
}
|
||||
|
||||
$tree = new Gazelle\User\InviteTree($user, $userMan);
|
||||
echo json_encode($Twig->render('user/invite-tree.twig', [
|
||||
...$tree->details($Viewer),
|
||||
'tree' => new Gazelle\User\InviteTree($user),
|
||||
'user' => $user,
|
||||
'viewer' => $Viewer,
|
||||
]));
|
||||
|
||||
@@ -345,7 +345,7 @@ if ($Viewer->permitted('users_linked_users')) {
|
||||
}
|
||||
|
||||
if ($Viewer->permitted('users_view_invites')) {
|
||||
$tree = new Gazelle\User\InviteTree($user, $userMan);
|
||||
$tree = new Gazelle\User\InviteTree($user);
|
||||
if ($tree->hasInvitees()) {
|
||||
?>
|
||||
<div class="box" id="invitetree_box">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
{% endif %}
|
||||
<form class="manage_form" name="user" action="" method="post">
|
||||
<input type="hidden" id="action" name="action" value="manipulate_tree" />
|
||||
<input type="hidden" name="auth" value="{{ auth }}" />
|
||||
<input type="hidden" name="auth" value="{{ viewer.auth }}" />
|
||||
<table class="layout">
|
||||
<tr>
|
||||
<td class="label"><strong>UserID or @username</strong></td>
|
||||
|
||||
@@ -6,12 +6,9 @@
|
||||
<div class="box pad">
|
||||
<div class="invitetree pad">
|
||||
{% include 'user/invite-tree.twig' with {
|
||||
'classes': classes,
|
||||
'depth': depth,
|
||||
'height': height,
|
||||
'info': info,
|
||||
'user': user,
|
||||
'viewer': viewer,
|
||||
'tree' : tree,
|
||||
'user' : user,
|
||||
'viewer' : viewer,
|
||||
} only %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,90 +1,97 @@
|
||||
{% if info.total == 0 %}
|
||||
<p>Nobody turned up to the gig.</p>
|
||||
{% else %}
|
||||
{% set invitee_total = info.total %}
|
||||
<p>
|
||||
This tree has {{ invitee_total|number_format }} entries, {{ info.branch|number_format }} branches, and a height of {{ height }}.
|
||||
{% set summary = tree.summary %}
|
||||
{% set max_depth = summary.depth %}
|
||||
{% set prev_depth = summary.depth %}
|
||||
{% set invitee_total = summary.total %}
|
||||
{% set override = viewer.isStaff or viewer.id == tree.user.id %}
|
||||
{% for invitee in tree.inviteTree %}
|
||||
{% if loop.first %}
|
||||
<p>This tree has {{ invitee_total|number_format }} {%
|
||||
if invitee_total == 1 %}entry{% else %}entries{% endif %}, {{
|
||||
summary.branch|number_format }} branch{{
|
||||
summary.branch|plural('es') }}, and a height of {{
|
||||
summary.depth }}.
|
||||
It has
|
||||
{%- for class_name, total in classes -%}
|
||||
{{ loop.last ? ' and' : (loop.first ? '' : ',') }}
|
||||
{{ total }}
|
||||
{% if total == 1 -%}
|
||||
{{ class_name }}
|
||||
{%- else -%}
|
||||
{%- if class_name == 'Torrent Celebrity' -%}
|
||||
Torrent Celebrities
|
||||
{%- else -%}
|
||||
{{ class_name }}s
|
||||
{%- endif -%}
|
||||
{%- endif %}
|
||||
({{ (total / invitee_total * 100)|number_format }}%)
|
||||
{%- endfor -%}.
|
||||
|
||||
{{ info.disabled }}{% if info.disabled == 1 %} user is {% else %} users are {% endif %}
|
||||
disabled ({{ (info.disabled / invitee_total * 100)|number_format }}%)
|
||||
and {{ info.donor }}{% if info.donor == 1 %} user has {% else %} users have {% endif %}
|
||||
donated ({{ (info.donor / invitee_total * 100)|number_format }}%).
|
||||
{%- for userclass, total in summary.userclass -%}
|
||||
{{ loop.last and invitee_total > 1 ? ' and' : (loop.first ? '' : ',') }}
|
||||
{{ total|number_format }}
|
||||
{% if total == 1 -%}
|
||||
{{ userclass -}}
|
||||
{% else -%}
|
||||
{% if userclass == 'Torrent Celebrity' %}Torrent Celebrities{% else %}{{ userclass }}s{% endif -%}
|
||||
{% endif %} ({{ (total / invitee_total * 100)|number_format }}%)
|
||||
{%- endfor -%}.
|
||||
{{ summary.disabled|number_format }}{% if summary.disabled == 1 %} user is {%
|
||||
else %} users are {% endif -%} disabled ({{
|
||||
(summary.disabled / invitee_total * 100)|number_format }}%) and {{
|
||||
summary.donor|number_format }}{% if summary.donor == 1 %} user has{%
|
||||
else %} users have{% endif %} donated ({{
|
||||
(summary.donor / invitee_total * 100)|number_format }}%).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The amount uploaded by direct invitees is {{ info.upload_top|octet_size }};
|
||||
the amount downloaded by direct invitees is {{ info.download_top|octet_size }} and the
|
||||
aggregate ratio is <span class="stat">{{ ratio(info.upload_top, info.download_top) }}</span>.
|
||||
The amount uploaded by direct invitees is {{ summary.direct.up|octet_size }};
|
||||
the amount downloaded by direct invitees is {{ summary.direct.down|octet_size }} and the
|
||||
aggregate ratio is <span class="stat">{{ ratio(summary.direct.up, summary.direct.down) }}</span>.
|
||||
</p>
|
||||
|
||||
{% if height > 1 %}
|
||||
{% if summary.depth > 1 %}
|
||||
<p>
|
||||
The total amount uploaded by the entire tree is {{ info.upload_total|octet_size }};
|
||||
the total amount downloaded is {{ info.download_total|octet_size }} and the
|
||||
aggregate ratio is <span class="stat">{{ ratio(info.upload_total, info.download_total) }}</span>.
|
||||
The total amount uploaded by the entire tree is {{ summary.uploaded|octet_size }};
|
||||
the total amount downloaded is {{ summary.downloaded|octet_size }} and the
|
||||
aggregate ratio is <span class="stat">{{ ratio(summary.uploaded, summary.downloaded) }}</span>.
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if info.paranoid %}
|
||||
{% if info.paranoid %}
|
||||
<p style="font-weight: bold;">
|
||||
{{ info.paranoid }} user{{ info.paranoid|plural }} ({{ (info.paranoid / invitee_total * 100)|number_format }}%)
|
||||
{{ info.paranoid == 1 ? 'is' : 'are' }} too paranoid to have their stats shown here,
|
||||
and {{ info.paranoid == 1 ? 'was' : 'were' }} not factored into the upload and download totals.
|
||||
{{ summary.paranoid|number_format }} user{{summary.paranoid|plural }} ({{ (summary.paranoid / invitee_total * 100)|number_format }}%)
|
||||
{{ summary.paranoid == 1 ? 'is' : 'are' }} too paranoid to have their stats shown here,
|
||||
and {{ summary.paranoid == 1 ? 'was' : 'were' }} not factored into the upload and download totals.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% set max_depth = depth %}
|
||||
{% set prev_depth = depth %}
|
||||
|
||||
{% for invitee in info.tree %}
|
||||
{% if invitee.depth > prev_depth %}
|
||||
{{ '<ul class="invitetree"><li>'|repeat(invitee.depth - prev_depth)|raw }}
|
||||
{% elseif invitee.depth < prev_depth %}
|
||||
{{ '</li></ul>'|repeat(prev_depth - invitee.depth)|raw }}
|
||||
</li>
|
||||
<li>
|
||||
{% else %}
|
||||
</li>
|
||||
<li>
|
||||
{% endif %}
|
||||
<strong>{{ invitee.user.id|user_full }}</strong>
|
||||
{% if max_depth < invitee.depth %}
|
||||
{% set max_depth = invitee.depth %}
|
||||
{% endif %}
|
||||
{% set prev_depth = invitee.depth %}
|
||||
|
||||
{% set paranoia = invitee.user.propertyVisibleMulti(viewer, ['uploaded', 'downloaded']) -%}
|
||||
{% if paranoia == constant('PARANOIA_HIDE') -%}
|
||||
Hidden
|
||||
{%- else -%}
|
||||
{% if paranoia == constant('PARANOIA_OVERRIDDEN') %}<i>{% endif %}
|
||||
Uploaded: <strong>{{ invitee.user.uploadedSize|octet_size }}</strong>
|
||||
Downloaded: <strong>{{ invitee.user.downloadedSize|octet_size }}</strong>
|
||||
Ratio: <strong>{{ ratio(invitee.user.uploadedSize, invitee.user.downloadedSize) }}</strong>
|
||||
<span title="Required ratio"{%
|
||||
if invitee.user.onRatioWatch %} style="color:crimson"{% endif
|
||||
%}>({{ invitee.user.requiredRatio|number_format(2) }})</span>,
|
||||
{% if paranoia == constant('PARANOIA_OVERRIDDEN') %}</i>{% endif %}
|
||||
joined: {{ invitee.user.created|time_diff(1) -}}
|
||||
{% if invitee.user.propertyVisible(viewer, 'lastseen') -%}
|
||||
, last seen {{ invitee.user.lastAccessRealtime|time_diff(1) }}
|
||||
{% endif %}
|
||||
<hr />
|
||||
{% endif %}
|
||||
{% if invitee.depth > prev_depth %}
|
||||
{{ '<ul class="invitetree"><li>'|repeat(invitee.depth - prev_depth)|raw }}
|
||||
{% elseif invitee.depth < prev_depth %}
|
||||
{{ '</li></ul>'|repeat(prev_depth - invitee.depth)|raw }}
|
||||
</li>
|
||||
<li>
|
||||
{% else %}
|
||||
</li>
|
||||
<li>
|
||||
{% endif %}
|
||||
<strong><a href="user.php?id={{ invitee.user_id }}">{{ invitee.username }}</a></strong>
|
||||
{% if invitee.disabled %} <a href="rules.php"><img src="{{ constant('STATIC_SERVER')
|
||||
}}/common/symbols/disabled.png" alt="Banned" title="Disabled" class="tooltip" /></a>{% endif %}
|
||||
{% if max_depth < invitee.depth %}
|
||||
{% set max_depth = invitee.depth %}
|
||||
{% endif %}
|
||||
{% set prev_depth = invitee.depth %}
|
||||
{% if invitee.paranoid_up %}<i>{% endif %}
|
||||
uploaded <strong>{% if override %}{{ invitee.uploaded|octet_size }}{% else %}Hidden{% endif %}</strong>
|
||||
{%- if invitee.paranoid_up %}</i>{% endif %},
|
||||
{% if invitee.paranoid_down %}<i>{% endif %}
|
||||
downloaded <strong>{% if override %}{{ invitee.downloaded|octet_size }}{% else %}Hidden{% endif %}</strong>
|
||||
{%- if invitee.paranoid_down %}</i>{% endif %},
|
||||
ratio <strong>
|
||||
{%- if invitee.paranoid_down or invitee.paranoid_up %}
|
||||
{% if override -%}
|
||||
<i>{{ ratio(invitee.uploaded, invitee.downloaded) }}</i>
|
||||
{% else -%}
|
||||
Hidden
|
||||
{% endif -%}
|
||||
{% else -%}
|
||||
{{ ratio(invitee.uploaded, invitee.downloaded) }}
|
||||
{% endif -%}
|
||||
</strong>
|
||||
<span title="Required ratio"{% if invitee.on_ratio_watch %} style="color:crimson"{% endif %}>({{ invitee.required_ratio|number_format(2) }})</span>,
|
||||
joined: {{ invitee.created|time_diff(1) -}}
|
||||
{% if not invitee.paranoia_last_seen -%}
|
||||
, last seen {{ invitee.last_seen|time_diff(1) }}
|
||||
{% endif %}
|
||||
{%- endif -%}
|
||||
{% if loop.last %}
|
||||
{{ '</li></ul>'|repeat(prev_depth - invitee.depth)|raw }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>Nobody turned up to the gig.</p>
|
||||
{% endfor %}
|
||||
{{ '</li></ul>'|repeat(prev_depth - depth)|raw }}
|
||||
{% endif %}
|
||||
|
||||
@@ -20,7 +20,7 @@ class InviteTest extends TestCase {
|
||||
$this->user->remove();
|
||||
}
|
||||
|
||||
public function testInvite(): void {
|
||||
public function testInviter(): void {
|
||||
$this->assertFalse($this->user->disableInvites(), 'invite-not-disabled');
|
||||
$this->assertFalse($this->user->permitted('users_view_invites'), 'invite-users-view-invites');
|
||||
$this->assertFalse($this->user->permitted('site_send_unlimited_invites'), 'invite-site-send-unlimited-invites');
|
||||
@@ -74,18 +74,34 @@ class InviteTest extends TestCase {
|
||||
$this->assertEquals($this->invitee->id(), $inviteList[0], 'invite-list-has-invitee');
|
||||
|
||||
$this->assertTrue($this->invitee->isUnconfirmed(), 'invitee-unconfirmed');
|
||||
$this->assertInstanceOf(User::class, (new Manager\User())->findByAnnounceKey($this->invitee->announceKey()), 'invitee-confirmable');
|
||||
$this->assertInstanceOf(
|
||||
User::class,
|
||||
(new Manager\User())->findByAnnounceKey($this->invitee->announceKey()),
|
||||
'invitee-confirmable'
|
||||
);
|
||||
|
||||
// invite tree functionality
|
||||
$inviteTree = new User\InviteTree($this->user, new Manager\User());
|
||||
$inviteTree = new User\InviteTree($this->user);
|
||||
$this->assertInstanceOf(User\InviteTree::class, $inviteTree, 'invite-tree-ctor');
|
||||
$this->assertGreaterThan(0, $inviteTree->treeId(), 'invite-tree-new-id');
|
||||
$this->assertTrue($inviteTree->hasInvitees(), 'invite-tree-has-invitees');
|
||||
$this->assertEquals(0, $inviteTree->depth(), 'invite-tree-depth');
|
||||
$list = $inviteTree->inviteeList();
|
||||
$this->assertCount(1, $list, 'invite-tree-list');
|
||||
$this->assertEquals($this->invitee->id(), $list[0], 'invite-tree-user-id');
|
||||
$this->assertGreaterThan(0, $inviteTree->position(), 'invite-tree-position');
|
||||
|
||||
// new invite tree functionality
|
||||
$summary = $inviteTree->summary();
|
||||
$this->assertCount(10, array_keys($summary), 'invite-tree-summary-keys');
|
||||
$this->assertEquals(1, $summary['branch'], 'invite-tree-branch');
|
||||
$this->assertEquals(1, $summary['depth'], 'invite-tree-depth');
|
||||
$this->assertEquals(1, $summary['total'], 'invite-tree-total');
|
||||
$this->assertEquals(0, $summary['disabled'], 'invite-tree-disabled');
|
||||
$this->assertEquals(0, $summary['donor'], 'invite-tree-donor');
|
||||
$this->assertEquals(0, $summary['downloaded'], 'invite-tree-downloaded');
|
||||
$this->assertEquals(0, $summary['paranoid'], 'invite-tree-paranoid');
|
||||
$this->assertEquals(STARTING_UPLOAD, $summary['direct']['up'], 'invite-tree-direct-up');
|
||||
$this->assertEquals(STARTING_UPLOAD, $summary['uploaded'], 'invite-tree-uploaded');
|
||||
$this->assertEquals(['User' => 1], $summary['userclass'], 'invite-tree-userclass');
|
||||
$this->assertCount(1, $inviteTree->inviteTree(), 'invite-tree-tree');
|
||||
}
|
||||
|
||||
public function testAncestry(): void {
|
||||
@@ -122,26 +138,28 @@ class InviteTest extends TestCase {
|
||||
|
||||
$this->assertEquals(
|
||||
"No action specified",
|
||||
(new User\InviteTree($this->user, $userMan))
|
||||
(new User\InviteTree($this->user))
|
||||
->manipulate(
|
||||
"",
|
||||
false,
|
||||
false,
|
||||
$tracker,
|
||||
$this->user,
|
||||
$userMan,
|
||||
),
|
||||
'invite-tree-manip-none'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"No invitees for {$this->user->username()}",
|
||||
(new User\InviteTree($this->user, $userMan))
|
||||
(new User\InviteTree($this->user))
|
||||
->manipulate(
|
||||
"phpunit invite tree comment",
|
||||
false,
|
||||
false,
|
||||
$tracker,
|
||||
$this->user,
|
||||
$userMan,
|
||||
),
|
||||
'invite-tree-manip-comment'
|
||||
);
|
||||
@@ -167,13 +185,14 @@ class InviteTest extends TestCase {
|
||||
|
||||
$this->assertStringContainsString(
|
||||
"Commented entire tree (1 user)",
|
||||
(new User\InviteTree($this->user, $userMan))
|
||||
(new User\InviteTree($this->user))
|
||||
->manipulate(
|
||||
"phpunit invite tree comment",
|
||||
false,
|
||||
false,
|
||||
$tracker,
|
||||
$this->user,
|
||||
$userMan,
|
||||
),
|
||||
'invite-tree-manip-comment'
|
||||
);
|
||||
@@ -191,13 +210,14 @@ class InviteTest extends TestCase {
|
||||
|
||||
$this->assertStringContainsString(
|
||||
"Revoked invites for entire tree (1 user)",
|
||||
(new User\InviteTree($this->user, $userMan))
|
||||
(new User\InviteTree($this->user))
|
||||
->manipulate(
|
||||
"",
|
||||
false,
|
||||
true,
|
||||
$tracker,
|
||||
$this->user,
|
||||
$userMan,
|
||||
),
|
||||
'invite-tree-manip-revoke'
|
||||
);
|
||||
@@ -215,13 +235,14 @@ class InviteTest extends TestCase {
|
||||
|
||||
$this->assertStringContainsString(
|
||||
"Banned entire tree (1 user)",
|
||||
(new User\InviteTree($this->user, $userMan))
|
||||
(new User\InviteTree($this->user))
|
||||
->manipulate(
|
||||
"",
|
||||
true,
|
||||
false,
|
||||
$tracker,
|
||||
$this->user,
|
||||
$userMan,
|
||||
),
|
||||
'invite-tree-manip-revoke'
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user