diff --git a/app/User/InviteTree.php b/app/User/InviteTree.php index 5a480c46d..3ecad9ae7 100644 --- a/app/User/InviteTree.php +++ b/app/User/InviteTree.php @@ -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, - ]; - } } diff --git a/app/UserCreator.php b/app/UserCreator.php index 06e1f3d66..4890b0f32 100644 --- a/app/UserCreator.php +++ b/app/UserCreator.php @@ -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(" diff --git a/bin/rebuild-invite-trees.php b/bin/rebuild-invite-trees.php deleted file mode 100644 index 4538e4927..000000000 --- a/bin/rebuild-invite-trees.php +++ /dev/null @@ -1,23 +0,0 @@ -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); -} diff --git a/sections/tools/managers/manipulate_tree.php b/sections/tools/managers/manipulate_tree.php index 7d4c56696..d1b3ff9d5 100644 --- a/sections/tools/managers/manipulate_tree.php +++ b/sections/tools/managers/manipulate_tree.php @@ -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, ]); diff --git a/sections/user/invitetree.php b/sections/user/invitetree.php index eac5a682a..5944f588f 100644 --- a/sections/user/invitetree.php +++ b/sections/user/invitetree.php @@ -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, ]); diff --git a/sections/user/load-invitetree.php b/sections/user/load-invitetree.php index 76eafc20a..199c7cad7 100644 --- a/sections/user/load-invitetree.php +++ b/sections/user/load-invitetree.php @@ -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, ])); diff --git a/sections/user/user.php b/sections/user/user.php index a6adb9213..c8f9ca772 100644 --- a/sections/user/user.php +++ b/sections/user/user.php @@ -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()) { ?>
diff --git a/templates/user/invite-tree-bulkedit.twig b/templates/user/invite-tree-bulkedit.twig index 4e9b437ff..f0315ff55 100644 --- a/templates/user/invite-tree-bulkedit.twig +++ b/templates/user/invite-tree-bulkedit.twig @@ -7,7 +7,7 @@ {% endif %}
- + diff --git a/templates/user/invite-tree-page.twig b/templates/user/invite-tree-page.twig index 9820b5ee6..501f254ec 100644 --- a/templates/user/invite-tree-page.twig +++ b/templates/user/invite-tree-page.twig @@ -6,12 +6,9 @@
{% 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 %}
diff --git a/templates/user/invite-tree.twig b/templates/user/invite-tree.twig index 45e3065c2..1b0667423 100644 --- a/templates/user/invite-tree.twig +++ b/templates/user/invite-tree.twig @@ -1,90 +1,97 @@ -{% if info.total == 0 %} -

Nobody turned up to the gig.

-{% else %} -{% set invitee_total = info.total %} -

- 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 %} +

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 }}%).

-

- 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 {{ ratio(info.upload_top, info.download_top) }}. + 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 {{ ratio(summary.direct.up, summary.direct.down) }}.

-{% if height > 1 %} +{% if summary.depth > 1 %}

- 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 {{ ratio(info.upload_total, info.download_total) }}. + 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 {{ ratio(summary.uploaded, summary.downloaded) }}.

-{% endif %} +{% endif %} -{% if info.paranoid %} +{% if info.paranoid %}

-{{ 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.

-{% endif %} - -{% set max_depth = depth %} -{% set prev_depth = depth %} - -{% for invitee in info.tree %} - {% if invitee.depth > prev_depth %} - {{ ''|repeat(prev_depth - invitee.depth)|raw }} - -
  • - {% else %} -
  • -
  • - {% endif %} - {{ invitee.user.id|user_full }} - {% 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') %}{% endif %} - Uploaded: {{ invitee.user.uploadedSize|octet_size }} - Downloaded: {{ invitee.user.downloadedSize|octet_size }} - Ratio: {{ ratio(invitee.user.uploadedSize, invitee.user.downloadedSize) }} - ({{ invitee.user.requiredRatio|number_format(2) }}), - {% if paranoia == constant('PARANOIA_OVERRIDDEN') %}{% endif %} - joined: {{ invitee.user.created|time_diff(1) -}} - {% if invitee.user.propertyVisible(viewer, 'lastseen') -%} - , last seen {{ invitee.user.lastAccessRealtime|time_diff(1) }} +{% endif %} +
    +{% endif %} +{% if invitee.depth > prev_depth %} +{{ '
    • '|repeat(invitee.depth - prev_depth)|raw }} +{% elseif invitee.depth < prev_depth %} +{{ '
    '|repeat(prev_depth - invitee.depth)|raw }} +
  • +
  • +{% else %} +
  • +
  • +{% endif %} + {{ invitee.username }} +{% if invitee.disabled %} Banned{% endif %} +{% if max_depth < invitee.depth %} +{% set max_depth = invitee.depth %} +{% endif %} +{% set prev_depth = invitee.depth %} +{% if invitee.paranoid_up %}{% endif %} + uploaded {% if override %}{{ invitee.uploaded|octet_size }}{% else %}Hidden{% endif %} +{%- if invitee.paranoid_up %}{% endif %}, +{% if invitee.paranoid_down %}{% endif %} + downloaded {% if override %}{{ invitee.downloaded|octet_size }}{% else %}Hidden{% endif %} +{%- if invitee.paranoid_down %}{% endif %}, + ratio  +{%- if invitee.paranoid_down or invitee.paranoid_up %} +{% if override -%} + {{ ratio(invitee.uploaded, invitee.downloaded) }} +{% else -%} + Hidden +{% endif -%} +{% else -%} +{{ ratio(invitee.uploaded, invitee.downloaded) }} +{% endif -%} + + ({{ invitee.required_ratio|number_format(2) }}), + 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 %} +{{ '
  • '|repeat(prev_depth - invitee.depth)|raw }} +{% endif %} +{% else %} +

    Nobody turned up to the gig.

    {% endfor %} -{{ ''|repeat(prev_depth - depth)|raw }} -{% endif %} diff --git a/tests/phpunit/InviteTest.php b/tests/phpunit/InviteTest.php index 36988c151..524f7e926 100644 --- a/tests/phpunit/InviteTest.php +++ b/tests/phpunit/InviteTest.php @@ -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' );
    UserID or @username