Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/Community/Actions/CreateGameClaimAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ private function maybeSendClaimWithUnresolvedTicketsAlert(User $currentUser, Gam
return;
}

$ticketCount = Ticket::forDeveloper($currentUser)->awaitingDeveloper()->count();
$ticketCount = Ticket::forAssignee($currentUser)->awaitingDeveloper()->count();
if ($ticketCount < 2) { // two or more suggests the developer may be ignoring tickets
return;
}
Expand Down
27 changes: 25 additions & 2 deletions app/Community/Enums/TicketType.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,38 @@

namespace App\Community\Enums;

use App\Platform\Enums\TicketableType;
use InvalidArgumentException;
use LogicException;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;

#[TypeScript]
enum TicketType: string
{
case TriggeredAtWrongTime = 'triggered_at_wrong_time';
case DidNotCancel = 'did_not_cancel';
case DidNotStart = 'did_not_start';
case DidNotSubmit = 'did_not_submit';
case DidNotTrigger = 'did_not_trigger';
case SubmittedWrongValue = 'submitted_wrong_value';
case TriggeredAtWrongTime = 'triggered_at_wrong_time';

public function appliesTo(TicketableType $type): bool
{
return match ($this) {
self::TriggeredAtWrongTime, self::DidNotTrigger => $type === TicketableType::Achievement,
self::DidNotStart, self::DidNotCancel, self::DidNotSubmit, self::SubmittedWrongValue => $type === TicketableType::Leaderboard,
};
}

public function label(): string
{
return match ($this) {
self::TriggeredAtWrongTime => 'Triggered at the wrong time',
self::DidNotCancel => 'Did not cancel',
self::DidNotStart => 'Did not start',
self::DidNotSubmit => 'Did not submit',
self::DidNotTrigger => 'Did not trigger',
self::SubmittedWrongValue => 'Submitted wrong value',
self::TriggeredAtWrongTime => 'Triggered at the wrong time',
};
}

Expand All @@ -31,6 +49,11 @@ public function toLegacyInteger(): int
return match ($this) {
self::TriggeredAtWrongTime => 1,
self::DidNotTrigger => 2,

self::DidNotStart,
self::DidNotCancel,
self::DidNotSubmit,
self::SubmittedWrongValue => throw new LogicException("TicketType {$this->value} has no legacy integer mapping."),
};
}

