diff --git a/client/src/api/api.ts b/client/src/api/api.ts index e61bce1a9f..25e45943e2 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -961,6 +961,49 @@ export interface CountryStatDto { */ 'count': number; } +/** + * + * @export + * @interface CourseAggregateStatsDto + */ +export interface CourseAggregateStatsDto { + /** + * + * @type {CountriesStatsDto} + * @memberof CourseAggregateStatsDto + */ + 'studentsCountries': CountriesStatsDto; + /** + * + * @type {CourseStatsDto} + * @memberof CourseAggregateStatsDto + */ + 'studentsStats': CourseStatsDto; + /** + * + * @type {CountriesStatsDto} + * @memberof CourseAggregateStatsDto + */ + 'mentorsCountries': CountriesStatsDto; + /** + * + * @type {CourseMentorsStatsDto} + * @memberof CourseAggregateStatsDto + */ + 'mentorsStats': CourseMentorsStatsDto; + /** + * + * @type {Array} + * @memberof CourseAggregateStatsDto + */ + 'courseTasks': Array; + /** + * + * @type {CountriesStatsDto} + * @memberof CourseAggregateStatsDto + */ + 'studentsCertificatesCountries': CountriesStatsDto; +} /** * * @export @@ -1476,49 +1519,6 @@ export interface CourseStatsDto { */ 'eligibleForCertificationCount': number; } -/** - * - * @export - * @interface CoursesStatsDto - */ -export interface CoursesStatsDto { - /** - * - * @type {CountriesStatsDto} - * @memberof CoursesStatsDto - */ - 'studentsCountries': CountriesStatsDto; - /** - * - * @type {CourseStatsDto} - * @memberof CourseStatsDto - */ - 'studentsStats': CourseStatsDto; - /** - * - * @type {CountriesStatsDto} - * @memberof CoursesStatsDto - */ - 'mentorsCountries': CountriesStatsDto; - /** - * - * @type {CourseMentorsStatsDto} - * @memberof CoursesStatsDto - */ - 'mentorsStats': CourseMentorsStatsDto; - /** - * - * @type {CourseTaskDto[]} - * @memberof CoursesStatsDto - */ - 'courseTasks': CourseTaskDto[]; - /** - * - * @type {CountriesStatsDto} - * @memberof CoursesStatsDto - */ - 'studentsCertificatesCountries': CountriesStatsDto; -} /** * * @export @@ -10174,47 +10174,6 @@ export const CourseStatsApiAxiosParamCreator = function (configuration?: Configu options: localVarRequestOptions, }; }, - /** - * - * @param {number[]} ids - * @param {number} year - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - getCoursesStats: async (ids: number[] = [], year?: number, options: AxiosRequestConfig = {}): Promise => { - const localVarUrlObj = new URL('/courses/aggregate/stats', DUMMY_BASE_URL); - let baseOptions; - - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { - method: 'GET', - ...baseOptions, - ...options - }; - - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - if (ids.length > 0) { - localVarQueryParameter.ids = ids - } - - if (year) { - localVarQueryParameter.year = year; - } - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * * @param {number} courseId @@ -10272,6 +10231,49 @@ export const CourseStatsApiAxiosParamCreator = function (configuration?: Configu + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {Array} ids + * @param {number} year + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getCoursesStats: async (ids: Array, year: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'ids' is not null or undefined + assertParamExists('getCoursesStats', 'ids', ids) + // verify required parameter 'year' is not null or undefined + assertParamExists('getCoursesStats', 'year', year) + const localVarPath = `/courses/aggregate/stats`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + if (ids) { + localVarQueryParameter['ids'] = ids; + } + + if (year !== undefined) { + localVarQueryParameter['year'] = year; + } + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -10358,17 +10360,6 @@ export const CourseStatsApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseStats(courseId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, - /** - * - * @param {number[]} ids - * @param {number} year - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async getCoursesStats(ids: number[], year: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getCoursesStats( ids, year, options); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, /** * * @param {number} courseId @@ -10389,6 +10380,17 @@ export const CourseStatsApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseStudentCountries(courseId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {Array} ids + * @param {number} year + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getCoursesStats(ids: Array, year: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getCoursesStats(ids, year, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {number} courseId @@ -10455,6 +10457,16 @@ export const CourseStatsApiFactory = function (configuration?: Configuration, ba getCourseStudentCountries(courseId: number, options?: any): AxiosPromise { return localVarFp.getCourseStudentCountries(courseId, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {Array} ids + * @param {number} year + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getCoursesStats(ids: Array, year: number, options?: any): AxiosPromise { + return localVarFp.getCoursesStats(ids, year, options).then((request) => request(axios, basePath)); + }, /** * * @param {number} courseId @@ -10509,15 +10521,14 @@ export class CourseStatsApi extends BaseAPI { } /** - * - * @param {number[]} ids - * @param {number} year + * + * @param {number} courseId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CourseStatsApi */ - public getCoursesStats(ids: number[], year: number, options?: AxiosRequestConfig) { - return CourseStatsApiFp(this.configuration).getCoursesStats( ids, year, options).then((request) => request(this.axios, this.basePath)); + public getCourseStudentCertificatesCountries(courseId: number, options?: AxiosRequestConfig) { + return CourseStatsApiFp(this.configuration).getCourseStudentCertificatesCountries(courseId, options).then((request) => request(this.axios, this.basePath)); } /** @@ -10527,19 +10538,20 @@ export class CourseStatsApi extends BaseAPI { * @throws {RequiredError} * @memberof CourseStatsApi */ - public getCourseStudentCertificatesCountries(courseId: number, options?: AxiosRequestConfig) { - return CourseStatsApiFp(this.configuration).getCourseStudentCertificatesCountries(courseId, options).then((request) => request(this.axios, this.basePath)); + public getCourseStudentCountries(courseId: number, options?: AxiosRequestConfig) { + return CourseStatsApiFp(this.configuration).getCourseStudentCountries(courseId, options).then((request) => request(this.axios, this.basePath)); } /** * - * @param {number} courseId + * @param {Array} ids + * @param {number} year * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CourseStatsApi */ - public getCourseStudentCountries(courseId: number, options?: AxiosRequestConfig) { - return CourseStatsApiFp(this.configuration).getCourseStudentCountries(courseId, options).then((request) => request(this.axios, this.basePath)); + public getCoursesStats(ids: Array, year: number, options?: AxiosRequestConfig) { + return CourseStatsApiFp(this.configuration).getCoursesStats(ids, year, options).then((request) => request(this.axios, this.basePath)); } /** @@ -11099,6 +11111,39 @@ export const CoursesApiAxiosParamCreator = function (configuration?: Configurati + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} courseAlias + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getCourseByAlias: async (courseAlias: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'courseAlias' is not null or undefined + assertParamExists('getCourseByAlias', 'courseAlias', courseAlias) + const localVarPath = `/courses/{courseAlias}` + .replace(`{${"courseAlias"}}`, encodeURIComponent(String(courseAlias))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -11287,6 +11332,16 @@ export const CoursesApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getCourse(courseId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {string} courseAlias + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getCourseByAlias(courseAlias: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseByAlias(courseAlias, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {*} [options] Override http request option. @@ -11366,6 +11421,15 @@ export const CoursesApiFactory = function (configuration?: Configuration, basePa getCourse(courseId: number, options?: any): AxiosPromise { return localVarFp.getCourse(courseId, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {string} courseAlias + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getCourseByAlias(courseAlias: string, options?: any): AxiosPromise { + return localVarFp.getCourseByAlias(courseAlias, options).then((request) => request(axios, basePath)); + }, /** * * @param {*} [options] Override http request option. @@ -11447,6 +11511,17 @@ export class CoursesApi extends BaseAPI { return CoursesApiFp(this.configuration).getCourse(courseId, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {string} courseAlias + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CoursesApi + */ + public getCourseByAlias(courseAlias: string, options?: AxiosRequestConfig) { + return CoursesApiFp(this.configuration).getCourseByAlias(courseAlias, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {*} [options] Override http request option. diff --git a/client/src/api/index.ts b/client/src/api/index.ts index 746ee393ac..75deac1e95 100644 --- a/client/src/api/index.ts +++ b/client/src/api/index.ts @@ -1,5 +1,5 @@ /* tslint:disable */ - +/* eslint-disable */ /** * * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) diff --git a/client/src/modules/Course/contexts/ActiveCourseContext.tsx b/client/src/modules/Course/contexts/ActiveCourseContext.tsx index f931b0185e..bfe7626d6c 100644 --- a/client/src/modules/Course/contexts/ActiveCourseContext.tsx +++ b/client/src/modules/Course/contexts/ActiveCourseContext.tsx @@ -1,4 +1,4 @@ -import { ProfileCourseDto } from 'api'; +import { CoursesApi, ProfileCourseDto } from 'api'; import { LoadingScreen } from 'components/LoadingScreen'; import { useRouter } from 'next/router'; import React, { useCallback, useContext, useMemo, useState } from 'react'; @@ -8,6 +8,7 @@ import { WelcomeCard } from 'components/WelcomeCard'; import { Alert, Col, notification, Row } from 'antd'; import useRequest from 'ahooks/lib/useRequest'; import { AxiosError } from 'axios'; +import { MentorRegistryService } from '@client/services/mentorRegistry'; type ActiveCourseContextType = { course: ProfileCourseDto; @@ -29,17 +30,20 @@ export const useActiveCourseContext = () => { type Props = React.PropsWithChildren<{ publicRoutes: string[]; + mentorConfirmRoute: string; }>; -export const ActiveCourseProvider = ({ children, publicRoutes }: Props) => { +export const ActiveCourseProvider = ({ children, publicRoutes, mentorConfirmRoute }: Props) => { const router = useRouter(); // course alias const alias = router.query.course; const isPublicRoute = publicRoutes?.includes(router.pathname); + const isMentorConfirmRoute = router.pathname === mentorConfirmRoute; const [storageCourseId, setStorageCourseId] = useLocalStorage('activeCourseId'); const [activeCourse, setActiveCourse] = useState(); + const [isMentorInvited, setIsMentorInvited] = useState(false); const { data, loading, refresh } = useRequest(() => resolveCourse(alias, storageCourseId), { ready: router.isReady && !isPublicRoute, @@ -58,6 +62,28 @@ export const ActiveCourseProvider = ({ children, publicRoutes }: Props) => { }, }); + const { loading: mentorLoading } = useRequest( + async () => { + const mentor = await new MentorRegistryService().getMentor(); + if (!mentor) { + return false; + } + const courseToConfirm = await new CoursesApi().getCourseByAlias(alias as string); + const isInvited = mentor?.preselectedCourses?.includes(courseToConfirm?.data.id) ?? false; + return isInvited; + }, + { + ready: router.isReady && isMentorConfirmRoute && typeof alias === 'string' && Boolean(alias), + onSuccess: isInvited => setIsMentorInvited(isInvited), + onError: () => { + notification.error({ + message: 'Mentor check failed', + description: 'Please try again later or contact course manager', + }); + }, + }, + ); + const setCourse = useCallback((course: ProfileCourseDto | null) => { if (course) { setActiveCourse(course); @@ -74,7 +100,9 @@ export const ActiveCourseProvider = ({ children, publicRoutes }: Props) => { return <>{children}; } - if (alias && activeCourse && activeCourse.alias !== alias) { + const noAccess = alias && activeCourse && activeCourse.alias !== alias && !isMentorInvited && !mentorLoading; + + if (noAccess) { return ( @@ -96,7 +124,7 @@ export const ActiveCourseProvider = ({ children, publicRoutes }: Props) => { return {children}; } - return ; + return ; }; async function resolveCourse( diff --git a/client/src/modules/CourseStatistics/components/StatCards/StatCards.tsx b/client/src/modules/CourseStatistics/components/StatCards/StatCards.tsx index 88c972a3cc..f602d8ba43 100644 --- a/client/src/modules/CourseStatistics/components/StatCards/StatCards.tsx +++ b/client/src/modules/CourseStatistics/components/StatCards/StatCards.tsx @@ -1,4 +1,4 @@ -import { CoursesStatsDto } from '@client/api'; +import { CourseAggregateStatsDto } from '@client/api'; import { StudentsCountriesCard } from '@client/modules/CourseStatistics/components/StudentsCountriesCard'; import { StudentsStatsCard } from '@client/modules/CourseStatistics/components/StudentsStatsCard'; import { MentorsCountriesCard } from '@client/modules/CourseStatistics/components/MentorsCountriesCard/MentorsCountriesCard'; @@ -12,7 +12,7 @@ import Masonry from 'react-masonry-css'; import css from 'styled-jsx/css'; type StatCardsProps = { - coursesData?: CoursesStatsDto; + coursesData?: CourseAggregateStatsDto; }; const gapSize = 24; diff --git a/client/src/modules/CourseStatistics/hooks/useCourseStats.tsx b/client/src/modules/CourseStatistics/hooks/useCourseStats.tsx index 30bf0dca1d..bd2b58d01b 100644 --- a/client/src/modules/CourseStatistics/hooks/useCourseStats.tsx +++ b/client/src/modules/CourseStatistics/hooks/useCourseStats.tsx @@ -1,15 +1,18 @@ import { useMessage } from '@client/hooks'; -import { CoursesStatsDto, CourseStatsApi } from '@client/api'; +import { CourseAggregateStatsDto, CourseStatsApi } from '@client/api'; import { useRequest } from 'ahooks'; const courseStatsApi = new CourseStatsApi(); type CourseStatsParams = { - ids?: number[]; + ids?: string[]; year?: number; }; -async function fetchCourseStats({ ids = [], year = 0 }: CourseStatsParams): Promise { +async function fetchCourseStats({ + ids = [], + year = 0, +}: CourseStatsParams): Promise { try { const { data } = await courseStatsApi.getCoursesStats(ids, year); return data; diff --git a/client/src/modules/CourseStatistics/pages/CourseStatistics.tsx b/client/src/modules/CourseStatistics/pages/CourseStatistics.tsx index 4c757a4fcb..8dbf949ffc 100644 --- a/client/src/modules/CourseStatistics/pages/CourseStatistics.tsx +++ b/client/src/modules/CourseStatistics/pages/CourseStatistics.tsx @@ -17,14 +17,14 @@ function CourseStatistic() { const [statScope, setStatScope] = useState(params.get('course') ? StatScope.Current : StatScope.Timeline); const { course } = useActiveCourseContext(); const { token } = theme.useToken(); - const [ids, setIds] = useState([course.id]); + const [ids, setIds] = useState([String(course.id)]); const selectedYear = Number(params.get('year')); const { loading, coursesData } = useCoursesStats({ ids, year: selectedYear }); const handleStatScope = (value: boolean) => { if (value) { setStatScope(StatScope.Current); - setIds([course.id]); + setIds([String(course.id)]); params.set('course', course.alias); params.delete('year'); } else { diff --git a/client/src/pages/_app.tsx b/client/src/pages/_app.tsx index 745a70a7ad..0d56b30e4c 100644 --- a/client/src/pages/_app.tsx +++ b/client/src/pages/_app.tsx @@ -23,7 +23,8 @@ class RsSchoolApp extends App { diff --git a/nestjs/src/courses/courses.controller.ts b/nestjs/src/courses/courses.controller.ts index 66382948ee..d811ffe7e9 100644 --- a/nestjs/src/courses/courses.controller.ts +++ b/nestjs/src/courses/courses.controller.ts @@ -48,6 +48,16 @@ export class CoursesController { return new CourseDto(created); } + @Get('/:courseAlias') + @ApiOperation({ operationId: 'getCourseByAlias' }) + @ApiForbiddenResponse() + @ApiOkResponse({ type: CourseDto }) + @UseGuards(DefaultGuard) + public async getByAlias(@Req() _: CurrentRequest, @Param('courseAlias') courseAlias: string) { + const data = await this.courseService.getByAlias(courseAlias); + return new CourseDto(data); + } + @Get('/:courseId') @ApiOperation({ operationId: 'getCourse' }) @ApiForbiddenResponse() diff --git a/nestjs/src/courses/courses.service.ts b/nestjs/src/courses/courses.service.ts index c4c8959b8b..ee47407e51 100644 --- a/nestjs/src/courses/courses.service.ts +++ b/nestjs/src/courses/courses.service.ts @@ -19,6 +19,10 @@ export class CoursesService { return this.repository.findOneOrFail({ where: { id }, relations: ['discipline'] }); } + public async getByAlias(alias: string) { + return this.repository.findOneOrFail({ where: { alias }, relations: ['discipline'] }); + } + public async update(id: number, course: UpdateCourseDto) { await this.repository.update(id, course); const updated = await this.repository.findOneByOrFail({ id }); diff --git a/nestjs/src/courses/stats/dto/course-stats.dto.ts b/nestjs/src/courses/stats/dto/course-stats.dto.ts index db303ee43d..1375dbef00 100644 --- a/nestjs/src/courses/stats/dto/course-stats.dto.ts +++ b/nestjs/src/courses/stats/dto/course-stats.dto.ts @@ -63,7 +63,7 @@ export class CourseAggregateStatsDto { @ApiProperty() mentorsStats: CourseMentorsStatsDto; - @ApiProperty() + @ApiProperty({ type: () => [CourseTaskDto] }) courseTasks: CourseTaskDto[]; @ApiProperty() diff --git a/nestjs/src/courses/task-verifications/task-verifications.service.ts b/nestjs/src/courses/task-verifications/task-verifications.service.ts index 3658aaf74c..2e5a8d8427 100644 --- a/nestjs/src/courses/task-verifications/task-verifications.service.ts +++ b/nestjs/src/courses/task-verifications/task-verifications.service.ts @@ -87,7 +87,7 @@ export class TaskVerificationsService { questionImage: taskQuestion.questionImage, }); }) - .filter(Boolean); + .filter((question): question is SelfEducationQuestionSelectedAnswersDto => question !== null); return new TaskVerificationAttemptDto(verification, questionsWithIncorrectAnswers); }); diff --git a/nestjs/src/spec.json b/nestjs/src/spec.json index 902ebc7547..65fae0c109 100644 --- a/nestjs/src/spec.json +++ b/nestjs/src/spec.json @@ -214,6 +214,21 @@ "tags": ["courses"] } }, + "/courses/{courseAlias}": { + "get": { + "operationId": "getCourseByAlias", + "parameters": [{ "name": "courseAlias", "required": true, "in": "path", "schema": { "type": "string" } }], + "responses": { + "200": { + "description": "", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CourseDto" } } } + }, + "403": { "description": "" } + }, + "summary": "", + "tags": ["courses"] + } + }, "/courses/{courseId}": { "get": { "operationId": "getCourse", @@ -765,6 +780,28 @@ "tags": ["courses tasks"] } }, + "/courses/aggregate/stats": { + "get": { + "operationId": "getCoursesStats", + "parameters": [ + { + "name": "ids", + "required": true, + "in": "query", + "schema": { "type": "array", "items": { "type": "string" } } + }, + { "name": "year", "required": true, "in": "query", "schema": { "type": "number" } } + ], + "responses": { + "200": { + "description": "", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CourseAggregateStatsDto" } } } + } + }, + "summary": "", + "tags": ["course stats"] + } + }, "/courses/{courseId}/stats": { "get": { "operationId": "getCourseStats", @@ -3723,6 +3760,16 @@ "properties": { "deadlineInHours": { "type": "number" } }, "required": ["deadlineInHours"] }, + "CountryStatDto": { + "type": "object", + "properties": { "countryName": { "type": "string" }, "count": { "type": "number" } }, + "required": ["countryName", "count"] + }, + "CountriesStatsDto": { + "type": "object", + "properties": { "countries": { "type": "array", "items": { "$ref": "#/components/schemas/CountryStatDto" } } }, + "required": ["countries"] + }, "CourseStatsDto": { "type": "object", "properties": { @@ -3749,15 +3796,24 @@ }, "required": ["mentorsActiveCount", "mentorsTotalCount", "epamMentorsCount"] }, - "CountryStatDto": { - "type": "object", - "properties": { "countryName": { "type": "string" }, "count": { "type": "number" } }, - "required": ["countryName", "count"] - }, - "CountriesStatsDto": { + "CourseAggregateStatsDto": { "type": "object", - "properties": { "countries": { "type": "array", "items": { "$ref": "#/components/schemas/CountryStatDto" } } }, - "required": ["countries"] + "properties": { + "studentsCountries": { "$ref": "#/components/schemas/CountriesStatsDto" }, + "studentsStats": { "$ref": "#/components/schemas/CourseStatsDto" }, + "mentorsCountries": { "$ref": "#/components/schemas/CountriesStatsDto" }, + "mentorsStats": { "$ref": "#/components/schemas/CourseMentorsStatsDto" }, + "courseTasks": { "type": "array", "items": { "$ref": "#/components/schemas/CourseTaskDto" } }, + "studentsCertificatesCountries": { "$ref": "#/components/schemas/CountriesStatsDto" } + }, + "required": [ + "studentsCountries", + "studentsStats", + "mentorsCountries", + "mentorsStats", + "courseTasks", + "studentsCertificatesCountries" + ] }, "TaskPerformanceStatsDto": { "type": "object",