Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to sort statistics results #78

Merged
merged 3 commits into from
Nov 12, 2023
Merged
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
16 changes: 15 additions & 1 deletion src/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ const typeDefs = gql`
NUMBER_OF_QUIZZES_COMPLETED_WITH_DESC
}

enum IndividualUserStatisticsSortOption {
QUIZZES_COMPLETED_DESC
AVERAGE_SCORE_DESC
}

type PageInfo {
hasNextPage: Boolean
startCursor: String
Expand Down Expand Up @@ -151,7 +156,16 @@ const typeDefs = gql`
"""
users(first: Int, after: String, sortedBy: UserSortOption): UserConnection
me: UserDetails
individualUserStatistics: [IndividualUserStatistic]
"""
Get statistics for every user.
Optionally sort using the sortedBy parameter.

Results from this endpoint may be delayed by up to 24 hours.
"""
individualUserStatistics(
"The sorting option to use"
sortedBy: IndividualUserStatisticsSortOption
): [IndividualUserStatistic]
}

type Mutation {
Expand Down
224 changes: 145 additions & 79 deletions src/quiz/quiz.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,115 +3,181 @@ import { QuizPersistence } from './quiz.persistence';
import { S3FileService } from '../file/s3.service';
import { Decimal } from '@prisma/client/runtime/library';

jest.mock('./quiz.persistence');

const mockPersistence = {
getQuizzesWithUserResults: jest.fn(),
getCompletionScoreWithQuizTypesForUser: jest.fn(),
};
const mockFileService = {};

const sut = new QuizService(mockPersistence as unknown as QuizPersistence, mockFileService as S3FileService);

describe('QuizService', () => {
beforeEach(() => {
jest.resetAllMocks();
});
describe('getQuizzesWithUserResults', () => {
it('must call getQuizzesWithUserResults on persistence with correct arguments and transform the result', async () => {
const persistenceResult = [
{
id: 'fake-id-one',
type: 'SHARK',
date: new Date('2023-01-01'),
uploadedAt: new Date('2023-01-02'),
uploadedByUserId: 'fake-user-id',
completions: [],
uploadedByUser: {
id: 'fake-user-id',
email: '[email protected]',
name: 'Joe Blogs',
},
},
{
id: 'fake-id-two',
type: 'BRAINWAVES',
date: new Date('2023-02-01'),
uploadedAt: new Date('2023-03-02'),
uploadedByUserId: 'fake-user-id',
completions: [
{
completedAt: new Date('2023-03-10'),
completedBy: [
{
user: {
id: 'fake-completion-user-id',
email: '[email protected]',
name: 'Completer',
},
},
],
score: new Decimal(10),
},
],
uploadedByUser: {
id: 'fake-user-id',
email: '[email protected]',
name: 'Joe Blogs',
},
},
];
mockPersistence.getQuizzesWithUserResults.mockImplementationOnce(() =>
Promise.resolve({ data: persistenceResult, hasMoreRows: false }),
);

const actual = await sut.getQuizzesWithUsersResults('[email protected]', 10);

expect(mockPersistence.getQuizzesWithUserResults).toHaveBeenCalledTimes(1);
expect(mockPersistence.getQuizzesWithUserResults).toHaveBeenCalledWith({
userEmail: '[email protected]',
limit: 10,
});

expect(actual).toEqual({
data: [
describe('quiz', () => {
describe('quiz.service', () => {
beforeEach(() => {
jest.restoreAllMocks();
});
describe('getQuizzesWithUserResults', () => {
it('must call getQuizzesWithUserResults on persistence with correct arguments and transform the result', async () => {
const persistenceResult = [
{
id: 'fake-id-one',
type: 'SHARK',
date: new Date('2023-01-01'),
uploadedAt: new Date('2023-01-02'),
uploadedBy: {
email: '[email protected]',
uploadedByUserId: 'fake-user-id',
completions: [],
uploadedByUser: {
id: 'fake-user-id',
email: '[email protected]',
name: 'Joe Blogs',
},
myCompletions: [],
},
{
id: 'fake-id-two',
type: 'BRAINWAVES',
date: new Date('2023-02-01'),
uploadedAt: new Date('2023-03-02'),
uploadedBy: {
email: '[email protected]',
id: 'fake-user-id',
name: 'Joe Blogs',
},
myCompletions: [
uploadedByUserId: 'fake-user-id',
completions: [
{
completedAt: new Date('2023-03-10'),
completedBy: [
{
id: 'fake-completion-user-id',
email: '[email protected]',
name: 'Completer',
user: {
id: 'fake-completion-user-id',
email: '[email protected]',
name: 'Completer',
},
},
],
score: 10,
score: new Decimal(10),
},
],
uploadedByUser: {
id: 'fake-user-id',
email: '[email protected]',
name: 'Joe Blogs',
},
},
],
hasMoreRows: false,
];
mockPersistence.getQuizzesWithUserResults.mockImplementationOnce(() =>
Promise.resolve({ data: persistenceResult, hasMoreRows: false }),
);

const actual = await sut.getQuizzesWithUsersResults('[email protected]', 10);

expect(mockPersistence.getQuizzesWithUserResults).toHaveBeenCalledTimes(1);
expect(mockPersistence.getQuizzesWithUserResults).toHaveBeenCalledWith({
userEmail: '[email protected]',
limit: 10,
});

expect(actual).toEqual({
data: [
{
id: 'fake-id-one',
type: 'SHARK',
date: new Date('2023-01-01'),
uploadedAt: new Date('2023-01-02'),
uploadedBy: {
email: '[email protected]',
id: 'fake-user-id',
name: 'Joe Blogs',
},
myCompletions: [],
},
{
id: 'fake-id-two',
type: 'BRAINWAVES',
date: new Date('2023-02-01'),
uploadedAt: new Date('2023-03-02'),
uploadedBy: {
email: '[email protected]',
id: 'fake-user-id',
name: 'Joe Blogs',
},
myCompletions: [
{
completedAt: new Date('2023-03-10'),
completedBy: [
{
id: 'fake-completion-user-id',
email: '[email protected]',
name: 'Completer',
},
],
score: 10,
},
],
},
],
hasMoreRows: false,
});
});
});
describe('quizScorePercentagesForUser', () => {
it('must call getCompletionScoreWithQuizTypesForUser on persistence with correct arguments and calculate percentages for the results', async () => {
mockPersistence.getCompletionScoreWithQuizTypesForUser.mockResolvedValueOnce({
data: [
{
id: '1',
quiz: {
type: 'SHARK',
},
score: new Decimal(10),
},
{
id: '2',
quiz: {
type: 'BRAINWAVES',
},
score: new Decimal(12.5),
},
],
hasMoreRows: false,
});

const actual = await sut.quizScorePercentagesForUser('[email protected]', 2, 'test-cursor');

expect(mockPersistence.getCompletionScoreWithQuizTypesForUser).toHaveBeenCalledTimes(1);
expect(mockPersistence.getCompletionScoreWithQuizTypesForUser).toHaveBeenCalledWith({
email: '[email protected]',
limit: 2,
afterId: 'test-cursor',
});

expect(actual).toEqual({
stats: [0.5, 0.25],
cursor: undefined,
});
});
it('must provide the correct cursor if more data is available', async () => {
mockPersistence.getCompletionScoreWithQuizTypesForUser.mockResolvedValueOnce({
data: [
{
id: '1',
quiz: {
type: 'SHARK',
},
score: new Decimal(10),
},
{
id: '2',
quiz: {
type: 'BRAINWAVES',
},
score: new Decimal(12.5),
},
],
hasMoreRows: true,
});

const actual = await sut.quizScorePercentagesForUser('[email protected]');

expect(actual).toEqual({
stats: [0.5, 0.25],
cursor: '2',
});
});
});
});
Expand Down
25 changes: 4 additions & 21 deletions src/quiz/quiz.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,29 +160,12 @@ export class QuizService {

/**
* Get a paginated list of quiz percentages for a user.
* @param filters Paging and filtering options.
* @param email The email of the user to get quiz percentages for.
* @param first The number of quiz percentages to get, defaults to the maximum of 100.
* @param afterId Optionally an id to use as a cursor to get quiz percentages after. This will have been provided in a previous call to this function.
* @returns A list of quiz percentages (as a number between 0 and 1) for the user in stats and a cursor to load the next set of scores.
*/
async quizScorePercentagesForUser({
email,
first = 100,
afterId,
}: {
/**
* The email of the user to get quiz percentages for.
*/
email: string;
/**
* The number of quiz percentages to get.
*/
first: number;
/**
* The cursor to start getting quiz percentages from.
* If not provided, the first quiz percentage will be returned.
* Will have been returned in the previous call to this function.
*/
afterId?: string;
}) {
async quizScorePercentagesForUser(email: string, first = 100, afterId?: string) {
const { data, hasMoreRows } = await this.#persistence.getCompletionScoreWithQuizTypesForUser({
email,
limit: first,
Expand Down
2 changes: 2 additions & 0 deletions src/statistics/statistics.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export interface IndividualUserStatistic {
totalQuizCompletions: number;
averageScorePercentage: number;
}

export type IndividualUserStatisticsSortOption = 'QUIZZES_COMPLETED_DESC' | 'AVERAGE_SCORE_DESC';
6 changes: 3 additions & 3 deletions src/statistics/statistics.gql.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { QuizlordContext } from '..';
import { authorisationService, statisticsService } from '../service.locator';
import { IndividualUserStatistic } from './statistics.dto';
import { IndividualUserStatistic, IndividualUserStatisticsSortOption } from './statistics.dto';

async function individualUserStatistics(
_p: unknown,
_: void,
{ sortedBy }: { sortedBy?: IndividualUserStatisticsSortOption },
context: QuizlordContext,
): Promise<IndividualUserStatistic[]> {
authorisationService.requireUserRole(context, 'USER');
return statisticsService.getIndividualUserStatistics();
return statisticsService.getIndividualUserStatistics(sortedBy);
}

export const statisticsQueries = {
Expand Down
Loading