mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-14 07:06:14 -05:00
Better calendar event template select contents
This commit is contained in:
190
app/features/calendar/CalendarRepository.server.test.ts
Normal file
190
app/features/calendar/CalendarRepository.server.test.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { sub } from "date-fns";
|
||||
import { beforeEach, describe, expect, test } from "vitest";
|
||||
import * as TournamentFactory from "~/db/seed/factories/TournamentFactory";
|
||||
import * as TournamentOrganizationFactory from "~/db/seed/factories/TournamentOrganizationFactory";
|
||||
import * as UserFactory from "~/db/seed/factories/UserFactory";
|
||||
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import * as CalendarRepository from "./CalendarRepository.server";
|
||||
|
||||
const users = UserFactory.pool();
|
||||
|
||||
describe("findRecentTournamentsByOrganizerUserId", () => {
|
||||
/** As many events as the dropdown has spots, so nothing is filled in. */
|
||||
const SPOTS_SHOWN = 10;
|
||||
|
||||
const organizerId = () => users.id(1);
|
||||
|
||||
beforeEach(async () => {
|
||||
await users.create(1);
|
||||
});
|
||||
|
||||
const daysAgo = (days: number) =>
|
||||
dateToDatabaseTimestamp(sub(new Date(), { days }));
|
||||
|
||||
const seedOrganizationWithSeries = (seriesNames: string[]) =>
|
||||
TournamentOrganizationFactory.create(
|
||||
{ ownerId: organizerId() },
|
||||
{
|
||||
series: seriesNames.map((name) => ({
|
||||
name,
|
||||
description: null,
|
||||
showLeaderboard: false,
|
||||
})),
|
||||
},
|
||||
);
|
||||
|
||||
const seedTournament = ({
|
||||
name,
|
||||
startedDaysAgo,
|
||||
organizationId = null,
|
||||
}: {
|
||||
name: string;
|
||||
startedDaysAgo: number;
|
||||
organizationId?: number | null;
|
||||
}) =>
|
||||
TournamentFactory.create({
|
||||
authorId: organizerId(),
|
||||
organizationId,
|
||||
name,
|
||||
startTimes: [daysAgo(startedDaysAgo)],
|
||||
});
|
||||
|
||||
/** Events of no series, newest first, starting from the given day. */
|
||||
const seedStandaloneTournaments = async ({
|
||||
count,
|
||||
organizationId,
|
||||
oldestStartedDaysAgo,
|
||||
}: {
|
||||
count: number;
|
||||
organizationId: number;
|
||||
oldestStartedDaysAgo: number;
|
||||
}) => {
|
||||
for (let index = 0; index < count; index++) {
|
||||
await seedTournament({
|
||||
name: `In The Zone ${index}`,
|
||||
startedDaysAgo: oldestStartedDaysAgo - index,
|
||||
organizationId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const recentTournamentNames = async () => {
|
||||
const tournaments =
|
||||
await CalendarRepository.findRecentTournamentsByOrganizerUserId(
|
||||
organizerId(),
|
||||
);
|
||||
|
||||
return tournaments.map((tournament) => tournament.name);
|
||||
};
|
||||
|
||||
test("drops the older edition of a series when there are more events than spots", async () => {
|
||||
const org = await seedOrganizationWithSeries(["Low Ink"]);
|
||||
|
||||
await seedTournament({
|
||||
name: "Low Ink February",
|
||||
startedDaysAgo: 1,
|
||||
organizationId: org.id,
|
||||
});
|
||||
await seedTournament({
|
||||
name: "Low Ink January",
|
||||
startedDaysAgo: 2,
|
||||
organizationId: org.id,
|
||||
});
|
||||
await seedStandaloneTournaments({
|
||||
count: SPOTS_SHOWN - 1,
|
||||
organizationId: org.id,
|
||||
oldestStartedDaysAgo: 11,
|
||||
});
|
||||
|
||||
const names = await recentTournamentNames();
|
||||
|
||||
expect(names).toHaveLength(SPOTS_SHOWN);
|
||||
expect(names).toContain("Low Ink February");
|
||||
expect(names).not.toContain("Low Ink January");
|
||||
});
|
||||
|
||||
test("keeps the latest edition of an older series over the older edition of a newer one", async () => {
|
||||
const org = await seedOrganizationWithSeries(["Low Ink", "Paddling Pool"]);
|
||||
|
||||
await seedTournament({
|
||||
name: "Low Ink February",
|
||||
startedDaysAgo: 1,
|
||||
organizationId: org.id,
|
||||
});
|
||||
await seedTournament({
|
||||
name: "Low Ink January",
|
||||
startedDaysAgo: 2,
|
||||
organizationId: org.id,
|
||||
});
|
||||
await seedStandaloneTournaments({
|
||||
count: SPOTS_SHOWN - 2,
|
||||
organizationId: org.id,
|
||||
oldestStartedDaysAgo: 11,
|
||||
});
|
||||
await seedTournament({
|
||||
name: "Paddling Pool October",
|
||||
startedDaysAgo: 100,
|
||||
organizationId: org.id,
|
||||
});
|
||||
|
||||
const names = await recentTournamentNames();
|
||||
|
||||
expect(names).toContain("Paddling Pool October");
|
||||
expect(names).not.toContain("Low Ink January");
|
||||
});
|
||||
|
||||
test("fills the remaining spots with older editions of a series", async () => {
|
||||
const org = await seedOrganizationWithSeries(["Low Ink"]);
|
||||
|
||||
await seedTournament({
|
||||
name: "Low Ink January",
|
||||
startedDaysAgo: 2,
|
||||
organizationId: org.id,
|
||||
});
|
||||
await seedTournament({
|
||||
name: "Low Ink February",
|
||||
startedDaysAgo: 1,
|
||||
organizationId: org.id,
|
||||
});
|
||||
|
||||
expect(await recentTournamentNames()).toEqual([
|
||||
"Low Ink February",
|
||||
"Low Ink January",
|
||||
]);
|
||||
});
|
||||
|
||||
test("keeps every event of an organization whose names match no series", async () => {
|
||||
const org = await seedOrganizationWithSeries(["Low Ink"]);
|
||||
|
||||
await seedStandaloneTournaments({
|
||||
count: SPOTS_SHOWN + 1,
|
||||
organizationId: org.id,
|
||||
oldestStartedDaysAgo: 11,
|
||||
});
|
||||
|
||||
expect(await recentTournamentNames()).toHaveLength(SPOTS_SHOWN);
|
||||
});
|
||||
|
||||
test("excludes events that started over a year ago", async () => {
|
||||
const org = await seedOrganizationWithSeries(["Low Ink"]);
|
||||
|
||||
await seedTournament({
|
||||
name: "Low Ink February",
|
||||
startedDaysAgo: 1,
|
||||
organizationId: org.id,
|
||||
});
|
||||
await seedTournament({
|
||||
name: "Low Ink January",
|
||||
startedDaysAgo: 400,
|
||||
organizationId: org.id,
|
||||
});
|
||||
|
||||
expect(await recentTournamentNames()).toEqual(["Low Ink February"]);
|
||||
});
|
||||
|
||||
test("includes events that belong to no organization", async () => {
|
||||
await seedTournament({ name: "Low Ink February", startedDaysAgo: 1 });
|
||||
|
||||
expect(await recentTournamentNames()).toEqual(["Low Ink February"]);
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,9 @@ import type { TournamentSettings } from "~/db/tables-json";
|
||||
import { EXCLUDED_TAGS } from "~/features/calendar/calendar-constants";
|
||||
import * as ChatRepository from "~/features/chat/ChatRepository.server";
|
||||
import * as Progression from "~/features/tournament-bracket/core/Progression";
|
||||
import * as Series from "~/features/tournament-organization/core/Series";
|
||||
import { getTentativeTier } from "~/features/tournament-organization/core/tentativeTiers.server";
|
||||
import * as TournamentOrganizationRepository from "~/features/tournament-organization/TournamentOrganizationRepository.server";
|
||||
import {
|
||||
databaseTimestampNow,
|
||||
databaseTimestampToDate,
|
||||
@@ -39,6 +41,8 @@ import {
|
||||
import type { CalendarEvent } from "./calendar-types";
|
||||
import { calendarEventSorter } from "./calendar-utils";
|
||||
|
||||
const RECENT_TOURNAMENTS_SHOWN = 10;
|
||||
|
||||
function hasBadge(eb: ExpressionBuilder<DB, "CalendarEventDate">) {
|
||||
return eb
|
||||
.exists(
|
||||
@@ -355,8 +359,14 @@ export async function findById(
|
||||
};
|
||||
}
|
||||
|
||||
export async function findRecentTournamentsByAuthorId(authorId: number) {
|
||||
return db
|
||||
/**
|
||||
* Tournaments from the past year organized by the user (as the author, via an
|
||||
* organization ADMIN/ORGANIZER role or as tournament staff ORGANIZER), newest first.
|
||||
* Only the latest event of each tournament series is included, unless there are fewer
|
||||
* series than spots to show, in which case the next newest events fill the rest.
|
||||
*/
|
||||
export async function findRecentTournamentsByOrganizerUserId(userId: number) {
|
||||
const tournaments = await db
|
||||
.selectFrom("CalendarEvent")
|
||||
.innerJoin("Tournament", "Tournament.id", "CalendarEvent.tournamentId")
|
||||
.innerJoin(
|
||||
@@ -364,15 +374,82 @@ export async function findRecentTournamentsByAuthorId(authorId: number) {
|
||||
"CalendarEvent.id",
|
||||
"CalendarEventDate.eventId",
|
||||
)
|
||||
.select([
|
||||
.select(({ fn }) => [
|
||||
"CalendarEvent.id",
|
||||
"CalendarEvent.name",
|
||||
"CalendarEventDate.startsAt",
|
||||
"CalendarEvent.organizationId",
|
||||
fn.min("CalendarEventDate.startsAt").as("startsAt"),
|
||||
])
|
||||
.where("CalendarEvent.authorId", "=", authorId)
|
||||
.orderBy("CalendarEvent.id", "desc")
|
||||
.limit(10)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb("CalendarEvent.authorId", "=", userId),
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom("TournamentOrganizationMember")
|
||||
.select("TournamentOrganizationMember.userId")
|
||||
.whereRef(
|
||||
"TournamentOrganizationMember.organizationId",
|
||||
"=",
|
||||
"CalendarEvent.organizationId",
|
||||
)
|
||||
.where("TournamentOrganizationMember.userId", "=", userId)
|
||||
.where("TournamentOrganizationMember.role", "in", [
|
||||
"ADMIN",
|
||||
"ORGANIZER",
|
||||
]),
|
||||
),
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom("TournamentStaff")
|
||||
.select("TournamentStaff.userId")
|
||||
.whereRef(
|
||||
"TournamentStaff.tournamentId",
|
||||
"=",
|
||||
"CalendarEvent.tournamentId",
|
||||
)
|
||||
.where("TournamentStaff.userId", "=", userId)
|
||||
.where("TournamentStaff.role", "=", "ORGANIZER"),
|
||||
),
|
||||
]),
|
||||
)
|
||||
.where(
|
||||
"CalendarEventDate.startsAt",
|
||||
">=",
|
||||
dateToDatabaseTimestamp(sub(new Date(), { years: 1 })),
|
||||
)
|
||||
.groupBy("CalendarEvent.id")
|
||||
.orderBy("startsAt", "desc")
|
||||
.execute();
|
||||
|
||||
const series =
|
||||
await TournamentOrganizationRepository.findAllSeriesByOrganizationIds(
|
||||
R.unique(
|
||||
tournaments
|
||||
.map((tournament) => tournament.organizationId)
|
||||
.filter((organizationId) => organizationId !== null),
|
||||
),
|
||||
);
|
||||
|
||||
const latestOfEachSeries = R.uniqueBy(tournaments, (tournament) => {
|
||||
const tournamentSeries = Series.findByEventName({
|
||||
series: series.filter(
|
||||
(oneSeries) => oneSeries.organizationId === tournament.organizationId,
|
||||
),
|
||||
eventName: tournament.name,
|
||||
});
|
||||
|
||||
return tournamentSeries
|
||||
? `series-${tournamentSeries.id}`
|
||||
: `event-${tournament.id}`;
|
||||
});
|
||||
|
||||
return R.sortBy(
|
||||
R.take(
|
||||
R.unique([...latestOfEachSeries, ...tournaments]),
|
||||
RECENT_TOURNAMENTS_SHOWN,
|
||||
),
|
||||
[(tournament) => tournament.startsAt, "desc"],
|
||||
);
|
||||
}
|
||||
|
||||
export async function findResultsByEventId(eventId: number) {
|
||||
|
||||
@@ -128,7 +128,9 @@ export const loader = async ({ url }: LoaderFunctionArgs) => {
|
||||
eventToCopy,
|
||||
recentTournaments:
|
||||
canAddTournaments && !eventToEdit
|
||||
? await CalendarRepository.findRecentTournamentsByAuthorId(user.id)
|
||||
? await CalendarRepository.findRecentTournamentsByOrganizerUserId(
|
||||
user.id,
|
||||
)
|
||||
: undefined,
|
||||
organizations,
|
||||
trophies,
|
||||
|
||||
@@ -516,15 +516,21 @@ export async function findAllEventsBySeries({
|
||||
return events.map(mapEvent);
|
||||
}
|
||||
|
||||
export function findAllSeriesByOrganizationId(organizationId: number) {
|
||||
/** Series belonging to any of the given organizations. */
|
||||
export async function findAllSeriesByOrganizationIds(
|
||||
organizationIds: number[],
|
||||
) {
|
||||
if (organizationIds.length === 0) return [];
|
||||
|
||||
return db
|
||||
.selectFrom("TournamentOrganizationSeries")
|
||||
.select([
|
||||
"TournamentOrganizationSeries.id",
|
||||
"TournamentOrganizationSeries.name",
|
||||
"TournamentOrganizationSeries.organizationId",
|
||||
"TournamentOrganizationSeries.substringMatches",
|
||||
])
|
||||
.where("TournamentOrganizationSeries.organizationId", "=", organizationId)
|
||||
.where("TournamentOrganizationSeries.organizationId", "in", organizationIds)
|
||||
.execute();
|
||||
}
|
||||
|
||||
|
||||
@@ -128,9 +128,9 @@ async function previousSeriesWins({
|
||||
|
||||
const series = Series.findByEventName({
|
||||
series:
|
||||
await TournamentOrganizationRepository.findAllSeriesByOrganizationId(
|
||||
await TournamentOrganizationRepository.findAllSeriesByOrganizationIds([
|
||||
organizationId,
|
||||
),
|
||||
]),
|
||||
eventName: tournamentName,
|
||||
});
|
||||
if (!series) return null;
|
||||
|
||||
8
changelog/2026-09-01-tournament-templates.md
Normal file
8
changelog/2026-09-01-tournament-templates.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
navItem: calendar
|
||||
type: feature
|
||||
---
|
||||
Better tournament templates when adding a new tournament
|
||||
|
||||
- Tournaments run by your organization or those you were added as staff can now be used as templates, not only the ones you added yourself
|
||||
- Only the latest event of each tournament series is shown, fitting more different templates in the list
|
||||
@@ -283,9 +283,10 @@ export function buildCases(fx: Fixtures): {
|
||||
}),
|
||||
);
|
||||
add(
|
||||
"CalendarRepository.findRecentTournamentsByAuthorId",
|
||||
"CalendarRepository.findRecentTournamentsByOrganizerUserId",
|
||||
fx.calendarAuthorId,
|
||||
(authorId) => CalendarRepository.findRecentTournamentsByAuthorId(authorId),
|
||||
(authorId) =>
|
||||
CalendarRepository.findRecentTournamentsByOrganizerUserId(authorId),
|
||||
);
|
||||
add("CalendarRepository.findResultsByEventId", fx.resultsEventId, (eventId) =>
|
||||
CalendarRepository.findResultsByEventId(eventId),
|
||||
|
||||
Reference in New Issue
Block a user