mirror of
https://github.com/OPSnet/Gazelle.git
synced 2026-01-16 18:04:34 -05:00
Add an invite source toolbox to track recruitments
This commit is contained in:
174
app/Manager/InviteSource.php
Normal file
174
app/Manager/InviteSource.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace Gazelle\Manager;
|
||||
|
||||
class InviteSource extends \Gazelle\Base {
|
||||
|
||||
public function create(string $name): int {
|
||||
$this->db->prepared_query("
|
||||
INSERT INTO invite_source (name) VALUES (?)
|
||||
", $name
|
||||
);
|
||||
return $this->db->inserted_id();
|
||||
}
|
||||
|
||||
public function createPendingInviteSource(int $inviteSourceId, string $inviteKey): int {
|
||||
$this->db->prepared_query("
|
||||
INSERT INTO invite_source_pending
|
||||
(invite_source_id, invite_key)
|
||||
VALUES (?, ?)
|
||||
", $inviteSourceId, $inviteKey
|
||||
);
|
||||
return $this->db->affected_rows();
|
||||
}
|
||||
|
||||
public function resolveInviteSource(string $inviteKey, int $userId): int {
|
||||
$inviteSourceId = $this->db->scalar("
|
||||
SELECT invite_source_id
|
||||
FROM invite_source_pending
|
||||
WHERE invite_key = ?
|
||||
", $inviteKey
|
||||
);
|
||||
if (!$inviteSourceId) {
|
||||
return 0;
|
||||
}
|
||||
$this->db->prepared_query("
|
||||
DELETE FROM invite_source_pending WHERE invite_key = ?
|
||||
", $inviteKey
|
||||
);
|
||||
$this->db->prepared_query("
|
||||
INSERT INTO user_has_invite_source
|
||||
(user_id, invite_source_id)
|
||||
VALUES (?, ?)
|
||||
", $userId, $inviteSourceId
|
||||
);
|
||||
return $this->db->affected_rows();
|
||||
}
|
||||
|
||||
public function findSourceNameByUserId(int $userId): ?string {
|
||||
return $this->db->scalar("
|
||||
SELECT i.name
|
||||
FROM invite_source i
|
||||
INNER JOIN user_has_invite_source uhis USING (invite_source_id)
|
||||
WHERE uhis.user_id = ?
|
||||
", $userId
|
||||
);
|
||||
}
|
||||
|
||||
public function remove(int $id): int {
|
||||
$this->db->prepared_query("
|
||||
DELETE FROM invite_source WHERE invite_source_id = ?
|
||||
", $id
|
||||
);
|
||||
return $this->db->affected_rows();
|
||||
}
|
||||
|
||||
public function listByUse(): array {
|
||||
$this->db->prepared_query("
|
||||
SELECT i.invite_source_id,
|
||||
i.name,
|
||||
count(DISTINCT ihis.user_id) AS inviter_total,
|
||||
count(DISTINCT uhis.user_id) AS user_total
|
||||
FROM invite_source i
|
||||
LEFT JOIN inviter_has_invite_source ihis USING (invite_source_id)
|
||||
LEFT JOIN user_has_invite_source uhis USING (invite_source_id)
|
||||
GROUP BY i.invite_source_id, i.name
|
||||
ORDER BY i.name
|
||||
");
|
||||
return $this->db->to_array(false, MYSQLI_ASSOC, false);
|
||||
}
|
||||
|
||||
public function summaryByInviter(): array {
|
||||
$this->db->prepared_query("
|
||||
SELECT ihis.user_id,
|
||||
group_concat(i.name ORDER BY i.name SEPARATOR ', ') as name_list
|
||||
FROM inviter_has_invite_source ihis
|
||||
INNER JOIN invite_source i USING (invite_source_id)
|
||||
INNER JOIN users_main um ON (um.ID = ihis.user_id)
|
||||
GROUP BY ihis.user_id
|
||||
ORDER BY um.username
|
||||
");
|
||||
return $this->db->to_array(false, MYSQLI_ASSOC, false);
|
||||
}
|
||||
|
||||
public function inviterConfiguration(int $userId): array {
|
||||
$this->db->prepared_query("
|
||||
SELECT i.invite_source_id,
|
||||
i.name,
|
||||
ihis.invite_source_id IS NOT NULL AS active
|
||||
FROM invite_source i
|
||||
LEFT JOIN inviter_has_invite_source ihis ON (i.invite_source_id = ihis.invite_source_id AND ihis.user_id = ?)
|
||||
ORDER BY i.name
|
||||
", $userId
|
||||
);
|
||||
return $this->db->to_array(false, MYSQLI_ASSOC, false);
|
||||
}
|
||||
|
||||
public function inviterConfigurationActive(int $userId): array {
|
||||
$this->db->prepared_query("
|
||||
SELECT i.invite_source_id,
|
||||
i.name,
|
||||
ihis.invite_source_id IS NOT NULL AS active
|
||||
FROM invite_source i
|
||||
INNER JOIN inviter_has_invite_source ihis ON (i.invite_source_id = ihis.invite_source_id AND ihis.user_id = ?)
|
||||
ORDER BY i.name
|
||||
", $userId
|
||||
);
|
||||
return $this->db->to_array(false, MYSQLI_ASSOC, false);
|
||||
}
|
||||
|
||||
public function modifyInviterConfiguration(int $userId, array $ids): int {
|
||||
$this->db->begin_transaction();
|
||||
$this->db->prepared_query("
|
||||
DELETE FROM inviter_has_invite_source WHERE user_id = ?
|
||||
", $userId
|
||||
);
|
||||
$userAndSourceId = [];
|
||||
foreach ($ids as $sourceId) {
|
||||
$userAndSourceId[] = $userId;
|
||||
$userAndSourceId[] = $sourceId;
|
||||
}
|
||||
$this->db->prepared_query("
|
||||
INSERT INTO inviter_has_invite_source (user_id, invite_source_id)
|
||||
VALUES " . placeholders($ids, '(?, ?)'), ...$userAndSourceId
|
||||
);
|
||||
$this->db->commit();
|
||||
return $this->db->affected_rows();
|
||||
}
|
||||
|
||||
public function userSource(int $userId) {
|
||||
$this->db->prepared_query("
|
||||
SELECT ui.UserID AS user_id,
|
||||
uhis.invite_source_id,
|
||||
i.name
|
||||
FROM users_info ui
|
||||
LEFT JOIN user_has_invite_source uhis ON (uhis.user_id = ui.UserID)
|
||||
LEFT JOIN invite_source i USING (invite_source_id)
|
||||
WHERE ui.inviter = ?
|
||||
", $userId
|
||||
);
|
||||
return $this->db->to_array('user_id', MYSQLI_ASSOC, false);
|
||||
}
|
||||
|
||||
public function modifyUserSource(int $userId, array $ids): int {
|
||||
$userAndSourceId = [];
|
||||
foreach ($ids as $inviteeId => $sourceId) {
|
||||
$userAndSourceId[] = $inviteeId;
|
||||
$userAndSourceId[] = $sourceId;
|
||||
}
|
||||
$this->db->begin_transaction();
|
||||
$this->db->prepared_query("
|
||||
DELETE uhis
|
||||
FROM user_has_invite_source uhis
|
||||
INNER JOIN users_info ui ON (ui.UserID = uhis.user_id)
|
||||
WHERE ui.Inviter = ?
|
||||
", $userId
|
||||
);
|
||||
$this->db->prepared_query("
|
||||
INSERT INTO user_has_invite_source (user_id, invite_source_id)
|
||||
VALUES " . placeholders($ids, '(?, ?)'), ...$userAndSourceId
|
||||
);
|
||||
$this->db->commit();
|
||||
return $this->db->affected_rows();
|
||||
}
|
||||
}
|
||||
@@ -6,16 +6,23 @@ class ExpireInvites extends \Gazelle\Schedule\Task
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
$userQuery = $this->db->prepared_query("SELECT InviterID FROM invites WHERE Expires < now()");
|
||||
$this->db->prepared_query("DELETE FROM invites WHERE Expires < now()");
|
||||
|
||||
$this->db->set_query_id($userQuery);
|
||||
$this->db->begin_transaction();
|
||||
$this->db->prepared_query("SELECT InviterID FROM invites WHERE Expires < now()");
|
||||
$users = $this->db->collect('InviterID', false);
|
||||
|
||||
$this->db->prepared_query("DELETE FROM invites WHERE Expires < now()");
|
||||
$this->db->prepared_query("
|
||||
DELETE isp FROM invite_source_pending isp
|
||||
LEFT JOIN invites i ON (i.InviteKey = isp.invite_key)
|
||||
WHERE i.InviteKey IS NULL
|
||||
");
|
||||
|
||||
foreach ($users as $user) {
|
||||
$this->db->prepared_query("UPDATE users_main SET Invites = Invites + 1 WHERE ID = ?", $user);
|
||||
$this->cache->deleteMulti(["u_$user", "user_info_heavy_$user"]);
|
||||
$this->debug("Expired invite from user $user", $user);
|
||||
$this->processed++;
|
||||
}
|
||||
$this->db->commit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1492,6 +1492,8 @@ class User extends BaseObject {
|
||||
public function isStaff(): bool { return $this->info()['isStaff']; }
|
||||
public function isDonor(): bool { return isset($this->info()['secondary_class'][DONOR]) || $this->isStaff(); }
|
||||
public function isFLS(): bool { return isset($this->info()['secondary_class'][FLS_TEAM]); }
|
||||
public function isInterviewer(): bool { return isset($this->info()['secondary_class'][INTERVIEWER]); }
|
||||
public function isRecruiter(): bool { return isset($this->info()['secondary_class'][RECRUITER]); }
|
||||
public function isStaffPMReader(): bool { return $this->isFLS() || $this->isStaff(); }
|
||||
|
||||
public function warningExpiry(): ?string {
|
||||
@@ -2436,17 +2438,14 @@ class User extends BaseObject {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a user is allowed to purchase an invite. User classes up to Elite are capped,
|
||||
* Checks whether a user is allowed to purchase an invite. Lower classes are capped,
|
||||
* users above this class will always return true.
|
||||
*
|
||||
* @param integer $minClass Minimum class level necessary to purchase invites
|
||||
* @return boolean false if insufficient funds, otherwise true
|
||||
*/
|
||||
public function canPurchaseInvite(): bool {
|
||||
if ($this->info()['DisableInvites']) {
|
||||
return false;
|
||||
}
|
||||
return $this->info()['effective_class'] >= MIN_INVITE_CLASS;
|
||||
return !$this->disableInvites() && $this->effectiveClass() >= MIN_INVITE_CLASS;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -125,6 +125,7 @@ class UserCreator extends Base {
|
||||
);
|
||||
|
||||
if ($inviterId) {
|
||||
(new \Gazelle\Manager\InviteSource)->resolveInviteSource($this->inviteKey, $this->id);
|
||||
$this->db->prepared_query("
|
||||
DELETE FROM invites WHERE InviteKey = ?
|
||||
", $this->inviteKey
|
||||
|
||||
@@ -162,24 +162,24 @@ if (!defined('FEATURE_EMAIL_REENABLE')) {
|
||||
define('FEATURE_EMAIL_REENABLE', true);
|
||||
}
|
||||
|
||||
// User class IDs needed for automatic promotions. Found in the 'permissions' table
|
||||
// Name of class Class ID (NOT level)
|
||||
define('ADMIN', '1');
|
||||
define('USER', '2');
|
||||
define('MEMBER', '3');
|
||||
define('POWER', '4');
|
||||
define('ELITE', '5');
|
||||
define('VIP', '6');
|
||||
define('TORRENT_MASTER','7');
|
||||
define('MOD', '11');
|
||||
define('SYSOP', '15');
|
||||
define('ARTIST', '19');
|
||||
define('DONOR', '20');
|
||||
define('FLS_TEAM', '23');
|
||||
define('POWER_TM', '22');
|
||||
define('ELITE_TM', '23');
|
||||
define('FORUM_MOD', '28');
|
||||
define('ULTIMATE_TM', '48');
|
||||
define('USER', 2);
|
||||
define('MEMBER', 3);
|
||||
define('POWER', 4);
|
||||
define('ELITE', 5);
|
||||
define('TORRENT_MASTER', 7);
|
||||
define('POWER_TM', 22);
|
||||
define('ELITE_TM', 23);
|
||||
define('ULTIMATE_TM', 48);
|
||||
define('FORUM_MOD', 28);
|
||||
define('MOD', 11);
|
||||
define('SYSOP', 15);
|
||||
|
||||
define('DONOR', 20);
|
||||
define('FLS_TEAM', 23);
|
||||
define('INTERVIEWER', 30);
|
||||
define('RECRUITER', 41);
|
||||
define('VIP', 6);
|
||||
|
||||
// Locked account constant
|
||||
define('STAFF_LOCKED', 1);
|
||||
|
||||
@@ -84,6 +84,7 @@ class Permissions {
|
||||
'admin_manage_polls' => 'Can manage polls',
|
||||
'admin_manage_forums' => 'Can manage forums (add/edit/delete)',
|
||||
'admin_manage_fls' => 'Can manage First Line Support (FLS) crew',
|
||||
'admin_manage_invite_source' => 'Can manage invite sources',
|
||||
'admin_manage_user_fls' => 'Can manage user FL tokens',
|
||||
'admin_manage_applicants' => 'Can manage job roles and user applications',
|
||||
'admin_manage_referrals' => 'Can manage referrals',
|
||||
|
||||
49
db/migrations/20210616100351_invite_source.php
Normal file
49
db/migrations/20210616100351_invite_source.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class InviteSource extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Change Method.
|
||||
*
|
||||
* Write your reversible migrations using this method.
|
||||
*
|
||||
* More information on writing migrations is available here:
|
||||
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
|
||||
*
|
||||
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||
* with the Table class.
|
||||
*/
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('invite_source', ['id' => false, 'primary_key' => ['invite_source_id']])
|
||||
->addColumn('invite_source_id', 'integer', ['limit' => 10, 'signed' => false, 'identity' => true])
|
||||
->addColumn('name', 'string', ['limit' => 20, 'encoding' => 'ascii'])
|
||||
->addIndex(['name'], ['unique' => true, 'name' => 'is_name_uidx'])
|
||||
->create();
|
||||
|
||||
$this->table('invite_source_pending', ['id' => false, 'primary_key' => ['invite_key']])
|
||||
->addColumn('user_id', 'integer', ['limit' => 10, 'signed' => false])
|
||||
->addColumn('invite_source_id', 'integer', ['limit' => 10, 'signed' => false])
|
||||
->addColumn('invite_key', 'string', ['limit' => 32])
|
||||
->addForeignKey('user_id', 'users_main', 'ID', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
|
||||
->addForeignKey('invite_source_id', 'invite_source', 'invite_source_id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
|
||||
->create();
|
||||
|
||||
$this->table('user_has_invite_source', ['id' => false, 'primary_key' => ['user_id']])
|
||||
->addColumn('user_id', 'integer', ['limit' => 10, 'signed' => false])
|
||||
->addColumn('invite_source_id', 'integer', ['limit' => 10, 'signed' => false])
|
||||
->addForeignKey('user_id', 'users_main', 'ID', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
|
||||
->addForeignKey('invite_source_id', 'invite_source', 'invite_source_id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
|
||||
->create();
|
||||
|
||||
$this->table('inviter_has_invite_source', ['id' => false, 'primary_key' => ['user_id', 'invite_source_id']])
|
||||
->addColumn('user_id', 'integer', ['limit' => 10, 'signed' => false])
|
||||
->addColumn('invite_source_id', 'integer', ['limit' => 10, 'signed' => false])
|
||||
->addForeignKey('user_id', 'users_main', 'ID', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
|
||||
->addForeignKey('invite_source_id', 'invite_source', 'invite_source_id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
12
db/seeds/InviteSource.php
Normal file
12
db/seeds/InviteSource.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
|
||||
use Phinx\Seed\AbstractSeed;
|
||||
|
||||
class InviteSource extends AbstractSeed
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
$this->table('invite_source')->insert(['name' => 'Personal'])->save();
|
||||
}
|
||||
}
|
||||
56
docs/05-InviteSource.txt
Normal file
56
docs/05-InviteSource.txt
Normal file
@@ -0,0 +1,56 @@
|
||||
From: Spine
|
||||
To: Moderators
|
||||
Date: 2021-06-19
|
||||
Subject: Orpheus Development Papers #5 - Invite Sources
|
||||
Version: 1
|
||||
|
||||
This feature helps you keep track of how people were invited via interviews
|
||||
and recruitments from other trackers. Most people who buy invites from the
|
||||
Bonus Shop for personal friends are not concerned by this. But for people
|
||||
who interview or recruit, it is helpful for them to be able to keep the
|
||||
source origins distinguished, as it is for staff.
|
||||
|
||||
1. Configure admin permissions for the appropriate user classes. Grant
|
||||
admin_manage_invite_source to moderators and above. This will allow these
|
||||
people to configure invite sources. It can also be done on a per-user basis
|
||||
via custom permissions.
|
||||
|
||||
Run the seeder: `phinx seed:run -s InviteSource`
|
||||
|
||||
/tools.php?action=permission
|
||||
|
||||
2. Add the invite sources: mnemonic names of trackers that everyone
|
||||
understands. Consider adding an Interview source as well if you do
|
||||
interviews, for interviewers and personal invites. These are stored in the
|
||||
invite_source table.
|
||||
|
||||
/tools.php?action=invite_source_config
|
||||
|
||||
Once added, and referred to (by a member who has been granted its use, a
|
||||
member who has been sourced from it), a source may no longer be removed.
|
||||
You will need to tidy up the database directly.
|
||||
|
||||
3. Grant sources to members. Search for users who have the R or IN
|
||||
secondary classes. On their profile page, a new "Invite Sources" section
|
||||
will appear. Check the sources that are appropriate for this user. These
|
||||
grants are stored in the inviter_has_invite_source table.
|
||||
|
||||
/tools.php?action=invite_source
|
||||
|
||||
4. Inform the members. When they invite people, they will see an additional
|
||||
select box showing the options that have been configured for them. They can
|
||||
then set the source when the invitation is issued. The source is tied to
|
||||
the invite key via the invite_source_pending table, so that when the
|
||||
invitee creates their account, the source is attached to their profile (in
|
||||
the user_has_invite_source table).
|
||||
|
||||
They can also go the list of their invitees and backfill the existing
|
||||
invitees.
|
||||
|
||||
/user.php?action=invite&edit=source
|
||||
|
||||
5. On the profile page, the Invite: information will now say "by <user>
|
||||
from <source>".
|
||||
|
||||
6. If a source is removed from a recruiter, all their invitees that were
|
||||
tagged as coming from that source will remain.
|
||||
@@ -111,6 +111,13 @@ switch ($_REQUEST['action']) {
|
||||
require_once('managers/take_global_notification.php');
|
||||
break;
|
||||
|
||||
case 'invite_source':
|
||||
require_once('managers/invite_source.php');
|
||||
break;
|
||||
case 'invite_source_config':
|
||||
require_once('managers/invite_source_config.php');
|
||||
break;
|
||||
|
||||
case 'irc':
|
||||
require_once('managers/irc_list.php');
|
||||
break;
|
||||
@@ -296,7 +303,7 @@ switch ($_REQUEST['action']) {
|
||||
break;
|
||||
|
||||
case 'periodic':
|
||||
$mode = isset($_REQUEST['mode']) ? $_REQUEST['mode'] : 'view';
|
||||
$mode = $_REQUEST['mode'] ?? 'view';
|
||||
switch ($mode) {
|
||||
case 'run_now':
|
||||
case 'view':
|
||||
|
||||
18
sections/tools/managers/invite_source.php
Normal file
18
sections/tools/managers/invite_source.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
$Viewer = new Gazelle\User($LoggedUser['ID']);
|
||||
if (!$Viewer->permitted('admin_manage_invite_source')) {
|
||||
error(403);
|
||||
}
|
||||
$user = (new Gazelle\Manager\User)->find(trim($_POST['user'] ?? ''));
|
||||
if ($user) {
|
||||
header("Location: user.php?id=" . $user->id() . "#invite_source");
|
||||
exit;
|
||||
}
|
||||
|
||||
View::show_header('Invite Sources Summary');
|
||||
echo $Twig->render('admin/invite-source.twig', [
|
||||
'auth' => $Viewer->auth(),
|
||||
'list' => (new Gazelle\Manager\InviteSource)->summaryByInviter(),
|
||||
]);
|
||||
View::show_footer();
|
||||
26
sections/tools/managers/invite_source_config.php
Normal file
26
sections/tools/managers/invite_source_config.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
$Viewer = new Gazelle\User($LoggedUser['ID']);
|
||||
if (!$Viewer->permitted('admin_manage_invite_source')) {
|
||||
error(403);
|
||||
}
|
||||
$manager = new Gazelle\Manager\InviteSource;
|
||||
|
||||
if (!empty($_POST['name'])) {
|
||||
authorize();
|
||||
$manager->create(trim($_POST['name']));
|
||||
}
|
||||
$remove = array_keys(array_filter($_POST, function ($x) { return preg_match('/^remove-\d+$/', $x);}, ARRAY_FILTER_USE_KEY));
|
||||
if ($remove) {
|
||||
authorize();
|
||||
foreach ($remove as $r) {
|
||||
$manager->remove((int)explode('-', $r)[1]);
|
||||
}
|
||||
}
|
||||
|
||||
View::show_header('Invite Sources');
|
||||
echo $Twig->render('admin/invite-source-config.twig', [
|
||||
'auth' => $Viewer->auth(),
|
||||
'list' => $manager->listByUse(),
|
||||
]);
|
||||
View::show_footer();
|
||||
@@ -124,6 +124,7 @@ Category('Community', [
|
||||
Item('Forum categories', 'tools.php?action=categories', All(['admin_manage_forums'])),
|
||||
Item('Forum departments', 'tools.php?action=forum', All(['admin_manage_forums'])),
|
||||
Item('Forum transitions', 'tools.php?action=forum_transitions', All(['admin_manage_forums'])),
|
||||
Item('Invite Sources', 'tools.php?action=invite_source', All(['admin_manage_invite_source'])),
|
||||
Item('IRC manager', 'tools.php?action=irc', All(['admin_manage_forums'])),
|
||||
Item('Navigation link manager', 'tools.php?action=navigation', All(['admin_manage_navigation'])),
|
||||
]);
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
<?php
|
||||
|
||||
$Viewer = new Gazelle\User($LoggedUser['ID']);
|
||||
$userMan = new Gazelle\Manager\User;
|
||||
|
||||
if (isset($_GET['userid'])) {
|
||||
if (!check_perms('users_view_invites')) {
|
||||
error(403);
|
||||
}
|
||||
$UserID = (int)$_GET['userid'];
|
||||
} else {
|
||||
$UserID = $LoggedUser['ID'];
|
||||
}
|
||||
$user = $userMan->findById($UserID);
|
||||
$user = $userMan->findById(isset($_REQUEST['userid']) ? (int)$_REQUEST['userid'] : $LoggedUser['ID']);
|
||||
if (is_null($user)) {
|
||||
error(404);
|
||||
}
|
||||
$userId = $user->id();
|
||||
$ownProfile = $user->id() == $Viewer->id();
|
||||
if (!($Viewer->permitted('users_view_invites') || ($ownProfile && $user->canPurchaseInvite()))) {
|
||||
error(403);
|
||||
}
|
||||
|
||||
$userSourceRaw = array_filter($_POST, function ($x) { return preg_match('/^user-\d+$/', $x); }, ARRAY_FILTER_USE_KEY);
|
||||
$userSource = [];
|
||||
foreach ($userSourceRaw as $fieldName => $fieldValue) {
|
||||
if (preg_match('/^user-(\d+)$/', $fieldName, $userMatch) && preg_match('/^s-(\d+)$/', $fieldValue, $sourceMatch)) {
|
||||
$userSource[$userMatch[1]] = (int)$sourceMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
$invSourceMan = new Gazelle\Manager\InviteSource;
|
||||
if (count($userSource)) {
|
||||
$invSourceMan->modifyUserSource($userId, $userSource);
|
||||
}
|
||||
|
||||
$heading = new \Gazelle\Util\SortableTableHeader('joined', [
|
||||
// see Gazelle\User::inviteList() for these table aliases
|
||||
@@ -28,14 +38,19 @@ $heading = new \Gazelle\Util\SortableTableHeader('joined', [
|
||||
]);
|
||||
|
||||
View::show_header('Invites');
|
||||
|
||||
echo $Twig->render('user/invited.twig', [
|
||||
'auth' => $LoggedUser['AuthKey'],
|
||||
'heading' => $heading,
|
||||
'invited' => $user->inviteList($heading->getOrderBy(), $heading->getOrderDir()),
|
||||
'invites_open' => $userMan->newUsersAllowed() || $user->permitted('site_can_invite_always'),
|
||||
'own_profile' => $user->id() == $LoggedUser['ID'],
|
||||
'user' => $user,
|
||||
'view_pool' => check_perms('users_view_invites'),
|
||||
'wiki_article' => 116,
|
||||
'auth' => $user->auth(),
|
||||
'edit_source' => ($_GET['edit'] ?? '') === 'source',
|
||||
'heading' => $heading,
|
||||
'invited' => $user->inviteList($heading->getOrderBy(), $heading->getOrderDir()),
|
||||
'inviter_config' => $invSourceMan->inviterConfigurationActive($userId),
|
||||
'invites_open' => $userMan->newUsersAllowed() || $user->permitted('site_can_invite_always'),
|
||||
'invite_source' => $invSourceMan->userSource($userId),
|
||||
'own_profile' => $ownProfile,
|
||||
'user' => $user,
|
||||
'user_source' => $invSourceMan->userSource($userId),
|
||||
'view_pool' => $user->permitted('users_view_invites'),
|
||||
'wiki_article' => 116,
|
||||
]);
|
||||
View::show_footer();
|
||||
|
||||
@@ -9,70 +9,63 @@ authorize();
|
||||
* Super sorry for doing that, but this is totally not reusable.
|
||||
*/
|
||||
|
||||
$user = new Gazelle\User($LoggedUser['ID']);
|
||||
$Viewer = new Gazelle\User($LoggedUser['ID']);
|
||||
// Can the member issue an invite?
|
||||
if (!$user->canInvite()) {
|
||||
if (!$Viewer->canInvite()) {
|
||||
error(403);
|
||||
}
|
||||
// Can the site allow an invite to be spent?
|
||||
if (!((new Gazelle\Manager\User)->newUsersAllowed() || check_perms('site_can_invite_always'))) {
|
||||
if (!((new Gazelle\Manager\User)->newUsersAllowed() || $Viewer->permitted('site_can_invite_always'))) {
|
||||
error(403);
|
||||
}
|
||||
|
||||
//MultiInvite
|
||||
$Email = $_POST['email'];
|
||||
if (strpos($Email, '|') !== false && check_perms('site_send_unlimited_invites')) {
|
||||
$Emails = explode('|', $Email);
|
||||
} else {
|
||||
$Emails = [$Email];
|
||||
if (!isset($_POST['agreement'])) {
|
||||
error("You must agree to the conditions for sending invitations.");
|
||||
}
|
||||
|
||||
foreach ($Emails as $CurEmail) {
|
||||
$CurEmail = trim($CurEmail);
|
||||
if (!preg_match(EMAIL_REGEXP, $CurEmail)) {
|
||||
if (count($Emails) > 1) {
|
||||
continue;
|
||||
} else {
|
||||
error('Invalid email.');
|
||||
}
|
||||
}
|
||||
$DB->prepared_query("
|
||||
SELECT 1
|
||||
FROM invites
|
||||
WHERE InviterID = ?
|
||||
AND Email = ?
|
||||
", $LoggedUser['ID'], $CurEmail
|
||||
);
|
||||
if ($DB->has_results()) {
|
||||
error('You already have a pending invite to that address!');
|
||||
}
|
||||
|
||||
$InviteKey = randomString();
|
||||
$DB->begin_transaction();
|
||||
$DB->prepared_query("
|
||||
INSERT INTO invites
|
||||
(InviterID, InviteKey, Email, Reason, Expires)
|
||||
VALUES (?, ?, ?, ?, now() + INTERVAL 3 DAY)
|
||||
", $LoggedUser['ID'], $InviteKey, $CurEmail, (check_perms('users_invite_notes') ? trim($_POST['reason'] ?? '') : '')
|
||||
);
|
||||
if (!check_perms('site_send_unlimited_invites')) {
|
||||
$DB->prepared_query("
|
||||
UPDATE users_main SET
|
||||
Invites = GREATEST(Invites, 1) - 1
|
||||
WHERE ID = ?
|
||||
", $LoggedUser['ID']
|
||||
);
|
||||
$user->flush();
|
||||
}
|
||||
$DB->commit();
|
||||
|
||||
(new Mail)->send($CurEmail, 'You have been invited to ' . SITE_NAME,
|
||||
$Twig->render('email/invite-member.twig', [
|
||||
'email' => $CurEmail,
|
||||
'key' => $InviteKey,
|
||||
'username' => $LoggedUser['Username'],
|
||||
])
|
||||
);
|
||||
$email = trim($_POST['email']);
|
||||
if (!preg_match(EMAIL_REGEXP, $email)) {
|
||||
error('Invalid email.');
|
||||
}
|
||||
$prior = $DB->scalar("
|
||||
SELECT Email
|
||||
FROM invites
|
||||
WHERE InviterID = ?
|
||||
AND Email = ?
|
||||
", $Viewer->id(), $email
|
||||
);
|
||||
if ($prior) {
|
||||
error('You already have a pending invite to that address!');
|
||||
}
|
||||
|
||||
$inviteKey = randomString();
|
||||
$DB->begin_transaction();
|
||||
$DB->prepared_query("
|
||||
INSERT INTO invites
|
||||
(InviterID, InviteKey, Email, Reason, Expires)
|
||||
VALUES (?, ?, ?, ?, now() + INTERVAL 3 DAY)
|
||||
", $Viewer->id(), $inviteKey, $email, trim($_POST['reason'] ?? '')
|
||||
);
|
||||
if (!$Viewer->permitted('site_send_unlimited_invites')) {
|
||||
$DB->prepared_query("
|
||||
UPDATE users_main SET
|
||||
Invites = GREATEST(Invites, 1) - 1
|
||||
WHERE ID = ?
|
||||
", $Viewer->id()
|
||||
);
|
||||
$Viewer->flush();
|
||||
}
|
||||
if (isset($_POST['user-0']) && preg_match('/^s-(\d+)$/', $_POST['user-0'], $match)) {
|
||||
(new Gazelle\Manager\InviteSource)->createPendingInviteSource($match[1], $inviteKey);
|
||||
}
|
||||
$DB->commit();
|
||||
|
||||
(new Mail)->send($email, 'You have been invited to ' . SITE_NAME,
|
||||
$Twig->render('email/invite-member.twig', [
|
||||
'email' => $email,
|
||||
'key' => $inviteKey,
|
||||
'username' => $Viewer->username(),
|
||||
])
|
||||
);
|
||||
|
||||
header('Location: user.php?action=invite');
|
||||
|
||||
@@ -672,4 +672,17 @@ if (count($set) || count($leechSet)) {
|
||||
$user->flush();
|
||||
}
|
||||
|
||||
if (isset($_POST['invite_source_update'])) {
|
||||
$source = array_keys(array_filter($_POST, function ($x) { return preg_match('/^source-\d+$/', $x);}, ARRAY_FILTER_USE_KEY));
|
||||
if ($source) {
|
||||
$ids = [];
|
||||
foreach ($source as $s) {
|
||||
$ids[] = ((int)explode('-', $s)[1]);
|
||||
}
|
||||
(new Gazelle\Manager\InviteSource)->modifyInviterConfiguration($user->id(), $ids);
|
||||
header("Location: tools.php?action=invite_source");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
header("location: user.php?id=$userId");
|
||||
|
||||
@@ -343,14 +343,24 @@ if (check_perms('users_view_keys') || $OwnProfile) {
|
||||
<li>Passkey: <a href="#" id="passkey" onclick="togglePassKey('<?= display_str($User->announceKey()) ?>'); return false;" class="brackets">View</a></li>
|
||||
<?php
|
||||
}
|
||||
if (check_perms('users_view_invites')) {
|
||||
if ($viewer->permitted('users_view_invites')) {
|
||||
if (is_null($User->inviter())) {
|
||||
$Invited = '<span style="font-style: italic;">Nobody</span>';
|
||||
} else {
|
||||
$Invited = '<a href="user.php?id=' . $User->inviter()->id() . '">' . $User->inviter()->username() . "</a>";
|
||||
$inviter = $userMan->findById($User->inviter()->id());
|
||||
$Invited = '<a href="user.php?id=' . $inviter->id() . '">' . $User->inviter()->username() . "</a>";
|
||||
if ($viewer->permitted('admin_manage_invite_source')) {
|
||||
$source = (new Gazelle\Manager\InviteSource)->findSourceNameByUserId($UserID);
|
||||
if (is_null($source) && ($inviter->isInterviewer() || $inviter->isRecruiter())) {
|
||||
$source = "<i>unconfirmed</i>";
|
||||
}
|
||||
if (!is_null($source)) {
|
||||
$Invited .= " from $source";
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<li>Invited by: <?=$Invited?></li>
|
||||
<li>Invited by: <?= $Invited ?></li>
|
||||
<li>Invites: <?= $User->disableInvites() ? 'X' : number_format($User->inviteCount()) ?>
|
||||
<?= '(' . $User->pendingInviteCount() . ' in use)' ?></li>
|
||||
<?php
|
||||
@@ -812,6 +822,12 @@ if (check_perms('users_mod') || $viewer->isStaff()) { ?>
|
||||
]);
|
||||
}
|
||||
|
||||
if ($User->isInterviewer() || $User->isRecruiter() || $User->isStaff()) {
|
||||
echo $Twig->render('user/edit-invite-sources.twig', [
|
||||
'list' => (new \Gazelle\Manager\InviteSource)->inviterConfiguration($User->id()),
|
||||
]);
|
||||
}
|
||||
|
||||
if (check_perms('users_give_donor')) {
|
||||
echo $Twig->render('donation/admin-panel.twig', [
|
||||
'user' => $User,
|
||||
|
||||
43
templates/admin/invite-source-config.twig
Normal file
43
templates/admin/invite-source-config.twig
Normal file
@@ -0,0 +1,43 @@
|
||||
<div class="header">
|
||||
<h2>Invite Source Configuration</h2>
|
||||
</div>
|
||||
<div class="linkbox">
|
||||
<a href="tools.php?action=invite_source" class="brackets">Inviter Summary</a>
|
||||
</div>
|
||||
<div class="thin pad">
|
||||
<div class="box pad">
|
||||
<h2>Sources</h2>
|
||||
<p>Add the name of the trackers from whence members recruit. You can then assign tracker names on an individual basis for recruiters.
|
||||
<form action="" method="post">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Inviter (recruiter) usage</th>
|
||||
<th>Invitee totals</th>
|
||||
<th>Remove?</th>
|
||||
<tr>
|
||||
{% for source in list %}
|
||||
<tr>
|
||||
<td>{{ source.name }}</td>
|
||||
<td>{{ source.inviter_total|number_format }}</td>
|
||||
<td>{{ source.user_total|number_format }}</td>
|
||||
<td>
|
||||
{% if source.inviter_total == 0 and source.user_total == 0 %}
|
||||
<input type="checkbox" name="remove-{{ source.invite_source_id }}" />
|
||||
{% else %}
|
||||
<i>in use</i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<br />
|
||||
New name: <input type="text" length="40" name="name" />
|
||||
<br />
|
||||
<br />
|
||||
<input type="hidden" name="auth" value="{{ auth }}" />
|
||||
<input type="hidden" name="action" value="invite_source_config" />
|
||||
<input type="submit" value="Update" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
37
templates/admin/invite-source.twig
Normal file
37
templates/admin/invite-source.twig
Normal file
@@ -0,0 +1,37 @@
|
||||
<div class="header">
|
||||
<h2>Invite Source Inviter Summary</h2>
|
||||
</div>
|
||||
<div class="linkbox">
|
||||
<a href="tools.php?action=invite_source_config" class="brackets">Configuration</a>
|
||||
<a href="user.php?action=search&secclass={{ constant('INTERVIEWER') }}&order=Joined&way=Ascending" class="brackets">Search Interviewers</a>
|
||||
<a href="user.php?action=search&secclass={{ constant('RECRUITER') }}&order=Joined&way=Ascending" class="brackets">Search Recruiters</a>
|
||||
</div>
|
||||
<div class="thin pad">
|
||||
<div class="box pad">
|
||||
<h2>Recruitment Summary</h2>
|
||||
<form action="" method="post">
|
||||
{% for u in list %}
|
||||
{% if loop.first %}
|
||||
<table>
|
||||
<tr>
|
||||
<th>Inviter (recruiter)</th>
|
||||
<th>Recruitment sites</th>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>{{ u.user_id|user_full }}</td>
|
||||
<td>{{ u.name_list }}</td>
|
||||
</tr>
|
||||
{% if loop.last %}
|
||||
</table>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<h3>No inviters have been configured</h3>
|
||||
{% endfor %}
|
||||
<br />
|
||||
<input type="hidden" name="auth" value="{{ auth }}" />
|
||||
<input type="hidden" name="action" value="invite_source" />
|
||||
Go to user page to edit sources (user id or @name): <input type="text" length="40" name="user" /> <input type="submit" value="Go!" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,6 +139,7 @@
|
||||
{{ privilege(default, user, 'admin_manage_contest') }}
|
||||
{{ privilege(default, user, 'admin_manage_polls') }}
|
||||
{{ privilege(default, user, 'admin_manage_fls') }}
|
||||
{{ privilege(default, user, 'admin_manage_invite_source') }}
|
||||
{{ privilege(default, user, 'admin_manage_user_fls') }}
|
||||
{{ privilege(default, user, 'admin_manage_applicants') }}
|
||||
{{ privilege(default, user, 'admin_manage_referrals') }}
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
{% macro checked(flag) -%}
|
||||
{%- macro checked(flag) -%}
|
||||
{%- if flag %} checked="checked"{% endif -%}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro selected(flag) -%}
|
||||
{%- macro selected(flag) -%}
|
||||
{%- if flag %} selected="selected"{% endif -%}
|
||||
{%- endmacro %}
|
||||
|
||||
{%- macro select_invite_source(user_id, config, source) -%}
|
||||
<select name="user-{{ user_id }}">
|
||||
<option name="s-none">---</option>
|
||||
{%- for c in config -%}
|
||||
<option value="s-{{ c.invite_source_id }}"{{ _self.selected(c.invite_source_id == source[user_id].invite_source_id) }}>{{ c.name }}</option>
|
||||
{%- endfor -%}
|
||||
</select>
|
||||
{%- endmacro %}
|
||||
|
||||
38
templates/user/edit-invite-sources.twig
Normal file
38
templates/user/edit-invite-sources.twig
Normal file
@@ -0,0 +1,38 @@
|
||||
{% from 'macro/form.twig' import checked %}
|
||||
<table class="layout" id="user_invite_source_box">
|
||||
<tr class="colhead">
|
||||
<td colspan="2">
|
||||
<a name="invite_source">Invite Sources</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% for site in list %}
|
||||
{% if loop.first %}
|
||||
<tr>
|
||||
<td class="label"> </td>
|
||||
<td>Check the sources from whence this member recruits or interviews.
|
||||
They will be able to qualify their invites with the source tracker, to distinguish them from personal invitations.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label"> </td>
|
||||
<td>Source</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td><label><input type="checkbox" name="source-{{ site.invite_source_id }}"{{ checked(site.active == 1) }} /> {{ site.name }}</label></td>
|
||||
</tr>
|
||||
{% if loop.last %}
|
||||
<tr>
|
||||
<td class="label"> </td>
|
||||
<td>
|
||||
<input type="submit" name="invite_source_update" value="Update" />
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td class="label"> </td>
|
||||
<td>No sources have been configured. <a href="tools.php?action=invite_source_config">Configure</a>!</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
@@ -35,7 +35,7 @@
|
||||
{% if own_profile or viewer.permitted('users_edit_profiles') %}
|
||||
<a href="user.php?action=edit&userid={{ user_id }}" class="brackets">Edit</a>
|
||||
{% endif %}
|
||||
{% if viewer.permitted('users_view_invites') %}
|
||||
{% if viewer.permitted('users_view_invites') or (user.canPurchaseInvite and own_profile) %}
|
||||
<a href="user.php?action=invite&userid={{ user_id }}" class="brackets">Invites</a>
|
||||
{% endif %}
|
||||
{% if viewer.permitted('admin_reports') %}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
{% from 'macro/form.twig' import select_invite_source %}
|
||||
|
||||
<div class="thin">
|
||||
<div class="header">
|
||||
<h2>{{ user.id|user_url }} › Invites</h2>
|
||||
@@ -9,6 +11,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% set is_site_inviter = inviter_config|length %}
|
||||
|
||||
{% if user.disableInvites %}
|
||||
<div class="box pad" style="text-align: center;">
|
||||
<strong class="important_text">Your invites have been disabled.
|
||||
@@ -29,33 +33,51 @@
|
||||
|
||||
{% elseif own_profile and user.canInvite %}
|
||||
<div class="box pad">
|
||||
<p>Please note that selling, trading, or publicly giving away our invitations — or responding
|
||||
to public invite requests — is strictly forbidden, and may result in you and your entire invite tree being banned.</p>
|
||||
<p>Please note that selling, trading, or publicly giving away our invitations — or responding
|
||||
to public invite requests — is strictly forbidden, and may result in you and your entire invite tree being banned.</p>
|
||||
<p>Do not send an invite to anyone who has previously had an {{ constant('SITE_NAME') }} account.
|
||||
Please direct them to {{ constant('BOT_DISABLED_CHAN') }} on {{ constant('BOT_SERVER') }} if they wish to reactivate their account.</p>
|
||||
<p>Remember that you are responsible for ALL invitees, and your account and/or privileges may be disabled due to your invitees' actions.
|
||||
You should know and trust the person you're inviting. If you aren't familiar enough with the user to trust them, do not invite them.</p>
|
||||
<p><em>Do not send an invite if you have not read or do not understand the information above.</em></p>
|
||||
</div>
|
||||
<div class="box box2">
|
||||
<form class="send_form pad" name="invite" action="user.php" method="post">
|
||||
<input type="hidden" name="action" value="take_invite" />
|
||||
<input type="hidden" name="auth" value="{{ auth }}" />
|
||||
{% if is_site_inviter %}
|
||||
<div class="field_div">
|
||||
<div class="label">Invite source:</div>
|
||||
<div class="input">
|
||||
<td class="nobr">{{ select_invite_source(0, inviter_config, user_source) }}</td>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="field_div">
|
||||
<div class="label">Email address:</div>
|
||||
<div class="input">
|
||||
<input type="email" name="email" size="60" />
|
||||
<input type="submit" value="Invite" />
|
||||
<input type="email" name="email" size="40" />
|
||||
</div>
|
||||
</div>
|
||||
{% if user.permitted('users_invite_notes') %}
|
||||
<div class="field_div">
|
||||
<div class="label">Staff Note:</div>
|
||||
<div class="input">
|
||||
<input type="text" name="reason" size="60" maxlength="255" />
|
||||
<input type="text" name="reason" size="40" maxlength="255" />
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="field_div">
|
||||
<div class="label"> </div>
|
||||
<div class="input">
|
||||
<label><input type="checkbox" name="agreement" /> I have read and agree to the information written above.</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field_div">
|
||||
<div class="label"> </div>
|
||||
<div class="input">
|
||||
<input type="hidden" name="action" value="take_invite" />
|
||||
<input type="hidden" name="auth" value="{{ auth }}" />
|
||||
<input type="submit" value="Invite" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -87,7 +109,18 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<h3>Invitee list</h3>
|
||||
{% if is_site_inviter %}
|
||||
<form action="" method="post">
|
||||
{% endif %}
|
||||
<h3>Invitee list
|
||||
{% if is_site_inviter %}
|
||||
{% if edit_source %}
|
||||
<a class="brackets" href="user.php?action=invite">View</a>
|
||||
{% else %}
|
||||
<a class="brackets" href="user.php?action=invite&edit=source">Edit sources</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</h3>
|
||||
<div class="box pad">
|
||||
<table class="invite_table m_table" width="100%">
|
||||
<tr class="colhead">
|
||||
@@ -98,6 +131,9 @@
|
||||
<td class="m_th_right nobr">{{ heading.emit('uploaded')|raw }}</td>
|
||||
<td class="m_th_right nobr">{{ heading.emit('downloaded')|raw }}</td>
|
||||
<td class="m_th_right nobr">{{ heading.emit('ratio')|raw }}</td>
|
||||
{% if is_site_inviter %}
|
||||
<td class="nobr">Source</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% for u in invited %}
|
||||
<tr class="row{{ cycle(['a', 'b'], loop.index0) }}">
|
||||
@@ -108,8 +144,27 @@
|
||||
<td class="td_up m_td_right">{{ u.uploaded|octet_size }}</td>
|
||||
<td class="td_dl m_td_right">{{ u.downloaded|octet_size }}</td>
|
||||
<td class="td_ratio m_td_right">{{ ratio(u.uploaded, u.downloaded) }}</td>
|
||||
{% if is_site_inviter %}
|
||||
{% if edit_source %}
|
||||
<td class="nobr">{{ select_invite_source(u.user_id, inviter_config, user_source) }}</td>
|
||||
{% else %}
|
||||
<td class="nobr">{{ user_source[u.user_id].name|default('<i>not set</i>')|raw }}</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if is_site_inviter and edit_source %}
|
||||
<tr>
|
||||
<td colspan="7"> </td>
|
||||
<td><input type="submit" value="Update" /></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% if is_site_inviter %}
|
||||
<input type="hidden" name="action" value="invite" />
|
||||
<input type="hidden" name="auth" value="{{ auth }}" />
|
||||
<input type="hidden" name="user_id" value="{{ user.id }}" />
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user