Expand Down
2 changes: 1 addition & 1 deletion app/Helpers/database/user.php
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ function GetDeveloperStatsFull(int $count, int $offset = 0, int $sortBy = 0, int
} elseif ($sortBy == 4) { // TicketsResolvedForOthers DESC
$query = "SELECT ua.id, SUM(!ISNULL(ach.id)) as total
FROM users as ua
LEFT JOIN tickets tick ON tick.resolver_id = ua.id AND tick.state = 'resolved' AND tick.resolver_id != tick.reporter_id
LEFT JOIN tickets tick ON tick.resolver_id = ua.id AND tick.state = 'resolved' AND tick.resolver_id != tick.reporter_id AND tick.ticketable_type = 'achievement'
LEFT JOIN achievements as ach ON ach.id = tick.ticketable_id AND ach.is_promoted = 1 AND ach.user_id != ua.id
WHERE $stateCond
GROUP BY ua.id
Expand Down
38 changes: 36 additions & 2 deletions app/Models/Achievement.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use App\Community\Enums\CommentableType;
use App\Platform\Contracts\HasPermalink;
use App\Platform\Contracts\HasVersionedTrigger;
use App\Platform\Contracts\Ticketable;
use App\Platform\Enums\AchievementAuthorTask;
use App\Platform\Enums\AchievementSetType;
use App\Platform\Enums\AchievementType;
Expand All @@ -22,6 +23,7 @@
use App\Platform\Events\AchievementTypeChanged;
use App\Platform\Events\AchievementUnpromoted;
use App\Support\Database\Eloquent\BaseModel;
use Carbon\CarbonInterface;
use Database\Factories\AchievementFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
Expand Down Expand Up @@ -51,7 +53,7 @@
/**
* @implements HasVersionedTrigger<Achievement>
*/
class Achievement extends BaseModel implements HasPermalink, HasVersionedTrigger
class Achievement extends BaseModel implements HasPermalink, HasVersionedTrigger, Ticketable
{
/*
* Community Traits
Expand Down Expand Up @@ -260,6 +262,38 @@ public function shouldBeSearchable(): bool
return $this->is_promoted;
}

// == ticketable

public function getTicketableType(): TicketableType
{
return TicketableType::Achievement;
}

public function getTicketableGame(): Game
{
return $this->game;
}

public function getTicketableAssignee(?CarbonInterface $at = null): ?User
{
return $this->getMaintainerAt($at ?? now());
}

public function getTicketableTitle(): string
{
return $this->title;
}

public function getTicketableUrl(): string
{
return $this->getCanonicalUrlAttribute();
}

public function getTicketableBadgeUrl(): ?string
{
return $this->getBadgeUrlAttribute();
}

// == helpers

public function ensureAuthorshipCredit(User $user, AchievementAuthorTask $task, ?Carbon $backdate = null): AchievementAuthor
Expand All @@ -270,7 +304,7 @@ public function ensureAuthorshipCredit(User $user, AchievementAuthorTask $task,
);
}

public function getMaintainerAt(Carbon $timestamp): ?User
public function getMaintainerAt(CarbonInterface $timestamp): ?User
{
$maintainer = $this->maintainers()
->where('effective_from', '<=', $timestamp)
Expand Down
64 changes: 63 additions & 1 deletion app/Models/Leaderboard.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
use App\Platform\Actions\RecalculateLeaderboardTopEntryAction;
use App\Platform\Contracts\HasPermalink;
use App\Platform\Contracts\HasVersionedTrigger;
use App\Platform\Contracts\Ticketable;
use App\Platform\Enums\LeaderboardState;
use App\Platform\Enums\TicketableType;
use App\Platform\Enums\ValueFormat;
use App\Support\Database\Eloquent\BaseModel;
use Carbon\CarbonInterface;
use Database\Factories\LeaderboardFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
Expand All @@ -30,7 +33,7 @@
/**
* @implements HasVersionedTrigger<Leaderboard>
*/
class Leaderboard extends BaseModel implements HasPermalink, HasVersionedTrigger
class Leaderboard extends BaseModel implements HasPermalink, HasVersionedTrigger, Ticketable
{
/*
* Shared Traits
Expand Down Expand Up @@ -105,6 +108,39 @@ public function getActivitylogOptions(): LogOptions
->dontSubmitEmptyLogs();
}

// == ticketable

public function getTicketableType(): TicketableType
{
return TicketableType::Leaderboard;
}

public function getTicketableGame(): Game
{
return $this->game;
}

public function getTicketableAssignee(?CarbonInterface $at = null): ?User
{
// leaderboards don't have a "maintainer" concept - the assignee is always the author.
return $this->developer;
}

public function getTicketableTitle(): string
{
return $this->title;
}

public function getTicketableUrl(): string
{
return $this->getCanonicalUrlAttribute();
}

public function getTicketableBadgeUrl(): ?string
{
return null;
}

// == accessors

public function getCanonicalUrlAttribute(): string
Expand Down Expand Up @@ -245,6 +281,14 @@ public function triggers(): MorphMany
->orderBy('version');
}

/**
* @return MorphMany<Ticket, $this>
*/
public function tickets(): MorphMany
{
return $this->morphMany(Ticket::class, 'ticketable');
}

// == scopes

/**
Expand All @@ -256,6 +300,24 @@ public function scopeVisible(Builder $query): Builder
return $query->where('order_column', '>=', 0);
}

/**
* @param Builder<Leaderboard> $query
* @return Builder<Leaderboard>
*/
public function scopePromoted(Builder $query): Builder
{
return $query->where('state', '!=', LeaderboardState::Unpromoted->value);
}

/**
* @param Builder<Leaderboard> $query
* @return Builder<Leaderboard>
*/
public function scopeUnpromoted(Builder $query): Builder
{
return $query->where('state', LeaderboardState::Unpromoted->value);
}

/**
* @param Builder<Leaderboard> $query
* @return Builder<Leaderboard>
Expand Down
68 changes: 54 additions & 14 deletions app/Models/Ticket.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use App\Community\Enums\TicketState;
use App\Community\Enums\TicketType;
use App\Platform\Enums\LeaderboardState;
use App\Platform\Enums\TicketableType;
use App\Support\Database\Eloquent\BaseModel;
use Database\Factories\TicketFactory;
use Illuminate\Database\Eloquent\Builder;
Expand All @@ -14,6 +16,7 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use LogicException;

class Ticket extends BaseModel
{
Expand Down Expand Up @@ -64,13 +67,27 @@ public function ticketable(): MorphTo
}

/**
* Unsafe without a `ticketable_type = achievement` filter.
* Use `ticketable()` or pair with `scopeForTicketableType`.
*
* @return BelongsTo<Achievement, $this>
*/
public function achievement(): BelongsTo
{
return $this->belongsTo(Achievement::class, 'ticketable_id');
}

/**
* Unsafe without a `ticketable_type = leaderboard` filter.
* Use `ticketable()` or pair with `scopeForTicketableType`.
*
* @return BelongsTo<Leaderboard, $this>
*/
public function leaderboard(): BelongsTo
{
return $this->belongsTo(Leaderboard::class, 'ticketable_id');
}

/**
* @return BelongsTo<User, $this>
*/
Expand Down Expand Up @@ -158,9 +175,11 @@ public function scopeQuarantined(Builder $query): Builder
*/
public function scopeForGame(Builder $query, Game $game): Builder
{
return $query->whereHas('achievement', function ($query) use ($game) {
$query->where('game_id', $game->id);
});
return $query->whereHasMorph(
'ticketable',
[Achievement::class, Leaderboard::class],
fn (Builder $q) => $q->where('game_id', $game->id),
);
}

/**
Expand All @@ -170,37 +189,58 @@ public function scopeForGame(Builder $query, Game $game): Builder
public function scopeForAchievement(Builder $query, Achievement $achievement): Builder
{
return $query->where('ticketable_id', $achievement->id)
->where('ticketable_type', 'achievement');
->where('ticketable_type', TicketableType::Achievement->value);
}

/**
* @param Builder<Ticket> $query
* @return Builder<Ticket>
*/
public function scopeForDeveloper(Builder $query, User $developer): Builder
public function scopeForAssignee(Builder $query, User $user): Builder
{
return $query->where('ticketable_author_id', $developer->id);
return $query->where('ticketable_author_id', $user->id);
}

/**
* @param Builder<Ticket> $query
* @return Builder<Ticket>
*/
public function scopeOfficialCore(Builder $query): Builder
public function scopeForTicketableType(Builder $query, TicketableType $type): Builder
{
return $query->whereHas('achievement', function ($query) {
$query->where('is_promoted', true);
});
return $query->where('ticketable_type', $type->value);
}

/**
* @param Builder<Ticket> $query
* @return Builder<Ticket>
*/
public function scopeUnofficial(Builder $query): Builder
public function scopePromoted(Builder $query): Builder
{
return $query->whereHas('achievement', function ($query) {
$query->where('is_promoted', false);
});
return $query->whereHasMorph(
'ticketable',
[Achievement::class, Leaderboard::class],
fn (Builder $q, string $type) => match ($type) {
Achievement::class => $q->where('is_promoted', true),
Leaderboard::class => $q->where('state', '!=', LeaderboardState::Unpromoted->value),
default => throw new LogicException("Unexpected ticketable type: {$type}"),
},
);
}

/**
* @param Builder<Ticket> $query
* @return Builder<Ticket>
*/
public function scopeUnpromoted(Builder $query): Builder
{
return $query->whereHasMorph(
'ticketable',
[Achievement::class, Leaderboard::class],
fn (Builder $q, string $type) => match ($type) {
Achievement::class => $q->where('is_promoted', false),
Leaderboard::class => $q->where('state', LeaderboardState::Unpromoted->value),
default => throw new LogicException("Unexpected ticketable type: {$type}"),
},
);
}
}
2 changes: 1 addition & 1 deletion app/Platform/Actions/BuildGamePageClaimDataAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public function execute(Game $game, ?User $user, Collection $achievementSetClaim
isSoleAuthor: $isSoleAuthor,
maxClaimCount: $maxClaimCount,
numClaimsRemaining: $this->calculateNumClaimsRemaining($user, $maxClaimCount),
numUnresolvedTickets: Ticket::forDeveloper($user)->awaitingDeveloper()->count(),
numUnresolvedTickets: Ticket::forAssignee($user)->awaitingDeveloper()->count(),
wouldBeCollaboration: $wouldBeCollaboration,
wouldBeRevision: $wouldBeRevision,
);
Expand Down
4 changes: 2 additions & 2 deletions app/Platform/Actions/BuildGameShowPagePropsAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -352,8 +352,8 @@ public function execute(
numLeaderboards: $this->gameLeaderboardService->getCount($backingGame, $isPromoted),
numMasters: $numMasters,
numOpenTickets: $isPromoted
? Ticket::forGame($backingGame)->open()->officialCore()->count()
: Ticket::forGame($backingGame)->open()->unofficial()->count(),
? Ticket::forGame($backingGame)->open()->promoted()->count()
: Ticket::forGame($backingGame)->open()->unpromoted()->count(),

numScreenshots: $game->gameScreenshots()->approved()->count(),
screenshots: Lazy::inertiaDeferred(fn () => $game->gameScreenshots()
Expand Down
Loading
Loading