diff --git a/src/activities/dtos/find-activities.input.ts b/src/activities/dtos/find-activities.input.ts index 56f9bb1..253cd34 100644 --- a/src/activities/dtos/find-activities.input.ts +++ b/src/activities/dtos/find-activities.input.ts @@ -18,6 +18,10 @@ export class FindActivitiesInput { @IsOptional() type?: ActivityType[]; + @Field(() => [String], { nullable: true }) + @IsOptional() + activityTypes?: string[]; + @Field({ nullable: true }) @IsOptional() dateFrom?: Date; diff --git a/src/activities/resolvers/activities.resolver.ts b/src/activities/resolvers/activities.resolver.ts index e92df9a..cde376b 100644 --- a/src/activities/resolvers/activities.resolver.ts +++ b/src/activities/resolvers/activities.resolver.ts @@ -38,6 +38,7 @@ import { } from '../../core/interceptors/data-loader.interceptor'; import { UserLoader } from '../../users/loaders/user.loader'; import DataLoader from 'dataloader'; +import { StatsActivities } from '../utils/stats-activities.class'; @Resolver(() => Activity) @UseInterceptors(DataLoaderInterceptor) @@ -60,6 +61,19 @@ export class ActivitiesResolver { return this.activitiesService.paginate(input, currentUser); } + @UseGuards(UserAuthGuard) + @Query(() => [StatsActivities]) + myActivitiesStatistics( + @CurrentUser() currentUser: User, + @Args('input', { nullable: true }) input: FindActivitiesInput = {}, + @Info() info: GraphQLResolveInfo, + ) { + info.cacheControl.setCacheHint({ scope: CacheScope.Private }); + input.userId = currentUser.id; + + return this.activitiesService.getStats(input, currentUser); + } + @UseGuards(UserAuthGuard) @Query(() => Activity) @UseFilters(NotFoundFilter) diff --git a/src/activities/resolvers/activity-routes.resolver.ts b/src/activities/resolvers/activity-routes.resolver.ts index d35d75f..e6077e2 100644 --- a/src/activities/resolvers/activity-routes.resolver.ts +++ b/src/activities/resolvers/activity-routes.resolver.ts @@ -34,7 +34,7 @@ import { Activity } from '../entities/activity.entity'; import { ActivityLoader } from '../loaders/activity.loader'; import { ActivityRoutesService } from '../services/activity-routes.service'; import { PaginatedActivityRoutes } from '../utils/paginated-activity-routes.class'; -import { StatsActivities } from '../utils/stats-activities.class'; +import { StatsRoutes } from '../utils/stats-routes.class'; import { GraphQLResolveInfo } from 'graphql'; import { CacheScope } from 'apollo-server-types'; import { CreateActivityRouteInput } from '../dtos/create-activity-route.input'; @@ -117,8 +117,8 @@ export class ActivityRoutesResolver { } @UseGuards(UserAuthGuard) - @Query(() => [StatsActivities]) - myActivityStatistics( + @Query(() => [StatsRoutes]) + myRoutesStatistics( @CurrentUser() currentUser: User, @Args('input', { nullable: true }) input: FindActivityRoutesInput = {}, @Info() info: GraphQLResolveInfo, diff --git a/src/activities/services/activities.service.ts b/src/activities/services/activities.service.ts index 7c349c2..6e3088a 100644 --- a/src/activities/services/activities.service.ts +++ b/src/activities/services/activities.service.ts @@ -17,6 +17,7 @@ import { setBuilderCache } from '../../core/utils/entity-cache/entity-cache-help import { getPublishStatusParams } from '../../core/utils/contributable-helpers'; import { InjectQueue } from '@nestjs/bull'; import { Queue } from 'bull'; +import { StatsActivities } from '../utils/stats-activities.class'; @Injectable() export class ActivitiesService { @@ -214,6 +215,41 @@ export class ActivitiesService { }); } + async getStats( + params: FindActivitiesInput = {}, + currentUser: User = null, + ): Promise { + + const builder = this.activitiesRepository + .createQueryBuilder('ac') + .select('EXTRACT(YEAR FROM ac.date)', 'year') + .addSelect('ac.type', 'activity_type') + .addSelect('count(*)', 'nr_activities') + .where('ac.user_id = :userId', { + userId: currentUser.id, + }) + + .groupBy('EXTRACT(YEAR FROM ac.date)') + .addGroupBy('ac.type') + .orderBy('year', 'ASC'); + + if (params.activityTypes != null) { + builder.andWhere('ac.type_id IN(:...activityTypes)', { + routeTypes: params.activityTypes, + }); + } + const raw = await builder.getRawMany(); + const myStats = raw.map((element) => { + return { + year: element.year, + nr_activities: element.nr_activities, + type: element.activity_type, + } as StatsActivities; + }); + return myStats; + + } + async find(params: FindActivitiesInput = {}): Promise { return (await this.buildQuery(params)).getMany(); } diff --git a/src/activities/services/activity-routes.service.ts b/src/activities/services/activity-routes.service.ts index bcc2f96..3e158d4 100644 --- a/src/activities/services/activity-routes.service.ts +++ b/src/activities/services/activity-routes.service.ts @@ -44,7 +44,7 @@ import { calculateScore, recalculateActivityRoutesScores, } from '../../crags/utils/calculate-scores'; -import { StatsActivities } from '../utils/stats-activities.class'; +import { StatsRoutes } from '../utils/stats-routes.class'; @Injectable() export class ActivityRoutesService { @@ -510,7 +510,7 @@ export class ActivityRoutesService { async getStats( params: FindActivityRoutesInput = {}, currentUser: User = null, - ): Promise { + ): Promise { const builder = this.activityRoutesRepository .createQueryBuilder('ar') .select('EXTRACT(YEAR FROM ar.date)', 'year') @@ -524,9 +524,6 @@ export class ActivityRoutesService { .where('ar.user_id = :userId', { userId: currentUser.id, }) - .andWhere('ar.ascent_type IN (:...ascentType)', { - ascentType: ['onsight', 'redpoint', 'flash'], - }) .andWhere( "(r.publish_status IN ('published', 'in_review') OR (r.publish_status = 'draft' AND ar.user_id = :userId))", { userId: currentUser.id }, @@ -536,7 +533,7 @@ export class ActivityRoutesService { .addGroupBy('r.difficulty') .addGroupBy('EXTRACT(YEAR FROM ar.date)') .addGroupBy('ar.ascent_type') - .orderBy('coalesce(p.difficulty, r.difficulty)', 'ASC') + .orderBy('coalesce(p.difficulty, r.difficulty)', 'DESC') .addOrderBy('year', 'ASC'); if (params.routeTypes != null) { @@ -649,7 +646,7 @@ export class ActivityRoutesService { if (!currentUser) { // Allow showing only public ascents to guests builder.andWhere('ar."publish" IN (:...publish)', { - publish: ['log', 'public'], + publish: ['public'], }); // Allow showing only published routes (no drafts or in_reviews) @@ -660,7 +657,7 @@ export class ActivityRoutesService { '(ar.user_id = :userId OR ar."publish" IN (:...publish))', { userId: currentUser.id, - publish: ['log', 'public'], + publish: ['public'], }, ); // TODO: should also allow showing club ascents diff --git a/src/activities/utils/stats-activities.class.ts b/src/activities/utils/stats-activities.class.ts index bf05128..61c0dfd 100644 --- a/src/activities/utils/stats-activities.class.ts +++ b/src/activities/utils/stats-activities.class.ts @@ -1,17 +1,14 @@ -import { Field, Float, Int, ObjectType } from '@nestjs/graphql'; +import { Field, Int, ObjectType } from '@nestjs/graphql'; @ObjectType() export class StatsActivities { @Field(() => Int) year: number; - @Field(() => Float) - difficulty: number; + @Field(() => Int) + nr_activities: number; @Field(() => String) - ascent_type: string; - - @Field(() => Int) - nr_routes: number; + type: string; } diff --git a/src/activities/utils/stats-routes.class.ts b/src/activities/utils/stats-routes.class.ts new file mode 100644 index 0000000..0396108 --- /dev/null +++ b/src/activities/utils/stats-routes.class.ts @@ -0,0 +1,17 @@ +import { Field, Float, Int, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class StatsRoutes { + @Field(() => Int) + year: number; + + @Field(() => Float) + difficulty: number; + + @Field(() => String) + ascent_type: string; + + @Field(() => Int) + nr_routes: number; + +} diff --git a/src/crags/crags.module.ts b/src/crags/crags.module.ts index 1bcb273..10b6783 100644 --- a/src/crags/crags.module.ts +++ b/src/crags/crags.module.ts @@ -71,6 +71,10 @@ import { RouteEvent } from './entities/route-event.entity'; import { Parking } from './entities/parking.entity'; import { ParkingsService } from './services/parkings.service'; import { AreaLoader } from './loaders/area.loader'; +import { ActivityRoutesService } from '../activities/services/activity-routes.service'; +import { ActivitiesModule } from '../activities/activities.module'; +import { ClubMember } from '../users/entities/club-member.entity'; +import { Club } from '../users/entities/club.entity'; @Module({ imports: [ @@ -98,7 +102,10 @@ import { AreaLoader } from './loaders/area.loader'; IceFallProperty, StarRatingVote, Parking, + Club, + ClubMember, ]), + forwardRef(() => ActivitiesModule), forwardRef(() => AuditModule), BullModule.registerQueue({ name: 'summary', @@ -148,6 +155,7 @@ import { AreaLoader } from './loaders/area.loader'; CragLoader, RouteLoader, ParkingsService, + ActivityRoutesService, ], controllers: [UploadController], exports: [ diff --git a/src/crags/resolvers/routes.resolver.ts b/src/crags/resolvers/routes.resolver.ts index 1785f06..995f1eb 100644 --- a/src/crags/resolvers/routes.resolver.ts +++ b/src/crags/resolvers/routes.resolver.ts @@ -49,6 +49,11 @@ import { LatestDifficultyVotesInput } from '../dtos/latest-difficulty-votes.inpu import { PaginatedDifficultyVotes } from '../utils/paginated-difficulty-votes'; import { MoveRouteToSectorInput } from '../dtos/move-route-to-sector.input'; import { SectorsService } from '../services/sectors.service'; +import { PaginatedActivityRoutes } from '../../activities/utils/paginated-activity-routes.class'; +import { ActivityRoutesService } from '../../activities/services/activity-routes.service'; +import { FindActivityRoutesInput } from '../../activities/dtos/find-activity-routes.input'; +import { StarRatingVotesService } from '../services/star-rating-votes.service'; +import { StarRatingVote } from '../entities/star-rating-vote.entity'; @Resolver(() => Route) @UseInterceptors(DataLoaderInterceptor) @@ -57,14 +62,18 @@ export class RoutesResolver { private routesService: RoutesService, private sectorsService: SectorsService, private difficultyVotesService: DifficultyVotesService, + private starRatingVotesService: StarRatingVotesService, private entityPropertiesService: EntityPropertiesService, private notificationService: NotificationService, + private activityRoutesService: ActivityRoutesService, ) {} /* QUERIES */ @Query(() => Route) @UseFilters(NotFoundFilter) + @AllowAny() + @UseGuards(UserAuthGuard) async route(@Args('id') id: string): Promise { return this.routesService.findOneById(id); } @@ -251,6 +260,11 @@ export class RoutesResolver { return this.difficultyVotesService.findByRouteId(route.id); } + @ResolveField('starRatingVotes', () => [StarRatingVote]) + async starRatingVotes(@Parent() route: Route): Promise { + return this.starRatingVotesService.findByRouteId(route.id); + } + @ResolveField('crag', () => Crag) async getCrag( @Parent() route: Route, @@ -286,4 +300,17 @@ export class RoutesResolver { ): Promise { return loader.load(route.routeTypeId); } + + @ResolveField('activityRoutes', () => PaginatedActivityRoutes) + @UseGuards(UserAuthGuard) + async activityRoutes( + @Parent() route: Route, + @Args('input', { nullable: true }) input: FindActivityRoutesInput = {}, + @CurrentUser() currentUser: User, + ): Promise { + return this.activityRoutesService.paginate( + { ...input, routeId: route.id }, + currentUser, + ); + } } diff --git a/src/crags/services/star-rating-votes.service.ts b/src/crags/services/star-rating-votes.service.ts index 6f3a1e3..0bea768 100644 --- a/src/crags/services/star-rating-votes.service.ts +++ b/src/crags/services/star-rating-votes.service.ts @@ -18,4 +18,11 @@ export class StarRatingVotesService { .andWhere('srv.route_id IN (:...routeIds)', { routeIds }) .getMany(); } + + async findByRouteId(routeId: string): Promise { + return this.starRatingVoteRepository.find({ + where: { routeId: routeId }, + order: { stars: 'ASC' }, + }); + } }