generate invite tree with a recursive CTE

This commit is contained in:
Spine
2024-12-09 05:55:50 +00:00
parent a763b750e0
commit 6362ab51e5
11 changed files with 281 additions and 358 deletions

View File

@@ -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,
];
}
}

View File

@@ -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("

View File

@@ -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);
}

View File

@@ -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,
]);

View File

@@ -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,
]);

View File

@@ -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,
]));

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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') -%}
&nbsp;Hidden
{%- else -%}
{% if paranoia == constant('PARANOIA_OVERRIDDEN') %}<i>{% endif %}
Uploaded:&nbsp;<strong>{{ invitee.user.uploadedSize|octet_size }}</strong>
Downloaded:&nbsp;<strong>{{ invitee.user.downloadedSize|octet_size }}</strong>
Ratio:&nbsp;<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&nbsp;<strong>{% if override %}{{ invitee.uploaded|octet_size }}{% else %}Hidden{% endif %}</strong>
{%- if invitee.paranoid_up %}</i>{% endif %},
{% if invitee.paranoid_down %}<i>{% endif %}
downloaded&nbsp;<strong>{% if override %}{{ invitee.downloaded|octet_size }}{% else %}Hidden{% endif %}</strong>
{%- if invitee.paranoid_down %}</i>{% endif %},
ratio&nbsp;<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 %}

View File

@@ -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'
);