From 9f101676f99faa64b2ac659fc8a1703467e98d24 Mon Sep 17 00:00:00 2001 From: Alex Berezhnykh Date: Sat, 22 Oct 2022 18:34:50 +0300 Subject: [PATCH 01/58] wip --- .../Controllers/StatisticsController.cs | 26 +++++++++++++++++-- .../HwProj.APIGateway.API.csproj | 7 +++++ .../HwProj.APIGateway.API/Startup.cs | 19 +++++++++++++- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs index 0f0617a02..8bd193da6 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; +using System; using System.Linq; using System.Net; +using System.Text.RegularExpressions; using System.Threading.Tasks; +using Google.Apis.Sheets.v4; using HwProj.APIGateway.API.Models.Statistics; using HwProj.AuthService.Client; using HwProj.CoursesService.Client; @@ -22,9 +25,11 @@ public class StatisticsController : AggregationController { private readonly ISolutionsServiceClient _solutionClient; private readonly ICoursesServiceClient _coursesClient; + private readonly SheetsService _sheetsService; public StatisticsController(ISolutionsServiceClient solutionClient, IAuthServiceClient authServiceClient, - ICoursesServiceClient coursesServiceClient) : + ICoursesServiceClient coursesServiceClient, + SheetsService sheetsService) : base(authServiceClient) { _solutionClient = solutionClient; @@ -50,6 +55,7 @@ public async Task GetLecturersStatistics(long courseId) }).ToArray(); return Ok(result); + _sheetsService = sheetsService; } [HttpGet("{courseId}")] @@ -123,6 +129,22 @@ public async Task GetChartStatistics(long courseId) return Ok(result); } + + public class SheetUrl + { + public string Url { get; set; } + } + + [HttpPost("getSheetTitles")] + public async Task GetSheetTitles([FromBody] SheetUrl sheetUrl) + { + var match = Regex.Match(sheetUrl.Url, "https://docs\\.google\\.com/spreadsheets/d/(?.+)/"); + if (!match.Success) return Array.Empty(); + + var spreadsheetId = match.Groups["id"].Value; + var sheet = await _sheetsService.Spreadsheets.Get(spreadsheetId).ExecuteAsync(); + return sheet.Sheets.Select(t => t.Properties.Title).ToArray(); + } private async Task> GetStudentsToMentorsDictionary( MentorToAssignedStudentsDTO[] mentorsToStudents) @@ -153,4 +175,4 @@ private async Task> GetStudentsToMentorsDic ); } } -} \ No newline at end of file +} diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj b/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj index 7238b2966..a8862edd4 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj +++ b/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj @@ -12,6 +12,7 @@ + @@ -27,4 +28,10 @@ + + + + Always + + diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs index cf053dec6..b778b773d 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs @@ -1,4 +1,8 @@ -using HwProj.AuthService.Client; +using System.IO; +using Google.Apis.Auth.OAuth2; +using Google.Apis.Services; +using Google.Apis.Sheets.v4; +using HwProj.AuthService.Client; using HwProj.ContentService.Client; using HwProj.CoursesService.Client; using HwProj.NotificationsService.Client; @@ -54,6 +58,7 @@ public void ConfigureServices(IServiceCollection services) services.AddHttpClient(); services.AddHttpContextAccessor(); + services.AddScoped(_ => ConfigureGoogleSheets()); services.AddAuthServiceClient(); services.AddCoursesServiceClient(); @@ -68,5 +73,17 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.ConfigureHwProj(env, "API Gateway"); } + + private static SheetsService ConfigureGoogleSheets() + { + using var stream = new FileStream("googlesheets_credentials.json", FileMode.Open, FileAccess.ReadWrite); + var credential = GoogleCredential.FromStream(stream).CreateScoped(SheetsService.Scope.Spreadsheets); + + return new SheetsService(new BaseClientService.Initializer + { + HttpClientInitializer = credential, + ApplicationName = "HwProjSheets" + }); + } } } From 7a94bca7e6b4ed723b48c871c44f2b021820e2bd Mon Sep 17 00:00:00 2001 From: Alex Berezhnykh Date: Sat, 22 Oct 2022 18:36:05 +0300 Subject: [PATCH 02/58] wip --- hwproj.front/src/api/api.ts | 87 +++++++++++++++++++ .../src/components/Courses/StudentStats.tsx | 41 +++++++-- 2 files changed, 119 insertions(+), 9 deletions(-) diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index fcc4dd749..0bf495799 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -1732,6 +1732,20 @@ export interface ScopeDTO { */ courseUnitId?: number; } +/** + * + * @export + * @interface SheetUrl + */ +export interface SheetUrl { + /** + * + * @type {string} + * @memberof SheetUrl + */ + url?: string; +} + /** * * @export @@ -8650,6 +8664,41 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur localVarUrlObj.search = null; localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {SheetUrl} [sheetUrl] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiStatisticsGetSheetTitlesPost(sheetUrl?: SheetUrl, options: any = {}): FetchArgs { + const localVarPath = `/api/Statistics/getSheetTitles`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'POST' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + localVarHeaderParameter['Content-Type'] = 'application/json-patch+json'; + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + delete localVarUrlObj.search; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + const needsSerialization = ("SheetUrl" !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + localVarRequestOptions.body = needsSerialization ? JSON.stringify(sheetUrl || {}) : (sheetUrl || ""); + return { url: url.format(localVarUrlObj), options: localVarRequestOptions, @@ -8718,6 +8767,24 @@ export const StatisticsApiFp = function(configuration?: Configuration) { }); }; }, + /** + * + * @param {SheetUrl} [sheetUrl] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiStatisticsGetSheetTitlesPost(sheetUrl?: SheetUrl, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise> { + const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).apiStatisticsGetSheetTitlesPost(sheetUrl, options); + return (fetch: FetchAPI = portableFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response.json(); + } else { + throw response; + } + }); + }; + }, } }; @@ -8754,6 +8821,15 @@ export const StatisticsApiFactory = function (configuration?: Configuration, fet statisticsGetLecturersStatistics(courseId: number, options?: any) { return StatisticsApiFp(configuration).statisticsGetLecturersStatistics(courseId, options)(fetch, basePath); }, + /** + * + * @param {SheetUrl} [sheetUrl] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiStatisticsGetSheetTitlesPost(sheetUrl?: SheetUrl, options?: any) { + return StatisticsApiFp(configuration).apiStatisticsGetSheetTitlesPost(sheetUrl, options)(fetch, basePath); + }, }; }; @@ -8797,6 +8873,17 @@ export class StatisticsApi extends BaseAPI { return StatisticsApiFp(this.configuration).statisticsGetLecturersStatistics(courseId, options)(this.fetch, this.basePath); } + /** + * + * @param {SheetUrl} [sheetUrl] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof StatisticsApi + */ + public apiStatisticsGetSheetTitlesPost(sheetUrl?: SheetUrl, options?: any) { + return StatisticsApiFp(this.configuration).apiStatisticsGetSheetTitlesPost(sheetUrl, options)(this.fetch, this.basePath); + } + } /** * SystemApi - fetch parameter creator diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index 50d55c772..efcac4437 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -3,12 +3,13 @@ import {CourseViewModel, HomeworkViewModel, StatisticsCourseMatesModel} from ".. import {useNavigate, useParams} from 'react-router-dom'; import {Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@material-ui/core"; import StudentStatsCell from "../Tasks/StudentStatsCell"; -import {Alert, Button, Chip, Typography} from "@mui/material"; +import {Alert, Button, Chip, Typography, MenuItem, Select, TextField} from "@mui/material"; import {grey} from "@material-ui/core/colors"; import StudentStatsUtils from "../../services/StudentStatsUtils"; import ShowChartIcon from "@mui/icons-material/ShowChart"; import {BonusTag, DefaultTags, TestTag} from "../Common/HomeworkTags"; import Lodash from "lodash" +import apiSingleton from "../../api/ApiSingleton"; interface IStudentStatsProps { course: CourseViewModel; @@ -19,14 +20,18 @@ interface IStudentStatsProps { } interface IStudentStatsState { - searched: string + searched: string; + googleDocUrl: string; + sheetTitles: string[]; } const greyBorder = grey[300] -const StudentStats: React.FC = (props) => { - const [state, setSearched] = useState({ - searched: "" +const StudentStats: React.FC = (props) => { + const [state, setState] = useState({ + searched: "", + googleDocUrl: "", + sheetTitles: [] }); const {courseId} = useParams(); const navigate = useNavigate(); @@ -34,18 +39,18 @@ const StudentStats: React.FC = (props) => { navigate(`/statistics/${courseId}/charts`) } - const {searched} = state + const {searched, googleDocUrl, sheetTitles} = state useEffect(() => { const keyDownHandler = (event: KeyboardEvent) => { if (event.ctrlKey || event.altKey) return if (searched && event.key === "Escape") { - setSearched({searched: ""}); + setState({...state, searched: ""}); } else if (searched && event.key === "Backspace") { - setSearched({searched: searched.slice(0, -1)}) + setState({...state, searched: searched.slice(0, -1)}) } else if (event.key.length === 1 && event.key.match(/[a-zA-Zа-яА-Я\s]/i) ) { - setSearched({searched: searched + event.key}) + setState({...state, searched: searched + event.key}) } }; @@ -98,6 +103,12 @@ const StudentStats: React.FC = (props) => { const hasHomeworks = homeworksMaxSum > 0 const hasTests = testsMaxSum > 0 + const handleGoogleDocUrlChange = async (value: string) => { + const titles = await apiSingleton.statisticsApi.apiStatisticsGetSheetTitlesGet(value) //Post/get? + setState({...state, googleDocUrl: value, sheetTitles: titles.value ?? []}); + } + + return (
{searched && @@ -296,6 +307,18 @@ const StudentStats: React.FC = (props) => { + { + event.persist() + handleGoogleDocUrlChange(event.target.value) + }}/> + {googleDocUrl && } +
); } From 00d26c74238f1a8a944f858d44aa43c30b15b941 Mon Sep 17 00:00:00 2001 From: Alex Berezhnykh Date: Sat, 22 Oct 2022 19:54:36 +0300 Subject: [PATCH 03/58] wip --- .../Controllers/StatisticsController.cs | 16 +++-- hwproj.front/src/api/api.ts | 29 ++++++++- .../src/components/Courses/StudentStats.tsx | 62 ++++++++++++++----- 3 files changed, 85 insertions(+), 22 deletions(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs index 8bd193da6..949d62394 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs @@ -13,6 +13,7 @@ using HwProj.Models.CoursesService.DTO; using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.Roles; +using HwProj.Models.Result; using HwProj.SolutionsService.Client; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -136,14 +137,21 @@ public class SheetUrl } [HttpPost("getSheetTitles")] - public async Task GetSheetTitles([FromBody] SheetUrl sheetUrl) + public async Task> GetSheetTitles([FromBody] SheetUrl sheetUrl) { var match = Regex.Match(sheetUrl.Url, "https://docs\\.google\\.com/spreadsheets/d/(?.+)/"); - if (!match.Success) return Array.Empty(); + if (!match.Success) return Result.Failed("Некорректная ссылка на страницу Google Docs"); var spreadsheetId = match.Groups["id"].Value; - var sheet = await _sheetsService.Spreadsheets.Get(spreadsheetId).ExecuteAsync(); - return sheet.Sheets.Select(t => t.Properties.Title).ToArray(); + try + { + var sheet = await _sheetsService.Spreadsheets.Get(spreadsheetId).ExecuteAsync(); + return Result.Success(sheet.Sheets.Select(t => t.Properties.Title).ToArray()); + } + catch (Exception ex) + { + return Result.Failed($"Ошибка при обращении к Google Docs: {ex.Message}"); + } } private async Task> GetStudentsToMentorsDictionary( diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index 0bf495799..cea3d3abd 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -1707,6 +1707,33 @@ export interface Result { */ errors?: Array; } + +/** + * + * @export + * @interface ResultTokenCredentials + */ +export interface ResultTokenCredentials { + /** + * + * @type {TokenCredentials} + * @memberof ResultTokenCredentials + */ + value?: TokenCredentials; + /** + * + * @type {boolean} + * @memberof ResultTokenCredentials + */ + succeeded?: boolean; + /** + * + * @type {Array} + * @memberof ResultTokenCredentials + */ + errors?: Array; +} + /** * * @export @@ -8773,7 +8800,7 @@ export const StatisticsApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsGetSheetTitlesPost(sheetUrl?: SheetUrl, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise> { + apiStatisticsGetSheetTitlesPost(sheetUrl?: SheetUrl, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).apiStatisticsGetSheetTitlesPost(sheetUrl, options); return (fetch: FetchAPI = portableFetch, basePath: string = BASE_PATH) => { return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index efcac4437..2c40c881d 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -1,9 +1,9 @@ import React, {useEffect, useState} from "react"; -import {CourseViewModel, HomeworkViewModel, StatisticsCourseMatesModel} from "../../api/"; +import {CourseViewModel, HomeworkViewModel, StatisticsCourseMatesModel, ResultString} from "../../api/"; import {useNavigate, useParams} from 'react-router-dom'; import {Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@material-ui/core"; import StudentStatsCell from "../Tasks/StudentStatsCell"; -import {Alert, Button, Chip, Typography, MenuItem, Select, TextField} from "@mui/material"; +import {Alert, Button, Chip, Typography, Grid, MenuItem, Select, TextField} from "@mui/material"; import {grey} from "@material-ui/core/colors"; import StudentStatsUtils from "../../services/StudentStatsUtils"; import ShowChartIcon from "@mui/icons-material/ShowChart"; @@ -22,7 +22,7 @@ interface IStudentStatsProps { interface IStudentStatsState { searched: string; googleDocUrl: string; - sheetTitles: string[]; + sheetTitles: ResultString | undefined; } const greyBorder = grey[300] @@ -31,7 +31,7 @@ const StudentStats: React.FC = (props) => { const [state, setState] = useState({ searched: "", googleDocUrl: "", - sheetTitles: [] + sheetTitles: undefined }); const {courseId} = useParams(); const navigate = useNavigate(); @@ -105,7 +105,7 @@ const StudentStats: React.FC = (props) => { const handleGoogleDocUrlChange = async (value: string) => { const titles = await apiSingleton.statisticsApi.apiStatisticsGetSheetTitlesGet(value) //Post/get? - setState({...state, googleDocUrl: value, sheetTitles: titles.value ?? []}); + setState({...state, googleDocUrl: value, sheetTitles: titles}); } @@ -307,18 +307,46 @@ const StudentStats: React.FC = (props) => { - { - event.persist() - handleGoogleDocUrlChange(event.target.value) - }}/> - {googleDocUrl && } - + + + + Для загрузки таблицы необходимо разрешить доступ на редактирование по ссылке для Google Docs + страницы + + + + + { + event.persist() + handleGoogleDocUrlChange(event.target.value) + }}/> + + {sheetTitles && !sheetTitles.succeeded && + + {sheetTitles!.errors![0]} + + } + {sheetTitles && sheetTitles.value && sheetTitles.value.length > 0 && + + } + {sheetTitles && sheetTitles.succeeded && + + } + + ); } From 2be37ee193abc31f46ffc042eb1c18322b68eec9 Mon Sep 17 00:00:00 2001 From: Alex Berezhnykh Date: Sat, 22 Oct 2022 19:58:24 +0300 Subject: [PATCH 04/58] wip --- hwproj.front/src/components/Courses/StudentStats.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index 2c40c881d..106c0940a 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -104,7 +104,9 @@ const StudentStats: React.FC = (props) => { const hasTests = testsMaxSum > 0 const handleGoogleDocUrlChange = async (value: string) => { - const titles = await apiSingleton.statisticsApi.apiStatisticsGetSheetTitlesGet(value) //Post/get? + const titles = value === "" + ? undefined + : await apiSingleton.statisticsApi.apiStatisticsGetSheetTitlesGet(value) setState({...state, googleDocUrl: value, sheetTitles: titles}); } From b0e436086ed8e5df38d722335352d4650ee44a68 Mon Sep 17 00:00:00 2001 From: Alex Berezhnykh Date: Sat, 22 Oct 2022 22:30:36 +0300 Subject: [PATCH 05/58] wip --- .../src/components/Courses/StudentStats.tsx | 75 +++-------------- .../Solutions/LoadStatsToGoogleDoc.tsx | 84 +++++++++++++++++++ 2 files changed, 97 insertions(+), 62 deletions(-) create mode 100644 hwproj.front/src/components/Solutions/LoadStatsToGoogleDoc.tsx diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index 106c0940a..51ce4edcd 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -3,13 +3,13 @@ import {CourseViewModel, HomeworkViewModel, StatisticsCourseMatesModel, ResultSt import {useNavigate, useParams} from 'react-router-dom'; import {Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@material-ui/core"; import StudentStatsCell from "../Tasks/StudentStatsCell"; -import {Alert, Button, Chip, Typography, Grid, MenuItem, Select, TextField} from "@mui/material"; +import {Alert, Chip, Typography, Grid} from "@mui/material"; import {grey} from "@material-ui/core/colors"; import StudentStatsUtils from "../../services/StudentStatsUtils"; import ShowChartIcon from "@mui/icons-material/ShowChart"; import {BonusTag, DefaultTags, TestTag} from "../Common/HomeworkTags"; import Lodash from "lodash" -import apiSingleton from "../../api/ApiSingleton"; +import LoadStatsToGoogleDoc from "components/Solutions/LoadStatsToGoogleDoc"; interface IStudentStatsProps { course: CourseViewModel; @@ -20,18 +20,14 @@ interface IStudentStatsProps { } interface IStudentStatsState { - searched: string; - googleDocUrl: string; - sheetTitles: ResultString | undefined; + searched: string } const greyBorder = grey[300] const StudentStats: React.FC = (props) => { - const [state, setState] = useState({ - searched: "", - googleDocUrl: "", - sheetTitles: undefined + const [state, setSearched] = useState({ + searched: "" }); const {courseId} = useParams(); const navigate = useNavigate(); @@ -39,18 +35,18 @@ const StudentStats: React.FC = (props) => { navigate(`/statistics/${courseId}/charts`) } - const {searched, googleDocUrl, sheetTitles} = state + const {searched} = state useEffect(() => { const keyDownHandler = (event: KeyboardEvent) => { if (event.ctrlKey || event.altKey) return if (searched && event.key === "Escape") { - setState({...state, searched: ""}); + setSearched({searched: ""}); } else if (searched && event.key === "Backspace") { - setState({...state, searched: searched.slice(0, -1)}) + setSearched({searched: searched.slice(0, -1)}) } else if (event.key.length === 1 && event.key.match(/[a-zA-Zа-яА-Я\s]/i) ) { - setState({...state, searched: searched + event.key}) + setSearched({searched: searched + event.key}) } }; @@ -103,14 +99,6 @@ const StudentStats: React.FC = (props) => { const hasHomeworks = homeworksMaxSum > 0 const hasTests = testsMaxSum > 0 - const handleGoogleDocUrlChange = async (value: string) => { - const titles = value === "" - ? undefined - : await apiSingleton.statisticsApi.apiStatisticsGetSheetTitlesGet(value) - setState({...state, googleDocUrl: value, sheetTitles: titles}); - } - - return (
{searched && @@ -309,48 +297,11 @@ const StudentStats: React.FC = (props) => { - - - - Для загрузки таблицы необходимо разрешить доступ на редактирование по ссылке для Google Docs - страницы - - - - - { - event.persist() - handleGoogleDocUrlChange(event.target.value) - }}/> - - {sheetTitles && !sheetTitles.succeeded && - - {sheetTitles!.errors![0]} - - } - {sheetTitles && sheetTitles.value && sheetTitles.value.length > 0 && - - } - {sheetTitles && sheetTitles.succeeded && - - } - - +
+ +
); } -export default StudentStats; +export default StudentStats; \ No newline at end of file diff --git a/hwproj.front/src/components/Solutions/LoadStatsToGoogleDoc.tsx b/hwproj.front/src/components/Solutions/LoadStatsToGoogleDoc.tsx new file mode 100644 index 000000000..6b1ae8784 --- /dev/null +++ b/hwproj.front/src/components/Solutions/LoadStatsToGoogleDoc.tsx @@ -0,0 +1,84 @@ +import React, {FC, useState} from "react"; +import {Alert, Button, Grid, MenuItem, Select, TextField} from "@mui/material"; +import {ResultString} from "../../api"; +import apiSingleton from "../../api/ApiSingleton"; + +interface LoadStatsToGoogleDocProps { +} + +interface LoadStatsToGoogleDocState { + googleDocUrl: string, + sheetTitles: ResultString | undefined, + selectedSheet: number, + isOpened: boolean +} + +const LoadStatsToGoogleDoc: FC = (props) => { + const [state, setState] = useState({ + selectedSheet: 0, + isOpened: false, + googleDocUrl: "", + sheetTitles: undefined + }) + + const {googleDocUrl, sheetTitles, isOpened, selectedSheet} = state + + //TODO: throttling + const handleGoogleDocUrlChange = async (value: string) => { + const titles = value === "" + ? undefined + : await apiSingleton.statisticsApi.apiStatisticsGetSheetTitlesPost({url: value}) + setState(prevState => ({...prevState, googleDocUrl: value, sheetTitles: titles})); + } + + return !isOpened + ? + : + + + Для загрузки таблицы необходимо разрешить доступ на редактирование по ссылке для Google Docs + страницы + + + + + { + event.persist() + handleGoogleDocUrlChange(event.target.value) + }}/> + + {sheetTitles && !sheetTitles.succeeded && + + {sheetTitles!.errors![0]} + + } + {sheetTitles && sheetTitles.value && sheetTitles.value.length > 0 && + + } + {sheetTitles && sheetTitles.succeeded && + + } + { + + } + + +} +export default LoadStatsToGoogleDoc; From 2323eff47127485a8ef5098d503b1c8125eeb7ba Mon Sep 17 00:00:00 2001 From: Alex Berezhnykh Date: Tue, 1 Nov 2022 23:53:32 +0300 Subject: [PATCH 06/58] wip wip feat: add Excel file Generate method; add tests refactor: make corrections feat: implement report uploading to google docs docs: add comments to the code refactor: create Google and Yandex services refactor tests feat: add requests to the Google Service fix: support of export to google docs fix: implement support of native file upload, change httpRequests types feat: simple yandex support is implemented refactor frontend part refactor frontend part --- .../Controllers/StatisticsController.cs | 97 +++-- .../ExportServices/GoogleService.cs | 379 ++++++++++++++++++ .../HwProj.APIGateway.API.csproj | 7 +- .../HwProj.APIGateway.API/Startup.cs | 30 +- .../TableGenerators/ExcelGenerator.cs | 266 ++++++++++++ .../HwProj.APIGateway.API/appsettings.json | 5 + .../HwProj.APIGateway.API/libman.json | 5 + .../ExcelGeneratorTests.cs | 278 +++++++++++++ .../HwProj.APIGateway.Tests/GoldFile.xlsx | Bin 0 -> 3351 bytes .../HwProj.APIGateway.Tests.csproj | 21 +- .../Controllers/SolutionsController.cs | 7 + HwProj.sln | 20 +- global.json | 7 + hwproj.front/src/App.tsx | 1 + hwproj.front/src/api/api.ts | 287 ++++++++++++- .../src/components/Courses/Course.tsx | 28 +- .../src/components/Courses/StudentStats.tsx | 11 +- .../components/Solutions/DownloadStats.tsx | 67 ++++ .../components/Solutions/ExportToGoogle.tsx | 144 +++++++ .../components/Solutions/ExportToYandex.tsx | 214 ++++++++++ .../Solutions/LoadStatsToGoogleDoc.tsx | 84 ---- .../src/components/Solutions/SaveStats.tsx | 135 +++++++ .../src/components/Solutions/YandexLogo.svg | 1 + 23 files changed, 1950 insertions(+), 144 deletions(-) create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/libman.json create mode 100644 HwProj.APIGateway/HwProj.APIGateway.Tests/ExcelGeneratorTests.cs create mode 100644 HwProj.APIGateway/HwProj.APIGateway.Tests/GoldFile.xlsx create mode 100644 global.json create mode 100644 hwproj.front/src/components/Solutions/DownloadStats.tsx create mode 100644 hwproj.front/src/components/Solutions/ExportToGoogle.tsx create mode 100644 hwproj.front/src/components/Solutions/ExportToYandex.tsx delete mode 100644 hwproj.front/src/components/Solutions/LoadStatsToGoogleDoc.tsx create mode 100644 hwproj.front/src/components/Solutions/SaveStats.tsx create mode 100644 hwproj.front/src/components/Solutions/YandexLogo.svg diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs index 949d62394..b907f2683 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs @@ -1,18 +1,18 @@ -using System; -using System.Collections.Generic; using System; +using System.Collections.Generic; using System.Linq; using System.Net; -using System.Text.RegularExpressions; using System.Threading.Tasks; -using Google.Apis.Sheets.v4; +using HwProj.APIGateway.API.ExportServices; using HwProj.APIGateway.API.Models.Statistics; +using HwProj.APIGateway.API.TableGenerators; using HwProj.AuthService.Client; using HwProj.CoursesService.Client; using HwProj.Models.AuthService.DTO; using HwProj.Models.CoursesService.DTO; using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.Roles; +using HwProj.CoursesService.Client; using HwProj.Models.Result; using HwProj.SolutionsService.Client; using Microsoft.AspNetCore.Authorization; @@ -26,12 +26,16 @@ public class StatisticsController : AggregationController { private readonly ISolutionsServiceClient _solutionClient; private readonly ICoursesServiceClient _coursesClient; - private readonly SheetsService _sheetsService; + private readonly ICoursesServiceClient _coursesClient; + private readonly GoogleService _googleService; - public StatisticsController(ISolutionsServiceClient solutionClient, IAuthServiceClient authServiceClient, + public StatisticsController( + ISolutionsServiceClient solutionClient, + ICoursesServiceClient coursesServiceClient, + IAuthServiceClient authServiceClient, ICoursesServiceClient coursesServiceClient, - SheetsService sheetsService) : - base(authServiceClient) + GoogleService googleService) + : base(authServiceClient) { _solutionClient = solutionClient; _coursesClient = coursesServiceClient; @@ -56,15 +60,27 @@ public async Task GetLecturersStatistics(long courseId) }).ToArray(); return Ok(result); - _sheetsService = sheetsService; + _coursesClient = coursesServiceClient; + _googleService = googleService; } [HttpGet("{courseId}")] [ProducesResponseType(typeof(StatisticsCourseMatesModel[]), (int)HttpStatusCode.OK)] public async Task GetCourseStatistics(long courseId) + { + var result = await GetStatistics(courseId); + if (result == null) + { + return Forbid(); + } + + return Ok(result); + } + + private async Task?> GetStatistics(long courseId) { var statistics = await _solutionClient.GetCourseStatistics(courseId, UserId); - if (statistics == null) return Forbid(); + if (statistics == null) return null; var studentIds = statistics.Select(t => t.StudentId).ToArray(); var getStudentsTask = AuthServiceClient.GetAccountsData(studentIds); @@ -128,30 +144,57 @@ public async Task GetChartStatistics(long courseId) BestStudentSolutions = statisticsMeasure.BestStudentSolutions }; - return Ok(result); + return result; } - public class SheetUrl + /// + /// Implements file download. + /// + /// The course Id the report is based on. + /// Id of the user requesting the report. + /// Name of the sheet on which the report will be generated. + /// File download process. + [HttpGet("getFile")] + public async Task GetFile(long courseId, string userId, string sheetName) { - public string Url { get; set; } + var course = await _coursesClient.GetCourseById(courseId, userId); + var statistics = await GetStatistics(courseId); + if (statistics == null || course == null) return Forbid(); + + var statisticStream = + await ExcelGenerator.Generate(statistics.ToList(), course, sheetName).GetAsByteArrayAsync(); + return new FileContentResult(statisticStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); } - [HttpPost("getSheetTitles")] - public async Task> GetSheetTitles([FromBody] SheetUrl sheetUrl) + [HttpGet("getSheetTitles")] + public async Task> GetSheetTitles(string sheetUrl) + => await _googleService.GetSheetTitles(sheetUrl); + + [HttpPost("processLink")] + public Result ProcessLink(string? sheetUrl) { - var match = Regex.Match(sheetUrl.Url, "https://docs\\.google\\.com/spreadsheets/d/(?.+)/"); - if (!match.Success) return Result.Failed("Некорректная ссылка на страницу Google Docs"); + if (sheetUrl == null) return Result.Failed("Некорректная ссылка"); + if (GoogleService.ParseLink(sheetUrl).Succeeded) return Result.Success(); + return Result.Failed("Некорректная ссылка"); + } - var spreadsheetId = match.Groups["id"].Value; - try - { - var sheet = await _sheetsService.Spreadsheets.Get(spreadsheetId).ExecuteAsync(); - return Result.Success(sheet.Sheets.Select(t => t.Properties.Title).ToArray()); - } - catch (Exception ex) - { - return Result.Failed($"Ошибка при обращении к Google Docs: {ex.Message}"); - } + /// + /// Implements sending a report to the Google Sheets. + /// + /// The course Id the report is based on. + /// Id of the user requesting the report. + /// Sheet Url parameter, required to make requests to the Google Sheets. + /// Sheet Name parameter, required to make requests to the Google Sheets. + /// Operation status. + [HttpGet("exportToSheet")] + public async Task ExportToGoogleSheets( + long courseId, string userId, string sheetUrl, string sheetName) + { + var course = await _coursesClient.GetCourseById(courseId, userId); + var statistics = await GetStatistics(courseId); + if (course == null || statistics == null) return Result.Failed("Ошибка при получении статистики"); + var result = await _googleService.Export(course, statistics, sheetUrl, sheetName); + return result; } private async Task> GetStudentsToMentorsDictionary( diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs new file mode 100644 index 000000000..97b16b992 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs @@ -0,0 +1,379 @@ +using System; +using HwProj.APIGateway.API.TableGenerators; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Google.Apis.Sheets.v4; +using Google.Apis.Sheets.v4.Data; +using HwProj.APIGateway.API.Models; +using HwProj.Models.CoursesService.ViewModels; +using HwProj.Models.Result; +using Microsoft.AspNetCore.Mvc; +using OfficeOpenXml; +using OfficeOpenXml.Style; + +namespace HwProj.APIGateway.API.ExportServices +{ + public class GoogleService + { + private readonly SheetsService _sheetsService; + + public GoogleService(SheetsService sheetsService) + { + _sheetsService = sheetsService; + } + + private static int SeparationColumnPixelWidth { get; set; } = 20; + + public async Task Export( + CourseDTO course, + IOrderedEnumerable statistics, + string sheetUrl, + string sheetName) + { + if (sheetName == string.Empty || sheetUrl == string.Empty) + return Result.Failed("Ошибка при получении данных о гугл-документе"); + + var gettingSpreadsheetIdResult = ParseLink(sheetUrl); + if (!gettingSpreadsheetIdResult.Succeeded) return Result.Failed(gettingSpreadsheetIdResult.Errors); + var spreadsheetId = gettingSpreadsheetIdResult.Value; + Result result; + try + { + var sheetId = await GetSheetId(spreadsheetId, sheetName); + if (sheetId == null) return Result.Failed("Лист с таким названием не найден"); + + var (valueRange, range, updateStyleRequestBody) = Generate( + statistics.ToList(), course, sheetName, (int)sheetId); + + var clearRequest = _sheetsService.Spreadsheets.Values.Clear(new ClearValuesRequest(), spreadsheetId, range); + await clearRequest.ExecuteAsync(); + var updateStyleRequest = _sheetsService.Spreadsheets.BatchUpdate(updateStyleRequestBody, spreadsheetId); + await updateStyleRequest.ExecuteAsync(); + var updateRequest = _sheetsService.Spreadsheets.Values.Update(valueRange, spreadsheetId, range); + updateRequest.ValueInputOption = SpreadsheetsResource.ValuesResource.UpdateRequest.ValueInputOptionEnum.USERENTERED; + await updateRequest.ExecuteAsync(); + result = Result.Success(); + } + catch (Exception e) + { + result = Result.Failed($"Ошибка: {e.Message}"); + } + + return result; + } + + public async Task> GetSheetTitles(string sheetUrl) + { + var processingResult = ParseLink(sheetUrl); + if (!processingResult.Succeeded) return Result.Failed(processingResult.Errors); + + var spreadsheetId = processingResult.Value; + try + { + var spreadsheet = await _sheetsService.Spreadsheets.Get(spreadsheetId).ExecuteAsync(); + return Result.Success(spreadsheet.Sheets.Select(t => t.Properties.Title).ToArray()); + } + catch (Exception ex) + { + return Result.Failed($"Ошибка при обращении к Google Docs: {ex.Message}"); + } + } + + public static Result ParseLink(string sheetUrl) + { + var match = Regex.Match(sheetUrl, "https://docs\\.google\\.com/spreadsheets/d/(?.+)/"); + return match.Success ? Result.Success(match.Groups["id"].Value) + : Result.Failed("Некорректная ссылка на страницу Google Docs"); + } + + /// + /// Generates query data to create a report in Google Sheets. + /// + /// Information about the success of the course participants. + /// Course information. + /// Building sheet name. + /// Building sheet Id. + /// Data for executing queries to the Google Sheets. + private static (ValueRange ValueRange, string Range, BatchUpdateSpreadsheetRequest UpdateStyleRequest) Generate + (List courseMatesModels, + CourseDTO course, + string sheetName, + int sheetId) + { + var package = ExcelGenerator.Generate(courseMatesModels, course, sheetName); + var worksheet = package.Workbook.Worksheets[sheetName]; + var rangeWithSheetTitle = worksheet.Dimension.FullAddress; + var range = worksheet.Dimension.LocalAddress; + + var headersFieldEndAddress = string.Empty; + var redCellsAddresses = new List(); + var grayCellsAddresses = new List(); + var cellsWithBorderAddresses = new List<(string CellAddress, string BorderType)>(); + + var valueRange = new ValueRange() + { + Values = new List>(), + }; + for (var i = 1; i <= worksheet.Dimension.End.Row; ++i) + { + var row = new List(); + for (var j = 1; j <= worksheet.Dimension.End.Column; ++j) + { + var cell = worksheet.Cells[i, j]; + row.Add(cell.Value); + if (cell.Style.Font.Bold) + { + headersFieldEndAddress = cell.LocalAddress; + } + + if (cell.Style.Fill.BackgroundColor.Rgb == ExcelGenerator.BlueArgbColor) + { + redCellsAddresses.Add(cell.LocalAddress); + } + else if (cell.Style.Fill.BackgroundColor.Rgb == ExcelGenerator.GrayArgbColor) + { + grayCellsAddresses.Add(cell.LocalAddress); + } + + if (cell.Style.Border.Top.Style != ExcelBorderStyle.None) + { + cellsWithBorderAddresses.Add((cell.LocalAddress, "top")); + } + if (cell.Style.Border.Bottom.Style != ExcelBorderStyle.None) + { + cellsWithBorderAddresses.Add((cell.LocalAddress, "bottom")); + } + if (cell.Style.Border.Left.Style != ExcelBorderStyle.None) + { + cellsWithBorderAddresses.Add((cell.LocalAddress, "left")); + } + if (cell.Style.Border.Right.Style != ExcelBorderStyle.None) + { + cellsWithBorderAddresses.Add((cell.LocalAddress, "right")); + } + } + + valueRange.Values.Add(row); + } + + var batchUpdateRequest = new BatchUpdateSpreadsheetRequest(); + batchUpdateRequest.Requests = new List(); + AddClearStylesRequest(batchUpdateRequest, worksheet, sheetId, range); + AddMergeRequests(batchUpdateRequest, worksheet, sheetId, worksheet.MergedCells); + AddColouredCellsRequests(batchUpdateRequest, worksheet, sheetId, redCellsAddresses, ExcelGenerator.BlueFloatArgbColor); + AddColouredCellsRequests(batchUpdateRequest, worksheet, sheetId, grayCellsAddresses, ExcelGenerator.GrayFloatArgbColor); + AddUpdateCellsWidthRequest(batchUpdateRequest, worksheet, sheetId, grayCellsAddresses, SeparationColumnPixelWidth); + AddCellsFormattingRequest(batchUpdateRequest, worksheet, sheetId, range); + AddHeadersFormattingRequest(batchUpdateRequest, worksheet, sheetId, $"{sheetName}!A1:{headersFieldEndAddress}"); + AddBordersFormattingRequest(batchUpdateRequest, worksheet, sheetId, cellsWithBorderAddresses, ExcelGenerator.EquivalentBorderStyle); + return (valueRange, rangeWithSheetTitle, batchUpdateRequest); + } + + private static GridRange FillGridRange(ExcelWorksheet worksheet, string rangeAddress, int sheetId) + { + var gridRange = new GridRange(); + var rangeInfo = worksheet.Cells[rangeAddress]; + gridRange.SheetId = sheetId; + gridRange.StartRowIndex = rangeInfo.Start.Row - 1; + gridRange.StartColumnIndex = rangeInfo.Start.Column - 1; + gridRange.EndRowIndex = rangeInfo.End.Row; + gridRange.EndColumnIndex = rangeInfo.End.Column; + return gridRange; + } + + private static void AddClearStylesRequest( + BatchUpdateSpreadsheetRequest batchUpdateRequest, + ExcelWorksheet worksheet, + int sheetId, + string range) + { + var clearStylesRequest = new RepeatCellRequest(); + clearStylesRequest.Range = FillGridRange(worksheet, range, sheetId); + var cell = new CellData(); + cell.UserEnteredFormat = new CellFormat(); + clearStylesRequest.Cell = cell; + clearStylesRequest.Fields = "userEnteredFormat"; + + var request = new Request(); + request.RepeatCell = clearStylesRequest; + batchUpdateRequest.Requests.Add(request); + } + + + private static void AddBordersFormattingRequest( + BatchUpdateSpreadsheetRequest batchUpdateRequest, + ExcelWorksheet worksheet, + int sheetId, + List<(string CellAddress, string BorderType)> cellsAddresses, + string bordersStyle) + { + foreach (var cell in cellsAddresses) + { + var updateBorderRequest = new UpdateBordersRequest(); + var gridRange = FillGridRange(worksheet, cell.CellAddress, sheetId); + updateBorderRequest.Range = gridRange; + var border = new Google.Apis.Sheets.v4.Data.Border(); + border.Style = bordersStyle; + switch (cell.BorderType) + { + case "top": + updateBorderRequest.Top = border; + break; + case "bottom": + updateBorderRequest.Bottom = border; + break; + case "left": + updateBorderRequest.Left = border; + break; + case "right": + updateBorderRequest.Right = border; + break; + } + + var request = new Request(); + request.UpdateBorders = updateBorderRequest; + batchUpdateRequest.Requests.Add(request); + } + } + + private static void AddHeadersFormattingRequest( + BatchUpdateSpreadsheetRequest batchUpdateRequest, + ExcelWorksheet worksheet, + int sheetId, + string range) + { + var styleBoldCellsRequest = new RepeatCellRequest(); + styleBoldCellsRequest.Range = FillGridRange(worksheet, range, sheetId); + var cell = new CellData(); + cell.UserEnteredFormat = new CellFormat(); + cell.UserEnteredFormat.TextFormat = new TextFormat(); + cell.UserEnteredFormat.TextFormat.Bold = true; + styleBoldCellsRequest.Cell = cell; + styleBoldCellsRequest.Fields = "userEnteredFormat(textFormat.bold)"; + + var request = new Request(); + request.RepeatCell = styleBoldCellsRequest; + batchUpdateRequest.Requests.Add(request); + } + + private static void AddCellsFormattingRequest( + BatchUpdateSpreadsheetRequest batchUpdateRequest, + ExcelWorksheet worksheet, + int sheetId, + string range) + { + var cellsFormatRequest = new RepeatCellRequest(); + cellsFormatRequest.Range = FillGridRange(worksheet, range, sheetId); + var cell = new CellData(); + cell.UserEnteredFormat = new CellFormat(); + cell.UserEnteredFormat.HorizontalAlignment = "CENTER"; + cell.UserEnteredFormat.VerticalAlignment = "MIDDLE"; + cell.UserEnteredFormat.TextFormat = new TextFormat(); + cell.UserEnteredFormat.TextFormat.FontSize = ExcelGenerator.FontSize; + cell.UserEnteredFormat.TextFormat.FontFamily = ExcelGenerator.FontFamily; + cellsFormatRequest.Cell = cell; + cellsFormatRequest.Fields = "userEnteredFormat(horizontalAlignment,verticalAlignment,textFormat.fontSize,textFormat.fontFamily)"; + + var request = new Request(); + request.RepeatCell = cellsFormatRequest; + batchUpdateRequest.Requests.Add(request); + } + + private static void AddMergeRequests( + BatchUpdateSpreadsheetRequest batchUpdateRequest, + ExcelWorksheet worksheet, + int sheetId, + ExcelWorksheet.MergeCellsCollection mergeCellsCollection) + { + for (var i = 0; i < mergeCellsCollection.Count; ++i) + { + var mergedCellsAddress = mergeCellsCollection[i]; + var gridRange = FillGridRange(worksheet, mergedCellsAddress, sheetId); + var mergeCellsRequest = new MergeCellsRequest(); + mergeCellsRequest.MergeType = "MERGE_ALL"; + mergeCellsRequest.Range = gridRange; + + var request = new Request(); + request.MergeCells = mergeCellsRequest; + batchUpdateRequest.Requests.Add(request); + } + } + + private static void AddUpdateCellsWidthRequest( + BatchUpdateSpreadsheetRequest batchUpdateRequest, + ExcelWorksheet worksheet, + int sheetId, + List cellsAddresses, + int cellsPixelWidth) + { + for (var i = 0; i < cellsAddresses.Count; ++i) + { + var cellAddress = cellsAddresses[i]; + var rangeInfo = worksheet.Cells[cellAddress]; + var updateWidthRequest = new UpdateDimensionPropertiesRequest(); + updateWidthRequest.Range = new DimensionRange() + { + SheetId = sheetId, + Dimension = "COLUMNS", + StartIndex = rangeInfo.Start.Column - 1, + EndIndex = rangeInfo.End.Column, + }; + updateWidthRequest.Fields = "*"; + updateWidthRequest.Properties = new DimensionProperties(); + updateWidthRequest.Properties.PixelSize = cellsPixelWidth; + + var request = new Request(); + request.UpdateDimensionProperties = updateWidthRequest; + batchUpdateRequest.Requests.Add(request); + } + } + + private static void AddColouredCellsRequests( + BatchUpdateSpreadsheetRequest batchUpdateRequest, + ExcelWorksheet worksheet, + int sheetId, + List colouredCellsAddresses, + (float Alpha, float Red, float Green, float Blue) color) + { + for (var i = 0; i < colouredCellsAddresses.Count; ++i) + { + var cellAddress = colouredCellsAddresses[i]; + var colorInRedRequest = new RepeatCellRequest(); + colorInRedRequest.Range = FillGridRange(worksheet, cellAddress, sheetId); + var cell = new CellData(); + cell.UserEnteredFormat = new CellFormat(); + cell.UserEnteredFormat.BackgroundColor = new Color() + { + Alpha = color.Alpha, + Red = color.Red, + Green = color.Green, + Blue = color.Blue, + }; + colorInRedRequest.Fields = "userEnteredFormat(backgroundColor)"; + colorInRedRequest.Cell = cell; + + var request = new Request(); + request.RepeatCell = colorInRedRequest; + batchUpdateRequest.Requests.Add(request); + } + } + + private async Task GetSheetId(string spreadsheetId, string sheetName) + { + var spreadsheetGetRequest = _sheetsService.Spreadsheets.Get(spreadsheetId); + spreadsheetGetRequest.IncludeGridData = true; + try + { + var spreadsheetResponse = await spreadsheetGetRequest.ExecuteAsync(); + var sheetId = spreadsheetResponse.Sheets.First(sheet => sheet.Properties.Title == sheetName).Properties.SheetId; + return sheetId; + } + catch (Exception) + { + return null; + } + } + } +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj b/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj index a8862edd4..d27fb272e 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj +++ b/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj @@ -10,6 +10,7 @@ + @@ -28,10 +29,4 @@ - - - - Always - - diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs index b778b773d..869e636f3 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs @@ -2,6 +2,8 @@ using Google.Apis.Auth.OAuth2; using Google.Apis.Services; using Google.Apis.Sheets.v4; +using HwProj.APIGateway.API.ExportServices; +using HwProj.APIGateway.API.Models; using HwProj.AuthService.Client; using HwProj.ContentService.Client; using HwProj.CoursesService.Client; @@ -18,13 +20,20 @@ using Microsoft.IdentityModel.Tokens; using IStudentsInfo; using StudentsInfo; +using HwProj.Utils.Authorization; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json.Linq; + namespace HwProj.APIGateway.API { public class Startup { + private readonly IConfigurationSection _sheetsConfiguration; + public Startup(IConfiguration configuration) { + _sheetsConfiguration = configuration.GetSection("GoogleSheets"); Configuration = configuration; } @@ -32,6 +41,7 @@ public Startup(IConfiguration configuration) public void ConfigureServices(IServiceCollection services) { + services.AddCors(); services.Configure(options => { options.MultipartBodyLengthLimit = 200 * 1024 * 1024; }); services.ConfigureHwProjServices("API Gateway"); services.AddSingleton(provider => @@ -58,7 +68,8 @@ public void ConfigureServices(IServiceCollection services) services.AddHttpClient(); services.AddHttpContextAccessor(); - services.AddScoped(_ => ConfigureGoogleSheets()); + services.AddSingleton(_ => ConfigureGoogleSheets(_sheetsConfiguration)); + services.AddSingleton(); services.AddAuthServiceClient(); services.AddCoursesServiceClient(); @@ -74,10 +85,21 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) app.ConfigureHwProj(env, "API Gateway"); } - private static SheetsService ConfigureGoogleSheets() + private static JToken Serialize(IConfigurationSection configurationSecton) + { + JObject obj = new JObject(); + foreach (var child in configurationSecton.GetChildren()) + { + obj.Add(child.Key, child.Value); + } + + return obj; + } + + private static SheetsService ConfigureGoogleSheets(IConfigurationSection _sheetsConfiguration) { - using var stream = new FileStream("googlesheets_credentials.json", FileMode.Open, FileAccess.ReadWrite); - var credential = GoogleCredential.FromStream(stream).CreateScoped(SheetsService.Scope.Spreadsheets); + var jsonObject = Serialize(_sheetsConfiguration); + var credential = GoogleCredential.FromJson(jsonObject.ToString()).CreateScoped(SheetsService.Scope.Spreadsheets); return new SheetsService(new BaseClientService.Initializer { diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs b/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs new file mode 100644 index 000000000..48727b580 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs @@ -0,0 +1,266 @@ +using System.Collections.Generic; +using System.Linq; +using HwProj.APIGateway.API.Models; +using HwProj.Models.CoursesService.ViewModels; +using HwProj.Models.SolutionsService; +using OfficeOpenXml; +using OfficeOpenXml.Style; + +namespace HwProj.APIGateway.API.TableGenerators +{ + /// + /// Implements course report generation. + /// + public static class ExcelGenerator + { + /// + /// Font used in the reports. + /// + public static string FontFamily { get; set; } = "Calibri"; + + /// + /// Font size used in the reports. + /// + public static int FontSize { get; set; } = 11; + + /// + /// Shade of red used in the reports. + /// + private static (int Alpha, int Red, int Green, int Blue) BlueIntArgbColor { get; set; } = (0, 0, 255, 255); + public static (float Alpha, float Red, float Green, float Blue) BlueFloatArgbColor { get; set; } = (0, 0, 1, 1); + public static string BlueArgbColor { get; set; } = "0000FFFF"; + + + /// + /// Shade of gray used in the reports. + /// + private static (int Alpha, int Red, int Green, int Blue) GrayIntArgbColor { get; set; } = (255, 80, 80, 80); + + public static (float Alpha, float Red, float Green, float Blue) GrayFloatArgbColor { get; set; } = + (1, (float)0.3137, (float)0.3137, (float)0.3137); + + public static string GrayArgbColor { get; set; } = "FF505050"; + + private static ExcelBorderStyle BorderStyle { get; set; } = ExcelBorderStyle.Thin; + + public static string EquivalentBorderStyle { get; set; } = "SOLID"; + + private static int SeparationColumnWidth { get; set; } = 2; + + /// + /// Generates course statistics file based on the model from HwProj.APIGateway.Tests.Test.xlsx file. + /// + /// Information about the success of the course participants. + /// Course information. + /// Name of the building sheet. + /// generated package. + public static ExcelPackage Generate( + List courseMatesModels, + CourseDTO course, + string sheetName) + { + ExcelPackage.LicenseContext = LicenseContext.NonCommercial; + var excelPackage = new ExcelPackage(); + var worksheet = excelPackage.Workbook.Worksheets.Add(sheetName); + + var rowsNumber = 3 + courseMatesModels.Count; + var position = new Position(1, 1); + + worksheet.Cells[position.Row, position.Column].Value = ""; + worksheet.Cells[position.Row, position.Column, position.Row + 2, position.Column].Merge = true; + ++position.Column; + + AddHomeworksHeaders(worksheet, course, position, rowsNumber, SeparationColumnWidth); + var columnsNumber = position.Column - 2; + position.ToNextRow(2); + + worksheet.Cells[1, 1, rowsNumber, columnsNumber].Style.Font.Size = FontSize; + worksheet.Cells[1, 1, rowsNumber, columnsNumber].Style.Font.Name = FontFamily; + + AddTasksHeaders(worksheet, course, position, rowsNumber); + position.ToNextRow(2); + + AddMinMaxCntHeadersWithBottomBorder(worksheet, course, position); + position.ToNextRow(1); + + var maxFieldPosition = new Position(position.Row, 3); + AddTasksMaxRatingInfo(worksheet, course, rowsNumber, maxFieldPosition); + + AddCourseMatesInfo(worksheet, courseMatesModels, position); + + var headersRange = worksheet.Cells[1, 1, 3, columnsNumber]; + headersRange.Style.Font.Bold = true; + + var range = worksheet.Cells[1, 1, rowsNumber, columnsNumber]; + range.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; + range.Style.VerticalAlignment = ExcelVerticalAlignment.Center; + + return excelPackage; + } + + private static void AddBorderedSeparationColumn(ExcelWorksheet worksheet, Position position, int heightInCells, int columnWidth) + { + var range = worksheet.Cells[1, position.Column, heightInCells, position.Column]; + range.Style.Fill.PatternType = ExcelFillStyle.Solid; + range.Style.Fill.BackgroundColor.SetColor(GrayIntArgbColor.Alpha, GrayIntArgbColor.Red, GrayIntArgbColor.Green, GrayIntArgbColor.Blue); + worksheet.Column(position.Column).Width = columnWidth; + ++position.Column; + } + + private static void AddHomeworksHeaders(ExcelWorksheet worksheet, CourseDTO course, Position position, + int heightInCells, int separationColumnWidth) + { + var homeworkNumber = 1; + for (var i = 0; i < course.Homeworks.Length; ++i) + { + var numberCellsToMerge = course.Homeworks[i].Tasks.Count * 3; + worksheet.Cells[position.Row, position.Column].Value + = $"/ {homeworkNumber.ToString()}: {course.Homeworks[i].Title}, {course.Homeworks[i].Date.ToString("dd.MM")}"; + worksheet.Cells[position.Row, position.Column, position.Row, position.Column + numberCellsToMerge - 1] + .Merge = true; + position.Column += numberCellsToMerge; + AddBorderedSeparationColumn(worksheet, position, heightInCells, separationColumnWidth); + ++homeworkNumber; + } + } + + private static void AddTasksHeaders(ExcelWorksheet worksheet, CourseDTO course, Position position, + int heightInCells) + { + for (var i = 0; i < course.Homeworks.Length; ++i) + { + var taskNumber = 1; + for (var j = 0; j < course.Homeworks[i].Tasks.Count; ++j) + { + if (taskNumber != 1) + { + var rangeForBordering = + worksheet.Cells[position.Row, position.Column, heightInCells, position.Column]; + rangeForBordering.Style.Border.Left.Style = BorderStyle; + } + + worksheet.Cells[position.Row, position.Column].Value + = $"{taskNumber.ToString()} {course.Homeworks[i].Tasks[j].Title}"; + worksheet.Cells[position.Row, position.Column, position.Row, position.Column + 2].Merge = true; + position.Column += 3; + ++taskNumber; + } + + ++position.Column; + } + } + + private static void AddMinMaxCntHeadersWithBottomBorder(ExcelWorksheet worksheet, CourseDTO course, + Position position) + { + for (var i = 0; i < course.Homeworks.Length; ++i) + { + var lengthInCells = course.Homeworks[i].Tasks.Count * 3; + for (var j = position.Column; j < position.Column + lengthInCells; j += 3) + { + worksheet.Cells[position.Row, j].Value = "min"; + worksheet.Cells[position.Row, j + 1].Value = "max"; + worksheet.Cells[position.Row, j + 2].Value = "cnt"; + worksheet.Cells[position.Row, j, position.Row, j + 2].Style.Border.Bottom.Style = BorderStyle; + } + + position.Column += lengthInCells; + ++position.Column; + } + } + + private static void AddTasksMaxRatingInfo( + ExcelWorksheet worksheet, + CourseDTO course, + int heightInCells, + Position firstMaxFieldPosition) + { + for (var i = 0; i < course.Homeworks.Length; ++i) + { + for (var j = 0; j < course.Homeworks[i].Tasks.Count; ++j) + { + for (var k = firstMaxFieldPosition.Row; k <= heightInCells; ++k) + { + worksheet.Cells[k, firstMaxFieldPosition.Column].Value + = course.Homeworks[i].Tasks[j].MaxRating; + } + + firstMaxFieldPosition.Column += 3; + } + + ++firstMaxFieldPosition.Column; + } + } + + private static void AddCourseMatesInfo( + ExcelWorksheet worksheet, + List courseMatesModels, + Position position) + { + for (var i = 0; i < courseMatesModels.Count; ++i) + { + worksheet.Cells[position.Row, position.Column].Value + = $"{courseMatesModels[i].Name} {courseMatesModels[i].Surname}"; + ++position.Column; + for (var j = 0; j < courseMatesModels[i].Homeworks.Count; ++j) + { + for (var k = 0; k < courseMatesModels[i].Homeworks[j].Tasks.Count; ++k) + { + var allSolutions = courseMatesModels[i].Homeworks[j].Tasks[k].Solution; + var solutions = allSolutions + .Where(solution => + solution.State == SolutionState.Rated || solution.State == SolutionState.Final); + var min = solutions.Max(solution => solution.Rating) ?? 0; + var cnt = solutions.Count(); + worksheet.Cells[position.Row, position.Column].Value = min; + worksheet.Cells[position.Row, position.Column + 2].Value = cnt; + if (cnt != allSolutions.Count()) + { + worksheet.Cells[position.Row, position.Column + 2].Style.Fill.PatternType = + ExcelFillStyle.Solid; + worksheet.Cells[position.Row, position.Column + 2].Style.Fill.BackgroundColor.SetColor( + BlueIntArgbColor.Alpha, BlueIntArgbColor.Red, BlueIntArgbColor.Green, BlueIntArgbColor.Blue); + } + + position.Column += 3; + } + + ++position.Column; + } + + position.ToNextRow(1); + } + } + + private class Position + { + /// + /// Initializes a new instance of the class. + /// + /// The row number at the current position. + /// The column number at the current position. + public Position(int rowPosition, int columnPosition) + { + this.Row = rowPosition; + this.Column = columnPosition; + } + + /// + /// Gets or sets the row number at the current position. + /// + public int Row { get; set; } + + /// + /// Gets or sets the column number at the current position. + /// + public int Column { get; set; } + + /// + /// Moves position to the next row optionally changing column component. + /// + /// New column component of the position. + public void ToNextRow(int nextRowColumnPosition) + => (this.Row, this.Column) = (this.Row + 1, nextRowColumnPosition); + } + } +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json b/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json index 92453a812..c55cba850 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json +++ b/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json @@ -12,5 +12,10 @@ "LdapHost": "ad.pu.ru", "LdapPort": 389, "SearchBase": "DC=ad,DC=pu,DC=ru" + }, + "EPPlus": { + "ExcelPackage": { + "LicenseContext": "NonCommercial" + } } } diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/libman.json b/HwProj.APIGateway/HwProj.APIGateway.API/libman.json new file mode 100644 index 000000000..ceee2710f --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/libman.json @@ -0,0 +1,5 @@ +{ + "version": "1.0", + "defaultProvider": "cdnjs", + "libraries": [] +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.Tests/ExcelGeneratorTests.cs b/HwProj.APIGateway/HwProj.APIGateway.Tests/ExcelGeneratorTests.cs new file mode 100644 index 000000000..91bd4d0fe --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.Tests/ExcelGeneratorTests.cs @@ -0,0 +1,278 @@ +using NUnit.Framework; +using HwProj.Models.CoursesService.ViewModels; +using HwProj.Models.StatisticsService; +using HwProj.APIGateway.API.Models; +using HwProj.APIGateway.API.TableGenerators; +using System.Collections.Generic; +using OfficeOpenXml; +using System.IO; +using System; +using NUnit.Framework.Interfaces; + +namespace HwProj.APIGateway.Tests +{ + [TestFixture] + public class ExcelGeneratorTests + { + private enum CellProperty + { + Value, + Style, + IsMerge, + } + + private static readonly string GoldFile = "GoldFile.xlsx"; + private static readonly string TestFile = "TestFile.xlsx"; + private static readonly string TestFileSheetName = "ТестЛист"; + private static readonly CourseMateViewModel[] CourseMates = + { + new CourseMateViewModel(), + new CourseMateViewModel() + }; + + private static readonly HomeworkViewModel[] Homeworks = + { + new HomeworkViewModel() + { + Title = "TestHomework1", + Date = new DateTime(2023, 6, 4), + Tasks = new List() + { + new HomeworkTaskViewModel() + { + Title = "Task1.1", + PublicationDate = new System.DateTime(2023, 6, 4, 14, 0, 0), + MaxRating = 8 + }, + new HomeworkTaskViewModel() + { + Title = "Task1.2", + PublicationDate = new System.DateTime(2023, 6, 4, 15, 0, 0), + MaxRating = 8 + } + } + }, + new HomeworkViewModel() + { + Title = "TestHomework2", + Date = new System.DateTime(2023, 6, 5), + Tasks = new List + { + new HomeworkTaskViewModel() + { + Title = "Task2.1", + PublicationDate = new System.DateTime(2023, 6, 5, 14, 0, 0), + MaxRating = 8 + }, + new HomeworkTaskViewModel() + { + Title = "Task2.2", + PublicationDate = new System.DateTime(2023, 6, 5, 15, 0, 0), + MaxRating = 8 + } + } + }, + }; + + private static readonly CourseDTO Course = new CourseDTO() + { + CourseMates = CourseMates, + Homeworks = Homeworks, + }; + + private static readonly List CourseMatesModels = new List + { + new StatisticsCourseMatesModel() + { + Name = "Иван", Surname = "Иванов", + Homeworks = new List + { + new StatisticsCourseHomeworksModel() + { + Tasks = new List + { + new StatisticsCourseTasksModel() + { + Solution = new List + { + new StatisticsCourseSolutionsModel + (new Models.SolutionsService.Solution() { State = Models.SolutionsService.SolutionState.Rated, Rating = 4 } ), + new StatisticsCourseSolutionsModel + (new Models.SolutionsService.Solution() { State = Models.SolutionsService.SolutionState.Posted } ), + } + }, + new StatisticsCourseTasksModel() + { + Solution = new List + { + new StatisticsCourseSolutionsModel + (new Models.SolutionsService.Solution() { State = Models.SolutionsService.SolutionState.Posted } ), + } + } + } + }, + new StatisticsCourseHomeworksModel() + { + Tasks = new List + { + new StatisticsCourseTasksModel() + { + Solution = new List() + }, + new StatisticsCourseTasksModel() + { + Solution = new List() + } + } + } + } + }, + new StatisticsCourseMatesModel() + { + Name = "Петр", Surname = "Петров", + Homeworks = new List + { + new StatisticsCourseHomeworksModel() + { + Tasks = new List + { + new StatisticsCourseTasksModel() + { + Solution = new List() + }, + new StatisticsCourseTasksModel() + { + Solution = new List + { + new StatisticsCourseSolutionsModel + (new Models.SolutionsService.Solution() { State = Models.SolutionsService.SolutionState.Rated, Rating = 5 } ), + new StatisticsCourseSolutionsModel + (new Models.SolutionsService.Solution() { State = Models.SolutionsService.SolutionState.Rated, Rating = 7 } ), + } + } + } + }, + new StatisticsCourseHomeworksModel() + { + Tasks = new List + { + new StatisticsCourseTasksModel() + { + Solution = new List() + }, + new StatisticsCourseTasksModel() + { + Solution = new List() + } + } + } + } + } + }; + + [OneTimeSetUp] + public void GenerateFile() + { + var testPackage = ExcelGenerator.Generate(CourseMatesModels, Course, TestFileSheetName); + var testFileInfo = new FileInfo(TestFile); + testPackage.SaveAs(testFileInfo); + } + + [Test] + public void CheckTheEquivalenceOfTwoSheetsValues() + { + using (var testPackage = new ExcelPackage(TestFile)) + { + var testSheet = testPackage.Workbook.Worksheets[TestFileSheetName]; + var goldFile = new FileInfo(GoldFile); + using (var goldPackage = new ExcelPackage(goldFile)) + { + var goldSheet = goldPackage.Workbook.Worksheets[TestFileSheetName]; + Assert.That(IsTwoExcelWorksheetsEquals(goldSheet, testSheet, 5, 14, CellProperty.Value)); + } + } + } + + [Test] + public void CheckTheEquivalenceOfTwoSheetsStructure() + { + using (var testPackage = new ExcelPackage(TestFile)) + { + var testSheet = testPackage.Workbook.Worksheets[TestFileSheetName]; + var goldFile = new FileInfo(GoldFile); + using (var goldPackage = new ExcelPackage(goldFile)) + { + var goldSheet = goldPackage.Workbook.Worksheets[TestFileSheetName]; + Assert.That(IsTwoExcelWorksheetsEquals(goldSheet, testSheet, 5, 14, CellProperty.IsMerge)); + } + } + } + + [Test] + public void CheckTheEquivalenceOfTwoSheetsStyle() + { + using (var testPackage = new ExcelPackage(TestFile)) + { + var testSheet = testPackage.Workbook.Worksheets[TestFileSheetName]; + var goldFile = new FileInfo(GoldFile); + using (var goldPackage = new ExcelPackage(goldFile)) + { + var goldSheet = goldPackage.Workbook.Worksheets[TestFileSheetName]; + Assert.That(IsTwoExcelWorksheetsEquals(goldSheet, testSheet, 5, 14, CellProperty.Style)); + } + } + } + + [OneTimeTearDown] + public void DeleteFileIfTestsArePassed() + { + if (TestContext.CurrentContext.Result.Outcome == ResultState.Success) + { + File.Delete(TestFile); + } + } + + private static bool IsTwoExcelWorksheetsEquals(ExcelWorksheet firstSheet, ExcelWorksheet secondSheet, + int lastRow, int lastCol, CellProperty cellPropertyToCompare) + { + var comparer = new Func (Equals); + switch (cellPropertyToCompare) + { + case CellProperty.Value: + comparer = ((firstCell, secondCell) => + Equals(firstCell.Value, secondCell.Value)); + break; + case CellProperty.Style: + comparer = ((firstCell, secondCell) => + firstCell.Style.Font.Bold == secondCell.Style.Font.Bold + && firstCell.Style.Font.Name == secondCell.Style.Font.Name + && Math.Abs(firstCell.Style.Font.Size - secondCell.Style.Font.Size) < 0.1 + && firstCell.Style.Fill.BackgroundColor.Rgb == secondCell.Style.Fill.BackgroundColor.Rgb + && firstCell.Style.VerticalAlignment == secondCell.Style.VerticalAlignment + && firstCell.Style.HorizontalAlignment == secondCell.Style.HorizontalAlignment + && firstCell.Style.Border.Left.Style == secondCell.Style.Border.Left.Style + && firstCell.Style.Border.Right.Style == secondCell.Style.Border.Right.Style + && firstCell.Style.Border.Bottom.Style == secondCell.Style.Border.Bottom.Style + && firstCell.Style.Border.Top.Style == secondCell.Style.Border.Top.Style); + break; + case CellProperty.IsMerge: + comparer = ((firstCell, secondCell) => + firstCell.Merge == secondCell.Merge); + break; + } + + for (var i = 1; i <= lastRow; ++i) + { + for (var j = 1; j <= lastCol; ++j) + { + if (!comparer(firstSheet.Cells[i, j], secondSheet.Cells[i, j])) + { + return false; + } + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.Tests/GoldFile.xlsx b/HwProj.APIGateway/HwProj.APIGateway.Tests/GoldFile.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..5b06e6f5c2cff4aedc6b047d84abc8f096db1af4 GIT binary patch literal 3351 zcmZ`+c{r47A0CFW%!C+AXskzBVule#3zI#v%?R0zEW?yRa;zy6#};9%S;`i&G^7+7 zCNjdw68exWOPR=WsNtK=`L6HaoSyf(-uIvP{$20ye(vXY->}}KHWm11);ts(H__1@y8XVAKZY2WXX=xHW-|0POv7Qby6p_H!d?L4iue8 zjWJam8%#>I@8d(kCtO=F;&a^YpOYsPn$`hcAzIP}49!@vxmu~N1W zu$cAG+xdjw5pYo6mkBAMufKkB{`kZ;fgT;GDEZ4}^((EG?dOraMCru46bFWjp9GDI zHVr=K5kMw}WX~-i^GlJk+8IfmgA>itu0ys7atpaCGAWk&@P3)vYqgm9))>SpyXu)J zm8yk_egoR*hDbxLw=j!{^ulJtcZHO4Y>*D$WD)A50YV`oeo3n6`pl*D@7k?BF?c?7 zM))$9`5tmfBa6RPF{wMkBpA#j*U2A?Bd93FeR z2k$Q|vltM|AU2yDPfv8!4#sV5e0y8#I2(Jaj~G=EDcu!SfWFV+!}flpYl&0ooEJH# zqcBns*pI&6ZbS{>-)dz~hlsJQBAyC-)LGN)O1^{2K7M%9mWm9awy>5%Vti{&)K+Eq|dBz96|Wh&Q`#?RfPusPehaA0q% zx4vuCEihZX#B7z9*=jIO(XTdgu`Dzd`+dcF>G|Xo4GQCZ}|1s2C(@9Vg z?&9uc6!VuPeF5HI>4a4-&&$Mzo%5wz)d>&TN@SC~m}NQtiz#T)X% zP$|NCSLF}o8gE*D!b!Lk=e~?PRpjkPBsFDQI1U3$)gOdZKS_#^D#3IVr|dLMEm-QA z!+b=>UnNnxJ_Lq}ohjj-obwy`&cSzSL@D^K$EjAK8yK_kI(Rf;$ad_Oz2#RblI-po z&a#)PDA$z2K_*p5RscZsFRK0$#%{uXDw4`OJ>C>Jgy_^h_d;{tIo_fn12IRAfSrA3 zES~j_`uoVtXP@I3%cE|@yqoT;jMEh}TR!6bv8o#g5zlf_D7%=0#7Q8L@#?MOQ6UHq zQKX~g9At68-vnrE>oR@>bL-%{NMR$qUELZ|+6G~9RK=GjW9&sJ5g3sPx-T5aDEykk z(r})96|_QC&tJDH+^KdOT~8k?Vu;IkeMCpjsQ6ptyo;~eVv+CCD$V2d6hTIQe3kI3&K)?Wi$lqcjcw(^u z1eHG@h@YwoGwPoW=VJ)!)44#|Z7%U@%Zxa!Fwc8jLNQ#rFe#@k->|tOYr+c2sO8r} z*K68`>npTxD(^giZh7_Ih}384NZZX+p7FmmOFyv5(;J8WJF<{Z=QHH*j+d%XeS82z2q=mC@~1b7 zDBC)Q^!D(vROE+dWBU%t0cTSk)Eb zoG!Xm?Mw|3=YErzW3^&++P@t~l8`((GSzM}{D zTI1Oy2Bt?_s7#o87JG3h{=*SU6F`PDUJ4%70%cX>daUSNwN?EnOevqr&#K5-AvJ+= zmSGt->w8{)D`-Gh>^=n7?j;rgDUO$fr=Kk;?rR(*wcM3w^Ei$x@TRdGyD4hISz+yV zVMx@6G>LL{8=v=jw%{eUZp~^|`AKcA?Dd?Zn!du+Oco+NzT_KPE&*MffcEg!%%FB3 z6YWwK>#7pfXz;6;)W8fsrqk-Jy6de}^ARH)uauFv6NaBI#^B({|hDQ|?9ZHqM zf9gLD-U($QPl1$UGK=Dkrko4PYWt@v#k)z!tQY!(sTM>gxhu!Ho`T8|x)w^V8pFn^ z^;@$7yN)f{slyvC?RYplKWksR52DsTmZm4IgxmWR?% zdCR=sje}MRevJ#@rmO!U_WbdbRyoQYqy?W%9*CblMxSrNGFR{?ePsi~ zy+d3LD4sr1vh1BH(*k$yDqfoPmQkEn@?O<%Wc@YcK257~-8@ES;?#JNmO`kzSFfdY zE$4glRv!w`xX>?jFj5=^j$R8;Zd1OIq{)gsZTXR8k#;ZNcDwPgZB!ZtS1$l{GN&K- zPx0`;LC8aPyj0@0KG-*Tev#E?0L`kQpGdDW!03;;pI)|^d1OQ4jqb6pG+hLH<~DMs zH9F@4UwmNjv6UEzcz@1Gmn7$`+gTiJQhLm>YW^#GQ1qkRhLMoG?RQYlm`ALeFH=ecwS#hi%zVK6c33*=A?wh4plv($=78lFPYQ7PtZt$tS4C z0eu5krLL}{zc?DzS0pG~Aw4Pd=L(@^2^^%>-W+lKAg5CBLgJMz-1L!HxuvzOfriK& z)Vbkdy@2akw^MdzJ>=hXSwS!RgJyzD$xS5@stxVv)UaOHYe#QP=|)Agxae1mxIO>y za!2r<_p>oit-z#2qDrgDC$2l_wdc)Ivw-K_%rz5Fi!R6v1u zbj&C}yjpcdbn@K|ZnV+Uv%2Dv;w{E8a^AR%cp#Rjsb^FD0Z__XbGAkBed-nV_?9jYw} z?hx!gC%X=8`R72}U4_i?XO+Ll+kWU?Mel~vnbVQk a2w}gz%}v-rKi=bDUN4yxpJAp90N}q~e{3-T literal 0 HcmV?d00001 diff --git a/HwProj.APIGateway/HwProj.APIGateway.Tests/HwProj.APIGateway.Tests.csproj b/HwProj.APIGateway/HwProj.APIGateway.Tests/HwProj.APIGateway.Tests.csproj index 89dd6ef3f..117822415 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.Tests/HwProj.APIGateway.Tests.csproj +++ b/HwProj.APIGateway/HwProj.APIGateway.Tests/HwProj.APIGateway.Tests.csproj @@ -8,17 +8,36 @@ + + Always + Never + + PreserveNewest + - + + + + + + + + + + + Always + + + diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs b/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs index 49422b339..430ca5696 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Net; using System.Threading.Tasks; @@ -205,6 +206,12 @@ public async Task GetCourseStat(long courseId) var course = await _coursesClient.GetCourseById(courseId); if (course == null) return NotFound(); + course.Homeworks = course.Homeworks.OrderBy(homework => homework.Date).ToArray(); + for (var i = 0; i < course.Homeworks.Length; ++i) + { + course.Homeworks[i].Tasks = course.Homeworks[i].Tasks.OrderBy(task => task.PublicationDate).ToList(); + } + var taskIds = course.Homeworks .SelectMany(t => t.Tasks) .Select(t => t.Id) diff --git a/HwProj.sln b/HwProj.sln index 61dabe1e8..e0ef544e4 100644 --- a/HwProj.sln +++ b/HwProj.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29001.49 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33403.182 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HwProj.Repositories", "HwProj.Common\HwProj.Repositories\HwProj.Repositories.csproj", "{4E5191DC-EC8B-44C8-BD85-198D2C0C16F7}" EndProject @@ -58,9 +58,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HwProj.SolutionsService.Cli EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HwProj.Exceptions", "HwProj.Common\HwProj.Exceptions\HwProj.Exceptions.csproj", "{51463655-7668-4C7D-9FDE-D4D7CDAA82B8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HwProj.SolutionsService.IntegrationTests", "HwProj.SolutionsService\HwProj.SolutionsService.IntegrationTests\HwProj.SolutionsService.IntegrationTests.csproj", "{9751B4E3-50A6-4678-A3AF-BE5CD828B151}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HwProj.SolutionsService.IntegrationTests", "HwProj.SolutionsService\HwProj.SolutionsService.IntegrationTests\HwProj.SolutionsService.IntegrationTests.csproj", "{9751B4E3-50A6-4678-A3AF-BE5CD828B151}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HwProj.AuthService.IntegrationTests", "HwProj.AuthService\HwProj.AuthService.IntegrationTests\HwProj.AuthService.IntegrationTests.csproj", "{EA822D2F-88C2-4B82-AA20-DD07FF0A9A1E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HwProj.AuthService.IntegrationTests", "HwProj.AuthService\HwProj.AuthService.IntegrationTests\HwProj.AuthService.IntegrationTests.csproj", "{EA822D2F-88C2-4B82-AA20-DD07FF0A9A1E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HwProj.APIGateway.Tests", "HwProj.APIGateway\HwProj.APIGateway.Tests\HwProj.APIGateway.Tests.csproj", "{E1D02140-1F92-47CF-9EF0-93FC5A007AAA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7F52CAA6-CDDF-42DD-9C5F-100A3400B0D0}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HwProj.ContentService", "HwProj.ContentService", "{3C318420-6DC8-4AF7-9966-9D7E6C8956B8}" EndProject @@ -182,6 +189,10 @@ Global {8DE955D7-FD97-4F11-B6D4-414E631B9F83}.Debug|Any CPU.Build.0 = Debug|Any CPU {8DE955D7-FD97-4F11-B6D4-414E631B9F83}.Release|Any CPU.ActiveCfg = Release|Any CPU {8DE955D7-FD97-4F11-B6D4-414E631B9F83}.Release|Any CPU.Build.0 = Release|Any CPU + {E1D02140-1F92-47CF-9EF0-93FC5A007AAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1D02140-1F92-47CF-9EF0-93FC5A007AAA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1D02140-1F92-47CF-9EF0-93FC5A007AAA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1D02140-1F92-47CF-9EF0-93FC5A007AAA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -212,6 +223,7 @@ Global {886E6A4F-9F11-482D-AADF-CCB1049B6C14} = {CCA598FB-F8A9-4F20-BCE5-BE21725156BD} {72A4C047-1F47-45DA-9BD6-54E62E7CFB78} = {CCA598FB-F8A9-4F20-BCE5-BE21725156BD} {8DE955D7-FD97-4F11-B6D4-414E631B9F83} = {CCA598FB-F8A9-4F20-BCE5-BE21725156BD} + {E1D02140-1F92-47CF-9EF0-93FC5A007AAA} = {DC0D1EE7-D2F8-4D15-8CC6-69A0A0A938D9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C03BF138-4A5B-4261-9495-6D3AC6CE9779} diff --git a/global.json b/global.json new file mode 100644 index 000000000..0ae7749a6 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "2.2.0", + "rollForward": "minor", + "allowPrerelease": true + } +} \ No newline at end of file diff --git a/hwproj.front/src/App.tsx b/hwproj.front/src/App.tsx index 359f29fa3..5d3ad5834 100644 --- a/hwproj.front/src/App.tsx +++ b/hwproj.front/src/App.tsx @@ -127,6 +127,7 @@ class App extends Component<{ navigate: any }, AppState> { }/> }/> }/> + }/> }/>
diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index cea3d3abd..60d6769ef 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -8698,13 +8698,143 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur }, /** * - * @param {SheetUrl} [sheetUrl] + * @param {number} [courseId] + * @param {string} [userId] + * @param {string} [sheetUrl] + * @param {string} [sheetName] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsGetSheetTitlesPost(sheetUrl?: SheetUrl, options: any = {}): FetchArgs { + apiStatisticsExportToSheetGet(courseId?: number, userId?: string, sheetUrl?: string, sheetName?: string, options: any = {}): FetchArgs { + const localVarPath = `/api/Statistics/exportToSheet`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'GET' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + if (courseId !== undefined) { + localVarQueryParameter['courseId'] = courseId; + } + + if (userId !== undefined) { + localVarQueryParameter['userId'] = userId; + } + + if (sheetUrl !== undefined) { + localVarQueryParameter['sheetUrl'] = sheetUrl; + } + + if (sheetName !== undefined) { + localVarQueryParameter['sheetName'] = sheetName; + } + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + delete localVarUrlObj.search; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {number} [courseId] + * @param {string} [userId] + * @param {string} [sheetName] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiStatisticsGetFileGet(courseId?: number, userId?: string, sheetName?: string, options: any = {}): FetchArgs { + const localVarPath = `/api/Statistics/getFile`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'GET' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + if (courseId !== undefined) { + localVarQueryParameter['courseId'] = courseId; + } + + if (userId !== undefined) { + localVarQueryParameter['userId'] = userId; + } + + if (sheetName !== undefined) { + localVarQueryParameter['sheetName'] = sheetName; + } + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + delete localVarUrlObj.search; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} [sheetUrl] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiStatisticsGetSheetTitlesGet(sheetUrl?: string, options: any = {}): FetchArgs { const localVarPath = `/api/Statistics/getSheetTitles`; const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'GET' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + if (sheetUrl !== undefined) { + localVarQueryParameter['sheetUrl'] = sheetUrl; + } + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + delete localVarUrlObj.search; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} [sheetUrl] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiStatisticsProcessLinkPost(sheetUrl?: string, options: any = {}): FetchArgs { + const localVarPath = `/api/Statistics/processLink`; + const localVarUrlObj = url.parse(localVarPath, true); const localVarRequestOptions = Object.assign({ method: 'POST' }, options); const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; @@ -8717,14 +8847,14 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur localVarHeaderParameter["Authorization"] = localVarApiKeyValue; } - localVarHeaderParameter['Content-Type'] = 'application/json-patch+json'; + if (sheetUrl !== undefined) { + localVarQueryParameter['sheetUrl'] = sheetUrl; + } localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 delete localVarUrlObj.search; localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); - const needsSerialization = ("SheetUrl" !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; - localVarRequestOptions.body = needsSerialization ? JSON.stringify(sheetUrl || {}) : (sheetUrl || ""); return { url: url.format(localVarUrlObj), @@ -8796,12 +8926,71 @@ export const StatisticsApiFp = function(configuration?: Configuration) { }, /** * - * @param {SheetUrl} [sheetUrl] + * @param {number} [courseId] + * @param {string} [userId] + * @param {string} [sheetUrl] + * @param {string} [sheetName] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiStatisticsExportToSheetGet(courseId?: number, userId?: string, sheetUrl?: string, sheetName?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).apiStatisticsExportToSheetGet(courseId, userId, sheetUrl, sheetName, options); + return (fetch: FetchAPI = portableFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response.json(); + } else { + throw response; + } + }); + }; + }, + /** + * + * @param {number} [courseId] + * @param {string} [userId] + * @param {string} [sheetName] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiStatisticsGetFileGet(courseId?: number, userId?: string, sheetName?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).apiStatisticsGetFileGet(courseId, userId, sheetName, options); + return (fetch: FetchAPI = portableFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response; + } else { + throw response; + } + }); + }; + }, + /** + * + * @param {string} [sheetUrl] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiStatisticsGetSheetTitlesGet(sheetUrl?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).apiStatisticsGetSheetTitlesGet(sheetUrl, options); + return (fetch: FetchAPI = portableFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response.json(); + } else { + throw response; + } + }); + }; + }, + /** + * + * @param {string} [sheetUrl] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsGetSheetTitlesPost(sheetUrl?: SheetUrl, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { - const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).apiStatisticsGetSheetTitlesPost(sheetUrl, options); + apiStatisticsProcessLinkPost(sheetUrl?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).apiStatisticsProcessLinkPost(sheetUrl, options); return (fetch: FetchAPI = portableFetch, basePath: string = BASE_PATH) => { return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { if (response.status >= 200 && response.status < 300) { @@ -8850,12 +9039,44 @@ export const StatisticsApiFactory = function (configuration?: Configuration, fet }, /** * - * @param {SheetUrl} [sheetUrl] + * @param {number} [courseId] + * @param {string} [userId] + * @param {string} [sheetUrl] + * @param {string} [sheetName] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsGetSheetTitlesPost(sheetUrl?: SheetUrl, options?: any) { - return StatisticsApiFp(configuration).apiStatisticsGetSheetTitlesPost(sheetUrl, options)(fetch, basePath); + apiStatisticsExportToSheetGet(courseId?: number, userId?: string, sheetUrl?: string, sheetName?: string, options?: any) { + return StatisticsApiFp(configuration).apiStatisticsExportToSheetGet(courseId, userId, sheetUrl, sheetName, options)(fetch, basePath); + }, + /** + * + * @param {number} [courseId] + * @param {string} [userId] + * @param {string} [sheetName] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiStatisticsGetFileGet(courseId?: number, userId?: string, sheetName?: string, options?: any) { + return StatisticsApiFp(configuration).apiStatisticsGetFileGet(courseId, userId, sheetName, options)(fetch, basePath); + }, + /** + * + * @param {string} [sheetUrl] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiStatisticsGetSheetTitlesGet(sheetUrl?: string, options?: any) { + return StatisticsApiFp(configuration).apiStatisticsGetSheetTitlesGet(sheetUrl, options)(fetch, basePath); + }, + /** + * + * @param {string} [sheetUrl] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiStatisticsProcessLinkPost(sheetUrl?: string, options?: any) { + return StatisticsApiFp(configuration).apiStatisticsProcessLinkPost(sheetUrl, options)(fetch, basePath); }, }; }; @@ -8902,13 +9123,51 @@ export class StatisticsApi extends BaseAPI { /** * - * @param {SheetUrl} [sheetUrl] + * @param {number} [courseId] + * @param {string} [userId] + * @param {string} [sheetUrl] + * @param {string} [sheetName] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof StatisticsApi + */ + public apiStatisticsExportToSheetGet(courseId?: number, userId?: string, sheetUrl?: string, sheetName?: string, options?: any) { + return StatisticsApiFp(this.configuration).apiStatisticsExportToSheetGet(courseId, userId, sheetUrl, sheetName, options)(this.fetch, this.basePath); + } + + /** + * + * @param {number} [courseId] + * @param {string} [userId] + * @param {string} [sheetName] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof StatisticsApi + */ + public apiStatisticsGetFileGet(courseId?: number, userId?: string, sheetName?: string, options?: any) { + return StatisticsApiFp(this.configuration).apiStatisticsGetFileGet(courseId, userId, sheetName, options)(this.fetch, this.basePath); + } + + /** + * + * @param {string} [sheetUrl] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof StatisticsApi + */ + public apiStatisticsGetSheetTitlesGet(sheetUrl?: string, options?: any) { + return StatisticsApiFp(this.configuration).apiStatisticsGetSheetTitlesGet(sheetUrl, options)(this.fetch, this.basePath); + } + + /** + * + * @param {string} [sheetUrl] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof StatisticsApi */ - public apiStatisticsGetSheetTitlesPost(sheetUrl?: SheetUrl, options?: any) { - return StatisticsApiFp(this.configuration).apiStatisticsGetSheetTitlesPost(sheetUrl, options)(this.fetch, this.basePath); + public apiStatisticsProcessLinkPost(sheetUrl?: string, options?: any) { + return StatisticsApiFp(this.configuration).apiStatisticsProcessLinkPost(sheetUrl, options)(this.fetch, this.basePath); } } diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index a482c4139..899d1e091 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -72,9 +72,25 @@ interface IPageState { tabValue: TabValue } +const getLastViewedCourseId = () => +{ + const sessionStorageCourseId = sessionStorage.getItem("courseId") + return sessionStorageCourseId === null ? "-1" : sessionStorageCourseId +} + +const updatedLastViewedCourseId = (courseId : string) => +{ + sessionStorage.setItem("courseId", courseId) +} + const Course: React.FC = () => { - const {courseId, tab} = useParams() + const {initialCourseId, tab} = useParams() const [searchParams] = useSearchParams() + + const isFromYandex = initialCourseId === undefined + const courseId = isFromYandex ? getLastViewedCourseId() : initialCourseId + + const navigate = useNavigate() const {enqueueSnackbar} = useSnackbar() @@ -260,6 +276,7 @@ const Course: React.FC = () => { } const setCurrentState = async () => { + updatedLastViewedCourseId(courseId) const course = await ApiSingleton.coursesApi.coursesGetCourseData(+courseId!) // У пользователя изменилась роль (иначе он не может стать лектором в курсе), @@ -283,7 +300,13 @@ const Course: React.FC = () => { mentors: course.mentors!, acceptedStudents: course.acceptedStudents!, newStudents: course.newStudents!, + studentSolutions: solutions, + tabValue: isFromYandex ? "stats" : "homeworks" })) + if (isFromYandex) + { + window.history.replaceState(null, "", `/courses/${courseId}`) + } } const getCourseFilesInfo = async () => { @@ -317,6 +340,8 @@ const Course: React.FC = () => { useEffect(() => changeTab(tab || "homeworks"), [tab, courseId, isFound]) + const userYandexId = new URLSearchParams(window.location.search).get("code") + const joinCourse = async () => { await ApiSingleton.coursesApi.coursesSignInCourse(+courseId!) .then(() => setCurrentState()); @@ -548,6 +573,7 @@ const Course: React.FC = () => { isMentor={isCourseMentor} course={courseState.course} solutions={studentSolutions} + yandexCode={userYandexId} /> } diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index 51ce4edcd..5019593fa 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -9,13 +9,14 @@ import StudentStatsUtils from "../../services/StudentStatsUtils"; import ShowChartIcon from "@mui/icons-material/ShowChart"; import {BonusTag, DefaultTags, TestTag} from "../Common/HomeworkTags"; import Lodash from "lodash" -import LoadStatsToGoogleDoc from "components/Solutions/LoadStatsToGoogleDoc"; +import SaveStats from "components/Solutions/SaveStats"; interface IStudentStatsProps { course: CourseViewModel; homeworks: HomeworkViewModel[]; isMentor: boolean; userId: string; + yandexCode: string | null; solutions: StatisticsCourseMatesModel[]; } @@ -298,10 +299,14 @@ const StudentStats: React.FC = (props) => {
- +
); } -export default StudentStats; \ No newline at end of file +export default StudentStats; diff --git a/hwproj.front/src/components/Solutions/DownloadStats.tsx b/hwproj.front/src/components/Solutions/DownloadStats.tsx new file mode 100644 index 000000000..0ab16988f --- /dev/null +++ b/hwproj.front/src/components/Solutions/DownloadStats.tsx @@ -0,0 +1,67 @@ +import React, { FC, useState } from "react"; +import { ResultString } from "../../api"; +import { Alert, Box, Button, CircularProgress, Grid, MenuItem, Select, TextField } from "@mui/material"; +import apiSingleton from "../../api/ApiSingleton"; + +interface DownloadStatsProps { + courseId: number | undefined + userId: string + onCancellation: () => void +} + +interface DownloadStatsState { + fileName: string, +} + +const DownloadStats: FC = (props: DownloadStatsProps) => { + const [state, setState] = useState({ + fileName: "", + }) + + const { fileName } = state + + const handleFileDownloading = (promise : Promise, fileName: string) => { + promise.then((response) => response.blob()) + .then((blob) => { + const url = window.URL.createObjectURL(new Blob([blob])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `${fileName}.xlsx`); + document.body.appendChild(link); + link.click(); + link.parentNode!.removeChild(link); + }) + } + + return + + + { + event.persist(); + setState({fileName: event.target.value}); + }} /> + + + + + + + + + + + +} +export default DownloadStats; \ No newline at end of file diff --git a/hwproj.front/src/components/Solutions/ExportToGoogle.tsx b/hwproj.front/src/components/Solutions/ExportToGoogle.tsx new file mode 100644 index 000000000..45ce2ea3c --- /dev/null +++ b/hwproj.front/src/components/Solutions/ExportToGoogle.tsx @@ -0,0 +1,144 @@ +import React, { FC, useState } from "react"; +import { useEffect } from 'react'; +import { ResultString } from "../../api"; +import { ResultExternalService } from "../../api"; +import { Alert, Box, Button, CircularProgress, Grid, MenuItem, Select, TextField } from "@mui/material"; +import apiSingleton from "../../api/ApiSingleton"; +import { green, red } from "@material-ui/core/colors"; + +enum LoadingStatus { + None, + Loading, + Success, + Error +} + +interface ExportToGoogleProps { + courseId: number | undefined + userId: string + onCancellation: () => void +} + +interface ExportToGoogleState { + url: string + googleSheetTitles: ResultString | undefined + selectedSheet: number + loadingStatus: LoadingStatus + error: string | null + +} + +const ExportToGoogle: FC = (props: ExportToGoogleProps) => { + const [state, setState] = useState({ + url: '', + selectedSheet: 0, + googleSheetTitles: undefined, + loadingStatus: LoadingStatus.None, + error: null + }) + + const {url, googleSheetTitles, selectedSheet, loadingStatus, error } = state + + const handleGoogleDocUrlChange = async (value: string) => { + const titles = await apiSingleton.statisticsApi.apiStatisticsGetSheetTitlesGet(value) + setState(prevState => ({ ...prevState, url: value, googleSheetTitles: titles })); + } + + const getGoogleSheetName = () => { + return (googleSheetTitles && googleSheetTitles.value + && googleSheetTitles.value.length > state.selectedSheet) + ? googleSheetTitles.value[state.selectedSheet] : ""; + } + + const buttonSx = { + ...(loadingStatus === LoadingStatus.Success && { + color: green[600], + }), + ...(loadingStatus === LoadingStatus.Error && { + color: red[600], + }), + }; + + return + + {(googleSheetTitles && !googleSheetTitles.succeeded && + + {googleSheetTitles!.errors![0]} + ) + || + (loadingStatus === LoadingStatus.Error && + + {error} + ) + || + ( + Для загрузки таблицы необходимо разрешить доступ на редактирование по ссылке для Google Sheets + ) + } + + + + + handleGoogleDocUrlChange(event.target.value) + } + /> + + {googleSheetTitles && googleSheetTitles.value && googleSheetTitles.value.length > 0 && + + } + {googleSheetTitles && googleSheetTitles.succeeded && + + + {loadingStatus === LoadingStatus.Loading && ( + + )} + + } + + + + + +} +export default ExportToGoogle; diff --git a/hwproj.front/src/components/Solutions/ExportToYandex.tsx b/hwproj.front/src/components/Solutions/ExportToYandex.tsx new file mode 100644 index 000000000..11add976c --- /dev/null +++ b/hwproj.front/src/components/Solutions/ExportToYandex.tsx @@ -0,0 +1,214 @@ +import React, { FC, useState } from "react"; +import { useEffect } from 'react'; +import { ResultString } from "../../api"; +import { ResultExternalService } from "../../api"; +import { Alert, Box, Button, CircularProgress, Grid, Link, MenuItem, Select, TextField } from "@mui/material"; +import apiSingleton from "../../api/ApiSingleton"; +import { green, red } from "@material-ui/core/colors"; + +enum LoadingStatus { + None, + Loading, + Success, + Error +} + +interface LocalStorageKey { + name: string + userId: string +} + +interface ExportToYandexProps { + courseId: number | undefined + userId: string + onCancellation: () => void + userCode: string | null +} + +interface ExportToYandexState { + fileName: string + userToken: string | null + loadingStatus: LoadingStatus + isAuthorizationError: boolean +} + +const ExportToYandex: FC = (props: ExportToYandexProps) => { + const [state, setState] = useState({ + fileName: "", + userToken: localStorage.getItem( + JSON.stringify({name: "yandexAccessToken", userId: `${props.userId}`})), + loadingStatus: LoadingStatus.None, + isAuthorizationError: false, + }) + + const { fileName, userToken, loadingStatus, isAuthorizationError } = state + + const setUserYandexToken = async (userConfirmationCode: string, userId: string) : Promise => { + const fetchBody = `grant_type=authorization_code&code=${userConfirmationCode}` + + `&client_id=${process.env.REACT_APP_YANDEX_CLIENT_ID}&client_secret=${process.env.REACT_APP_YANDEX_CLIENT_SECRET}`; + + const response = await fetch(`https://oauth.yandex.ru/token`, { + method: "post", + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; Charset=utf-8', + 'Host': 'https://oauth.yandex.ru/' + }, + body: fetchBody + }) + if (response.status === 200) { + const jsonResponse = await response.json(); + const token = jsonResponse.access_token; + if (token !== null && userId !== undefined) { + const localStorageKey : LocalStorageKey = { + name: 'yandexAccessToken', + userId: userId + } + localStorage.setItem(JSON.stringify(localStorageKey), token); + return token; + } + } + return "error"; + } + + const setCurrentState = async () => + { + if (userToken === null && props.userCode !== null) + { + const token = await setUserYandexToken(props.userCode, props.userId) + setState((prevState) => + ({...prevState, userToken: token === 'error' ? null : token, isAuthorizationError: token === 'error'})) + } + } + + useEffect(() => { + setCurrentState() + }, []) + + const handleExportClick = async () => + { + fetch(`https://cloud-api.yandex.net/v1/disk/resources/upload?path=app:/${fileName}.xlsx&overwrite=true`, + { + method: "get", + headers: { + 'Authorization': `${process.env.REACT_APP_YANDEX_AUTHORIZATION_TOKEN}`, + }}) + .then( async (response) => { + if (response.status >= 200 && response.status < 300) { + const jsonResponse = await response.json(); + const url = jsonResponse.href; + const fileData = await apiSingleton.statisticsApi.apiStatisticsGetFileGet(props.courseId, props.userId, "Лист 1"); + const data = await fileData.blob(); + const fileExportResponse = await fetch(url, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/octet-stream', + 'Content-Length': `${data.size}` + }, + body: data + }) + if (fileExportResponse.status >= 200 && fileExportResponse.status < 300) + { + setState((prevState) => ({...prevState, loadingStatus: LoadingStatus.Success})) + return; + } + } + + setState((prevState) => ({...prevState, loadingStatus: LoadingStatus.Error})) + }) + } + + const yacRequestLink = `https://oauth.yandex.ru/authorize?response_type=code&client_id=${process.env.REACT_APP_YANDEX_CLIENT_ID}` + + const buttonSx = { + ...(loadingStatus === LoadingStatus.Success && { + color: green[600], + }), + ...(loadingStatus === LoadingStatus.Error && { + color: red[600], + }), + }; + + return + {userToken === null && + + {!isAuthorizationError && + + + Для загрузки таблицы необходимо пройти авторизацию.{' '} + + Начать авторизацию + + + + } + {isAuthorizationError && + + Авторизация не пройдена. Попробуйте еще раз{' '} + + Начать авторизацию + + + } + + + + + } + {userToken !== null && + + + + Авторизация успешно пройдена. Файл будет загружен на диск по адресу + "Приложения/{process.env.REACT_APP_YANDEX_APPLICATION_NAME}/{fileName}.xlsx" + + + + + { + event.persist() + setState((prevState) => + ({...prevState, fileName: event.target.value, loadingStatus: LoadingStatus.None}))}} + /> + + + + + {loadingStatus === LoadingStatus.Loading && ( + + )} + + + + + + + + } + +} +export default ExportToYandex; diff --git a/hwproj.front/src/components/Solutions/LoadStatsToGoogleDoc.tsx b/hwproj.front/src/components/Solutions/LoadStatsToGoogleDoc.tsx deleted file mode 100644 index 6b1ae8784..000000000 --- a/hwproj.front/src/components/Solutions/LoadStatsToGoogleDoc.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React, {FC, useState} from "react"; -import {Alert, Button, Grid, MenuItem, Select, TextField} from "@mui/material"; -import {ResultString} from "../../api"; -import apiSingleton from "../../api/ApiSingleton"; - -interface LoadStatsToGoogleDocProps { -} - -interface LoadStatsToGoogleDocState { - googleDocUrl: string, - sheetTitles: ResultString | undefined, - selectedSheet: number, - isOpened: boolean -} - -const LoadStatsToGoogleDoc: FC = (props) => { - const [state, setState] = useState({ - selectedSheet: 0, - isOpened: false, - googleDocUrl: "", - sheetTitles: undefined - }) - - const {googleDocUrl, sheetTitles, isOpened, selectedSheet} = state - - //TODO: throttling - const handleGoogleDocUrlChange = async (value: string) => { - const titles = value === "" - ? undefined - : await apiSingleton.statisticsApi.apiStatisticsGetSheetTitlesPost({url: value}) - setState(prevState => ({...prevState, googleDocUrl: value, sheetTitles: titles})); - } - - return !isOpened - ? - : - - - Для загрузки таблицы необходимо разрешить доступ на редактирование по ссылке для Google Docs - страницы - - - - - { - event.persist() - handleGoogleDocUrlChange(event.target.value) - }}/> - - {sheetTitles && !sheetTitles.succeeded && - - {sheetTitles!.errors![0]} - - } - {sheetTitles && sheetTitles.value && sheetTitles.value.length > 0 && - - } - {sheetTitles && sheetTitles.succeeded && - - } - { - - } - - -} -export default LoadStatsToGoogleDoc; diff --git a/hwproj.front/src/components/Solutions/SaveStats.tsx b/hwproj.front/src/components/Solutions/SaveStats.tsx new file mode 100644 index 000000000..de6620c72 --- /dev/null +++ b/hwproj.front/src/components/Solutions/SaveStats.tsx @@ -0,0 +1,135 @@ +import React, {FC, useState} from "react"; +import {Alert, Box, Button, CircularProgress, Grid, MenuItem, Select, TextField} from "@mui/material"; +import SpeedDialIcon from '@mui/material/SpeedDialIcon'; +import GetAppIcon from '@material-ui/icons/GetApp'; +import SpeedDial from '@mui/material/SpeedDial'; +import SaveIcon from '@mui/icons-material/Save'; +import ShareIcon from '@mui/icons-material/Share'; +import SpeedDialAction from '@mui/material/SpeedDialAction'; +import {ResultString} from "../../api"; +import apiSingleton from "../../api/ApiSingleton"; +import {green} from "@mui/material/colors"; +import ExportToGoogle from "components/Solutions/ExportToGoogle"; +import ExportToYandex from "components/Solutions/ExportToYandex"; +import DownloadStats from "components/Solutions/DownloadStats"; +import YandexLogo from './YandexLogo.svg'; +import GoogleIcon from '@mui/icons-material/Google'; + +interface SaveStatsProps { + courseId : number | undefined; + userId : string; + yandexCode: string | null; +} + +enum SpeedDialActions { + None, + Download, + ShareWithGoogle, + ShareWithYandex +} + +enum SpeedDialView { + Opened, + Expanded +} + +interface SaveStatsState { + selectedAction: SpeedDialActions; + speedDialView : SpeedDialView; +} + +const SaveStats: FC = (props : SaveStatsProps) => { + const [state, setState] = useState({ + selectedAction: props.yandexCode === null ? SpeedDialActions.None : SpeedDialActions.ShareWithYandex, + speedDialView: SpeedDialView.Opened + }) + + const {selectedAction, speedDialView} = state + + const handleCancellation = () => { + setState(prevState => ({...prevState, speedDialView: SpeedDialView.Opened, selectedAction: SpeedDialActions.None})); + } + + const handleSpeedDialItemClick = (operation : string) => { + switch ( operation ) { + case 'download': + setState(prevState => + ({...prevState, selectedAction: SpeedDialActions.Download})); + break; + case 'shareWithGoogle': + setState(prevState => + ({...prevState, selectedAction: SpeedDialActions.ShareWithGoogle})); + break; + case 'shareWithYandex': + setState(prevState => + ({...prevState, selectedAction: SpeedDialActions.ShareWithYandex})); + break; + default: + break; + } + } + + const handleChangeSpeedDialView = () => + setState(prevState => ({ + ...prevState, + speedDialView: speedDialView === SpeedDialView.Opened ? SpeedDialView.Expanded : SpeedDialView.Opened + })) + + const actions = [ + { icon: , name: 'Сохранить', operation: 'download' }, + { icon: , name: 'Отправить в Google', operation: 'shareWithGoogle' }, + // Icon by Icons8 ("https://icons8.com") + { icon: Y, name: 'Отправить в Яндекс', operation: 'shareWithYandex' }, + ]; + + return ( +
+ {selectedAction === SpeedDialActions.None && + + } + direction="right" + onClick={handleChangeSpeedDialView} + sx={{ '& .MuiFab-primary': { width: 45, height: 45 } }} + open={speedDialView === SpeedDialView.Expanded} + > + {actions.map((action) => ( + { + handleSpeedDialItemClick(action.operation) + }} + /> + ))} + + + } + {selectedAction === SpeedDialActions.Download && + handleCancellation()} + /> + } + {selectedAction === SpeedDialActions.ShareWithGoogle && + handleCancellation()} + /> + } + {selectedAction === SpeedDialActions.ShareWithYandex && + handleCancellation()} + userCode={props.yandexCode} + /> + } +
+ ) +} +export default SaveStats; diff --git a/hwproj.front/src/components/Solutions/YandexLogo.svg b/hwproj.front/src/components/Solutions/YandexLogo.svg new file mode 100644 index 000000000..25a7ddddd --- /dev/null +++ b/hwproj.front/src/components/Solutions/YandexLogo.svg @@ -0,0 +1 @@ + \ No newline at end of file From f8c8635d4d1eb18f16eb15f485bdb8268f25a81d Mon Sep 17 00:00:00 2001 From: Alex Berezhnykh Date: Sun, 7 May 2023 04:52:48 +0300 Subject: [PATCH 07/58] StudentSolutionsPage: support students navigation --- hwproj.front/src/services/StudentStatsUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hwproj.front/src/services/StudentStatsUtils.ts b/hwproj.front/src/services/StudentStatsUtils.ts index bcb6708ad..2145ea591 100644 --- a/hwproj.front/src/services/StudentStatsUtils.ts +++ b/hwproj.front/src/services/StudentStatsUtils.ts @@ -1,4 +1,4 @@ -import {Solution, SolutionState} from "../api"; +import {Solution, SolutionState} from "../api"; import {colorBetween} from "./JsUtils"; import Utils from "./Utils"; From 8ff0ea08fd3dc14441cf6bb4b1f782a5c18f1d54 Mon Sep 17 00:00:00 2001 From: Alex Berezhnykh Date: Mon, 8 May 2023 00:20:16 +0300 Subject: [PATCH 08/58] StudentSolutionsPage: optimize requests (#246) --- .../Controllers/SolutionsController.cs | 23 +++++++++++++++++++ .../Controllers/StatisticsController.cs | 10 ++++---- .../Models/TaskSolutionsPageModel.cs | 12 ++++++++++ hwproj.front/src/App.tsx | 2 +- 4 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Models/TaskSolutionsPageModel.cs diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs index cb1793305..93d01cc18 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs @@ -259,6 +259,29 @@ public async Task GetTaskSolutionsPageData(long taskId) return Ok(result); } + [Authorize] + [HttpGet("courses/{courseId}/task/{taskId}")] + [ProducesResponseType(typeof(UserTaskSolutionPreviews[]), (int)HttpStatusCode.OK)] + public async Task GetCourseTaskSolutionsPageData(long courseId, long taskId) + { + var statistics = await _solutionsClient.GetCourseTaskStatistics(courseId, taskId, UserId); + + var studentIds = statistics.Select(t => t.StudentId).ToArray(); + var usersData = await AuthServiceClient.GetAccountsData(studentIds); + + var result = statistics + .Zip(usersData, (statistic, accountData) => new UserTaskSolutionPreviews + { + Solutions = statistic.Solutions.Select(s => new StatisticsCourseSolutionsModel(s)).ToArray(), + User = accountData + }) + .OrderBy(t => t.User.Surname) + .ThenBy(t => t.User.Surname) + .ToArray(); + + return Ok(result); + } + [HttpPost("{taskId}")] [Authorize(Roles = Roles.StudentRole)] [ProducesResponseType(typeof(long), (int)HttpStatusCode.OK)] diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs index b907f2683..c4e3ad2d2 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs @@ -151,13 +151,12 @@ public async Task GetChartStatistics(long courseId) /// Implements file download. /// /// The course Id the report is based on. - /// Id of the user requesting the report. /// Name of the sheet on which the report will be generated. /// File download process. [HttpGet("getFile")] - public async Task GetFile(long courseId, string userId, string sheetName) + public async Task GetFile(long courseId, string sheetName) { - var course = await _coursesClient.GetCourseById(courseId, userId); + var course = await _coursesClient.GetCourseById(courseId); var statistics = await GetStatistics(courseId); if (statistics == null || course == null) return Forbid(); @@ -182,15 +181,14 @@ public Result ProcessLink(string? sheetUrl) /// Implements sending a report to the Google Sheets. /// /// The course Id the report is based on. - /// Id of the user requesting the report. /// Sheet Url parameter, required to make requests to the Google Sheets. /// Sheet Name parameter, required to make requests to the Google Sheets. /// Operation status. [HttpGet("exportToSheet")] public async Task ExportToGoogleSheets( - long courseId, string userId, string sheetUrl, string sheetName) + long courseId, string sheetUrl, string sheetName) { - var course = await _coursesClient.GetCourseById(courseId, userId); + var course = await _coursesClient.GetCourseById(courseId); var statistics = await GetStatistics(courseId); if (course == null || statistics == null) return Result.Failed("Ошибка при получении статистики"); var result = await _googleService.Export(course, statistics, sheetUrl, sheetName); diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Models/TaskSolutionsPageModel.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Models/TaskSolutionsPageModel.cs new file mode 100644 index 000000000..240c8491b --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Models/TaskSolutionsPageModel.cs @@ -0,0 +1,12 @@ +using HwProj.APIGateway.API.Models.Solutions; +using HwProj.Models.CoursesService.ViewModels; + +namespace HwProj.APIGateway.API.Models +{ + public class TaskSolutionsPageModel + { + public UserTaskSolutions[] StudentsTaskSolutions { get; set; } + public HomeworkTaskViewModel Task { get; set; } + public CoursePreview Course { get; set; } + } +} diff --git a/hwproj.front/src/App.tsx b/hwproj.front/src/App.tsx index 5d3ad5834..d5df5b77c 100644 --- a/hwproj.front/src/App.tsx +++ b/hwproj.front/src/App.tsx @@ -136,4 +136,4 @@ class App extends Component<{ navigate: any }, AppState> { } } -export default withRouter(App); +export default withRouter(App); \ No newline at end of file From 7fc0f18ce211e8833712aa2b20b900cde04d0965 Mon Sep 17 00:00:00 2001 From: Alex Berezhnykh Date: Mon, 8 May 2023 00:54:01 +0300 Subject: [PATCH 09/58] Revert "StudentSolutionsPage: optimize requests (#246)" This reverts commit 945eabcb2d779a06184506ce3da0566c533beb26. --- .../Controllers/SolutionsController.cs | 24 - .../Models/TaskSolutionsPageModel.cs | 12 - .../SolutionsService/Solution.cs | 6 +- .../SolutionsService/StudentSolutions.cs | 8 - hwproj.front/src/api/api.ts | 727 +++++++++--------- 5 files changed, 364 insertions(+), 413 deletions(-) delete mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Models/TaskSolutionsPageModel.cs delete mode 100644 HwProj.Common/HwProj.Models/SolutionsService/StudentSolutions.cs diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs index 93d01cc18..2f340c795 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs @@ -13,7 +13,6 @@ using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.Roles; using HwProj.Models.SolutionsService; -using HwProj.Models.StatisticsService; using HwProj.SolutionsService.Client; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -259,29 +258,6 @@ public async Task GetTaskSolutionsPageData(long taskId) return Ok(result); } - [Authorize] - [HttpGet("courses/{courseId}/task/{taskId}")] - [ProducesResponseType(typeof(UserTaskSolutionPreviews[]), (int)HttpStatusCode.OK)] - public async Task GetCourseTaskSolutionsPageData(long courseId, long taskId) - { - var statistics = await _solutionsClient.GetCourseTaskStatistics(courseId, taskId, UserId); - - var studentIds = statistics.Select(t => t.StudentId).ToArray(); - var usersData = await AuthServiceClient.GetAccountsData(studentIds); - - var result = statistics - .Zip(usersData, (statistic, accountData) => new UserTaskSolutionPreviews - { - Solutions = statistic.Solutions.Select(s => new StatisticsCourseSolutionsModel(s)).ToArray(), - User = accountData - }) - .OrderBy(t => t.User.Surname) - .ThenBy(t => t.User.Surname) - .ToArray(); - - return Ok(result); - } - [HttpPost("{taskId}")] [Authorize(Roles = Roles.StudentRole)] [ProducesResponseType(typeof(long), (int)HttpStatusCode.OK)] diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Models/TaskSolutionsPageModel.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Models/TaskSolutionsPageModel.cs deleted file mode 100644 index 240c8491b..000000000 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Models/TaskSolutionsPageModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -using HwProj.APIGateway.API.Models.Solutions; -using HwProj.Models.CoursesService.ViewModels; - -namespace HwProj.APIGateway.API.Models -{ - public class TaskSolutionsPageModel - { - public UserTaskSolutions[] StudentsTaskSolutions { get; set; } - public HomeworkTaskViewModel Task { get; set; } - public CoursePreview Course { get; set; } - } -} diff --git a/HwProj.Common/HwProj.Models/SolutionsService/Solution.cs b/HwProj.Common/HwProj.Models/SolutionsService/Solution.cs index 3a41dfeec..5a0063b43 100644 --- a/HwProj.Common/HwProj.Models/SolutionsService/Solution.cs +++ b/HwProj.Common/HwProj.Models/SolutionsService/Solution.cs @@ -12,15 +12,15 @@ public class Solution : IEntity public string Comment { get; set; } public SolutionState State { get; set; } - + public int Rating { get; set; } - + public string StudentId { get; set; } public string? LecturerId { get; set; } public long? GroupId { get; set; } - + public long TaskId { get; set; } public DateTime PublicationDate { get; set; } diff --git a/HwProj.Common/HwProj.Models/SolutionsService/StudentSolutions.cs b/HwProj.Common/HwProj.Models/SolutionsService/StudentSolutions.cs deleted file mode 100644 index 29b466125..000000000 --- a/HwProj.Common/HwProj.Models/SolutionsService/StudentSolutions.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace HwProj.Models.SolutionsService -{ - public class StudentSolutions - { - public string StudentId { get; set; } - public Solution[] Solutions { get; set; } - } -} diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index 60d6769ef..74f556d47 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -270,8 +270,8 @@ export interface BooleanResult { */ export interface CategorizedNotifications { /** - * - * @type {CategoryState} + * + * @type {number} * @memberof CategorizedNotifications */ category?: CategoryState; @@ -907,8 +907,8 @@ export interface GetSolutionModel { */ comment?: string; /** - * - * @type {SolutionState} + * + * @type {number} * @memberof GetSolutionModel */ state?: SolutionState; @@ -1489,8 +1489,8 @@ export interface NotificationViewModel { */ owner?: string; /** - * - * @type {CategoryState} + * + * @type {number} * @memberof NotificationViewModel */ category?: CategoryState; @@ -1708,6 +1708,32 @@ export interface Result { errors?: Array; } +/** + * + * @export + * @interface ResultString + */ +export interface ResultString { + /** + * + * @type {Array} + * @memberof ResultString + */ + value?: Array; + /** + * + * @type {boolean} + * @memberof ResultString + */ + succeeded?: boolean; + /** + * + * @type {Array} + * @memberof ResultString + */ + errors?: Array; +} + /** * * @export @@ -1715,19 +1741,19 @@ export interface Result { */ export interface ResultTokenCredentials { /** - * + * * @type {TokenCredentials} * @memberof ResultTokenCredentials */ value?: TokenCredentials; /** - * + * * @type {boolean} * @memberof ResultTokenCredentials */ succeeded?: boolean; /** - * + * * @type {Array} * @memberof ResultTokenCredentials */ @@ -1798,8 +1824,8 @@ export interface Solution { */ comment?: string; /** - * - * @type {SolutionState} + * + * @type {number} * @memberof Solution */ state?: SolutionState; @@ -2350,8 +2376,8 @@ export interface TaskDeadlineView { */ deadline?: TaskDeadlineDto; /** - * - * @type {SolutionState} + * + * @type {number} * @memberof TaskDeadlineView */ solutionState?: SolutionState; @@ -2381,8 +2407,8 @@ export interface TaskDeadlineView { */ export interface TaskSolutionStatisticsPageData { /** - * - * @type {Array} + * + * @type {Array} * @memberof TaskSolutionStatisticsPageData */ taskSolutions?: Array; @@ -2393,8 +2419,8 @@ export interface TaskSolutionStatisticsPageData { */ courseId?: number; /** - * - * @type {Array} + * + * @type {Array} * @memberof TaskSolutionStatisticsPageData */ statsForTasks?: Array; @@ -2626,8 +2652,8 @@ export interface UserTaskSolutions { */ solutions?: Array; /** - * - * @type {StudentDataDto} + * + * @type {AccountDataDto} * @memberof UserTaskSolutions */ student?: StudentDataDto; @@ -2688,30 +2714,17 @@ export interface UserTaskSolutionsPageData { */ courseMates?: Array; /** - * - * @type {Array} + * + * @type {HomeworkTaskViewModel} * @memberof UserTaskSolutionsPageData */ - taskSolutions?: Array; -} -/** - * - * @export - * @interface WorkspaceViewModel - */ -export interface WorkspaceViewModel { - /** - * - * @type {Array} - * @memberof WorkspaceViewModel - */ - students?: Array; + task?: HomeworkTaskViewModel; /** - * - * @type {Array} - * @memberof WorkspaceViewModel + * + * @type {Array} + * @memberof UserTaskSolutionsPageData */ - homeworks?: Array; + taskSolutions?: Array; } /** * AccountApi - fetch parameter creator @@ -2720,8 +2733,8 @@ export interface WorkspaceViewModel { export const AccountApiFetchParamCreator = function (configuration?: Configuration) { return { /** - * - * @param {string} [code] + * + * @param {EditExternalViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -2755,8 +2768,8 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {EditAccountViewModel} [body] + * + * @param {EditAccountViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -2820,8 +2833,8 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {UrlDto} [body] + * + * @param {string} userId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -2885,8 +2898,7 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {string} userId + * * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -2921,8 +2933,8 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {InviteLecturerViewModel} [body] + * + * @param {InviteLecturerViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -2956,8 +2968,8 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {LoginViewModel} [body] + * + * @param {LoginViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3021,8 +3033,8 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {RegisterViewModel} [body] + * + * @param {RegisterViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3056,8 +3068,8 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {RequestPasswordRecoveryViewModel} [body] + * + * @param {RequestPasswordRecoveryViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3091,8 +3103,8 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {ResetPasswordViewModel} [body] + * + * @param {ResetPasswordViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3135,8 +3147,8 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati export const AccountApiFp = function(configuration?: Configuration) { return { /** - * - * @param {string} [code] + * + * @param {EditExternalViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3153,8 +3165,8 @@ export const AccountApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {EditAccountViewModel} [body] + * + * @param {EditAccountViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3188,8 +3200,8 @@ export const AccountApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {UrlDto} [body] + * + * @param {string} userId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3223,8 +3235,8 @@ export const AccountApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {string} userId + * + * @param {InviteLecturerViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3241,8 +3253,8 @@ export const AccountApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {InviteLecturerViewModel} [body] + * + * @param {LoginViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3259,8 +3271,7 @@ export const AccountApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {LoginViewModel} [body] + * * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3277,7 +3288,8 @@ export const AccountApiFp = function(configuration?: Configuration) { }; }, /** - * + * + * @param {RegisterViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3294,8 +3306,8 @@ export const AccountApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {RegisterViewModel} [body] + * + * @param {RequestPasswordRecoveryViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3312,8 +3324,8 @@ export const AccountApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {RequestPasswordRecoveryViewModel} [body] + * + * @param {ResetPasswordViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3357,8 +3369,8 @@ export const AccountApiFp = function(configuration?: Configuration) { export const AccountApiFactory = function (configuration?: Configuration, fetch?: FetchAPI, basePath?: string) { return { /** - * - * @param {string} [code] + * + * @param {EditExternalViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3366,8 +3378,8 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? return AccountApiFp(configuration).accountAuthorizeGithub(code, options)(fetch, basePath); }, /** - * - * @param {EditAccountViewModel} [body] + * + * @param {EditAccountViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3383,8 +3395,8 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? return AccountApiFp(configuration).accountGetAllStudents(options)(fetch, basePath); }, /** - * - * @param {UrlDto} [body] + * + * @param {string} userId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3400,8 +3412,8 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? return AccountApiFp(configuration).accountGetUserData(options)(fetch, basePath); }, /** - * - * @param {string} userId + * + * @param {InviteLecturerViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3409,8 +3421,8 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? return AccountApiFp(configuration).accountGetUserDataById(userId, options)(fetch, basePath); }, /** - * - * @param {InviteLecturerViewModel} [body] + * + * @param {LoginViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3418,8 +3430,7 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? return AccountApiFp(configuration).accountInviteNewLecturer(body, options)(fetch, basePath); }, /** - * - * @param {LoginViewModel} [body] + * * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3427,7 +3438,8 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? return AccountApiFp(configuration).accountLogin(body, options)(fetch, basePath); }, /** - * + * + * @param {RegisterViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3435,8 +3447,8 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? return AccountApiFp(configuration).accountRefreshToken(options)(fetch, basePath); }, /** - * - * @param {RegisterViewModel} [body] + * + * @param {RequestPasswordRecoveryViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3444,8 +3456,8 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? return AccountApiFp(configuration).accountRegister(body, options)(fetch, basePath); }, /** - * - * @param {RequestPasswordRecoveryViewModel} [body] + * + * @param {ResetPasswordViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3472,8 +3484,8 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? */ export class AccountApi extends BaseAPI { /** - * - * @param {string} [code] + * + * @param {EditExternalViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AccountApi @@ -3483,8 +3495,8 @@ export class AccountApi extends BaseAPI { } /** - * - * @param {EditAccountViewModel} [body] + * + * @param {EditAccountViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AccountApi @@ -3504,8 +3516,8 @@ export class AccountApi extends BaseAPI { } /** - * - * @param {UrlDto} [body] + * + * @param {string} userId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AccountApi @@ -3525,8 +3537,8 @@ export class AccountApi extends BaseAPI { } /** - * - * @param {string} userId + * + * @param {InviteLecturerViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AccountApi @@ -3536,8 +3548,8 @@ export class AccountApi extends BaseAPI { } /** - * - * @param {InviteLecturerViewModel} [body] + * + * @param {LoginViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AccountApi @@ -3547,8 +3559,7 @@ export class AccountApi extends BaseAPI { } /** - * - * @param {LoginViewModel} [body] + * * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AccountApi @@ -3558,7 +3569,8 @@ export class AccountApi extends BaseAPI { } /** - * + * + * @param {RegisterViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AccountApi @@ -3568,8 +3580,8 @@ export class AccountApi extends BaseAPI { } /** - * - * @param {RegisterViewModel} [body] + * + * @param {RequestPasswordRecoveryViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AccountApi @@ -3579,8 +3591,8 @@ export class AccountApi extends BaseAPI { } /** - * - * @param {RequestPasswordRecoveryViewModel} [body] + * + * @param {ResetPasswordViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AccountApi @@ -3655,9 +3667,9 @@ export const CourseGroupsApiFetchParamCreator = function (configuration?: Config }; }, /** - * - * @param {number} courseId - * @param {CreateGroupViewModel} [body] + * + * @param {number} courseId + * @param {CreateGroupViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3810,8 +3822,10 @@ export const CourseGroupsApiFetchParamCreator = function (configuration?: Config }; }, /** - * - * @param {number} groupId + * + * @param {number} courseId + * @param {number} groupId + * @param {string} [userId] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3846,8 +3860,10 @@ export const CourseGroupsApiFetchParamCreator = function (configuration?: Config }; }, /** - * - * @param {number} groupId + * + * @param {number} courseId + * @param {number} groupId + * @param {UpdateGroupViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3882,10 +3898,8 @@ export const CourseGroupsApiFetchParamCreator = function (configuration?: Config }; }, /** - * - * @param {number} courseId - * @param {number} groupId - * @param {string} [userId] + * + * @param {number} groupId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3929,10 +3943,8 @@ export const CourseGroupsApiFetchParamCreator = function (configuration?: Config }; }, /** - * - * @param {number} courseId - * @param {number} groupId - * @param {UpdateGroupViewModel} [body] + * + * @param {number} groupId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4005,9 +4017,9 @@ export const CourseGroupsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} courseId - * @param {CreateGroupViewModel} [body] + * + * @param {number} courseId + * @param {CreateGroupViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4079,8 +4091,10 @@ export const CourseGroupsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} groupId + * + * @param {number} courseId + * @param {number} groupId + * @param {string} [userId] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4097,8 +4111,10 @@ export const CourseGroupsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} groupId + * + * @param {number} courseId + * @param {number} groupId + * @param {UpdateGroupViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4115,10 +4131,8 @@ export const CourseGroupsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} courseId - * @param {number} groupId - * @param {string} [userId] + * + * @param {number} groupId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4135,10 +4149,8 @@ export const CourseGroupsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} courseId - * @param {number} groupId - * @param {UpdateGroupViewModel} [body] + * + * @param {number} groupId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4175,9 +4187,9 @@ export const CourseGroupsApiFactory = function (configuration?: Configuration, f return CourseGroupsApiFp(configuration).courseGroupsAddStudentInGroup(courseId, groupId, userId, options)(fetch, basePath); }, /** - * - * @param {number} courseId - * @param {CreateGroupViewModel} [body] + * + * @param {number} courseId + * @param {CreateGroupViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4213,8 +4225,10 @@ export const CourseGroupsApiFactory = function (configuration?: Configuration, f return CourseGroupsApiFp(configuration).courseGroupsGetCourseGroupsById(courseId, options)(fetch, basePath); }, /** - * - * @param {number} groupId + * + * @param {number} courseId + * @param {number} groupId + * @param {string} [userId] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4222,8 +4236,10 @@ export const CourseGroupsApiFactory = function (configuration?: Configuration, f return CourseGroupsApiFp(configuration).courseGroupsGetGroup(groupId, options)(fetch, basePath); }, /** - * - * @param {number} groupId + * + * @param {number} courseId + * @param {number} groupId + * @param {UpdateGroupViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4231,10 +4247,8 @@ export const CourseGroupsApiFactory = function (configuration?: Configuration, f return CourseGroupsApiFp(configuration).courseGroupsGetGroupTasks(groupId, options)(fetch, basePath); }, /** - * - * @param {number} courseId - * @param {number} groupId - * @param {string} [userId] + * + * @param {number} groupId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4242,10 +4256,8 @@ export const CourseGroupsApiFactory = function (configuration?: Configuration, f return CourseGroupsApiFp(configuration).courseGroupsRemoveStudentFromGroup(courseId, groupId, userId, options)(fetch, basePath); }, /** - * - * @param {number} courseId - * @param {number} groupId - * @param {UpdateGroupViewModel} [body] + * + * @param {number} groupId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4276,9 +4288,9 @@ export class CourseGroupsApi extends BaseAPI { } /** - * - * @param {number} courseId - * @param {CreateGroupViewModel} [body] + * + * @param {number} courseId + * @param {CreateGroupViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CourseGroupsApi @@ -4322,8 +4334,10 @@ export class CourseGroupsApi extends BaseAPI { } /** - * - * @param {number} groupId + * + * @param {number} courseId + * @param {number} groupId + * @param {string} [userId] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CourseGroupsApi @@ -4333,8 +4347,10 @@ export class CourseGroupsApi extends BaseAPI { } /** - * - * @param {number} groupId + * + * @param {number} courseId + * @param {number} groupId + * @param {UpdateGroupViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CourseGroupsApi @@ -4344,10 +4360,8 @@ export class CourseGroupsApi extends BaseAPI { } /** - * - * @param {number} courseId - * @param {number} groupId - * @param {string} [userId] + * + * @param {number} groupId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CourseGroupsApi @@ -4357,10 +4371,8 @@ export class CourseGroupsApi extends BaseAPI { } /** - * - * @param {number} courseId - * @param {number} groupId - * @param {UpdateGroupViewModel} [body] + * + * @param {number} groupId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CourseGroupsApi @@ -4532,10 +4544,8 @@ export const CoursesApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {number} courseId - * @param {string} mentorId - * @param {EditMentorWorkspaceDTO} [body] + * + * @param {number} courseId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4615,7 +4625,8 @@ export const CoursesApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * + * + * @param {CreateCourseViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4968,9 +4979,9 @@ export const CoursesApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {number} courseId - * @param {UpdateCourseViewModel} [body] + * + * @param {number} courseId + * @param {UpdateCourseViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5009,10 +5020,7 @@ export const CoursesApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {number} courseId - * @param {string} studentId - * @param {StudentCharacteristicsDto} [body] + * * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5139,10 +5147,8 @@ export const CoursesApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} courseId - * @param {string} mentorId - * @param {EditMentorWorkspaceDTO} [body] + * + * @param {number} courseId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5177,7 +5183,8 @@ export const CoursesApiFp = function(configuration?: Configuration) { }; }, /** - * + * + * @param {CreateCourseViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5194,8 +5201,7 @@ export const CoursesApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} courseId + * * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5356,9 +5362,9 @@ export const CoursesApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} courseId - * @param {UpdateCourseViewModel} [body] + * + * @param {number} courseId + * @param {UpdateCourseViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5375,10 +5381,7 @@ export const CoursesApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} courseId - * @param {string} studentId - * @param {StudentCharacteristicsDto} [body] + * * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5442,10 +5445,8 @@ export const CoursesApiFactory = function (configuration?: Configuration, fetch? return CoursesApiFp(configuration).coursesDeleteCourse(courseId, options)(fetch, basePath); }, /** - * - * @param {number} courseId - * @param {string} mentorId - * @param {EditMentorWorkspaceDTO} [body] + * + * @param {number} courseId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5453,8 +5454,8 @@ export const CoursesApiFactory = function (configuration?: Configuration, fetch? return CoursesApiFp(configuration).coursesEditMentorWorkspace(courseId, mentorId, body, options)(fetch, basePath); }, /** - * - * @param {number} courseId + * + * @param {CreateCourseViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5551,9 +5552,9 @@ export const CoursesApiFactory = function (configuration?: Configuration, fetch? return CoursesApiFp(configuration).coursesSignInCourse(courseId, options)(fetch, basePath); }, /** - * - * @param {number} courseId - * @param {UpdateCourseViewModel} [body] + * + * @param {number} courseId + * @param {UpdateCourseViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5561,10 +5562,7 @@ export const CoursesApiFactory = function (configuration?: Configuration, fetch? return CoursesApiFp(configuration).coursesUpdateCourse(courseId, body, options)(fetch, basePath); }, /** - * - * @param {number} courseId - * @param {string} studentId - * @param {StudentCharacteristicsDto} [body] + * * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5628,10 +5626,8 @@ export class CoursesApi extends BaseAPI { } /** - * - * @param {number} courseId - * @param {string} mentorId - * @param {EditMentorWorkspaceDTO} [body] + * + * @param {number} courseId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CoursesApi @@ -5641,8 +5637,8 @@ export class CoursesApi extends BaseAPI { } /** - * - * @param {number} courseId + * + * @param {CreateCourseViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CoursesApi @@ -5761,9 +5757,9 @@ export class CoursesApi extends BaseAPI { } /** - * - * @param {number} courseId - * @param {UpdateCourseViewModel} [body] + * + * @param {number} courseId + * @param {UpdateCourseViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CoursesApi @@ -5773,10 +5769,7 @@ export class CoursesApi extends BaseAPI { } /** - * - * @param {number} courseId - * @param {string} studentId - * @param {StudentCharacteristicsDto} [body] + * * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CoursesApi @@ -6830,9 +6823,9 @@ export class FilesApi extends BaseAPI { export const HomeworksApiFetchParamCreator = function (configuration?: Configuration) { return { /** - * - * @param {number} courseId - * @param {CreateHomeworkViewModel} [body] + * + * @param {number} courseId + * @param {CreateHomeworkViewModel} [homeworkViewModel] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -6979,9 +6972,9 @@ export const HomeworksApiFetchParamCreator = function (configuration?: Configura }; }, /** - * - * @param {number} homeworkId - * @param {CreateHomeworkViewModel} [body] + * + * @param {number} homeworkId + * @param {CreateHomeworkViewModel} [homeworkViewModel] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7029,9 +7022,9 @@ export const HomeworksApiFetchParamCreator = function (configuration?: Configura export const HomeworksApiFp = function(configuration?: Configuration) { return { /** - * - * @param {number} courseId - * @param {CreateHomeworkViewModel} [body] + * + * @param {number} courseId + * @param {CreateHomeworkViewModel} [homeworkViewModel] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7102,9 +7095,9 @@ export const HomeworksApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} homeworkId - * @param {CreateHomeworkViewModel} [body] + * + * @param {number} homeworkId + * @param {CreateHomeworkViewModel} [homeworkViewModel] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7130,9 +7123,9 @@ export const HomeworksApiFp = function(configuration?: Configuration) { export const HomeworksApiFactory = function (configuration?: Configuration, fetch?: FetchAPI, basePath?: string) { return { /** - * - * @param {number} courseId - * @param {CreateHomeworkViewModel} [body] + * + * @param {number} courseId + * @param {CreateHomeworkViewModel} [homeworkViewModel] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7167,9 +7160,9 @@ export const HomeworksApiFactory = function (configuration?: Configuration, fetc return HomeworksApiFp(configuration).homeworksGetHomework(homeworkId, options)(fetch, basePath); }, /** - * - * @param {number} homeworkId - * @param {CreateHomeworkViewModel} [body] + * + * @param {number} homeworkId + * @param {CreateHomeworkViewModel} [homeworkViewModel] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7187,9 +7180,9 @@ export const HomeworksApiFactory = function (configuration?: Configuration, fetc */ export class HomeworksApi extends BaseAPI { /** - * - * @param {number} courseId - * @param {CreateHomeworkViewModel} [body] + * + * @param {number} courseId + * @param {CreateHomeworkViewModel} [homeworkViewModel] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof HomeworksApi @@ -7232,9 +7225,9 @@ export class HomeworksApi extends BaseAPI { } /** - * - * @param {number} homeworkId - * @param {CreateHomeworkViewModel} [body] + * + * @param {number} homeworkId + * @param {CreateHomeworkViewModel} [homeworkViewModel] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof HomeworksApi @@ -7251,8 +7244,7 @@ export class HomeworksApi extends BaseAPI { export const NotificationsApiFetchParamCreator = function (configuration?: Configuration) { return { /** - * - * @param {NotificationsSettingDto} [body] + * * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7316,7 +7308,8 @@ export const NotificationsApiFetchParamCreator = function (configuration?: Confi }; }, /** - * + * + * @param {Array} [notificationIds] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7376,8 +7369,8 @@ export const NotificationsApiFetchParamCreator = function (configuration?: Confi }; }, /** - * - * @param {Array} [body] + * + * @param {NotificationsSettingDto} [newSetting] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7420,8 +7413,7 @@ export const NotificationsApiFetchParamCreator = function (configuration?: Confi export const NotificationsApiFp = function(configuration?: Configuration) { return { /** - * - * @param {NotificationsSettingDto} [body] + * * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7455,7 +7447,8 @@ export const NotificationsApiFp = function(configuration?: Configuration) { }; }, /** - * + * + * @param {Array} [notificationIds] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7489,8 +7482,8 @@ export const NotificationsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {Array} [body] + * + * @param {NotificationsSettingDto} [newSetting] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7516,8 +7509,7 @@ export const NotificationsApiFp = function(configuration?: Configuration) { export const NotificationsApiFactory = function (configuration?: Configuration, fetch?: FetchAPI, basePath?: string) { return { /** - * - * @param {NotificationsSettingDto} [body] + * * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7533,7 +7525,8 @@ export const NotificationsApiFactory = function (configuration?: Configuration, return NotificationsApiFp(configuration).notificationsGet(options)(fetch, basePath); }, /** - * + * + * @param {Array} [notificationIds] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7549,8 +7542,8 @@ export const NotificationsApiFactory = function (configuration?: Configuration, return NotificationsApiFp(configuration).notificationsGetSettings(options)(fetch, basePath); }, /** - * - * @param {Array} [body] + * + * @param {NotificationsSettingDto} [newSetting] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7568,8 +7561,7 @@ export const NotificationsApiFactory = function (configuration?: Configuration, */ export class NotificationsApi extends BaseAPI { /** - * - * @param {NotificationsSettingDto} [body] + * * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof NotificationsApi @@ -7589,7 +7581,8 @@ export class NotificationsApi extends BaseAPI { } /** - * + * + * @param {Array} [notificationIds] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof NotificationsApi @@ -7609,8 +7602,8 @@ export class NotificationsApi extends BaseAPI { } /** - * - * @param {Array} [body] + * + * @param {NotificationsSettingDto} [newSetting] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof NotificationsApi @@ -7703,8 +7696,9 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura }; }, /** - * - * @param {number} solutionId + * + * @param {number} taskId + * @param {SolutionViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7775,9 +7769,8 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura }; }, /** - * - * @param {number} taskId - * @param {string} studentId + * + * @param {number} taskId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7817,8 +7810,8 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura }; }, /** - * - * @param {number} taskId + * + * @param {number} solutionId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7888,8 +7881,9 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura }; }, /** - * - * @param {number} taskId + * + * @param {number} taskId + * @param {SolutionViewModel} [solution] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7924,8 +7918,9 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura }; }, /** - * - * @param {number} solutionId + * + * @param {number} solutionId + * @param {RateSolutionModel} [rateSolutionModel] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7960,9 +7955,9 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura }; }, /** - * - * @param {number} taskId - * @param {SolutionViewModel} [body] + * + * @param {number} taskId + * @param {string} studentId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8001,9 +7996,8 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura }; }, /** - * - * @param {number} taskId - * @param {SolutionViewModel} [body] + * + * @param {number} taskId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8042,9 +8036,8 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura }; }, /** - * - * @param {number} solutionId - * @param {RateSolutionModel} [body] + * + * @param {number} [taskId] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8129,8 +8122,9 @@ export const SolutionsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} solutionId + * + * @param {number} taskId + * @param {SolutionViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8165,9 +8159,8 @@ export const SolutionsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} taskId - * @param {string} studentId + * + * @param {number} taskId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8184,8 +8177,8 @@ export const SolutionsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} taskId + * + * @param {number} solutionId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8220,8 +8213,9 @@ export const SolutionsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} taskId + * + * @param {number} taskId + * @param {SolutionViewModel} [solution] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8238,8 +8232,9 @@ export const SolutionsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} solutionId + * + * @param {number} solutionId + * @param {RateSolutionModel} [rateSolutionModel] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8256,9 +8251,9 @@ export const SolutionsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} taskId - * @param {SolutionViewModel} [body] + * + * @param {number} taskId + * @param {string} studentId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8275,9 +8270,8 @@ export const SolutionsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} taskId - * @param {SolutionViewModel} [body] + * + * @param {number} taskId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8294,9 +8288,8 @@ export const SolutionsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} solutionId - * @param {RateSolutionModel} [body] + * + * @param {number} [taskId] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8331,9 +8324,9 @@ export const SolutionsApiFactory = function (configuration?: Configuration, fetc return SolutionsApiFp(configuration).solutionsDeleteSolution(solutionId, options)(fetch, basePath); }, /** - * - * @param {number} [taskId] - * @param {number} [solutionId] + * + * @param {number} taskId + * @param {SolutionViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8359,9 +8352,8 @@ export const SolutionsApiFactory = function (configuration?: Configuration, fetc return SolutionsApiFp(configuration).solutionsGetSolutionById(solutionId, options)(fetch, basePath); }, /** - * - * @param {number} taskId - * @param {string} studentId + * + * @param {number} taskId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8369,8 +8361,8 @@ export const SolutionsApiFactory = function (configuration?: Configuration, fetc return SolutionsApiFp(configuration).solutionsGetStudentSolution(taskId, studentId, options)(fetch, basePath); }, /** - * - * @param {number} taskId + * + * @param {number} solutionId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8387,8 +8379,9 @@ export const SolutionsApiFactory = function (configuration?: Configuration, fetc return SolutionsApiFp(configuration).solutionsGetUnratedSolutions(taskId, options)(fetch, basePath); }, /** - * - * @param {number} taskId + * + * @param {number} taskId + * @param {SolutionViewModel} [solution] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8396,8 +8389,9 @@ export const SolutionsApiFactory = function (configuration?: Configuration, fetc return SolutionsApiFp(configuration).solutionsGiveUp(taskId, options)(fetch, basePath); }, /** - * - * @param {number} solutionId + * + * @param {number} solutionId + * @param {RateSolutionModel} [rateSolutionModel] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8405,9 +8399,9 @@ export const SolutionsApiFactory = function (configuration?: Configuration, fetc return SolutionsApiFp(configuration).solutionsMarkSolution(solutionId, options)(fetch, basePath); }, /** - * - * @param {number} taskId - * @param {SolutionViewModel} [body] + * + * @param {number} taskId + * @param {string} studentId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8415,9 +8409,8 @@ export const SolutionsApiFactory = function (configuration?: Configuration, fetc return SolutionsApiFp(configuration).solutionsPostEmptySolutionWithRate(taskId, body, options)(fetch, basePath); }, /** - * - * @param {number} taskId - * @param {SolutionViewModel} [body] + * + * @param {number} taskId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8425,9 +8418,8 @@ export const SolutionsApiFactory = function (configuration?: Configuration, fetc return SolutionsApiFp(configuration).solutionsPostSolution(taskId, body, options)(fetch, basePath); }, /** - * - * @param {number} solutionId - * @param {RateSolutionModel} [body] + * + * @param {number} [taskId] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8456,9 +8448,9 @@ export class SolutionsApi extends BaseAPI { } /** - * - * @param {number} [taskId] - * @param {number} [solutionId] + * + * @param {number} taskId + * @param {SolutionViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof SolutionsApi @@ -8490,9 +8482,8 @@ export class SolutionsApi extends BaseAPI { } /** - * - * @param {number} taskId - * @param {string} studentId + * + * @param {number} taskId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof SolutionsApi @@ -8502,8 +8493,8 @@ export class SolutionsApi extends BaseAPI { } /** - * - * @param {number} taskId + * + * @param {number} solutionId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof SolutionsApi @@ -8524,8 +8515,9 @@ export class SolutionsApi extends BaseAPI { } /** - * - * @param {number} taskId + * + * @param {number} taskId + * @param {SolutionViewModel} [solution] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof SolutionsApi @@ -8535,8 +8527,9 @@ export class SolutionsApi extends BaseAPI { } /** - * - * @param {number} solutionId + * + * @param {number} solutionId + * @param {RateSolutionModel} [rateSolutionModel] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof SolutionsApi @@ -8546,9 +8539,9 @@ export class SolutionsApi extends BaseAPI { } /** - * - * @param {number} taskId - * @param {SolutionViewModel} [body] + * + * @param {number} taskId + * @param {string} studentId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof SolutionsApi @@ -8558,9 +8551,8 @@ export class SolutionsApi extends BaseAPI { } /** - * - * @param {number} taskId - * @param {SolutionViewModel} [body] + * + * @param {number} taskId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof SolutionsApi @@ -8570,9 +8562,8 @@ export class SolutionsApi extends BaseAPI { } /** - * - * @param {number} solutionId - * @param {RateSolutionModel} [body] + * + * @param {number} [taskId] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof SolutionsApi @@ -9348,9 +9339,9 @@ export const TasksApiFetchParamCreator = function (configuration?: Configuration }; }, /** - * - * @param {number} homeworkId - * @param {CreateTaskViewModel} [body] + * + * @param {number} homeworkId + * @param {CreateTaskViewModel} [taskViewModel] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -9497,8 +9488,9 @@ export const TasksApiFetchParamCreator = function (configuration?: Configuration }; }, /** - * - * @param {number} taskId + * + * @param {number} taskId + * @param {CreateTaskViewModel} [taskViewModel] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -9619,9 +9611,9 @@ export const TasksApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} homeworkId - * @param {CreateTaskViewModel} [body] + * + * @param {number} homeworkId + * @param {CreateTaskViewModel} [taskViewModel] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -9692,8 +9684,9 @@ export const TasksApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} taskId + * + * @param {number} taskId + * @param {CreateTaskViewModel} [taskViewModel] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -9756,9 +9749,9 @@ export const TasksApiFactory = function (configuration?: Configuration, fetch?: return TasksApiFp(configuration).tasksAddQuestionForTask(body, options)(fetch, basePath); }, /** - * - * @param {number} homeworkId - * @param {CreateTaskViewModel} [body] + * + * @param {number} homeworkId + * @param {CreateTaskViewModel} [taskViewModel] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -9793,8 +9786,9 @@ export const TasksApiFactory = function (configuration?: Configuration, fetch?: return TasksApiFp(configuration).tasksGetQuestionsForTask(taskId, options)(fetch, basePath); }, /** - * - * @param {number} taskId + * + * @param {number} taskId + * @param {CreateTaskViewModel} [taskViewModel] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -9844,9 +9838,9 @@ export class TasksApi extends BaseAPI { } /** - * - * @param {number} homeworkId - * @param {CreateTaskViewModel} [body] + * + * @param {number} homeworkId + * @param {CreateTaskViewModel} [taskViewModel] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof TasksApi @@ -9889,8 +9883,9 @@ export class TasksApi extends BaseAPI { } /** - * - * @param {number} taskId + * + * @param {number} taskId + * @param {CreateTaskViewModel} [taskViewModel] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof TasksApi From defc69c6636868cb3607fccacb2c45083d5c6dd0 Mon Sep 17 00:00:00 2001 From: Alex Berezhnykh Date: Tue, 9 May 2023 08:19:55 +0300 Subject: [PATCH 10/58] StudentSolutionsPage: optimize requests --- .../Controllers/SolutionsController.cs | 24 ++++++++++ .../Models/TaskSolutionsPageModel.cs | 12 +++++ .../SolutionsService/Solution.cs | 6 +-- .../SolutionsService/StudentSolutions.cs | 8 ++++ hwproj.front/src/api/api.ts | 44 +++++++++++++++++++ .../components/Solutions/UnratedSolutions.tsx | 2 +- 6 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Models/TaskSolutionsPageModel.cs create mode 100644 HwProj.Common/HwProj.Models/SolutionsService/StudentSolutions.cs diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs index 2f340c795..9044eb206 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs @@ -13,6 +13,7 @@ using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.Roles; using HwProj.Models.SolutionsService; +using HwProj.Models.StatisticsService; using HwProj.SolutionsService.Client; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -258,6 +259,29 @@ public async Task GetTaskSolutionsPageData(long taskId) return Ok(result); } + [Authorize] + [HttpGet("courses/{courseId}/task/{taskId}")] + [ProducesResponseType(typeof(UserTaskSolutionPreviews[]), (int)HttpStatusCode.OK)] + public async Task GetCourseTaskSolutionsPageData(long courseId, long taskId) + { + var statistics = await _solutionsClient.GetCourseTaskStatistics(courseId, taskId, UserId); + + var studentIds = statistics.Select(t => t.StudentId).ToArray(); + var usersData = await AuthServiceClient.GetAccountsData(studentIds); + + var result = statistics + .Zip(usersData, (statistic, accountData) => new UserTaskSolutionPreviews + { + Solutions = statistic.Solutions.Select(s => new StatisticsCourseSolutionsModel(s)).ToArray(), + User = accountData + }) + .OrderBy(t => t.User.Surname) + .ThenBy(t => t.User.Name) + .ToArray(); + + return Ok(result); + } + [HttpPost("{taskId}")] [Authorize(Roles = Roles.StudentRole)] [ProducesResponseType(typeof(long), (int)HttpStatusCode.OK)] diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Models/TaskSolutionsPageModel.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Models/TaskSolutionsPageModel.cs new file mode 100644 index 000000000..240c8491b --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Models/TaskSolutionsPageModel.cs @@ -0,0 +1,12 @@ +using HwProj.APIGateway.API.Models.Solutions; +using HwProj.Models.CoursesService.ViewModels; + +namespace HwProj.APIGateway.API.Models +{ + public class TaskSolutionsPageModel + { + public UserTaskSolutions[] StudentsTaskSolutions { get; set; } + public HomeworkTaskViewModel Task { get; set; } + public CoursePreview Course { get; set; } + } +} diff --git a/HwProj.Common/HwProj.Models/SolutionsService/Solution.cs b/HwProj.Common/HwProj.Models/SolutionsService/Solution.cs index 5a0063b43..3a41dfeec 100644 --- a/HwProj.Common/HwProj.Models/SolutionsService/Solution.cs +++ b/HwProj.Common/HwProj.Models/SolutionsService/Solution.cs @@ -12,15 +12,15 @@ public class Solution : IEntity public string Comment { get; set; } public SolutionState State { get; set; } - + public int Rating { get; set; } - + public string StudentId { get; set; } public string? LecturerId { get; set; } public long? GroupId { get; set; } - + public long TaskId { get; set; } public DateTime PublicationDate { get; set; } diff --git a/HwProj.Common/HwProj.Models/SolutionsService/StudentSolutions.cs b/HwProj.Common/HwProj.Models/SolutionsService/StudentSolutions.cs new file mode 100644 index 000000000..29b466125 --- /dev/null +++ b/HwProj.Common/HwProj.Models/SolutionsService/StudentSolutions.cs @@ -0,0 +1,8 @@ +namespace HwProj.Models.SolutionsService +{ + public class StudentSolutions + { + public string StudentId { get; set; } + public Solution[] Solutions { get; set; } + } +} diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index 74f556d47..cd68171bc 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -2639,6 +2639,7 @@ export interface UserDataDto { */ taskDeadlines?: Array; } + /** * * @export @@ -7726,6 +7727,8 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 localVarUrlObj.search = null; localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + const needsSerialization = ("SolutionViewModel" !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + localVarRequestOptions.body = needsSerialization ? JSON.stringify(model || {}) : (model || ""); return { url: url.format(localVarUrlObj), @@ -8140,6 +8143,25 @@ export const SolutionsApiFp = function(configuration?: Configuration) { }); }; }, + /** + * + * @param {number} courseId + * @param {number} taskId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiSolutionsCoursesByCourseIdTaskByTaskIdGet(courseId: number, taskId: number, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise> { + const localVarFetchArgs = SolutionsApiFetchParamCreator(configuration).apiSolutionsCoursesByCourseIdTaskByTaskIdGet(courseId, taskId, options); + return (fetch: FetchAPI = portableFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response.json(); + } else { + throw response; + } + }); + }; + }, /** * * @param {number} solutionId @@ -8333,6 +8355,16 @@ export const SolutionsApiFactory = function (configuration?: Configuration, fetc solutionsGetSolutionAchievement(taskId?: number, solutionId?: number, options?: any) { return SolutionsApiFp(configuration).solutionsGetSolutionAchievement(taskId, solutionId, options)(fetch, basePath); }, + /** + * + * @param {number} courseId + * @param {number} taskId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiSolutionsCoursesByCourseIdTaskByTaskIdGet(courseId: number, taskId: number, options?: any) { + return SolutionsApiFp(configuration).apiSolutionsCoursesByCourseIdTaskByTaskIdGet(courseId, taskId, options)(fetch, basePath); + }, /** * * @param {number} solutionId @@ -8459,6 +8491,18 @@ export class SolutionsApi extends BaseAPI { return SolutionsApiFp(this.configuration).solutionsGetSolutionAchievement(taskId, solutionId, options)(this.fetch, this.basePath); } + /** + * + * @param {number} courseId + * @param {number} taskId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SolutionsApi + */ + public apiSolutionsCoursesByCourseIdTaskByTaskIdGet(courseId: number, taskId: number, options?: any) { + return SolutionsApiFp(this.configuration).apiSolutionsCoursesByCourseIdTaskByTaskIdGet(courseId, taskId, options)(this.fetch, this.basePath); + } + /** * * @param {number} solutionId diff --git a/hwproj.front/src/components/Solutions/UnratedSolutions.tsx b/hwproj.front/src/components/Solutions/UnratedSolutions.tsx index a06ac61a2..493e4fbde 100644 --- a/hwproj.front/src/components/Solutions/UnratedSolutions.tsx +++ b/hwproj.front/src/components/Solutions/UnratedSolutions.tsx @@ -201,7 +201,7 @@ const UnratedSolutions: FC = (props) => { From 605b74088745d5e6b64133e0b903c3faabc06b37 Mon Sep 17 00:00:00 2001 From: Alex Berezhnykh Date: Tue, 9 May 2023 08:26:40 +0300 Subject: [PATCH 11/58] Revert "StudentSolutionsPage: optimize requests" This reverts commit 8add6c72ec43f230b99d16c092fd95e53fce12c3. --- .../Controllers/SolutionsController.cs | 24 ------- .../Models/TaskSolutionsPageModel.cs | 12 ---- .../SolutionsService/Solution.cs | 6 +- .../SolutionsService/StudentSolutions.cs | 8 --- hwproj.front/src/api/api.ts | 70 +++++-------------- .../components/Solutions/UnratedSolutions.tsx | 2 +- 6 files changed, 21 insertions(+), 101 deletions(-) delete mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Models/TaskSolutionsPageModel.cs delete mode 100644 HwProj.Common/HwProj.Models/SolutionsService/StudentSolutions.cs diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs index 9044eb206..2f340c795 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs @@ -13,7 +13,6 @@ using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.Roles; using HwProj.Models.SolutionsService; -using HwProj.Models.StatisticsService; using HwProj.SolutionsService.Client; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -259,29 +258,6 @@ public async Task GetTaskSolutionsPageData(long taskId) return Ok(result); } - [Authorize] - [HttpGet("courses/{courseId}/task/{taskId}")] - [ProducesResponseType(typeof(UserTaskSolutionPreviews[]), (int)HttpStatusCode.OK)] - public async Task GetCourseTaskSolutionsPageData(long courseId, long taskId) - { - var statistics = await _solutionsClient.GetCourseTaskStatistics(courseId, taskId, UserId); - - var studentIds = statistics.Select(t => t.StudentId).ToArray(); - var usersData = await AuthServiceClient.GetAccountsData(studentIds); - - var result = statistics - .Zip(usersData, (statistic, accountData) => new UserTaskSolutionPreviews - { - Solutions = statistic.Solutions.Select(s => new StatisticsCourseSolutionsModel(s)).ToArray(), - User = accountData - }) - .OrderBy(t => t.User.Surname) - .ThenBy(t => t.User.Name) - .ToArray(); - - return Ok(result); - } - [HttpPost("{taskId}")] [Authorize(Roles = Roles.StudentRole)] [ProducesResponseType(typeof(long), (int)HttpStatusCode.OK)] diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Models/TaskSolutionsPageModel.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Models/TaskSolutionsPageModel.cs deleted file mode 100644 index 240c8491b..000000000 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Models/TaskSolutionsPageModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -using HwProj.APIGateway.API.Models.Solutions; -using HwProj.Models.CoursesService.ViewModels; - -namespace HwProj.APIGateway.API.Models -{ - public class TaskSolutionsPageModel - { - public UserTaskSolutions[] StudentsTaskSolutions { get; set; } - public HomeworkTaskViewModel Task { get; set; } - public CoursePreview Course { get; set; } - } -} diff --git a/HwProj.Common/HwProj.Models/SolutionsService/Solution.cs b/HwProj.Common/HwProj.Models/SolutionsService/Solution.cs index 3a41dfeec..5a0063b43 100644 --- a/HwProj.Common/HwProj.Models/SolutionsService/Solution.cs +++ b/HwProj.Common/HwProj.Models/SolutionsService/Solution.cs @@ -12,15 +12,15 @@ public class Solution : IEntity public string Comment { get; set; } public SolutionState State { get; set; } - + public int Rating { get; set; } - + public string StudentId { get; set; } public string? LecturerId { get; set; } public long? GroupId { get; set; } - + public long TaskId { get; set; } public DateTime PublicationDate { get; set; } diff --git a/HwProj.Common/HwProj.Models/SolutionsService/StudentSolutions.cs b/HwProj.Common/HwProj.Models/SolutionsService/StudentSolutions.cs deleted file mode 100644 index 29b466125..000000000 --- a/HwProj.Common/HwProj.Models/SolutionsService/StudentSolutions.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace HwProj.Models.SolutionsService -{ - public class StudentSolutions - { - public string StudentId { get; set; } - public Solution[] Solutions { get; set; } - } -} diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index cd68171bc..b366d3ae9 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -7690,6 +7690,8 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 localVarUrlObj.search = null; localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + const needsSerialization = ("SolutionViewModel" !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + localVarRequestOptions.body = needsSerialization ? JSON.stringify(model || {}) : (model || ""); return { url: url.format(localVarUrlObj), @@ -7697,19 +7699,24 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura }; }, /** - * - * @param {number} taskId - * @param {SolutionViewModel} [model] + * + * @param {number} courseId + * @param {number} taskId * @param {*} [options] Override http request option. * @throws {RequiredError} */ - solutionsGetSolutionActuality(solutionId: number, options: any = {}): FetchArgs { - // verify required parameter 'solutionId' is not null or undefined - if (solutionId === null || solutionId === undefined) { - throw new RequiredError('solutionId','Required parameter solutionId was null or undefined when calling solutionsGetSolutionActuality.'); + apiSolutionsCoursesByCourseIdTaskByTaskIdGet(courseId: number, taskId: number, options: any = {}): FetchArgs { + // verify required parameter 'courseId' is not null or undefined + if (courseId === null || courseId === undefined) { + throw new RequiredError('courseId','Required parameter courseId was null or undefined when calling apiSolutionsCoursesByCourseIdTaskByTaskIdGet.'); } - const localVarPath = `/api/Solutions/actuality/{solutionId}` - .replace(`{${"solutionId"}}`, encodeURIComponent(String(solutionId))); + // verify required parameter 'taskId' is not null or undefined + if (taskId === null || taskId === undefined) { + throw new RequiredError('taskId','Required parameter taskId was null or undefined when calling apiSolutionsCoursesByCourseIdTaskByTaskIdGet.'); + } + const localVarPath = `/api/Solutions/courses/{courseId}/task/{taskId}` + .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))) + .replace(`{${"taskId"}}`, encodeURIComponent(String(taskId))); const localVarUrlObj = url.parse(localVarPath, true); const localVarRequestOptions = Object.assign({ method: 'GET' }, options); const localVarHeaderParameter = {} as any; @@ -7725,10 +7732,8 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 - localVarUrlObj.search = null; + delete localVarUrlObj.search; localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); - const needsSerialization = ("SolutionViewModel" !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; - localVarRequestOptions.body = needsSerialization ? JSON.stringify(model || {}) : (model || ""); return { url: url.format(localVarUrlObj), @@ -8143,25 +8148,6 @@ export const SolutionsApiFp = function(configuration?: Configuration) { }); }; }, - /** - * - * @param {number} courseId - * @param {number} taskId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - apiSolutionsCoursesByCourseIdTaskByTaskIdGet(courseId: number, taskId: number, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise> { - const localVarFetchArgs = SolutionsApiFetchParamCreator(configuration).apiSolutionsCoursesByCourseIdTaskByTaskIdGet(courseId, taskId, options); - return (fetch: FetchAPI = portableFetch, basePath: string = BASE_PATH) => { - return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { - if (response.status >= 200 && response.status < 300) { - return response.json(); - } else { - throw response; - } - }); - }; - }, /** * * @param {number} solutionId @@ -8355,16 +8341,6 @@ export const SolutionsApiFactory = function (configuration?: Configuration, fetc solutionsGetSolutionAchievement(taskId?: number, solutionId?: number, options?: any) { return SolutionsApiFp(configuration).solutionsGetSolutionAchievement(taskId, solutionId, options)(fetch, basePath); }, - /** - * - * @param {number} courseId - * @param {number} taskId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - apiSolutionsCoursesByCourseIdTaskByTaskIdGet(courseId: number, taskId: number, options?: any) { - return SolutionsApiFp(configuration).apiSolutionsCoursesByCourseIdTaskByTaskIdGet(courseId, taskId, options)(fetch, basePath); - }, /** * * @param {number} solutionId @@ -8491,18 +8467,6 @@ export class SolutionsApi extends BaseAPI { return SolutionsApiFp(this.configuration).solutionsGetSolutionAchievement(taskId, solutionId, options)(this.fetch, this.basePath); } - /** - * - * @param {number} courseId - * @param {number} taskId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof SolutionsApi - */ - public apiSolutionsCoursesByCourseIdTaskByTaskIdGet(courseId: number, taskId: number, options?: any) { - return SolutionsApiFp(this.configuration).apiSolutionsCoursesByCourseIdTaskByTaskIdGet(courseId, taskId, options)(this.fetch, this.basePath); - } - /** * * @param {number} solutionId diff --git a/hwproj.front/src/components/Solutions/UnratedSolutions.tsx b/hwproj.front/src/components/Solutions/UnratedSolutions.tsx index 493e4fbde..a06ac61a2 100644 --- a/hwproj.front/src/components/Solutions/UnratedSolutions.tsx +++ b/hwproj.front/src/components/Solutions/UnratedSolutions.tsx @@ -201,7 +201,7 @@ const UnratedSolutions: FC = (props) => { From 864ab1d4e41add3bfd760529879d2170c2fa248d Mon Sep 17 00:00:00 2001 From: Alex Berezhnykh Date: Sat, 13 May 2023 05:23:42 +0300 Subject: [PATCH 12/58] StudentSolutionsPage: optimize requests --- .../Controllers/SolutionsController.cs | 43 +++++++++++++++++++ .../HwProj.APIGateway.API/Startup.cs | 3 -- .../SolutionsService/Solution.cs | 6 +-- .../SolutionsService/StudentSolutions.cs | 8 ++++ 4 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 HwProj.Common/HwProj.Models/SolutionsService/StudentSolutions.cs diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs index 2f340c795..518fd656f 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs @@ -13,7 +13,9 @@ using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.Roles; using HwProj.Models.SolutionsService; +using HwProj.Models.StatisticsService; using HwProj.SolutionsService.Client; +using HwProj.Utils.Authorization; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -258,6 +260,47 @@ public async Task GetTaskSolutionsPageData(long taskId) return Ok(result); } + [Authorize] + [HttpGet("tasks/{taskId}")] + [ProducesResponseType(typeof(TaskSolutionStatisticsPageData), (int)HttpStatusCode.OK)] + public async Task GetTaskSolutionsPageData(long taskId) + { + var course = await _coursesServiceClient.GetCourseByTask(taskId); + //TODO: CourseMentorOnlyAttribute + if (course == null || !course.MentorIds.Contains(UserId)) return Forbid(); + + var studentIds = course.CourseMates + .Where(t => t.IsAccepted) + .Select(t => t.StudentId) + .ToArray(); + + var getStudentsDataTask = AuthServiceClient.GetAccountsData(studentIds); + var getStatisticsTask = _solutionsClient.GetTaskSolutionStatistics(taskId); + + await Task.WhenAll(getStudentsDataTask, getStatisticsTask); + + var usersData = getStudentsDataTask.Result; + var statistics = getStatisticsTask.Result; + var statisticsDict = statistics.ToDictionary(t => t.StudentId); + + var result = new TaskSolutionStatisticsPageData() + { + CourseId = course.Id, + StudentsSolutions = studentIds.Zip(usersData, (studentId, accountData) => new UserTaskSolutionPreviews + { + Solutions = statisticsDict.TryGetValue(studentId, out var studentSolutions) + ? studentSolutions.Solutions.Select(s => new StatisticsCourseSolutionsModel(s)).ToArray() + : Array.Empty(), + User = accountData + }) + .OrderBy(t => t.User.Surname) + .ThenBy(t => t.User.Name) + .ToArray() + }; + + return Ok(result); + } + [HttpPost("{taskId}")] [Authorize(Roles = Roles.StudentRole)] [ProducesResponseType(typeof(long), (int)HttpStatusCode.OK)] diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs index 869e636f3..8e6d1e769 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs @@ -3,7 +3,6 @@ using Google.Apis.Services; using Google.Apis.Sheets.v4; using HwProj.APIGateway.API.ExportServices; -using HwProj.APIGateway.API.Models; using HwProj.AuthService.Client; using HwProj.ContentService.Client; using HwProj.CoursesService.Client; @@ -20,8 +19,6 @@ using Microsoft.IdentityModel.Tokens; using IStudentsInfo; using StudentsInfo; -using HwProj.Utils.Authorization; -using Microsoft.EntityFrameworkCore; using Newtonsoft.Json.Linq; diff --git a/HwProj.Common/HwProj.Models/SolutionsService/Solution.cs b/HwProj.Common/HwProj.Models/SolutionsService/Solution.cs index 5a0063b43..3a41dfeec 100644 --- a/HwProj.Common/HwProj.Models/SolutionsService/Solution.cs +++ b/HwProj.Common/HwProj.Models/SolutionsService/Solution.cs @@ -12,15 +12,15 @@ public class Solution : IEntity public string Comment { get; set; } public SolutionState State { get; set; } - + public int Rating { get; set; } - + public string StudentId { get; set; } public string? LecturerId { get; set; } public long? GroupId { get; set; } - + public long TaskId { get; set; } public DateTime PublicationDate { get; set; } diff --git a/HwProj.Common/HwProj.Models/SolutionsService/StudentSolutions.cs b/HwProj.Common/HwProj.Models/SolutionsService/StudentSolutions.cs new file mode 100644 index 000000000..29b466125 --- /dev/null +++ b/HwProj.Common/HwProj.Models/SolutionsService/StudentSolutions.cs @@ -0,0 +1,8 @@ +namespace HwProj.Models.SolutionsService +{ + public class StudentSolutions + { + public string StudentId { get; set; } + public Solution[] Solutions { get; set; } + } +} From 80328f498a96f2fa7060c60e7af51a28c1ebe6d2 Mon Sep 17 00:00:00 2001 From: Alex Berezhnykh Date: Sat, 13 May 2023 06:08:14 +0300 Subject: [PATCH 13/58] Cleanup --- .../HwProj.APIGateway.API/Controllers/SolutionsController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs index 518fd656f..58526e821 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs @@ -15,7 +15,6 @@ using HwProj.Models.SolutionsService; using HwProj.Models.StatisticsService; using HwProj.SolutionsService.Client; -using HwProj.Utils.Authorization; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; From c0f8253120afcc514f5d26ed5d4559ae496eee1e Mon Sep 17 00:00:00 2001 From: Alex Berezhnykh Date: Sat, 13 May 2023 07:46:33 +0300 Subject: [PATCH 14/58] Solutions: optimize requests --- .../Controllers/SolutionsController.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs index 58526e821..2eba5823c 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs @@ -285,11 +285,11 @@ public async Task GetTaskSolutionsPageData(long taskId) var result = new TaskSolutionStatisticsPageData() { CourseId = course.Id, - StudentsSolutions = studentIds.Zip(usersData, (studentId, accountData) => new UserTaskSolutionPreviews + StudentsSolutions = studentIds.Zip(usersData, (studentId, accountData) => new UserTaskSolutions { Solutions = statisticsDict.TryGetValue(studentId, out var studentSolutions) - ? studentSolutions.Solutions.Select(s => new StatisticsCourseSolutionsModel(s)).ToArray() - : Array.Empty(), + ? studentSolutions.Solutions + : Array.Empty(), User = accountData }) .OrderBy(t => t.User.Surname) From 416271513bac7e63e2e6d3a3aebbf0fd9e875ef2 Mon Sep 17 00:00:00 2001 From: Yuriy Date: Sat, 13 May 2023 22:09:31 +0300 Subject: [PATCH 15/58] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D1=8C=20=D0=BE=D1=86=D0=B5=D0=BD=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=20=D0=B1=D0=B5=D0=B7=20=D1=80?= =?UTF-8?q?=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=20=20(#247)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SolutionsService/SolutionViewModel.cs | 2 +- hwproj.front/src/api/api.ts | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/HwProj.Common/HwProj.Models/SolutionsService/SolutionViewModel.cs b/HwProj.Common/HwProj.Models/SolutionsService/SolutionViewModel.cs index 048822f85..4387b5a98 100644 --- a/HwProj.Common/HwProj.Models/SolutionsService/SolutionViewModel.cs +++ b/HwProj.Common/HwProj.Models/SolutionsService/SolutionViewModel.cs @@ -9,7 +9,7 @@ public class SolutionViewModel public string Comment { get; set; } public string StudentId { get; set; } - + public string[]? GroupMateIds { get; set; } public DateTime PublicationDate { get; set; } diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index b366d3ae9..a71cbe638 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -7919,6 +7919,8 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 localVarUrlObj.search = null; localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + const needsSerialization = ("SolutionViewModel" !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + localVarRequestOptions.body = needsSerialization ? JSON.stringify(solution || {}) : (solution || ""); return { url: url.format(localVarUrlObj), @@ -8239,6 +8241,25 @@ export const SolutionsApiFp = function(configuration?: Configuration) { }); }; }, + /** + * + * @param {number} taskId + * @param {SolutionViewModel} [model] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiSolutionsRateEmptySolutionByTaskIdPost(taskId: number, model?: SolutionViewModel, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = SolutionsApiFetchParamCreator(configuration).apiSolutionsRateEmptySolutionByTaskIdPost(taskId, model, options); + return (fetch: FetchAPI = portableFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response; + } else { + throw response; + } + }); + }; + }, /** * * @param {number} solutionId @@ -8396,6 +8417,16 @@ export const SolutionsApiFactory = function (configuration?: Configuration, fetc solutionsGiveUp(taskId: number, options?: any) { return SolutionsApiFp(configuration).solutionsGiveUp(taskId, options)(fetch, basePath); }, + /** + * + * @param {number} taskId + * @param {SolutionViewModel} [model] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiSolutionsRateEmptySolutionByTaskIdPost(taskId: number, model?: SolutionViewModel, options?: any) { + return SolutionsApiFp(configuration).apiSolutionsRateEmptySolutionByTaskIdPost(taskId, model, options)(fetch, basePath); + }, /** * * @param {number} solutionId @@ -8534,6 +8565,18 @@ export class SolutionsApi extends BaseAPI { return SolutionsApiFp(this.configuration).solutionsGiveUp(taskId, options)(this.fetch, this.basePath); } + /** + * + * @param {number} taskId + * @param {SolutionViewModel} [model] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SolutionsApi + */ + public apiSolutionsRateEmptySolutionByTaskIdPost(taskId: number, model?: SolutionViewModel, options?: any) { + return SolutionsApiFp(this.configuration).apiSolutionsRateEmptySolutionByTaskIdPost(taskId, model, options)(this.fetch, this.basePath); + } + /** * * @param {number} solutionId From 046ca171ae546385065e6e404c41c2211c53aa26 Mon Sep 17 00:00:00 2001 From: YuriUfimtsev Date: Tue, 30 May 2023 21:56:56 +0300 Subject: [PATCH 16/58] refactor --- HwProj.APIGateway/HwProj.APIGateway.API/libman.json | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/libman.json diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/libman.json b/HwProj.APIGateway/HwProj.APIGateway.API/libman.json deleted file mode 100644 index ceee2710f..000000000 --- a/HwProj.APIGateway/HwProj.APIGateway.API/libman.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "1.0", - "defaultProvider": "cdnjs", - "libraries": [] -} \ No newline at end of file From 22cc4c997a3a14fabed5e66676c5f95f241eb47d Mon Sep 17 00:00:00 2001 From: YuriUfimtsev Date: Wed, 31 May 2023 14:32:21 +0300 Subject: [PATCH 17/58] fix: fix the occurred errors --- .../HwProj.APIGateway.API/ExportServices/GoogleService.cs | 2 +- hwproj.front/src/components/Courses/Course.tsx | 2 +- hwproj.front/src/components/Notifications.tsx | 2 +- hwproj.front/src/components/Solutions/ExportToGoogle.tsx | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs index 97b16b992..0e59290ff 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs @@ -77,7 +77,7 @@ public async Task> GetSheetTitles(string sheetUrl) } catch (Exception ex) { - return Result.Failed($"Ошибка при обращении к Google Docs: {ex.Message}"); + return Result.Failed($"Ошибка при обращении к Google Sheets: {ex.Message}"); } } diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index 899d1e091..c7bcd982a 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -236,7 +236,7 @@ const Course: React.FC = () => { }, []); const [pageState, setPageState] = useState({ - tabValue: "homeworks" + tabValue: isFromYandex ? "stats" : "homeworks" }) const { diff --git a/hwproj.front/src/components/Notifications.tsx b/hwproj.front/src/components/Notifications.tsx index 684f623e0..3abdb3024 100644 --- a/hwproj.front/src/components/Notifications.tsx +++ b/hwproj.front/src/components/Notifications.tsx @@ -276,4 +276,4 @@ const Notifications: FC = (props) => { } -export default Notifications +export default Notifications \ No newline at end of file diff --git a/hwproj.front/src/components/Solutions/ExportToGoogle.tsx b/hwproj.front/src/components/Solutions/ExportToGoogle.tsx index 45ce2ea3c..5b9d969f2 100644 --- a/hwproj.front/src/components/Solutions/ExportToGoogle.tsx +++ b/hwproj.front/src/components/Solutions/ExportToGoogle.tsx @@ -1,7 +1,6 @@ import React, { FC, useState } from "react"; import { useEffect } from 'react'; import { ResultString } from "../../api"; -import { ResultExternalService } from "../../api"; import { Alert, Box, Button, CircularProgress, Grid, MenuItem, Select, TextField } from "@mui/material"; import apiSingleton from "../../api/ApiSingleton"; import { green, red } from "@material-ui/core/colors"; From ab270f14434913a3742d5248b5a4cfc8b867c529 Mon Sep 17 00:00:00 2001 From: YuriUfimtsev Date: Wed, 31 May 2023 15:10:52 +0300 Subject: [PATCH 18/58] refactor --- .../ExportServices/GoogleService.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs index 0e59290ff..63a98752c 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs @@ -17,11 +17,11 @@ namespace HwProj.APIGateway.API.ExportServices { public class GoogleService { - private readonly SheetsService _sheetsService; + private readonly SheetsService _internalGoogleSheetsService; - public GoogleService(SheetsService sheetsService) + public GoogleService(SheetsService internalGoogleSheetsService) { - _sheetsService = sheetsService; + _internalGoogleSheetsService = internalGoogleSheetsService; } private static int SeparationColumnPixelWidth { get; set; } = 20; @@ -47,11 +47,11 @@ public async Task Export( var (valueRange, range, updateStyleRequestBody) = Generate( statistics.ToList(), course, sheetName, (int)sheetId); - var clearRequest = _sheetsService.Spreadsheets.Values.Clear(new ClearValuesRequest(), spreadsheetId, range); + var clearRequest = _internalGoogleSheetsService.Spreadsheets.Values.Clear(new ClearValuesRequest(), spreadsheetId, range); await clearRequest.ExecuteAsync(); - var updateStyleRequest = _sheetsService.Spreadsheets.BatchUpdate(updateStyleRequestBody, spreadsheetId); + var updateStyleRequest = _internalGoogleSheetsService.Spreadsheets.BatchUpdate(updateStyleRequestBody, spreadsheetId); await updateStyleRequest.ExecuteAsync(); - var updateRequest = _sheetsService.Spreadsheets.Values.Update(valueRange, spreadsheetId, range); + var updateRequest = _internalGoogleSheetsService.Spreadsheets.Values.Update(valueRange, spreadsheetId, range); updateRequest.ValueInputOption = SpreadsheetsResource.ValuesResource.UpdateRequest.ValueInputOptionEnum.USERENTERED; await updateRequest.ExecuteAsync(); result = Result.Success(); @@ -72,7 +72,7 @@ public async Task> GetSheetTitles(string sheetUrl) var spreadsheetId = processingResult.Value; try { - var spreadsheet = await _sheetsService.Spreadsheets.Get(spreadsheetId).ExecuteAsync(); + var spreadsheet = await _internalGoogleSheetsService.Spreadsheets.Get(spreadsheetId).ExecuteAsync(); return Result.Success(spreadsheet.Sheets.Select(t => t.Properties.Title).ToArray()); } catch (Exception ex) @@ -362,7 +362,7 @@ private static void AddColouredCellsRequests( private async Task GetSheetId(string spreadsheetId, string sheetName) { - var spreadsheetGetRequest = _sheetsService.Spreadsheets.Get(spreadsheetId); + var spreadsheetGetRequest = _internalGoogleSheetsService.Spreadsheets.Get(spreadsheetId); spreadsheetGetRequest.IncludeGridData = true; try { From 361620a6f283621271078d851466886b9a42685c Mon Sep 17 00:00:00 2001 From: YuriUfimtsev Date: Mon, 24 Jul 2023 02:20:13 +0500 Subject: [PATCH 19/58] refactor --- hwproj.front/src/components/Courses/Course.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index c7bcd982a..af1e6d8c3 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -340,7 +340,7 @@ const Course: React.FC = () => { useEffect(() => changeTab(tab || "homeworks"), [tab, courseId, isFound]) - const userYandexId = new URLSearchParams(window.location.search).get("code") + const yandexCode = new URLSearchParams(window.location.search).get("code") const joinCourse = async () => { await ApiSingleton.coursesApi.coursesSignInCourse(+courseId!) @@ -573,7 +573,7 @@ const Course: React.FC = () => { isMentor={isCourseMentor} course={courseState.course} solutions={studentSolutions} - yandexCode={userYandexId} + yandexCode={yandexCode} /> } From 0525a43f04316b76671169d813e391654bbf768b Mon Sep 17 00:00:00 2001 From: YuriUfimtsev Date: Mon, 24 Jul 2023 13:18:18 +0500 Subject: [PATCH 20/58] refactor --- .../Controllers/SolutionsController.cs | 41 ------------------- .../SolutionsStatsDomainTests.cs | 10 ++--- 2 files changed, 5 insertions(+), 46 deletions(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs index 2eba5823c..cb1793305 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs @@ -259,47 +259,6 @@ public async Task GetTaskSolutionsPageData(long taskId) return Ok(result); } - [Authorize] - [HttpGet("tasks/{taskId}")] - [ProducesResponseType(typeof(TaskSolutionStatisticsPageData), (int)HttpStatusCode.OK)] - public async Task GetTaskSolutionsPageData(long taskId) - { - var course = await _coursesServiceClient.GetCourseByTask(taskId); - //TODO: CourseMentorOnlyAttribute - if (course == null || !course.MentorIds.Contains(UserId)) return Forbid(); - - var studentIds = course.CourseMates - .Where(t => t.IsAccepted) - .Select(t => t.StudentId) - .ToArray(); - - var getStudentsDataTask = AuthServiceClient.GetAccountsData(studentIds); - var getStatisticsTask = _solutionsClient.GetTaskSolutionStatistics(taskId); - - await Task.WhenAll(getStudentsDataTask, getStatisticsTask); - - var usersData = getStudentsDataTask.Result; - var statistics = getStatisticsTask.Result; - var statisticsDict = statistics.ToDictionary(t => t.StudentId); - - var result = new TaskSolutionStatisticsPageData() - { - CourseId = course.Id, - StudentsSolutions = studentIds.Zip(usersData, (studentId, accountData) => new UserTaskSolutions - { - Solutions = statisticsDict.TryGetValue(studentId, out var studentSolutions) - ? studentSolutions.Solutions - : Array.Empty(), - User = accountData - }) - .OrderBy(t => t.User.Surname) - .ThenBy(t => t.User.Name) - .ToArray() - }; - - return Ok(result); - } - [HttpPost("{taskId}")] [Authorize(Roles = Roles.StudentRole)] [ProducesResponseType(typeof(long), (int)HttpStatusCode.OK)] diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.IntegrationTests/SolutionsStatsDomainTests.cs b/HwProj.SolutionsService/HwProj.SolutionsService.IntegrationTests/SolutionsStatsDomainTests.cs index 3637e03ec..0cb10b37f 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.IntegrationTests/SolutionsStatsDomainTests.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.IntegrationTests/SolutionsStatsDomainTests.cs @@ -58,13 +58,13 @@ private List GenerateTestSolutionsForTask(int amount, long taskId) => }) .ToList(); - private GroupViewModel GenerateGroupViewModel(long id, string[] studentIds) => new() + private GroupViewModel GenerateGroupViewModel(long id, string[] studentIds) => new GroupViewModel { Id = id, StudentsIds = studentIds }; - private HomeworkViewModel GenerateHomeworkViewModel(long id, long courseId, int taskAmount) => new() + private HomeworkViewModel GenerateHomeworkViewModel(long id, long courseId, int taskAmount) => new HomeworkViewModel { Id = id, Title = "Test", @@ -85,7 +85,7 @@ private List MakeTestSolutions(CourseMateViewModel[] courseMates, Grou solutions[2].StudentId = courseMates[1].StudentId; solutions[0].GroupId = groups[0].Id; solutions[1].GroupId = groups[1].Id; - + return solutions; } @@ -130,7 +130,7 @@ public async Task GetCourseStatisticsTest() thirdStudentSolutions.Should().HaveCount(1); thirdStudentSolutions[0].Id.Should().Be(1); } - + [Test] public async Task GetCourseTaskStatisticsTest() { @@ -138,7 +138,7 @@ public async Task GetCourseTaskStatisticsTest() var group1 = GenerateGroupViewModel(1, new[] { courseMates[0].StudentId, courseMates[1].StudentId }); var group2 = GenerateGroupViewModel(2, new[] { courseMates[1].StudentId, courseMates[2].StudentId }); var groups = new[] { group1, group2 }; - + var solutions = GenerateTestSolutionsForTask(3, 1); solutions[0].StudentId = courseMates[0].StudentId; solutions[0].GroupId = group1.Id; From 99cfc062b233ffa0a64f5a55e05b360470cb6cb1 Mon Sep 17 00:00:00 2001 From: YuriUfimtsev Date: Thu, 17 Aug 2023 15:14:23 +0500 Subject: [PATCH 21/58] fix: correct export to google frontend bug --- .../TableGenerators/ExcelGenerator.cs | 2 +- hwproj.front/src/api/api.ts | 105 +++++------------- .../components/Solutions/ExportToGoogle.tsx | 1 - 3 files changed, 27 insertions(+), 81 deletions(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs b/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs index 48727b580..050763f02 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs @@ -214,7 +214,7 @@ private static void AddCourseMatesInfo( var cnt = solutions.Count(); worksheet.Cells[position.Row, position.Column].Value = min; worksheet.Cells[position.Row, position.Column + 2].Value = cnt; - if (cnt != allSolutions.Count()) + if (cnt != allSolutions.Count) { worksheet.Cells[position.Row, position.Column + 2].Style.Fill.PatternType = ExcelFillStyle.Solid; diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index a71cbe638..3c6813527 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -7882,6 +7882,8 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 localVarUrlObj.search = null; localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + const needsSerialization = ("SolutionViewModel" !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + localVarRequestOptions.body = needsSerialization ? JSON.stringify(solution || {}) : (solution || ""); return { url: url.format(localVarUrlObj), @@ -7889,18 +7891,18 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura }; }, /** - * - * @param {number} taskId - * @param {SolutionViewModel} [solution] + * + * @param {number} taskId + * @param {SolutionViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - solutionsGiveUp(taskId: number, options: any = {}): FetchArgs { + apiSolutionsRateEmptySolutionByTaskIdPost(taskId: number, model?: SolutionViewModel, options: any = {}): FetchArgs { // verify required parameter 'taskId' is not null or undefined if (taskId === null || taskId === undefined) { - throw new RequiredError('taskId','Required parameter taskId was null or undefined when calling solutionsGiveUp.'); + throw new RequiredError('taskId','Required parameter taskId was null or undefined when calling apiSolutionsRateEmptySolutionByTaskIdPost.'); } - const localVarPath = `/api/Solutions/giveUp/{taskId}` + const localVarPath = `/api/Solutions/rateEmptySolution/{taskId}` .replace(`{${"taskId"}}`, encodeURIComponent(String(taskId))); const localVarUrlObj = url.parse(localVarPath, true); const localVarRequestOptions = Object.assign({ method: 'POST' }, options); @@ -7915,12 +7917,14 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura localVarHeaderParameter["Authorization"] = localVarApiKeyValue; } + localVarHeaderParameter['Content-Type'] = 'application/json-patch+json'; + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 - localVarUrlObj.search = null; + delete localVarUrlObj.search; localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); const needsSerialization = ("SolutionViewModel" !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; - localVarRequestOptions.body = needsSerialization ? JSON.stringify(solution || {}) : (solution || ""); + localVarRequestOptions.body = needsSerialization ? JSON.stringify(model || {}) : (model || ""); return { url: url.format(localVarUrlObj), @@ -8241,25 +8245,6 @@ export const SolutionsApiFp = function(configuration?: Configuration) { }); }; }, - /** - * - * @param {number} taskId - * @param {SolutionViewModel} [model] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - apiSolutionsRateEmptySolutionByTaskIdPost(taskId: number, model?: SolutionViewModel, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { - const localVarFetchArgs = SolutionsApiFetchParamCreator(configuration).apiSolutionsRateEmptySolutionByTaskIdPost(taskId, model, options); - return (fetch: FetchAPI = portableFetch, basePath: string = BASE_PATH) => { - return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { - if (response.status >= 200 && response.status < 300) { - return response; - } else { - throw response; - } - }); - }; - }, /** * * @param {number} solutionId @@ -8417,16 +8402,6 @@ export const SolutionsApiFactory = function (configuration?: Configuration, fetc solutionsGiveUp(taskId: number, options?: any) { return SolutionsApiFp(configuration).solutionsGiveUp(taskId, options)(fetch, basePath); }, - /** - * - * @param {number} taskId - * @param {SolutionViewModel} [model] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - apiSolutionsRateEmptySolutionByTaskIdPost(taskId: number, model?: SolutionViewModel, options?: any) { - return SolutionsApiFp(configuration).apiSolutionsRateEmptySolutionByTaskIdPost(taskId, model, options)(fetch, basePath); - }, /** * * @param {number} solutionId @@ -8565,18 +8540,6 @@ export class SolutionsApi extends BaseAPI { return SolutionsApiFp(this.configuration).solutionsGiveUp(taskId, options)(this.fetch, this.basePath); } - /** - * - * @param {number} taskId - * @param {SolutionViewModel} [model] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof SolutionsApi - */ - public apiSolutionsRateEmptySolutionByTaskIdPost(taskId: number, model?: SolutionViewModel, options?: any) { - return SolutionsApiFp(this.configuration).apiSolutionsRateEmptySolutionByTaskIdPost(taskId, model, options)(this.fetch, this.basePath); - } - /** * * @param {number} solutionId @@ -8741,13 +8704,12 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur /** * * @param {number} [courseId] - * @param {string} [userId] * @param {string} [sheetUrl] * @param {string} [sheetName] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsExportToSheetGet(courseId?: number, userId?: string, sheetUrl?: string, sheetName?: string, options: any = {}): FetchArgs { + apiStatisticsExportToSheetGet(courseId?: number, sheetUrl?: string, sheetName?: string, options: any = {}): FetchArgs { const localVarPath = `/api/Statistics/exportToSheet`; const localVarUrlObj = url.parse(localVarPath, true); const localVarRequestOptions = Object.assign({ method: 'GET' }, options); @@ -8766,10 +8728,6 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur localVarQueryParameter['courseId'] = courseId; } - if (userId !== undefined) { - localVarQueryParameter['userId'] = userId; - } - if (sheetUrl !== undefined) { localVarQueryParameter['sheetUrl'] = sheetUrl; } @@ -8791,12 +8749,11 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur /** * * @param {number} [courseId] - * @param {string} [userId] * @param {string} [sheetName] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsGetFileGet(courseId?: number, userId?: string, sheetName?: string, options: any = {}): FetchArgs { + apiStatisticsGetFileGet(courseId?: number, sheetName?: string, options: any = {}): FetchArgs { const localVarPath = `/api/Statistics/getFile`; const localVarUrlObj = url.parse(localVarPath, true); const localVarRequestOptions = Object.assign({ method: 'GET' }, options); @@ -8815,10 +8772,6 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur localVarQueryParameter['courseId'] = courseId; } - if (userId !== undefined) { - localVarQueryParameter['userId'] = userId; - } - if (sheetName !== undefined) { localVarQueryParameter['sheetName'] = sheetName; } @@ -8969,14 +8922,13 @@ export const StatisticsApiFp = function(configuration?: Configuration) { /** * * @param {number} [courseId] - * @param {string} [userId] * @param {string} [sheetUrl] * @param {string} [sheetName] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsExportToSheetGet(courseId?: number, userId?: string, sheetUrl?: string, sheetName?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { - const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).apiStatisticsExportToSheetGet(courseId, userId, sheetUrl, sheetName, options); + apiStatisticsExportToSheetGet(courseId?: number, sheetUrl?: string, sheetName?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).apiStatisticsExportToSheetGet(courseId, sheetUrl, sheetName, options); return (fetch: FetchAPI = portableFetch, basePath: string = BASE_PATH) => { return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { if (response.status >= 200 && response.status < 300) { @@ -8990,13 +8942,12 @@ export const StatisticsApiFp = function(configuration?: Configuration) { /** * * @param {number} [courseId] - * @param {string} [userId] * @param {string} [sheetName] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsGetFileGet(courseId?: number, userId?: string, sheetName?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { - const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).apiStatisticsGetFileGet(courseId, userId, sheetName, options); + apiStatisticsGetFileGet(courseId?: number, sheetName?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).apiStatisticsGetFileGet(courseId, sheetName, options); return (fetch: FetchAPI = portableFetch, basePath: string = BASE_PATH) => { return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { if (response.status >= 200 && response.status < 300) { @@ -9082,25 +9033,23 @@ export const StatisticsApiFactory = function (configuration?: Configuration, fet /** * * @param {number} [courseId] - * @param {string} [userId] * @param {string} [sheetUrl] * @param {string} [sheetName] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsExportToSheetGet(courseId?: number, userId?: string, sheetUrl?: string, sheetName?: string, options?: any) { - return StatisticsApiFp(configuration).apiStatisticsExportToSheetGet(courseId, userId, sheetUrl, sheetName, options)(fetch, basePath); + apiStatisticsExportToSheetGet(courseId?: number, sheetUrl?: string, sheetName?: string, options?: any) { + return StatisticsApiFp(configuration).apiStatisticsExportToSheetGet(courseId, sheetUrl, sheetName, options)(fetch, basePath); }, /** * * @param {number} [courseId] - * @param {string} [userId] * @param {string} [sheetName] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsGetFileGet(courseId?: number, userId?: string, sheetName?: string, options?: any) { - return StatisticsApiFp(configuration).apiStatisticsGetFileGet(courseId, userId, sheetName, options)(fetch, basePath); + apiStatisticsGetFileGet(courseId?: number, sheetName?: string, options?: any) { + return StatisticsApiFp(configuration).apiStatisticsGetFileGet(courseId, sheetName, options)(fetch, basePath); }, /** * @@ -9166,28 +9115,26 @@ export class StatisticsApi extends BaseAPI { /** * * @param {number} [courseId] - * @param {string} [userId] * @param {string} [sheetUrl] * @param {string} [sheetName] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof StatisticsApi */ - public apiStatisticsExportToSheetGet(courseId?: number, userId?: string, sheetUrl?: string, sheetName?: string, options?: any) { - return StatisticsApiFp(this.configuration).apiStatisticsExportToSheetGet(courseId, userId, sheetUrl, sheetName, options)(this.fetch, this.basePath); + public apiStatisticsExportToSheetGet(courseId?: number, sheetUrl?: string, sheetName?: string, options?: any) { + return StatisticsApiFp(this.configuration).apiStatisticsExportToSheetGet(courseId, sheetUrl, sheetName, options)(this.fetch, this.basePath); } /** * * @param {number} [courseId] - * @param {string} [userId] * @param {string} [sheetName] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof StatisticsApi */ - public apiStatisticsGetFileGet(courseId?: number, userId?: string, sheetName?: string, options?: any) { - return StatisticsApiFp(this.configuration).apiStatisticsGetFileGet(courseId, userId, sheetName, options)(this.fetch, this.basePath); + public apiStatisticsGetFileGet(courseId?: number, sheetName?: string, options?: any) { + return StatisticsApiFp(this.configuration).apiStatisticsGetFileGet(courseId, sheetName, options)(this.fetch, this.basePath); } /** diff --git a/hwproj.front/src/components/Solutions/ExportToGoogle.tsx b/hwproj.front/src/components/Solutions/ExportToGoogle.tsx index 5b9d969f2..3dd0b0e40 100644 --- a/hwproj.front/src/components/Solutions/ExportToGoogle.tsx +++ b/hwproj.front/src/components/Solutions/ExportToGoogle.tsx @@ -101,7 +101,6 @@ const ExportToGoogle: FC = (props: ExportToGoogleProps) => setState((prevState) => ({...prevState, loadingStatus: LoadingStatus.Loading})) const result = await apiSingleton.statisticsApi.apiStatisticsExportToSheetGet( props.courseId, - props.userId, url, getGoogleSheetName()) setState((prevState) => From a961e45796737816d8e5dbd27770945de5cb4547 Mon Sep 17 00:00:00 2001 From: YuriUfimtsev Date: Mon, 26 Feb 2024 01:35:26 +0300 Subject: [PATCH 22/58] Fix 'range exceeds grid limits' bug --- .../ExportServices/GoogleService.cs | 53 ++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs index 63a98752c..a73f4b84e 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs @@ -41,11 +41,24 @@ public async Task Export( Result result; try { - var sheetId = await GetSheetId(spreadsheetId, sheetName); - if (sheetId == null) return Result.Failed("Лист с таким названием не найден"); + var sheetProperties = await GetSheetProperties(spreadsheetId, sheetName); + if (sheetProperties?.SheetId == null || sheetProperties.GridProperties.RowCount == null || + sheetProperties.GridProperties.ColumnCount == null) + return Result.Failed("Лист с таким названием не найден"); var (valueRange, range, updateStyleRequestBody) = Generate( - statistics.ToList(), course, sheetName, (int)sheetId); + statistics.ToList(), course, sheetName, (int)sheetProperties.SheetId); + + var rowDifference = valueRange.Values.Count - (int)sheetProperties.GridProperties.RowCount; + var columnDifference = valueRange.Values.First().Count - + (int)sheetProperties.GridProperties.ColumnCount; + if (rowDifference > 0 || columnDifference > 0) + { + var appendDimensionRequest = _internalGoogleSheetsService.Spreadsheets. + BatchUpdate(GetAppendDimensionBatchRequest(rowDifference, columnDifference), + spreadsheetId); + await appendDimensionRequest.ExecuteAsync(); + } var clearRequest = _internalGoogleSheetsService.Spreadsheets.Values.Clear(new ClearValuesRequest(), spreadsheetId, range); await clearRequest.ExecuteAsync(); @@ -88,6 +101,35 @@ public static Result ParseLink(string sheetUrl) : Result.Failed("Некорректная ссылка на страницу Google Docs"); } + private static BatchUpdateSpreadsheetRequest GetAppendDimensionBatchRequest(int rowDifference, int columnDifference) + { + var batchUpdateRequest = new BatchUpdateSpreadsheetRequest(); + batchUpdateRequest.Requests = new List(); + + if (rowDifference > 0) + { + var appendRowsRequest = new AppendDimensionRequest(); + appendRowsRequest.Dimension = "ROWS"; + appendRowsRequest.Length = rowDifference; + var request = new Request(); + request.AppendDimension = appendRowsRequest; + batchUpdateRequest.Requests.Add(request); + } + + if (columnDifference > 0) + { + var appendColumnsRequest = new AppendDimensionRequest(); + appendColumnsRequest.Dimension = "COLUMNS"; + appendColumnsRequest.Length = columnDifference; + var request = new Request(); + request.AppendDimension = appendColumnsRequest; + batchUpdateRequest.Requests.Add(request); + + } + + return batchUpdateRequest; + } + /// /// Generates query data to create a report in Google Sheets. /// @@ -360,15 +402,14 @@ private static void AddColouredCellsRequests( } } - private async Task GetSheetId(string spreadsheetId, string sheetName) + private async Task GetSheetProperties(string spreadsheetId, string sheetName) { var spreadsheetGetRequest = _internalGoogleSheetsService.Spreadsheets.Get(spreadsheetId); spreadsheetGetRequest.IncludeGridData = true; try { var spreadsheetResponse = await spreadsheetGetRequest.ExecuteAsync(); - var sheetId = spreadsheetResponse.Sheets.First(sheet => sheet.Properties.Title == sheetName).Properties.SheetId; - return sheetId; + return spreadsheetResponse.Sheets.First(sheet => sheet.Properties.Title == sheetName).Properties; } catch (Exception) { From 5b4aeb3d5dcee272a8262d3cb90d832c24f9442d Mon Sep 17 00:00:00 2001 From: YuriUfimtsev Date: Mon, 26 Feb 2024 22:48:11 +0300 Subject: [PATCH 23/58] fix: change min rating finding algorithm --- .../HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs b/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs index 050763f02..283ee44f1 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs @@ -66,7 +66,6 @@ public static ExcelPackage Generate( var rowsNumber = 3 + courseMatesModels.Count; var position = new Position(1, 1); - worksheet.Cells[position.Row, position.Column].Value = ""; worksheet.Cells[position.Row, position.Column, position.Row + 2, position.Column].Merge = true; ++position.Column; @@ -115,7 +114,7 @@ private static void AddHomeworksHeaders(ExcelWorksheet worksheet, CourseDTO cour { var numberCellsToMerge = course.Homeworks[i].Tasks.Count * 3; worksheet.Cells[position.Row, position.Column].Value - = $"/ {homeworkNumber.ToString()}: {course.Homeworks[i].Title}, {course.Homeworks[i].Date.ToString("dd.MM")}"; + = $"h/w {homeworkNumber.ToString()}: {course.Homeworks[i].Title}, {course.Homeworks[i].Date.ToString("dd.MM")}"; worksheet.Cells[position.Row, position.Column, position.Row, position.Column + numberCellsToMerge - 1] .Merge = true; position.Column += numberCellsToMerge; @@ -210,7 +209,8 @@ private static void AddCourseMatesInfo( var solutions = allSolutions .Where(solution => solution.State == SolutionState.Rated || solution.State == SolutionState.Final); - var min = solutions.Max(solution => solution.Rating) ?? 0; + var min = solutions + .Where(solution => solution.State == SolutionState.Final); var cnt = solutions.Count(); worksheet.Cells[position.Row, position.Column].Value = min; worksheet.Cells[position.Row, position.Column + 2].Value = cnt; From 7bfabc8d77f04cba3ea9489bf9c571aca20fc928 Mon Sep 17 00:00:00 2001 From: YuriUfimtsev Date: Sat, 2 Mar 2024 22:14:01 +0300 Subject: [PATCH 24/58] rebase; change Date to PublicationDate --- .../TableGenerators/ExcelGenerator.cs | 2 +- .../ExcelGeneratorTests.cs | 36 +++++++++---------- .../Controllers/SolutionsController.cs | 2 +- hwproj.front/src/api/api.ts | 2 +- 4 files changed, 19 insertions(+), 23 deletions(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs b/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs index 283ee44f1..1c9bc80df 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs @@ -114,7 +114,7 @@ private static void AddHomeworksHeaders(ExcelWorksheet worksheet, CourseDTO cour { var numberCellsToMerge = course.Homeworks[i].Tasks.Count * 3; worksheet.Cells[position.Row, position.Column].Value - = $"h/w {homeworkNumber.ToString()}: {course.Homeworks[i].Title}, {course.Homeworks[i].Date.ToString("dd.MM")}"; + = $"h/w {homeworkNumber.ToString()}: {course.Homeworks[i].Title}, {course.Homeworks[i].PublicationDate.ToString("dd.MM")}"; worksheet.Cells[position.Row, position.Column, position.Row, position.Column + numberCellsToMerge - 1] .Merge = true; position.Column += numberCellsToMerge; diff --git a/HwProj.APIGateway/HwProj.APIGateway.Tests/ExcelGeneratorTests.cs b/HwProj.APIGateway/HwProj.APIGateway.Tests/ExcelGeneratorTests.cs index 91bd4d0fe..8d967a788 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.Tests/ExcelGeneratorTests.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.Tests/ExcelGeneratorTests.cs @@ -7,6 +7,7 @@ using OfficeOpenXml; using System.IO; using System; +using HwProj.Models.SolutionsService; using NUnit.Framework.Interfaces; namespace HwProj.APIGateway.Tests @@ -35,7 +36,7 @@ private enum CellProperty new HomeworkViewModel() { Title = "TestHomework1", - Date = new DateTime(2023, 6, 4), + PublicationDate = new DateTime(2023, 6, 4), Tasks = new List() { new HomeworkTaskViewModel() @@ -55,7 +56,7 @@ private enum CellProperty new HomeworkViewModel() { Title = "TestHomework2", - Date = new System.DateTime(2023, 6, 5), + PublicationDate = new System.DateTime(2023, 6, 5), Tasks = new List { new HomeworkTaskViewModel() @@ -93,20 +94,17 @@ private enum CellProperty { new StatisticsCourseTasksModel() { - Solution = new List + Solution = new List { - new StatisticsCourseSolutionsModel - (new Models.SolutionsService.Solution() { State = Models.SolutionsService.SolutionState.Rated, Rating = 4 } ), - new StatisticsCourseSolutionsModel - (new Models.SolutionsService.Solution() { State = Models.SolutionsService.SolutionState.Posted } ), + new Solution { State = SolutionState.Rated, Rating = 4 }, + new Solution() { State = SolutionState.Posted } } }, new StatisticsCourseTasksModel() { - Solution = new List + Solution = new List { - new StatisticsCourseSolutionsModel - (new Models.SolutionsService.Solution() { State = Models.SolutionsService.SolutionState.Posted } ), + new Solution() { State = SolutionState.Posted } } } } @@ -117,11 +115,11 @@ private enum CellProperty { new StatisticsCourseTasksModel() { - Solution = new List() + Solution = new List() }, new StatisticsCourseTasksModel() { - Solution = new List() + Solution = new List() } } } @@ -138,16 +136,14 @@ private enum CellProperty { new StatisticsCourseTasksModel() { - Solution = new List() + Solution = new List() }, new StatisticsCourseTasksModel() { - Solution = new List + Solution = new List { - new StatisticsCourseSolutionsModel - (new Models.SolutionsService.Solution() { State = Models.SolutionsService.SolutionState.Rated, Rating = 5 } ), - new StatisticsCourseSolutionsModel - (new Models.SolutionsService.Solution() { State = Models.SolutionsService.SolutionState.Rated, Rating = 7 } ), + new Solution() { State = SolutionState.Rated, Rating = 5 }, + new Solution() { State = SolutionState.Rated, Rating = 7 } } } } @@ -158,11 +154,11 @@ private enum CellProperty { new StatisticsCourseTasksModel() { - Solution = new List() + Solution = new List() }, new StatisticsCourseTasksModel() { - Solution = new List() + Solution = new List() } } } diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs b/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs index 430ca5696..d66ac39c0 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs @@ -206,7 +206,7 @@ public async Task GetCourseStat(long courseId) var course = await _coursesClient.GetCourseById(courseId); if (course == null) return NotFound(); - course.Homeworks = course.Homeworks.OrderBy(homework => homework.Date).ToArray(); + course.Homeworks = course.Homeworks.OrderBy(homework => homework.PublicationDate).ToArray(); for (var i = 0; i < course.Homeworks.Length; ++i) { course.Homeworks[i].Tasks = course.Homeworks[i].Tasks.OrderBy(task => task.PublicationDate).ToList(); diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index 3c6813527..15df91704 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -54,7 +54,7 @@ export interface FetchArgs { * @class BaseAPI */ export class BaseAPI { - protected configuration: Configuration | undefined; + protected configuration?: Configuration | undefined; constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected fetch: FetchAPI = isomorphicFetch) { if (configuration) { From f8e67d015994d0912b289011941c7c5f78bd92d5 Mon Sep 17 00:00:00 2001 From: YuriUfimtsev Date: Sun, 3 Mar 2024 19:46:30 +0300 Subject: [PATCH 25/58] refactor; improve on-page search --- .../TableGenerators/ExcelGenerator.cs | 5 +- .../src/components/Courses/Course.tsx | 49 ++++++------- .../src/components/Courses/StudentStats.tsx | 17 +++-- .../components/Solutions/ExportToYandex.tsx | 1 - .../src/components/Solutions/SaveStats.tsx | 69 ++++++++++--------- 5 files changed, 74 insertions(+), 67 deletions(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs b/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs index 1c9bc80df..61e964554 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs @@ -139,7 +139,7 @@ private static void AddTasksHeaders(ExcelWorksheet worksheet, CourseDTO course, } worksheet.Cells[position.Row, position.Column].Value - = $"{taskNumber.ToString()} {course.Homeworks[i].Tasks[j].Title}"; + = $"{taskNumber.ToString()}. {course.Homeworks[i].Tasks[j].Title}"; worksheet.Cells[position.Row, position.Column, position.Row, position.Column + 2].Merge = true; position.Column += 3; ++taskNumber; @@ -209,8 +209,7 @@ private static void AddCourseMatesInfo( var solutions = allSolutions .Where(solution => solution.State == SolutionState.Rated || solution.State == SolutionState.Final); - var min = solutions - .Where(solution => solution.State == SolutionState.Final); + var min = solutions.Any() ? solutions.Last().Rating : 0; var cnt = solutions.Count(); worksheet.Cells[position.Row, position.Column].Value = min; worksheet.Cells[position.Row, position.Column + 2].Value = cnt; diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index af1e6d8c3..95c575bf1 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -84,11 +84,11 @@ const updatedLastViewedCourseId = (courseId : string) => } const Course: React.FC = () => { - const {initialCourseId, tab} = useParams() + const {courseId, tab} = useParams() const [searchParams] = useSearchParams() - const isFromYandex = initialCourseId === undefined - const courseId = isFromYandex ? getLastViewedCourseId() : initialCourseId + const isFromYandex = courseId === undefined + const validatedCourseId = isFromYandex ? getLastViewedCourseId() : courseId const navigate = useNavigate() @@ -264,20 +264,22 @@ const Course: React.FC = () => { const hasAccessToMaterials = course.isOpen || isCourseMentor || isAcceptedStudent const changeTab = (newTab: string) => { - if (isAcceptableTabValue(newTab) && newTab !== pageState.tabValue) { - if (newTab === "stats" && !showStatsTab) return; - if (newTab === "applications" && !showApplicationsTab) return; - - setPageState(prevState => ({ - ...prevState, - tabValue: newTab - })); + if (!isFromYandex) { + if (isAcceptableTabValue(newTab) && newTab !== pageState.tabValue) { + if (newTab === "stats" && !showStatsTab) return; + if (newTab === "applications" && !showApplicationsTab) return; + + setPageState(prevState => ({ + ...prevState, + tabValue: newTab + })); + } } } const setCurrentState = async () => { - updatedLastViewedCourseId(courseId) - const course = await ApiSingleton.coursesApi.coursesGetCourseData(+courseId!) + updatedLastViewedCourseId(validatedCourseId) + const course = await ApiSingleton.coursesApi.coursesGetCourseData(+validatedCourseId!) // У пользователя изменилась роль (иначе он не может стать лектором в курсе), // однако он все ещё использует токен с прежней ролью @@ -291,6 +293,8 @@ const Course: React.FC = () => { return } + const solutions = await ApiSingleton.statisticsApi.apiStatisticsByCourseIdGet(+validatedCourseId!) + setCourseState(prevState => ({ ...prevState, isFound: true, @@ -303,9 +307,8 @@ const Course: React.FC = () => { studentSolutions: solutions, tabValue: isFromYandex ? "stats" : "homeworks" })) - if (isFromYandex) - { - window.history.replaceState(null, "", `/courses/${courseId}`) + if (isFromYandex) { + window.history.replaceState(null, "", `/courses/${validatedCourseId}/stats`) } } @@ -337,13 +340,13 @@ const Course: React.FC = () => { ApiSingleton.statisticsApi.statisticsGetCourseStatistics(+courseId!) .then(res => setStudentSolutions(res)) }, [courseId]) - - useEffect(() => changeTab(tab || "homeworks"), [tab, courseId, isFound]) + + useEffect(() => changeTab(tab || "homeworks"), [tab, validatedCourseId, isFound]) const yandexCode = new URLSearchParams(window.location.search).get("code") const joinCourse = async () => { - await ApiSingleton.coursesApi.coursesSignInCourse(+courseId!) + await ApiSingleton.coursesApi.coursesSignInCourse(+validatedCourseId!) .then(() => setCurrentState()); } @@ -499,9 +502,9 @@ const Course: React.FC = () => { value={tabValue === "homeworks" ? 0 : tabValue === "stats" ? 1 : 2} indicatorColor="primary" onChange={(event, value) => { - if (value === 0 && !isExpert) navigate(`/courses/${courseId}/homeworks`) - if (value === 1) navigate(`/courses/${courseId}/stats`) - if (value === 2 && !isExpert) navigate(`/courses/${courseId}/applications`) + if (value === 0 && !isExpert) navigate(`/courses/${validatedCourseId}/homeworks`) + if (value === 1) navigate(`/courses/${validatedCourseId}/stats`) + if (value === 2 && !isExpert) navigate(`/courses/${validatedCourseId}/applications`) }} > {hasAccessToMaterials && !isExpert && @@ -582,7 +585,7 @@ const Course: React.FC = () => { onUpdate={() => setCurrentState()} course={courseState.course} students={courseState.newStudents} - courseId={courseId!} + courseId={validatedCourseId!} /> } diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index 5019593fa..3d7e35f3b 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -22,13 +22,15 @@ interface IStudentStatsProps { interface IStudentStatsState { searched: string + isSaveStatsActionOpened: boolean } const greyBorder = grey[300] const StudentStats: React.FC = (props) => { const [state, setSearched] = useState({ - searched: "" + searched: "", + isSaveStatsActionOpened: false }); const {courseId} = useParams(); const navigate = useNavigate(); @@ -36,24 +38,25 @@ const StudentStats: React.FC = (props) => { navigate(`/statistics/${courseId}/charts`) } - const {searched} = state + const {searched, isSaveStatsActionOpened} = state useEffect(() => { const keyDownHandler = (event: KeyboardEvent) => { + if (isSaveStatsActionOpened) return if (event.ctrlKey || event.altKey) return if (searched && event.key === "Escape") { - setSearched({searched: ""}); + setSearched({...state, searched: ""}); } else if (searched && event.key === "Backspace") { - setSearched({searched: searched.slice(0, -1)}) + setSearched({...state, searched: searched.slice(0, -1)}) } else if (event.key.length === 1 && event.key.match(/[a-zA-Zа-яА-Я\s]/i) ) { - setSearched({searched: searched + event.key}) + setSearched({...state, searched: searched + event.key}) } }; document.addEventListener('keydown', keyDownHandler); return () => document.removeEventListener('keydown', keyDownHandler); - }, [searched]); + }, [searched, isSaveStatsActionOpened]); const homeworks = props.homeworks.filter(h => h.tasks && h.tasks.length > 0) const solutions = searched @@ -303,6 +306,8 @@ const StudentStats: React.FC = (props) => { courseId={props.course.id} userId={props.userId} yandexCode={props.yandexCode} + onActionOpening={() => setSearched({searched, isSaveStatsActionOpened: true})} + onActionClosing={() => setSearched({searched, isSaveStatsActionOpened: false})} /> diff --git a/hwproj.front/src/components/Solutions/ExportToYandex.tsx b/hwproj.front/src/components/Solutions/ExportToYandex.tsx index 11add976c..4ad563e4d 100644 --- a/hwproj.front/src/components/Solutions/ExportToYandex.tsx +++ b/hwproj.front/src/components/Solutions/ExportToYandex.tsx @@ -1,7 +1,6 @@ import React, { FC, useState } from "react"; import { useEffect } from 'react'; import { ResultString } from "../../api"; -import { ResultExternalService } from "../../api"; import { Alert, Box, Button, CircularProgress, Grid, Link, MenuItem, Select, TextField } from "@mui/material"; import apiSingleton from "../../api/ApiSingleton"; import { green, red } from "@material-ui/core/colors"; diff --git a/hwproj.front/src/components/Solutions/SaveStats.tsx b/hwproj.front/src/components/Solutions/SaveStats.tsx index de6620c72..03e784b81 100644 --- a/hwproj.front/src/components/Solutions/SaveStats.tsx +++ b/hwproj.front/src/components/Solutions/SaveStats.tsx @@ -1,4 +1,4 @@ -import React, {FC, useState} from "react"; +import React, {FC, useState, useEffect} from "react"; import {Alert, Box, Button, CircularProgress, Grid, MenuItem, Select, TextField} from "@mui/material"; import SpeedDialIcon from '@mui/material/SpeedDialIcon'; import GetAppIcon from '@material-ui/icons/GetApp'; @@ -19,61 +19,62 @@ interface SaveStatsProps { courseId : number | undefined; userId : string; yandexCode: string | null; + onActionOpening: () => void; + onActionClosing: () => void; + } -enum SpeedDialActions { - None, +enum SpeedDialView { + Collapsed, + Expanded, Download, ShareWithGoogle, ShareWithYandex } -enum SpeedDialView { - Opened, - Expanded -} - interface SaveStatsState { - selectedAction: SpeedDialActions; - speedDialView : SpeedDialView; + selectedView : SpeedDialView; } const SaveStats: FC = (props : SaveStatsProps) => { - const [state, setState] = useState({ - selectedAction: props.yandexCode === null ? SpeedDialActions.None : SpeedDialActions.ShareWithYandex, - speedDialView: SpeedDialView.Opened + const [state, setSelectedView] = useState({ + selectedView: props.yandexCode === null ? SpeedDialView.Collapsed : SpeedDialView.ShareWithYandex, }) - const {selectedAction, speedDialView} = state + const {selectedView} = state - const handleCancellation = () => { - setState(prevState => ({...prevState, speedDialView: SpeedDialView.Opened, selectedAction: SpeedDialActions.None})); - } + useEffect(() => { + if (selectedView === SpeedDialView.Download || + selectedView === SpeedDialView.ShareWithGoogle || + selectedView === SpeedDialView.ShareWithYandex) { + props.onActionOpening() + return + } + props.onActionClosing() + }, [selectedView]); const handleSpeedDialItemClick = (operation : string) => { switch ( operation ) { case 'download': - setState(prevState => - ({...prevState, selectedAction: SpeedDialActions.Download})); + setSelectedView({selectedView: SpeedDialView.Download}); break; case 'shareWithGoogle': - setState(prevState => - ({...prevState, selectedAction: SpeedDialActions.ShareWithGoogle})); + setSelectedView({selectedView: SpeedDialView.ShareWithGoogle}); break; case 'shareWithYandex': - setState(prevState => - ({...prevState, selectedAction: SpeedDialActions.ShareWithYandex})); + setSelectedView({selectedView: SpeedDialView.ShareWithYandex}); break; default: break; } } - const handleChangeSpeedDialView = () => - setState(prevState => ({ - ...prevState, - speedDialView: speedDialView === SpeedDialView.Opened ? SpeedDialView.Expanded : SpeedDialView.Opened - })) + const handleCancellation = () => setSelectedView({selectedView: SpeedDialView.Collapsed}); + + const handleChangingView = () => + setSelectedView({selectedView: selectedView === SpeedDialView.Collapsed ? + SpeedDialView.Expanded : SpeedDialView.Collapsed + }) const actions = [ { icon: , name: 'Сохранить', operation: 'download' }, @@ -84,15 +85,15 @@ const SaveStats: FC = (props : SaveStatsProps) => { return (
- {selectedAction === SpeedDialActions.None && + {(selectedView === SpeedDialView.Collapsed || selectedView === SpeedDialView.Expanded) && } direction="right" - onClick={handleChangeSpeedDialView} + onClickCapture={handleChangingView} sx={{ '& .MuiFab-primary': { width: 45, height: 45 } }} - open={speedDialView === SpeedDialView.Expanded} + open={selectedView !== SpeedDialView.Collapsed} > {actions.map((action) => ( = (props : SaveStatsProps) => { } - {selectedAction === SpeedDialActions.Download && + {selectedView === SpeedDialView.Download && handleCancellation()} /> } - {selectedAction === SpeedDialActions.ShareWithGoogle && + {selectedView === SpeedDialView.ShareWithGoogle && handleCancellation()} /> } - {selectedAction === SpeedDialActions.ShareWithYandex && + {selectedView === SpeedDialView.ShareWithYandex && Date: Wed, 30 Apr 2025 16:00:45 +0300 Subject: [PATCH 26/58] update backend --- .../Controllers/StatisticsController.cs | 12 ++++-------- .../ExportServices/GoogleService.cs | 3 +-- .../TableGenerators/ExcelGenerator.cs | 2 +- .../HwProj.APIGateway.Tests/ExcelGeneratorTests.cs | 2 +- .../SolutionsService/SolutionViewModel.cs | 1 + global.json | 7 ------- 6 files changed, 8 insertions(+), 19 deletions(-) delete mode 100644 global.json diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs index c4e3ad2d2..8dc9abbb7 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs @@ -12,7 +12,6 @@ using HwProj.Models.CoursesService.DTO; using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.Roles; -using HwProj.CoursesService.Client; using HwProj.Models.Result; using HwProj.SolutionsService.Client; using Microsoft.AspNetCore.Authorization; @@ -26,19 +25,18 @@ public class StatisticsController : AggregationController { private readonly ISolutionsServiceClient _solutionClient; private readonly ICoursesServiceClient _coursesClient; - private readonly ICoursesServiceClient _coursesClient; private readonly GoogleService _googleService; public StatisticsController( ISolutionsServiceClient solutionClient, ICoursesServiceClient coursesServiceClient, IAuthServiceClient authServiceClient, - ICoursesServiceClient coursesServiceClient, GoogleService googleService) : base(authServiceClient) { _solutionClient = solutionClient; _coursesClient = coursesServiceClient; + _googleService = googleService; } [HttpGet("{courseId}/lecturers")] @@ -60,8 +58,6 @@ public async Task GetLecturersStatistics(long courseId) }).ToArray(); return Ok(result); - _coursesClient = coursesServiceClient; - _googleService = googleService; } [HttpGet("{courseId}")] @@ -104,7 +100,7 @@ public async Task GetCourseStatistics(long courseId) }; }).OrderBy(t => t.Surname).ThenBy(t => t.Name); - return Ok(result); + return result; } [HttpGet("{courseId}/charts")] @@ -144,7 +140,7 @@ public async Task GetChartStatistics(long courseId) BestStudentSolutions = statisticsMeasure.BestStudentSolutions }; - return result; + return Ok(result); } /// @@ -194,7 +190,7 @@ public async Task ExportToGoogleSheets( var result = await _googleService.Export(course, statistics, sheetUrl, sheetName); return result; } - + private async Task> GetStudentsToMentorsDictionary( MentorToAssignedStudentsDTO[] mentorsToStudents) { diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs index a73f4b84e..7f3761afb 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs @@ -6,10 +6,9 @@ using System.Threading.Tasks; using Google.Apis.Sheets.v4; using Google.Apis.Sheets.v4.Data; -using HwProj.APIGateway.API.Models; +using HwProj.APIGateway.API.Models.Statistics; using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.Result; -using Microsoft.AspNetCore.Mvc; using OfficeOpenXml; using OfficeOpenXml.Style; diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs b/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs index 61e964554..b184889e8 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using HwProj.APIGateway.API.Models; +using HwProj.APIGateway.API.Models.Statistics; using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.SolutionsService; using OfficeOpenXml; diff --git a/HwProj.APIGateway/HwProj.APIGateway.Tests/ExcelGeneratorTests.cs b/HwProj.APIGateway/HwProj.APIGateway.Tests/ExcelGeneratorTests.cs index 8d967a788..aa08c11b4 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.Tests/ExcelGeneratorTests.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.Tests/ExcelGeneratorTests.cs @@ -1,7 +1,7 @@ using NUnit.Framework; using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.StatisticsService; -using HwProj.APIGateway.API.Models; +using HwProj.APIGateway.API.Models.Statistics; using HwProj.APIGateway.API.TableGenerators; using System.Collections.Generic; using OfficeOpenXml; diff --git a/HwProj.Common/HwProj.Models/SolutionsService/SolutionViewModel.cs b/HwProj.Common/HwProj.Models/SolutionsService/SolutionViewModel.cs index 4387b5a98..d8d06821d 100644 --- a/HwProj.Common/HwProj.Models/SolutionsService/SolutionViewModel.cs +++ b/HwProj.Common/HwProj.Models/SolutionsService/SolutionViewModel.cs @@ -11,6 +11,7 @@ public class SolutionViewModel public string StudentId { get; set; } public string[]? GroupMateIds { get; set; } + public DateTime PublicationDate { get; set; } public string LecturerComment { get; set; } diff --git a/global.json b/global.json deleted file mode 100644 index 0ae7749a6..000000000 --- a/global.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "sdk": { - "version": "2.2.0", - "rollForward": "minor", - "allowPrerelease": true - } -} \ No newline at end of file From a23ffc212a40f6079c2e54fb4e370bf5c4460692 Mon Sep 17 00:00:00 2001 From: bygu4 Date: Wed, 30 Apr 2025 16:24:30 +0300 Subject: [PATCH 27/58] update api client --- hwproj.front/src/api/api.ts | 1003 +++++++++++++++++------------------ 1 file changed, 496 insertions(+), 507 deletions(-) diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index 15df91704..a2743bc05 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -54,7 +54,7 @@ export interface FetchArgs { * @class BaseAPI */ export class BaseAPI { - protected configuration?: Configuration | undefined; + protected configuration: Configuration | undefined; constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected fetch: FetchAPI = isomorphicFetch) { if (configuration) { @@ -270,8 +270,8 @@ export interface BooleanResult { */ export interface CategorizedNotifications { /** - * - * @type {number} + * + * @type {CategoryState} * @memberof CategorizedNotifications */ category?: CategoryState; @@ -907,8 +907,8 @@ export interface GetSolutionModel { */ comment?: string; /** - * - * @type {number} + * + * @type {SolutionState} * @memberof GetSolutionModel */ state?: SolutionState; @@ -1489,8 +1489,8 @@ export interface NotificationViewModel { */ owner?: string; /** - * - * @type {number} + * + * @type {CategoryState} * @memberof NotificationViewModel */ category?: CategoryState; @@ -1707,59 +1707,6 @@ export interface Result { */ errors?: Array; } - -/** - * - * @export - * @interface ResultString - */ -export interface ResultString { - /** - * - * @type {Array} - * @memberof ResultString - */ - value?: Array; - /** - * - * @type {boolean} - * @memberof ResultString - */ - succeeded?: boolean; - /** - * - * @type {Array} - * @memberof ResultString - */ - errors?: Array; -} - -/** - * - * @export - * @interface ResultTokenCredentials - */ -export interface ResultTokenCredentials { - /** - * - * @type {TokenCredentials} - * @memberof ResultTokenCredentials - */ - value?: TokenCredentials; - /** - * - * @type {boolean} - * @memberof ResultTokenCredentials - */ - succeeded?: boolean; - /** - * - * @type {Array} - * @memberof ResultTokenCredentials - */ - errors?: Array; -} - /** * * @export @@ -1824,8 +1771,8 @@ export interface Solution { */ comment?: string; /** - * - * @type {number} + * + * @type {SolutionState} * @memberof Solution */ state?: SolutionState; @@ -2191,6 +2138,31 @@ export interface StatisticsLecturersModel { */ numberOfCheckedUniqueSolutions?: number; } +/** + * + * @export + * @interface StringArrayResult + */ +export interface StringArrayResult { + /** + * + * @type {Array} + * @memberof StringArrayResult + */ + value?: Array; + /** + * + * @type {boolean} + * @memberof StringArrayResult + */ + succeeded?: boolean; + /** + * + * @type {Array} + * @memberof StringArrayResult + */ + errors?: Array; +} /** * * @export @@ -2376,8 +2348,8 @@ export interface TaskDeadlineView { */ deadline?: TaskDeadlineDto; /** - * - * @type {number} + * + * @type {SolutionState} * @memberof TaskDeadlineView */ solutionState?: SolutionState; @@ -2407,8 +2379,8 @@ export interface TaskDeadlineView { */ export interface TaskSolutionStatisticsPageData { /** - * - * @type {Array} + * + * @type {Array} * @memberof TaskSolutionStatisticsPageData */ taskSolutions?: Array; @@ -2419,8 +2391,8 @@ export interface TaskSolutionStatisticsPageData { */ courseId?: number; /** - * - * @type {Array} + * + * @type {Array} * @memberof TaskSolutionStatisticsPageData */ statsForTasks?: Array; @@ -2639,7 +2611,6 @@ export interface UserDataDto { */ taskDeadlines?: Array; } - /** * * @export @@ -2653,8 +2624,8 @@ export interface UserTaskSolutions { */ solutions?: Array; /** - * - * @type {AccountDataDto} + * + * @type {StudentDataDto} * @memberof UserTaskSolutions */ student?: StudentDataDto; @@ -2715,17 +2686,30 @@ export interface UserTaskSolutionsPageData { */ courseMates?: Array; /** - * - * @type {HomeworkTaskViewModel} + * + * @type {Array} * @memberof UserTaskSolutionsPageData */ - task?: HomeworkTaskViewModel; + taskSolutions?: Array; +} +/** + * + * @export + * @interface WorkspaceViewModel + */ +export interface WorkspaceViewModel { /** - * - * @type {Array} - * @memberof UserTaskSolutionsPageData + * + * @type {Array} + * @memberof WorkspaceViewModel */ - taskSolutions?: Array; + students?: Array; + /** + * + * @type {Array} + * @memberof WorkspaceViewModel + */ + homeworks?: Array; } /** * AccountApi - fetch parameter creator @@ -2734,8 +2718,8 @@ export interface UserTaskSolutionsPageData { export const AccountApiFetchParamCreator = function (configuration?: Configuration) { return { /** - * - * @param {EditExternalViewModel} [model] + * + * @param {string} [code] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -2769,8 +2753,8 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {EditAccountViewModel} [model] + * + * @param {EditAccountViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -2834,8 +2818,8 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {string} userId + * + * @param {UrlDto} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -2899,7 +2883,8 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * + * + * @param {string} userId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -2934,8 +2919,8 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {InviteLecturerViewModel} [model] + * + * @param {InviteLecturerViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -2969,8 +2954,8 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {LoginViewModel} [model] + * + * @param {LoginViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3034,8 +3019,8 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {RegisterViewModel} [model] + * + * @param {RegisterViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3069,8 +3054,8 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {RequestPasswordRecoveryViewModel} [model] + * + * @param {RequestPasswordRecoveryViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3104,8 +3089,8 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {ResetPasswordViewModel} [model] + * + * @param {ResetPasswordViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3148,8 +3133,8 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati export const AccountApiFp = function(configuration?: Configuration) { return { /** - * - * @param {EditExternalViewModel} [model] + * + * @param {string} [code] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3166,8 +3151,8 @@ export const AccountApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {EditAccountViewModel} [model] + * + * @param {EditAccountViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3201,8 +3186,8 @@ export const AccountApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {string} userId + * + * @param {UrlDto} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3236,8 +3221,8 @@ export const AccountApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {InviteLecturerViewModel} [model] + * + * @param {string} userId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3254,8 +3239,8 @@ export const AccountApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {LoginViewModel} [model] + * + * @param {InviteLecturerViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3272,7 +3257,8 @@ export const AccountApiFp = function(configuration?: Configuration) { }; }, /** - * + * + * @param {LoginViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3289,8 +3275,7 @@ export const AccountApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {RegisterViewModel} [model] + * * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3307,8 +3292,8 @@ export const AccountApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {RequestPasswordRecoveryViewModel} [model] + * + * @param {RegisterViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3325,8 +3310,8 @@ export const AccountApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {ResetPasswordViewModel} [model] + * + * @param {RequestPasswordRecoveryViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3370,8 +3355,8 @@ export const AccountApiFp = function(configuration?: Configuration) { export const AccountApiFactory = function (configuration?: Configuration, fetch?: FetchAPI, basePath?: string) { return { /** - * - * @param {EditExternalViewModel} [model] + * + * @param {string} [code] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3379,8 +3364,8 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? return AccountApiFp(configuration).accountAuthorizeGithub(code, options)(fetch, basePath); }, /** - * - * @param {EditAccountViewModel} [model] + * + * @param {EditAccountViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3396,8 +3381,8 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? return AccountApiFp(configuration).accountGetAllStudents(options)(fetch, basePath); }, /** - * - * @param {string} userId + * + * @param {UrlDto} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3413,8 +3398,8 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? return AccountApiFp(configuration).accountGetUserData(options)(fetch, basePath); }, /** - * - * @param {InviteLecturerViewModel} [model] + * + * @param {string} userId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3422,8 +3407,8 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? return AccountApiFp(configuration).accountGetUserDataById(userId, options)(fetch, basePath); }, /** - * - * @param {LoginViewModel} [model] + * + * @param {InviteLecturerViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3431,7 +3416,8 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? return AccountApiFp(configuration).accountInviteNewLecturer(body, options)(fetch, basePath); }, /** - * + * + * @param {LoginViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3439,8 +3425,7 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? return AccountApiFp(configuration).accountLogin(body, options)(fetch, basePath); }, /** - * - * @param {RegisterViewModel} [model] + * * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3448,8 +3433,8 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? return AccountApiFp(configuration).accountRefreshToken(options)(fetch, basePath); }, /** - * - * @param {RequestPasswordRecoveryViewModel} [model] + * + * @param {RegisterViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3457,8 +3442,8 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? return AccountApiFp(configuration).accountRegister(body, options)(fetch, basePath); }, /** - * - * @param {ResetPasswordViewModel} [model] + * + * @param {RequestPasswordRecoveryViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3485,8 +3470,8 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? */ export class AccountApi extends BaseAPI { /** - * - * @param {EditExternalViewModel} [model] + * + * @param {string} [code] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AccountApi @@ -3496,8 +3481,8 @@ export class AccountApi extends BaseAPI { } /** - * - * @param {EditAccountViewModel} [model] + * + * @param {EditAccountViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AccountApi @@ -3517,8 +3502,8 @@ export class AccountApi extends BaseAPI { } /** - * - * @param {string} userId + * + * @param {UrlDto} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AccountApi @@ -3538,8 +3523,8 @@ export class AccountApi extends BaseAPI { } /** - * - * @param {InviteLecturerViewModel} [model] + * + * @param {string} userId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AccountApi @@ -3549,8 +3534,8 @@ export class AccountApi extends BaseAPI { } /** - * - * @param {LoginViewModel} [model] + * + * @param {InviteLecturerViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AccountApi @@ -3560,7 +3545,8 @@ export class AccountApi extends BaseAPI { } /** - * + * + * @param {LoginViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AccountApi @@ -3570,8 +3556,7 @@ export class AccountApi extends BaseAPI { } /** - * - * @param {RegisterViewModel} [model] + * * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AccountApi @@ -3581,8 +3566,8 @@ export class AccountApi extends BaseAPI { } /** - * - * @param {RequestPasswordRecoveryViewModel} [model] + * + * @param {RegisterViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AccountApi @@ -3592,8 +3577,8 @@ export class AccountApi extends BaseAPI { } /** - * - * @param {ResetPasswordViewModel} [model] + * + * @param {RequestPasswordRecoveryViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AccountApi @@ -3668,9 +3653,9 @@ export const CourseGroupsApiFetchParamCreator = function (configuration?: Config }; }, /** - * - * @param {number} courseId - * @param {CreateGroupViewModel} [model] + * + * @param {number} courseId + * @param {CreateGroupViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3823,10 +3808,8 @@ export const CourseGroupsApiFetchParamCreator = function (configuration?: Config }; }, /** - * - * @param {number} courseId - * @param {number} groupId - * @param {string} [userId] + * + * @param {number} groupId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3861,10 +3844,8 @@ export const CourseGroupsApiFetchParamCreator = function (configuration?: Config }; }, /** - * - * @param {number} courseId - * @param {number} groupId - * @param {UpdateGroupViewModel} [model] + * + * @param {number} groupId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3899,8 +3880,10 @@ export const CourseGroupsApiFetchParamCreator = function (configuration?: Config }; }, /** - * - * @param {number} groupId + * + * @param {number} courseId + * @param {number} groupId + * @param {string} [userId] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -3944,8 +3927,10 @@ export const CourseGroupsApiFetchParamCreator = function (configuration?: Config }; }, /** - * - * @param {number} groupId + * + * @param {number} courseId + * @param {number} groupId + * @param {UpdateGroupViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4018,9 +4003,9 @@ export const CourseGroupsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} courseId - * @param {CreateGroupViewModel} [model] + * + * @param {number} courseId + * @param {CreateGroupViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4092,10 +4077,8 @@ export const CourseGroupsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} courseId - * @param {number} groupId - * @param {string} [userId] + * + * @param {number} groupId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4112,10 +4095,8 @@ export const CourseGroupsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} courseId - * @param {number} groupId - * @param {UpdateGroupViewModel} [model] + * + * @param {number} groupId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4132,8 +4113,10 @@ export const CourseGroupsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} groupId + * + * @param {number} courseId + * @param {number} groupId + * @param {string} [userId] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4150,8 +4133,10 @@ export const CourseGroupsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} groupId + * + * @param {number} courseId + * @param {number} groupId + * @param {UpdateGroupViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4188,9 +4173,9 @@ export const CourseGroupsApiFactory = function (configuration?: Configuration, f return CourseGroupsApiFp(configuration).courseGroupsAddStudentInGroup(courseId, groupId, userId, options)(fetch, basePath); }, /** - * - * @param {number} courseId - * @param {CreateGroupViewModel} [model] + * + * @param {number} courseId + * @param {CreateGroupViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4226,10 +4211,8 @@ export const CourseGroupsApiFactory = function (configuration?: Configuration, f return CourseGroupsApiFp(configuration).courseGroupsGetCourseGroupsById(courseId, options)(fetch, basePath); }, /** - * - * @param {number} courseId - * @param {number} groupId - * @param {string} [userId] + * + * @param {number} groupId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4237,10 +4220,8 @@ export const CourseGroupsApiFactory = function (configuration?: Configuration, f return CourseGroupsApiFp(configuration).courseGroupsGetGroup(groupId, options)(fetch, basePath); }, /** - * - * @param {number} courseId - * @param {number} groupId - * @param {UpdateGroupViewModel} [model] + * + * @param {number} groupId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4248,8 +4229,10 @@ export const CourseGroupsApiFactory = function (configuration?: Configuration, f return CourseGroupsApiFp(configuration).courseGroupsGetGroupTasks(groupId, options)(fetch, basePath); }, /** - * - * @param {number} groupId + * + * @param {number} courseId + * @param {number} groupId + * @param {string} [userId] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4257,8 +4240,10 @@ export const CourseGroupsApiFactory = function (configuration?: Configuration, f return CourseGroupsApiFp(configuration).courseGroupsRemoveStudentFromGroup(courseId, groupId, userId, options)(fetch, basePath); }, /** - * - * @param {number} groupId + * + * @param {number} courseId + * @param {number} groupId + * @param {UpdateGroupViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4289,9 +4274,9 @@ export class CourseGroupsApi extends BaseAPI { } /** - * - * @param {number} courseId - * @param {CreateGroupViewModel} [model] + * + * @param {number} courseId + * @param {CreateGroupViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CourseGroupsApi @@ -4335,10 +4320,8 @@ export class CourseGroupsApi extends BaseAPI { } /** - * - * @param {number} courseId - * @param {number} groupId - * @param {string} [userId] + * + * @param {number} groupId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CourseGroupsApi @@ -4348,10 +4331,8 @@ export class CourseGroupsApi extends BaseAPI { } /** - * - * @param {number} courseId - * @param {number} groupId - * @param {UpdateGroupViewModel} [model] + * + * @param {number} groupId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CourseGroupsApi @@ -4361,8 +4342,10 @@ export class CourseGroupsApi extends BaseAPI { } /** - * - * @param {number} groupId + * + * @param {number} courseId + * @param {number} groupId + * @param {string} [userId] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CourseGroupsApi @@ -4372,8 +4355,10 @@ export class CourseGroupsApi extends BaseAPI { } /** - * - * @param {number} groupId + * + * @param {number} courseId + * @param {number} groupId + * @param {UpdateGroupViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CourseGroupsApi @@ -4545,8 +4530,10 @@ export const CoursesApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {number} courseId + * + * @param {number} courseId + * @param {string} mentorId + * @param {EditMentorWorkspaceDTO} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4626,8 +4613,7 @@ export const CoursesApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {CreateCourseViewModel} [model] + * * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -4980,9 +4966,9 @@ export const CoursesApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * - * @param {number} courseId - * @param {UpdateCourseViewModel} [model] + * + * @param {number} courseId + * @param {UpdateCourseViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5021,7 +5007,10 @@ export const CoursesApiFetchParamCreator = function (configuration?: Configurati }; }, /** - * + * + * @param {number} courseId + * @param {string} studentId + * @param {StudentCharacteristicsDto} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5148,8 +5137,10 @@ export const CoursesApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} courseId + * + * @param {number} courseId + * @param {string} mentorId + * @param {EditMentorWorkspaceDTO} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5184,8 +5175,7 @@ export const CoursesApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {CreateCourseViewModel} [model] + * * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5202,7 +5192,8 @@ export const CoursesApiFp = function(configuration?: Configuration) { }; }, /** - * + * + * @param {number} courseId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5363,9 +5354,9 @@ export const CoursesApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} courseId - * @param {UpdateCourseViewModel} [model] + * + * @param {number} courseId + * @param {UpdateCourseViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5382,7 +5373,10 @@ export const CoursesApiFp = function(configuration?: Configuration) { }; }, /** - * + * + * @param {number} courseId + * @param {string} studentId + * @param {StudentCharacteristicsDto} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5446,8 +5440,10 @@ export const CoursesApiFactory = function (configuration?: Configuration, fetch? return CoursesApiFp(configuration).coursesDeleteCourse(courseId, options)(fetch, basePath); }, /** - * - * @param {number} courseId + * + * @param {number} courseId + * @param {string} mentorId + * @param {EditMentorWorkspaceDTO} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5455,8 +5451,8 @@ export const CoursesApiFactory = function (configuration?: Configuration, fetch? return CoursesApiFp(configuration).coursesEditMentorWorkspace(courseId, mentorId, body, options)(fetch, basePath); }, /** - * - * @param {CreateCourseViewModel} [model] + * + * @param {number} courseId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5553,9 +5549,9 @@ export const CoursesApiFactory = function (configuration?: Configuration, fetch? return CoursesApiFp(configuration).coursesSignInCourse(courseId, options)(fetch, basePath); }, /** - * - * @param {number} courseId - * @param {UpdateCourseViewModel} [model] + * + * @param {number} courseId + * @param {UpdateCourseViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5563,7 +5559,10 @@ export const CoursesApiFactory = function (configuration?: Configuration, fetch? return CoursesApiFp(configuration).coursesUpdateCourse(courseId, body, options)(fetch, basePath); }, /** - * + * + * @param {number} courseId + * @param {string} studentId + * @param {StudentCharacteristicsDto} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -5627,8 +5626,10 @@ export class CoursesApi extends BaseAPI { } /** - * - * @param {number} courseId + * + * @param {number} courseId + * @param {string} mentorId + * @param {EditMentorWorkspaceDTO} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CoursesApi @@ -5638,8 +5639,8 @@ export class CoursesApi extends BaseAPI { } /** - * - * @param {CreateCourseViewModel} [model] + * + * @param {number} courseId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CoursesApi @@ -5758,9 +5759,9 @@ export class CoursesApi extends BaseAPI { } /** - * - * @param {number} courseId - * @param {UpdateCourseViewModel} [model] + * + * @param {number} courseId + * @param {UpdateCourseViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CoursesApi @@ -5770,7 +5771,10 @@ export class CoursesApi extends BaseAPI { } /** - * + * + * @param {number} courseId + * @param {string} studentId + * @param {StudentCharacteristicsDto} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CoursesApi @@ -6824,9 +6828,9 @@ export class FilesApi extends BaseAPI { export const HomeworksApiFetchParamCreator = function (configuration?: Configuration) { return { /** - * - * @param {number} courseId - * @param {CreateHomeworkViewModel} [homeworkViewModel] + * + * @param {number} courseId + * @param {CreateHomeworkViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -6973,9 +6977,9 @@ export const HomeworksApiFetchParamCreator = function (configuration?: Configura }; }, /** - * - * @param {number} homeworkId - * @param {CreateHomeworkViewModel} [homeworkViewModel] + * + * @param {number} homeworkId + * @param {CreateHomeworkViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7023,9 +7027,9 @@ export const HomeworksApiFetchParamCreator = function (configuration?: Configura export const HomeworksApiFp = function(configuration?: Configuration) { return { /** - * - * @param {number} courseId - * @param {CreateHomeworkViewModel} [homeworkViewModel] + * + * @param {number} courseId + * @param {CreateHomeworkViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7096,9 +7100,9 @@ export const HomeworksApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} homeworkId - * @param {CreateHomeworkViewModel} [homeworkViewModel] + * + * @param {number} homeworkId + * @param {CreateHomeworkViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7124,9 +7128,9 @@ export const HomeworksApiFp = function(configuration?: Configuration) { export const HomeworksApiFactory = function (configuration?: Configuration, fetch?: FetchAPI, basePath?: string) { return { /** - * - * @param {number} courseId - * @param {CreateHomeworkViewModel} [homeworkViewModel] + * + * @param {number} courseId + * @param {CreateHomeworkViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7161,9 +7165,9 @@ export const HomeworksApiFactory = function (configuration?: Configuration, fetc return HomeworksApiFp(configuration).homeworksGetHomework(homeworkId, options)(fetch, basePath); }, /** - * - * @param {number} homeworkId - * @param {CreateHomeworkViewModel} [homeworkViewModel] + * + * @param {number} homeworkId + * @param {CreateHomeworkViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7181,9 +7185,9 @@ export const HomeworksApiFactory = function (configuration?: Configuration, fetc */ export class HomeworksApi extends BaseAPI { /** - * - * @param {number} courseId - * @param {CreateHomeworkViewModel} [homeworkViewModel] + * + * @param {number} courseId + * @param {CreateHomeworkViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof HomeworksApi @@ -7226,9 +7230,9 @@ export class HomeworksApi extends BaseAPI { } /** - * - * @param {number} homeworkId - * @param {CreateHomeworkViewModel} [homeworkViewModel] + * + * @param {number} homeworkId + * @param {CreateHomeworkViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof HomeworksApi @@ -7245,7 +7249,8 @@ export class HomeworksApi extends BaseAPI { export const NotificationsApiFetchParamCreator = function (configuration?: Configuration) { return { /** - * + * + * @param {NotificationsSettingDto} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7309,8 +7314,7 @@ export const NotificationsApiFetchParamCreator = function (configuration?: Confi }; }, /** - * - * @param {Array} [notificationIds] + * * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7370,8 +7374,8 @@ export const NotificationsApiFetchParamCreator = function (configuration?: Confi }; }, /** - * - * @param {NotificationsSettingDto} [newSetting] + * + * @param {Array} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7414,7 +7418,8 @@ export const NotificationsApiFetchParamCreator = function (configuration?: Confi export const NotificationsApiFp = function(configuration?: Configuration) { return { /** - * + * + * @param {NotificationsSettingDto} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7448,8 +7453,7 @@ export const NotificationsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {Array} [notificationIds] + * * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7483,8 +7487,8 @@ export const NotificationsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {NotificationsSettingDto} [newSetting] + * + * @param {Array} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7510,7 +7514,8 @@ export const NotificationsApiFp = function(configuration?: Configuration) { export const NotificationsApiFactory = function (configuration?: Configuration, fetch?: FetchAPI, basePath?: string) { return { /** - * + * + * @param {NotificationsSettingDto} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7526,8 +7531,7 @@ export const NotificationsApiFactory = function (configuration?: Configuration, return NotificationsApiFp(configuration).notificationsGet(options)(fetch, basePath); }, /** - * - * @param {Array} [notificationIds] + * * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7543,8 +7547,8 @@ export const NotificationsApiFactory = function (configuration?: Configuration, return NotificationsApiFp(configuration).notificationsGetSettings(options)(fetch, basePath); }, /** - * - * @param {NotificationsSettingDto} [newSetting] + * + * @param {Array} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7562,7 +7566,8 @@ export const NotificationsApiFactory = function (configuration?: Configuration, */ export class NotificationsApi extends BaseAPI { /** - * + * + * @param {NotificationsSettingDto} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof NotificationsApi @@ -7582,8 +7587,7 @@ export class NotificationsApi extends BaseAPI { } /** - * - * @param {Array} [notificationIds] + * * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof NotificationsApi @@ -7603,8 +7607,8 @@ export class NotificationsApi extends BaseAPI { } /** - * - * @param {NotificationsSettingDto} [newSetting] + * + * @param {Array} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof NotificationsApi @@ -7690,8 +7694,6 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 localVarUrlObj.search = null; localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); - const needsSerialization = ("SolutionViewModel" !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; - localVarRequestOptions.body = needsSerialization ? JSON.stringify(model || {}) : (model || ""); return { url: url.format(localVarUrlObj), @@ -7700,23 +7702,17 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura }, /** * - * @param {number} courseId - * @param {number} taskId + * @param {number} solutionId * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiSolutionsCoursesByCourseIdTaskByTaskIdGet(courseId: number, taskId: number, options: any = {}): FetchArgs { - // verify required parameter 'courseId' is not null or undefined - if (courseId === null || courseId === undefined) { - throw new RequiredError('courseId','Required parameter courseId was null or undefined when calling apiSolutionsCoursesByCourseIdTaskByTaskIdGet.'); - } - // verify required parameter 'taskId' is not null or undefined - if (taskId === null || taskId === undefined) { - throw new RequiredError('taskId','Required parameter taskId was null or undefined when calling apiSolutionsCoursesByCourseIdTaskByTaskIdGet.'); + solutionsGetSolutionActuality(solutionId: number, options: any = {}): FetchArgs { + // verify required parameter 'solutionId' is not null or undefined + if (solutionId === null || solutionId === undefined) { + throw new RequiredError('solutionId','Required parameter solutionId was null or undefined when calling solutionsGetSolutionActuality.'); } - const localVarPath = `/api/Solutions/courses/{courseId}/task/{taskId}` - .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))) - .replace(`{${"taskId"}}`, encodeURIComponent(String(taskId))); + const localVarPath = `/api/Solutions/actuality/{solutionId}` + .replace(`{${"solutionId"}}`, encodeURIComponent(String(solutionId))); const localVarUrlObj = url.parse(localVarPath, true); const localVarRequestOptions = Object.assign({ method: 'GET' }, options); const localVarHeaderParameter = {} as any; @@ -7732,7 +7728,7 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 - delete localVarUrlObj.search; + localVarUrlObj.search = null; localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); return { @@ -7777,8 +7773,9 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura }; }, /** - * - * @param {number} taskId + * + * @param {number} taskId + * @param {string} studentId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7818,8 +7815,8 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura }; }, /** - * - * @param {number} solutionId + * + * @param {number} taskId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7882,8 +7879,6 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 localVarUrlObj.search = null; localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); - const needsSerialization = ("SolutionViewModel" !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; - localVarRequestOptions.body = needsSerialization ? JSON.stringify(solution || {}) : (solution || ""); return { url: url.format(localVarUrlObj), @@ -7893,16 +7888,15 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura /** * * @param {number} taskId - * @param {SolutionViewModel} [model] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiSolutionsRateEmptySolutionByTaskIdPost(taskId: number, model?: SolutionViewModel, options: any = {}): FetchArgs { + solutionsGiveUp(taskId: number, options: any = {}): FetchArgs { // verify required parameter 'taskId' is not null or undefined if (taskId === null || taskId === undefined) { - throw new RequiredError('taskId','Required parameter taskId was null or undefined when calling apiSolutionsRateEmptySolutionByTaskIdPost.'); + throw new RequiredError('taskId','Required parameter taskId was null or undefined when calling solutionsGiveUp.'); } - const localVarPath = `/api/Solutions/rateEmptySolution/{taskId}` + const localVarPath = `/api/Solutions/giveUp/{taskId}` .replace(`{${"taskId"}}`, encodeURIComponent(String(taskId))); const localVarUrlObj = url.parse(localVarPath, true); const localVarRequestOptions = Object.assign({ method: 'POST' }, options); @@ -7917,14 +7911,10 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura localVarHeaderParameter["Authorization"] = localVarApiKeyValue; } - localVarHeaderParameter['Content-Type'] = 'application/json-patch+json'; - localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 - delete localVarUrlObj.search; + localVarUrlObj.search = null; localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); - const needsSerialization = ("SolutionViewModel" !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; - localVarRequestOptions.body = needsSerialization ? JSON.stringify(model || {}) : (model || ""); return { url: url.format(localVarUrlObj), @@ -7932,9 +7922,8 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura }; }, /** - * - * @param {number} solutionId - * @param {RateSolutionModel} [rateSolutionModel] + * + * @param {number} solutionId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -7969,9 +7958,9 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura }; }, /** - * - * @param {number} taskId - * @param {string} studentId + * + * @param {number} taskId + * @param {SolutionViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8010,8 +7999,9 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura }; }, /** - * - * @param {number} taskId + * + * @param {number} taskId + * @param {SolutionViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8050,8 +8040,9 @@ export const SolutionsApiFetchParamCreator = function (configuration?: Configura }; }, /** - * - * @param {number} [taskId] + * + * @param {number} solutionId + * @param {RateSolutionModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8136,9 +8127,8 @@ export const SolutionsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} taskId - * @param {SolutionViewModel} [model] + * + * @param {number} solutionId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8173,8 +8163,9 @@ export const SolutionsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} taskId + * + * @param {number} taskId + * @param {string} studentId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8191,8 +8182,8 @@ export const SolutionsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} solutionId + * + * @param {number} taskId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8227,9 +8218,8 @@ export const SolutionsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} taskId - * @param {SolutionViewModel} [solution] + * + * @param {number} taskId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8246,9 +8236,8 @@ export const SolutionsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} solutionId - * @param {RateSolutionModel} [rateSolutionModel] + * + * @param {number} solutionId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8265,9 +8254,9 @@ export const SolutionsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} taskId - * @param {string} studentId + * + * @param {number} taskId + * @param {SolutionViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8284,8 +8273,9 @@ export const SolutionsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} taskId + * + * @param {number} taskId + * @param {SolutionViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8302,8 +8292,9 @@ export const SolutionsApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} [taskId] + * + * @param {number} solutionId + * @param {RateSolutionModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8338,9 +8329,9 @@ export const SolutionsApiFactory = function (configuration?: Configuration, fetc return SolutionsApiFp(configuration).solutionsDeleteSolution(solutionId, options)(fetch, basePath); }, /** - * - * @param {number} taskId - * @param {SolutionViewModel} [model] + * + * @param {number} [taskId] + * @param {number} [solutionId] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8366,8 +8357,9 @@ export const SolutionsApiFactory = function (configuration?: Configuration, fetc return SolutionsApiFp(configuration).solutionsGetSolutionById(solutionId, options)(fetch, basePath); }, /** - * - * @param {number} taskId + * + * @param {number} taskId + * @param {string} studentId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8375,8 +8367,8 @@ export const SolutionsApiFactory = function (configuration?: Configuration, fetc return SolutionsApiFp(configuration).solutionsGetStudentSolution(taskId, studentId, options)(fetch, basePath); }, /** - * - * @param {number} solutionId + * + * @param {number} taskId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8393,9 +8385,8 @@ export const SolutionsApiFactory = function (configuration?: Configuration, fetc return SolutionsApiFp(configuration).solutionsGetUnratedSolutions(taskId, options)(fetch, basePath); }, /** - * - * @param {number} taskId - * @param {SolutionViewModel} [solution] + * + * @param {number} taskId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8403,9 +8394,8 @@ export const SolutionsApiFactory = function (configuration?: Configuration, fetc return SolutionsApiFp(configuration).solutionsGiveUp(taskId, options)(fetch, basePath); }, /** - * - * @param {number} solutionId - * @param {RateSolutionModel} [rateSolutionModel] + * + * @param {number} solutionId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8413,9 +8403,9 @@ export const SolutionsApiFactory = function (configuration?: Configuration, fetc return SolutionsApiFp(configuration).solutionsMarkSolution(solutionId, options)(fetch, basePath); }, /** - * - * @param {number} taskId - * @param {string} studentId + * + * @param {number} taskId + * @param {SolutionViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8423,8 +8413,9 @@ export const SolutionsApiFactory = function (configuration?: Configuration, fetc return SolutionsApiFp(configuration).solutionsPostEmptySolutionWithRate(taskId, body, options)(fetch, basePath); }, /** - * - * @param {number} taskId + * + * @param {number} taskId + * @param {SolutionViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8432,8 +8423,9 @@ export const SolutionsApiFactory = function (configuration?: Configuration, fetc return SolutionsApiFp(configuration).solutionsPostSolution(taskId, body, options)(fetch, basePath); }, /** - * - * @param {number} [taskId] + * + * @param {number} solutionId + * @param {RateSolutionModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -8462,9 +8454,9 @@ export class SolutionsApi extends BaseAPI { } /** - * - * @param {number} taskId - * @param {SolutionViewModel} [model] + * + * @param {number} [taskId] + * @param {number} [solutionId] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof SolutionsApi @@ -8496,8 +8488,9 @@ export class SolutionsApi extends BaseAPI { } /** - * - * @param {number} taskId + * + * @param {number} taskId + * @param {string} studentId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof SolutionsApi @@ -8507,8 +8500,8 @@ export class SolutionsApi extends BaseAPI { } /** - * - * @param {number} solutionId + * + * @param {number} taskId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof SolutionsApi @@ -8529,9 +8522,8 @@ export class SolutionsApi extends BaseAPI { } /** - * - * @param {number} taskId - * @param {SolutionViewModel} [solution] + * + * @param {number} taskId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof SolutionsApi @@ -8541,9 +8533,8 @@ export class SolutionsApi extends BaseAPI { } /** - * - * @param {number} solutionId - * @param {RateSolutionModel} [rateSolutionModel] + * + * @param {number} solutionId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof SolutionsApi @@ -8553,9 +8544,9 @@ export class SolutionsApi extends BaseAPI { } /** - * - * @param {number} taskId - * @param {string} studentId + * + * @param {number} taskId + * @param {SolutionViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof SolutionsApi @@ -8565,8 +8556,9 @@ export class SolutionsApi extends BaseAPI { } /** - * - * @param {number} taskId + * + * @param {number} taskId + * @param {SolutionViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof SolutionsApi @@ -8576,8 +8568,9 @@ export class SolutionsApi extends BaseAPI { } /** - * - * @param {number} [taskId] + * + * @param {number} solutionId + * @param {RateSolutionModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof SolutionsApi @@ -8595,17 +8588,14 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur return { /** * - * @param {number} courseId + * @param {number} [courseId] + * @param {string} [sheetUrl] + * @param {string} [sheetName] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - statisticsGetChartStatistics(courseId: number, options: any = {}): FetchArgs { - // verify required parameter 'courseId' is not null or undefined - if (courseId === null || courseId === undefined) { - throw new RequiredError('courseId','Required parameter courseId was null or undefined when calling statisticsGetChartStatistics.'); - } - const localVarPath = `/api/Statistics/{courseId}/charts` - .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))); + statisticsExportToGoogleSheets(courseId?: number, sheetUrl?: string, sheetName?: string, options: any = {}): FetchArgs { + const localVarPath = `/api/Statistics/exportToSheet`; const localVarUrlObj = url.parse(localVarPath, true); const localVarRequestOptions = Object.assign({ method: 'GET' }, options); const localVarHeaderParameter = {} as any; @@ -8619,6 +8609,18 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur localVarHeaderParameter["Authorization"] = localVarApiKeyValue; } + if (courseId !== undefined) { + localVarQueryParameter['courseId'] = courseId; + } + + if (sheetUrl !== undefined) { + localVarQueryParameter['sheetUrl'] = sheetUrl; + } + + if (sheetName !== undefined) { + localVarQueryParameter['sheetName'] = sheetName; + } + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 localVarUrlObj.search = null; @@ -8635,12 +8637,12 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur * @param {*} [options] Override http request option. * @throws {RequiredError} */ - statisticsGetCourseStatistics(courseId: number, options: any = {}): FetchArgs { + statisticsGetChartStatistics(courseId: number, options: any = {}): FetchArgs { // verify required parameter 'courseId' is not null or undefined if (courseId === null || courseId === undefined) { - throw new RequiredError('courseId','Required parameter courseId was null or undefined when calling statisticsGetCourseStatistics.'); + throw new RequiredError('courseId','Required parameter courseId was null or undefined when calling statisticsGetChartStatistics.'); } - const localVarPath = `/api/Statistics/{courseId}` + const localVarPath = `/api/Statistics/{courseId}/charts` .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))); const localVarUrlObj = url.parse(localVarPath, true); const localVarRequestOptions = Object.assign({ method: 'GET' }, options); @@ -8671,12 +8673,12 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur * @param {*} [options] Override http request option. * @throws {RequiredError} */ - statisticsGetLecturersStatistics(courseId: number, options: any = {}): FetchArgs { + statisticsGetCourseStatistics(courseId: number, options: any = {}): FetchArgs { // verify required parameter 'courseId' is not null or undefined if (courseId === null || courseId === undefined) { - throw new RequiredError('courseId','Required parameter courseId was null or undefined when calling statisticsGetLecturersStatistics.'); + throw new RequiredError('courseId','Required parameter courseId was null or undefined when calling statisticsGetCourseStatistics.'); } - const localVarPath = `/api/Statistics/{courseId}/lecturers` + const localVarPath = `/api/Statistics/{courseId}` .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))); const localVarUrlObj = url.parse(localVarPath, true); const localVarRequestOptions = Object.assign({ method: 'GET' }, options); @@ -8704,13 +8706,12 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur /** * * @param {number} [courseId] - * @param {string} [sheetUrl] * @param {string} [sheetName] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsExportToSheetGet(courseId?: number, sheetUrl?: string, sheetName?: string, options: any = {}): FetchArgs { - const localVarPath = `/api/Statistics/exportToSheet`; + statisticsGetFile(courseId?: number, sheetName?: string, options: any = {}): FetchArgs { + const localVarPath = `/api/Statistics/getFile`; const localVarUrlObj = url.parse(localVarPath, true); const localVarRequestOptions = Object.assign({ method: 'GET' }, options); const localVarHeaderParameter = {} as any; @@ -8728,17 +8729,13 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur localVarQueryParameter['courseId'] = courseId; } - if (sheetUrl !== undefined) { - localVarQueryParameter['sheetUrl'] = sheetUrl; - } - if (sheetName !== undefined) { localVarQueryParameter['sheetName'] = sheetName; } localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 - delete localVarUrlObj.search; + localVarUrlObj.search = null; localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); return { @@ -8748,13 +8745,17 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur }, /** * - * @param {number} [courseId] - * @param {string} [sheetName] + * @param {number} courseId * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsGetFileGet(courseId?: number, sheetName?: string, options: any = {}): FetchArgs { - const localVarPath = `/api/Statistics/getFile`; + statisticsGetLecturersStatistics(courseId: number, options: any = {}): FetchArgs { + // verify required parameter 'courseId' is not null or undefined + if (courseId === null || courseId === undefined) { + throw new RequiredError('courseId','Required parameter courseId was null or undefined when calling statisticsGetLecturersStatistics.'); + } + const localVarPath = `/api/Statistics/{courseId}/lecturers` + .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))); const localVarUrlObj = url.parse(localVarPath, true); const localVarRequestOptions = Object.assign({ method: 'GET' }, options); const localVarHeaderParameter = {} as any; @@ -8768,17 +8769,9 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur localVarHeaderParameter["Authorization"] = localVarApiKeyValue; } - if (courseId !== undefined) { - localVarQueryParameter['courseId'] = courseId; - } - - if (sheetName !== undefined) { - localVarQueryParameter['sheetName'] = sheetName; - } - localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 - delete localVarUrlObj.search; + localVarUrlObj.search = null; localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); return { @@ -8792,7 +8785,7 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsGetSheetTitlesGet(sheetUrl?: string, options: any = {}): FetchArgs { + statisticsGetSheetTitles(sheetUrl?: string, options: any = {}): FetchArgs { const localVarPath = `/api/Statistics/getSheetTitles`; const localVarUrlObj = url.parse(localVarPath, true); const localVarRequestOptions = Object.assign({ method: 'GET' }, options); @@ -8813,7 +8806,7 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 - delete localVarUrlObj.search; + localVarUrlObj.search = null; localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); return { @@ -8827,7 +8820,7 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsProcessLinkPost(sheetUrl?: string, options: any = {}): FetchArgs { + statisticsProcessLink(sheetUrl?: string, options: any = {}): FetchArgs { const localVarPath = `/api/Statistics/processLink`; const localVarUrlObj = url.parse(localVarPath, true); const localVarRequestOptions = Object.assign({ method: 'POST' }, options); @@ -8848,7 +8841,7 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 - delete localVarUrlObj.search; + localVarUrlObj.search = null; localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); return { @@ -8867,12 +8860,14 @@ export const StatisticsApiFp = function(configuration?: Configuration) { return { /** * - * @param {number} courseId + * @param {number} [courseId] + * @param {string} [sheetUrl] + * @param {string} [sheetName] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - statisticsGetChartStatistics(courseId: number, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { - const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).statisticsGetChartStatistics(courseId, options); + statisticsExportToGoogleSheets(courseId?: number, sheetUrl?: string, sheetName?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).statisticsExportToGoogleSheets(courseId, sheetUrl, sheetName, options); return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { if (response.status >= 200 && response.status < 300) { @@ -8889,8 +8884,8 @@ export const StatisticsApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - statisticsGetCourseStatistics(courseId: number, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise> { - const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).statisticsGetCourseStatistics(courseId, options); + statisticsGetChartStatistics(courseId: number, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).statisticsGetChartStatistics(courseId, options); return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { if (response.status >= 200 && response.status < 300) { @@ -8907,8 +8902,8 @@ export const StatisticsApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - statisticsGetLecturersStatistics(courseId: number, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise> { - const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).statisticsGetLecturersStatistics(courseId, options); + statisticsGetCourseStatistics(courseId: number, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise> { + const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).statisticsGetCourseStatistics(courseId, options); return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { if (response.status >= 200 && response.status < 300) { @@ -8922,17 +8917,16 @@ export const StatisticsApiFp = function(configuration?: Configuration) { /** * * @param {number} [courseId] - * @param {string} [sheetUrl] * @param {string} [sheetName] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsExportToSheetGet(courseId?: number, sheetUrl?: string, sheetName?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { - const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).apiStatisticsExportToSheetGet(courseId, sheetUrl, sheetName, options); - return (fetch: FetchAPI = portableFetch, basePath: string = BASE_PATH) => { + statisticsGetFile(courseId?: number, sheetName?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).statisticsGetFile(courseId, sheetName, options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { if (response.status >= 200 && response.status < 300) { - return response.json(); + return response; } else { throw response; } @@ -8941,17 +8935,16 @@ export const StatisticsApiFp = function(configuration?: Configuration) { }, /** * - * @param {number} [courseId] - * @param {string} [sheetName] + * @param {number} courseId * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsGetFileGet(courseId?: number, sheetName?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { - const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).apiStatisticsGetFileGet(courseId, sheetName, options); - return (fetch: FetchAPI = portableFetch, basePath: string = BASE_PATH) => { + statisticsGetLecturersStatistics(courseId: number, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise> { + const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).statisticsGetLecturersStatistics(courseId, options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { if (response.status >= 200 && response.status < 300) { - return response; + return response.json(); } else { throw response; } @@ -8964,9 +8957,9 @@ export const StatisticsApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsGetSheetTitlesGet(sheetUrl?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { - const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).apiStatisticsGetSheetTitlesGet(sheetUrl, options); - return (fetch: FetchAPI = portableFetch, basePath: string = BASE_PATH) => { + statisticsGetSheetTitles(sheetUrl?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).statisticsGetSheetTitles(sheetUrl, options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { if (response.status >= 200 && response.status < 300) { return response.json(); @@ -8982,9 +8975,9 @@ export const StatisticsApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsProcessLinkPost(sheetUrl?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { - const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).apiStatisticsProcessLinkPost(sheetUrl, options); - return (fetch: FetchAPI = portableFetch, basePath: string = BASE_PATH) => { + statisticsProcessLink(sheetUrl?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).statisticsProcessLink(sheetUrl, options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { if (response.status >= 200 && response.status < 300) { return response.json(); @@ -9005,12 +8998,14 @@ export const StatisticsApiFactory = function (configuration?: Configuration, fet return { /** * - * @param {number} courseId + * @param {number} [courseId] + * @param {string} [sheetUrl] + * @param {string} [sheetName] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - statisticsGetChartStatistics(courseId: number, options?: any) { - return StatisticsApiFp(configuration).statisticsGetChartStatistics(courseId, options)(fetch, basePath); + statisticsExportToGoogleSheets(courseId?: number, sheetUrl?: string, sheetName?: string, options?: any) { + return StatisticsApiFp(configuration).statisticsExportToGoogleSheets(courseId, sheetUrl, sheetName, options)(fetch, basePath); }, /** * @@ -9018,8 +9013,8 @@ export const StatisticsApiFactory = function (configuration?: Configuration, fet * @param {*} [options] Override http request option. * @throws {RequiredError} */ - statisticsGetCourseStatistics(courseId: number, options?: any) { - return StatisticsApiFp(configuration).statisticsGetCourseStatistics(courseId, options)(fetch, basePath); + statisticsGetChartStatistics(courseId: number, options?: any) { + return StatisticsApiFp(configuration).statisticsGetChartStatistics(courseId, options)(fetch, basePath); }, /** * @@ -9027,29 +9022,27 @@ export const StatisticsApiFactory = function (configuration?: Configuration, fet * @param {*} [options] Override http request option. * @throws {RequiredError} */ - statisticsGetLecturersStatistics(courseId: number, options?: any) { - return StatisticsApiFp(configuration).statisticsGetLecturersStatistics(courseId, options)(fetch, basePath); + statisticsGetCourseStatistics(courseId: number, options?: any) { + return StatisticsApiFp(configuration).statisticsGetCourseStatistics(courseId, options)(fetch, basePath); }, /** * * @param {number} [courseId] - * @param {string} [sheetUrl] * @param {string} [sheetName] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsExportToSheetGet(courseId?: number, sheetUrl?: string, sheetName?: string, options?: any) { - return StatisticsApiFp(configuration).apiStatisticsExportToSheetGet(courseId, sheetUrl, sheetName, options)(fetch, basePath); + statisticsGetFile(courseId?: number, sheetName?: string, options?: any) { + return StatisticsApiFp(configuration).statisticsGetFile(courseId, sheetName, options)(fetch, basePath); }, /** * - * @param {number} [courseId] - * @param {string} [sheetName] + * @param {number} courseId * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsGetFileGet(courseId?: number, sheetName?: string, options?: any) { - return StatisticsApiFp(configuration).apiStatisticsGetFileGet(courseId, sheetName, options)(fetch, basePath); + statisticsGetLecturersStatistics(courseId: number, options?: any) { + return StatisticsApiFp(configuration).statisticsGetLecturersStatistics(courseId, options)(fetch, basePath); }, /** * @@ -9057,8 +9050,8 @@ export const StatisticsApiFactory = function (configuration?: Configuration, fet * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsGetSheetTitlesGet(sheetUrl?: string, options?: any) { - return StatisticsApiFp(configuration).apiStatisticsGetSheetTitlesGet(sheetUrl, options)(fetch, basePath); + statisticsGetSheetTitles(sheetUrl?: string, options?: any) { + return StatisticsApiFp(configuration).statisticsGetSheetTitles(sheetUrl, options)(fetch, basePath); }, /** * @@ -9066,8 +9059,8 @@ export const StatisticsApiFactory = function (configuration?: Configuration, fet * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsProcessLinkPost(sheetUrl?: string, options?: any) { - return StatisticsApiFp(configuration).apiStatisticsProcessLinkPost(sheetUrl, options)(fetch, basePath); + statisticsProcessLink(sheetUrl?: string, options?: any) { + return StatisticsApiFp(configuration).statisticsProcessLink(sheetUrl, options)(fetch, basePath); }, }; }; @@ -9081,13 +9074,15 @@ export const StatisticsApiFactory = function (configuration?: Configuration, fet export class StatisticsApi extends BaseAPI { /** * - * @param {number} courseId + * @param {number} [courseId] + * @param {string} [sheetUrl] + * @param {string} [sheetName] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof StatisticsApi */ - public statisticsGetChartStatistics(courseId: number, options?: any) { - return StatisticsApiFp(this.configuration).statisticsGetChartStatistics(courseId, options)(this.fetch, this.basePath); + public statisticsExportToGoogleSheets(courseId?: number, sheetUrl?: string, sheetName?: string, options?: any) { + return StatisticsApiFp(this.configuration).statisticsExportToGoogleSheets(courseId, sheetUrl, sheetName, options)(this.fetch, this.basePath); } /** @@ -9097,8 +9092,8 @@ export class StatisticsApi extends BaseAPI { * @throws {RequiredError} * @memberof StatisticsApi */ - public statisticsGetCourseStatistics(courseId: number, options?: any) { - return StatisticsApiFp(this.configuration).statisticsGetCourseStatistics(courseId, options)(this.fetch, this.basePath); + public statisticsGetChartStatistics(courseId: number, options?: any) { + return StatisticsApiFp(this.configuration).statisticsGetChartStatistics(courseId, options)(this.fetch, this.basePath); } /** @@ -9108,33 +9103,31 @@ export class StatisticsApi extends BaseAPI { * @throws {RequiredError} * @memberof StatisticsApi */ - public statisticsGetLecturersStatistics(courseId: number, options?: any) { - return StatisticsApiFp(this.configuration).statisticsGetLecturersStatistics(courseId, options)(this.fetch, this.basePath); + public statisticsGetCourseStatistics(courseId: number, options?: any) { + return StatisticsApiFp(this.configuration).statisticsGetCourseStatistics(courseId, options)(this.fetch, this.basePath); } /** * * @param {number} [courseId] - * @param {string} [sheetUrl] * @param {string} [sheetName] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof StatisticsApi */ - public apiStatisticsExportToSheetGet(courseId?: number, sheetUrl?: string, sheetName?: string, options?: any) { - return StatisticsApiFp(this.configuration).apiStatisticsExportToSheetGet(courseId, sheetUrl, sheetName, options)(this.fetch, this.basePath); + public statisticsGetFile(courseId?: number, sheetName?: string, options?: any) { + return StatisticsApiFp(this.configuration).statisticsGetFile(courseId, sheetName, options)(this.fetch, this.basePath); } /** * - * @param {number} [courseId] - * @param {string} [sheetName] + * @param {number} courseId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof StatisticsApi */ - public apiStatisticsGetFileGet(courseId?: number, sheetName?: string, options?: any) { - return StatisticsApiFp(this.configuration).apiStatisticsGetFileGet(courseId, sheetName, options)(this.fetch, this.basePath); + public statisticsGetLecturersStatistics(courseId: number, options?: any) { + return StatisticsApiFp(this.configuration).statisticsGetLecturersStatistics(courseId, options)(this.fetch, this.basePath); } /** @@ -9144,8 +9137,8 @@ export class StatisticsApi extends BaseAPI { * @throws {RequiredError} * @memberof StatisticsApi */ - public apiStatisticsGetSheetTitlesGet(sheetUrl?: string, options?: any) { - return StatisticsApiFp(this.configuration).apiStatisticsGetSheetTitlesGet(sheetUrl, options)(this.fetch, this.basePath); + public statisticsGetSheetTitles(sheetUrl?: string, options?: any) { + return StatisticsApiFp(this.configuration).statisticsGetSheetTitles(sheetUrl, options)(this.fetch, this.basePath); } /** @@ -9155,8 +9148,8 @@ export class StatisticsApi extends BaseAPI { * @throws {RequiredError} * @memberof StatisticsApi */ - public apiStatisticsProcessLinkPost(sheetUrl?: string, options?: any) { - return StatisticsApiFp(this.configuration).apiStatisticsProcessLinkPost(sheetUrl, options)(this.fetch, this.basePath); + public statisticsProcessLink(sheetUrl?: string, options?: any) { + return StatisticsApiFp(this.configuration).statisticsProcessLink(sheetUrl, options)(this.fetch, this.basePath); } } @@ -9337,9 +9330,9 @@ export const TasksApiFetchParamCreator = function (configuration?: Configuration }; }, /** - * - * @param {number} homeworkId - * @param {CreateTaskViewModel} [taskViewModel] + * + * @param {number} homeworkId + * @param {CreateTaskViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -9486,9 +9479,8 @@ export const TasksApiFetchParamCreator = function (configuration?: Configuration }; }, /** - * - * @param {number} taskId - * @param {CreateTaskViewModel} [taskViewModel] + * + * @param {number} taskId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -9609,9 +9601,9 @@ export const TasksApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} homeworkId - * @param {CreateTaskViewModel} [taskViewModel] + * + * @param {number} homeworkId + * @param {CreateTaskViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -9682,9 +9674,8 @@ export const TasksApiFp = function(configuration?: Configuration) { }; }, /** - * - * @param {number} taskId - * @param {CreateTaskViewModel} [taskViewModel] + * + * @param {number} taskId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -9747,9 +9738,9 @@ export const TasksApiFactory = function (configuration?: Configuration, fetch?: return TasksApiFp(configuration).tasksAddQuestionForTask(body, options)(fetch, basePath); }, /** - * - * @param {number} homeworkId - * @param {CreateTaskViewModel} [taskViewModel] + * + * @param {number} homeworkId + * @param {CreateTaskViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -9784,9 +9775,8 @@ export const TasksApiFactory = function (configuration?: Configuration, fetch?: return TasksApiFp(configuration).tasksGetQuestionsForTask(taskId, options)(fetch, basePath); }, /** - * - * @param {number} taskId - * @param {CreateTaskViewModel} [taskViewModel] + * + * @param {number} taskId * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -9836,9 +9826,9 @@ export class TasksApi extends BaseAPI { } /** - * - * @param {number} homeworkId - * @param {CreateTaskViewModel} [taskViewModel] + * + * @param {number} homeworkId + * @param {CreateTaskViewModel} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof TasksApi @@ -9881,9 +9871,8 @@ export class TasksApi extends BaseAPI { } /** - * - * @param {number} taskId - * @param {CreateTaskViewModel} [taskViewModel] + * + * @param {number} taskId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof TasksApi From 75290fb4e2fec48e0ea328317450c5e25a7824d0 Mon Sep 17 00:00:00 2001 From: bygu4 Date: Wed, 30 Apr 2025 17:25:25 +0300 Subject: [PATCH 28/58] update front --- hwproj.front/src/components/Courses/Course.tsx | 2 +- .../src/components/Courses/StudentStats.tsx | 4 ++-- .../src/components/Solutions/DownloadStats.tsx | 7 +++---- .../src/components/Solutions/ExportToGoogle.tsx | 14 ++++++-------- .../src/components/Solutions/ExportToYandex.tsx | 9 ++++----- .../src/components/Solutions/SaveStats.tsx | 12 +++--------- 6 files changed, 19 insertions(+), 29 deletions(-) diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index 95c575bf1..7c1eef5f3 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -293,7 +293,7 @@ const Course: React.FC = () => { return } - const solutions = await ApiSingleton.statisticsApi.apiStatisticsByCourseIdGet(+validatedCourseId!) + const solutions = await ApiSingleton.statisticsApi.statisticsGetCourseStatistics(+validatedCourseId!) setCourseState(prevState => ({ ...prevState, diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index 3d7e35f3b..959bc1d4e 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -1,9 +1,9 @@ import React, {useEffect, useState} from "react"; -import {CourseViewModel, HomeworkViewModel, StatisticsCourseMatesModel, ResultString} from "../../api/"; +import {CourseViewModel, HomeworkViewModel, StatisticsCourseMatesModel} from "../../api/"; import {useNavigate, useParams} from 'react-router-dom'; import {Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@material-ui/core"; import StudentStatsCell from "../Tasks/StudentStatsCell"; -import {Alert, Chip, Typography, Grid} from "@mui/material"; +import {Alert, Button, Chip, Typography} from "@mui/material"; import {grey} from "@material-ui/core/colors"; import StudentStatsUtils from "../../services/StudentStatsUtils"; import ShowChartIcon from "@mui/icons-material/ShowChart"; diff --git a/hwproj.front/src/components/Solutions/DownloadStats.tsx b/hwproj.front/src/components/Solutions/DownloadStats.tsx index 0ab16988f..95beafcd3 100644 --- a/hwproj.front/src/components/Solutions/DownloadStats.tsx +++ b/hwproj.front/src/components/Solutions/DownloadStats.tsx @@ -1,6 +1,5 @@ -import React, { FC, useState } from "react"; -import { ResultString } from "../../api"; -import { Alert, Box, Button, CircularProgress, Grid, MenuItem, Select, TextField } from "@mui/material"; +import { FC, useState } from "react"; +import { Box, Button, Grid, TextField } from "@mui/material"; import apiSingleton from "../../api/ApiSingleton"; interface DownloadStatsProps { @@ -46,7 +45,7 @@ const DownloadStats: FC = (props: DownloadStatsProps) => { + + + + + + + Графики успеваемости + + + + + + + + Выгрузить таблицу + + + setSearched({searched, isSaveStatsActionOpened: true})} + onActionClosing={() => setSearched({searched, isSaveStatsActionOpened: false})} + /> + + + +
+ ) + } + return (
{searched && @@ -146,15 +232,8 @@ const StudentStats: React.FC = (props) => { )} - - {solutions.length > 0 && - - } + + {solutions.length > 0 && } {hasHomeworks && = (props) => { -
- setSearched({searched, isSaveStatsActionOpened: true})} - onActionClosing={() => setSearched({searched, isSaveStatsActionOpened: false})} - /> -
); } From 988beaff9d57d7113191346832480c1df1bb7e00 Mon Sep 17 00:00:00 2001 From: bygu4 Date: Sun, 4 May 2025 01:22:45 +0300 Subject: [PATCH 36/58] add summary generation on backend --- .../TableGenerators/ExcelGenerator.cs | 81 +++++++++++++++++-- 1 file changed, 75 insertions(+), 6 deletions(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs b/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs index 4067b0f87..a95c6edca 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using HwProj.APIGateway.API.Models.Statistics; +using HwProj.Models.CoursesService; using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.SolutionsService; using OfficeOpenXml; @@ -83,9 +84,13 @@ public static ExcelPackage Generate( position.ToNextRow(1); var maxFieldPosition = new Position(position.Row, 3); - AddTasksMaxRatingInfo(worksheet, course, rowsNumber, maxFieldPosition); + var (maxRatingForHw, maxRatingForTests) = AddTasksMaxRatingInfo( + worksheet, course, rowsNumber, maxFieldPosition); - AddCourseMatesInfo(worksheet, courseMatesModels, position); + var totalRatings = AddCourseMatesInfo(course, worksheet, courseMatesModels, position); + + columnsNumber += AddSummary( + worksheet, maxRatingForHw, maxRatingForTests, totalRatings, rowsNumber, SeparationColumnWidth); var headersRange = worksheet.Cells[1, 1, 3, columnsNumber]; headersRange.Style.Font.Bold = true; @@ -171,12 +176,15 @@ private static void AddMinMaxCntHeadersWithBottomBorder(ExcelWorksheet worksheet } } - private static void AddTasksMaxRatingInfo( + private static (int, int) AddTasksMaxRatingInfo( ExcelWorksheet worksheet, CourseDTO course, int heightInCells, Position firstMaxFieldPosition) { + var maxRatingForHw = 0; + var maxRatingForTests = 0; + for (var i = 0; i < course.Homeworks.Length; ++i) { var numberOfTasks = course.Homeworks[i].Tasks.Count; @@ -184,10 +192,14 @@ private static void AddTasksMaxRatingInfo( for (var j = 0; j < numberOfTasks; ++j) { + var maxRating = course.Homeworks[i].Tasks[j].MaxRating; + var isTest = course.Homeworks[i].Tasks[j].Tags.Contains(HomeworkTags.Test); + if (isTest) maxRatingForTests += maxRating; + else maxRatingForHw += maxRating; + for (var k = firstMaxFieldPosition.Row; k <= heightInCells; ++k) { - worksheet.Cells[k, firstMaxFieldPosition.Column].Value - = course.Homeworks[i].Tasks[j].MaxRating; + worksheet.Cells[k, firstMaxFieldPosition.Column].Value = maxRating; } firstMaxFieldPosition.Column += 3; @@ -195,20 +207,30 @@ private static void AddTasksMaxRatingInfo( ++firstMaxFieldPosition.Column; } + + return (maxRatingForHw, maxRatingForTests); } - private static void AddCourseMatesInfo( + private static List<(int, int)> AddCourseMatesInfo( + CourseDTO course, ExcelWorksheet worksheet, List courseMatesModels, Position position) { + var totalRatings = new List<(int, int)>(); + for (var i = 0; i < courseMatesModels.Count; ++i) { + var (hwRating, testRating) = (0, 0); worksheet.Cells[position.Row, position.Column].Value = $"{courseMatesModels[i].Name} {courseMatesModels[i].Surname}"; ++position.Column; + for (var j = 0; j < courseMatesModels[i].Homeworks.Count; ++j) { + var homeworkModel = course.Homeworks.FirstOrDefault(h => h.Id == courseMatesModels[i].Homeworks[j].Id); + var isTest = homeworkModel.Tags.Contains(HomeworkTags.Test); + for (var k = 0; k < courseMatesModels[i].Homeworks[j].Tasks.Count; ++k) { var allSolutions = courseMatesModels[i].Homeworks[j].Tasks[k].Solution; @@ -227,14 +249,61 @@ private static void AddCourseMatesInfo( BlueIntArgbColor.Alpha, BlueIntArgbColor.Red, BlueIntArgbColor.Green, BlueIntArgbColor.Blue); } + if (isTest) testRating += min; + else hwRating += min; position.Column += 3; } ++position.Column; } + totalRatings.Add((hwRating, testRating)); position.ToNextRow(1); } + + return totalRatings; + } + + private static int AddSummary(ExcelWorksheet worksheet, + int maxRatingForHw, + int maxRatingForTests, + List<(int, int)> totalRatings, + int heightInCells, + int separationColumnWidth) + { + if (totalRatings.Count == 0) return 0; + var hasHomework = maxRatingForHw > 0; + var hasTests = maxRatingForTests > 0; + + if (hasTests) + { + worksheet.Cells[1, 2].Insert(eShiftTypeInsert.EntireColumn); + worksheet.Cells[1, 2].Value = "Summary"; + worksheet.Cells[2, 2].Value = $"Test ({maxRatingForTests})"; + worksheet.Cells[2, 2, 3, 2].Merge = true; + worksheet.Cells[3, 2].Style.Border.Bottom.Style = BorderStyle; + worksheet.Cells[4, 2, 4 + totalRatings.Count - 1, 2].FillList(totalRatings.Select(p => p.Item2)); + } + if (hasHomework) + { + worksheet.Cells[1, 2].Insert(eShiftTypeInsert.EntireColumn); + worksheet.Cells[1, 2].Value = "Summary"; + worksheet.Cells[2, 2].Value = $"HW ({maxRatingForHw})"; + worksheet.Cells[2, 2, 3, 2].Merge = true; + worksheet.Cells[3, 2].Style.Border.Bottom.Style = BorderStyle; + worksheet.Cells[4, 2, 4 + totalRatings.Count - 1, 2].FillList(totalRatings.Select(p => p.Item1)); + } + + var cellsToMerge = (hasHomework ? 1 : 0) + (hasTests ? 1 : 0); + if (cellsToMerge > 0) + { + worksheet.Cells[1, 2, 1, 1 + cellsToMerge].Merge = true; + worksheet.Cells[1, 2 + cellsToMerge].Insert(eShiftTypeInsert.EntireColumn); + AddBorderedSeparationColumn( + worksheet, new Position(1, 2 + cellsToMerge), heightInCells, separationColumnWidth); + } + + return cellsToMerge; } private class Position From 335f0fa560413391795ebf2c4be7fd79608aa7be Mon Sep 17 00:00:00 2001 From: bygu4 Date: Sun, 4 May 2025 16:48:18 +0300 Subject: [PATCH 37/58] correct worksheet formatting --- .../ExportServices/GoogleService.cs | 50 ++++++-- .../TableGenerators/ExcelGenerator.cs | 120 ++++++++++++------ 2 files changed, 115 insertions(+), 55 deletions(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs index 3f43106ed..65868dd15 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs @@ -25,6 +25,15 @@ public GoogleService(SheetsService internalGoogleSheetsService) private static int SeparationColumnPixelWidth { get; set; } = 20; + + private static Color WhiteColor { get; set; } = new Color() + { + Alpha = 1, + Red = 1, + Green = 1, + Blue = 1, + }; + public async Task Export( CourseDTO course, IOrderedEnumerable statistics, @@ -154,8 +163,9 @@ private static (ValueRange ValueRange, string Range, BatchUpdateSpreadsheetReque var range = worksheet.Dimension.LocalAddress; var headersFieldEndAddress = string.Empty; - var redCellsAddresses = new List(); + var blueCellsAddresses = new List(); var grayCellsAddresses = new List(); + var testHeaderCellsAddresses = new List(); var cellsWithBorderAddresses = new List<(string CellAddress, string BorderType)>(); var valueRange = new ValueRange() @@ -176,12 +186,16 @@ private static (ValueRange ValueRange, string Range, BatchUpdateSpreadsheetReque if (cell.Style.Fill.BackgroundColor.Rgb == ExcelGenerator.BlueArgbColor) { - redCellsAddresses.Add(cell.LocalAddress); + blueCellsAddresses.Add(cell.LocalAddress); } else if (cell.Style.Fill.BackgroundColor.Rgb == ExcelGenerator.GrayArgbColor) { grayCellsAddresses.Add(cell.LocalAddress); } + else if (cell.Style.Fill.BackgroundColor.Rgb == ExcelGenerator.TestHeaderArgbColor) + { + testHeaderCellsAddresses.Add(cell.LocalAddress); + } if (cell.Style.Border.Top.Style != ExcelBorderStyle.None) { @@ -207,17 +221,19 @@ private static (ValueRange ValueRange, string Range, BatchUpdateSpreadsheetReque var batchUpdateRequest = new BatchUpdateSpreadsheetRequest(); batchUpdateRequest.Requests = new List(); AddClearStylesRequest(batchUpdateRequest, worksheet, sheetId, range); - AddMergeRequests(batchUpdateRequest, worksheet, sheetId, worksheet.MergedCells); - AddColouredCellsRequests(batchUpdateRequest, worksheet, sheetId, redCellsAddresses, ExcelGenerator.BlueFloatArgbColor); - AddColouredCellsRequests(batchUpdateRequest, worksheet, sheetId, grayCellsAddresses, ExcelGenerator.GrayFloatArgbColor); + AddColouredCellsRequests(batchUpdateRequest, worksheet, sheetId, blueCellsAddresses, ExcelGenerator.BlueFloatColor); + AddColouredCellsRequests(batchUpdateRequest, worksheet, sheetId, grayCellsAddresses, ExcelGenerator.GrayFloatColor); + AddColouredCellsRequests( + batchUpdateRequest, worksheet, sheetId, testHeaderCellsAddresses, ExcelGenerator.TestHeaderFloatColor, WhiteColor); AddUpdateCellsWidthRequest(batchUpdateRequest, worksheet, sheetId, grayCellsAddresses, SeparationColumnPixelWidth); AddCellsFormattingRequest(batchUpdateRequest, worksheet, sheetId, range); AddHeadersFormattingRequest(batchUpdateRequest, worksheet, sheetId, $"{sheetName}!A1:{headersFieldEndAddress}"); AddBordersFormattingRequest(batchUpdateRequest, worksheet, sheetId, cellsWithBorderAddresses, ExcelGenerator.EquivalentBorderStyle); + AddMergeRequests(batchUpdateRequest, worksheet, sheetId, worksheet.MergedCells); return (valueRange, rangeWithSheetTitle, batchUpdateRequest); } - private static GridRange FillGridRange(ExcelWorksheet worksheet, string rangeAddress, int sheetId) + private static GridRange FillGridRange(ExcelWorksheet worksheet, string rangeAddress, int sheetId) { var gridRange = new GridRange(); var rangeInfo = worksheet.Cells[rangeAddress]; @@ -247,7 +263,6 @@ private static void AddClearStylesRequest( batchUpdateRequest.Requests.Add(request); } - private static void AddBordersFormattingRequest( BatchUpdateSpreadsheetRequest batchUpdateRequest, ExcelWorksheet worksheet, @@ -381,7 +396,8 @@ private static void AddColouredCellsRequests( ExcelWorksheet worksheet, int sheetId, List colouredCellsAddresses, - (float Alpha, float Red, float Green, float Blue) color) + (float Alpha, float Red, float Green, float Blue) fillColor, + Color? fontColor = null) { for (var i = 0; i < colouredCellsAddresses.Count; ++i) { @@ -392,12 +408,20 @@ private static void AddColouredCellsRequests( cell.UserEnteredFormat = new CellFormat(); cell.UserEnteredFormat.BackgroundColor = new Color() { - Alpha = color.Alpha, - Red = color.Red, - Green = color.Green, - Blue = color.Blue, + Alpha = fillColor.Alpha, + Red = fillColor.Red, + Green = fillColor.Green, + Blue = fillColor.Blue, }; - colorInRedRequest.Fields = "userEnteredFormat(backgroundColor)"; + + if (fontColor != null) + { + cell.UserEnteredFormat.TextFormat = new TextFormat(); + cell.UserEnteredFormat.TextFormat.ForegroundColor = fontColor; + } + + colorInRedRequest.Fields = + $"userEnteredFormat(backgroundColor{(fontColor != null ? ",textFormat.foregroundColor" : "")})"; colorInRedRequest.Cell = cell; var request = new Request(); diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs b/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs index a95c6edca..84aa8aac3 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs @@ -1,9 +1,12 @@ +using System; using System.Collections.Generic; +using System.Drawing; using System.Linq; using HwProj.APIGateway.API.Models.Statistics; using HwProj.Models.CoursesService; using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.SolutionsService; +using Microsoft.EntityFrameworkCore.Internal; using OfficeOpenXml; using OfficeOpenXml.Style; @@ -25,21 +28,28 @@ public static class ExcelGenerator public static int FontSize { get; set; } = 11; /// - /// Shade of red used in the reports. + /// Shade of blue used in the reports. /// - private static (int Alpha, int Red, int Green, int Blue) BlueIntArgbColor { get; set; } = (0, 0, 255, 255); - public static (float Alpha, float Red, float Green, float Blue) BlueFloatArgbColor { get; set; } = (0, 0, 1, 1); - public static string BlueArgbColor { get; set; } = "0000FFFF"; + private static Color BlueIntColor { get; set; } = Color.FromArgb(255, 0, 255, 255); + public static string BlueArgbColor { get; set; } = "FF00FFFF"; + public static (float Alpha, float Red, float Green, float Blue) BlueFloatColor { get; set; } = + (1, 0, 1, 1); /// /// Shade of gray used in the reports. /// - private static (int Alpha, int Red, int Green, int Blue) GrayIntArgbColor { get; set; } = (255, 80, 80, 80); - - public static (float Alpha, float Red, float Green, float Blue) GrayFloatArgbColor { get; set; } = + private static Color GrayIntColor { get; set; } = Color.FromArgb(255, 80, 80, 80); + public static string GrayArgbColor { get; set; } = "FF505050"; + public static (float Alpha, float Red, float Green, float Blue) GrayFloatColor { get; set; } = (1, (float)0.3137, (float)0.3137, (float)0.3137); - public static string GrayArgbColor { get; set; } = "FF505050"; + /// + /// Header color for tests. + /// + private static Color TestHeaderIntColor { get; set; } = Color.FromArgb(255, 63, 81, 181); + public static string TestHeaderArgbColor = "FF3F51B5"; + public static (float Alpha, float Red, float Green, float Blue) TestHeaderFloatColor { get; set; } = + (1, (float)0.2471, (float)0.3176, (float)0.7098); private static ExcelBorderStyle BorderStyle { get; set; } = ExcelBorderStyle.Thin; @@ -47,6 +57,17 @@ public static class ExcelGenerator private static int SeparationColumnWidth { get; set; } = 2; + private static string GetTagLabel(string tag) + { + return tag switch + { + HomeworkTags.Test => "Тест", + HomeworkTags.BonusTask => "Бонус", + HomeworkTags.GroupWork => "Командное", + _ => tag, + }; + } + /// /// Generates course statistics file based on the model from HwProj.APIGateway.Tests.Test.xlsx file. /// @@ -74,13 +95,10 @@ public static ExcelPackage Generate( var columnsNumber = position.Column - 1; position.ToNextRow(2); - worksheet.Cells[1, 1, rowsNumber, columnsNumber].Style.Font.Size = FontSize; - worksheet.Cells[1, 1, rowsNumber, columnsNumber].Style.Font.Name = FontFamily; - AddTasksHeaders(worksheet, course, position, rowsNumber); position.ToNextRow(2); - AddMinMaxCntHeadersWithBottomBorder(worksheet, course, position); + AddRatingHeadersWithBottomBorder(worksheet, course, position); position.ToNextRow(1); var maxFieldPosition = new Position(position.Row, 3); @@ -96,6 +114,8 @@ public static ExcelPackage Generate( headersRange.Style.Font.Bold = true; var range = worksheet.Cells[1, 1, rowsNumber, columnsNumber]; + range.Style.Font.Size = FontSize; + range.Style.Font.Name = FontFamily; range.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; range.Style.VerticalAlignment = ExcelVerticalAlignment.Center; @@ -106,7 +126,7 @@ private static void AddBorderedSeparationColumn(ExcelWorksheet worksheet, Positi { var range = worksheet.Cells[1, position.Column, heightInCells, position.Column]; range.Style.Fill.PatternType = ExcelFillStyle.Solid; - range.Style.Fill.BackgroundColor.SetColor(GrayIntArgbColor.Alpha, GrayIntArgbColor.Red, GrayIntArgbColor.Green, GrayIntArgbColor.Blue); + range.Style.Fill.BackgroundColor.SetColor(GrayIntColor); worksheet.Column(position.Column).Width = columnWidth; ++position.Column; } @@ -119,10 +139,25 @@ private static void AddHomeworksHeaders(ExcelWorksheet worksheet, CourseDTO cour var numberCellsToMerge = course.Homeworks[i].Tasks.Count * 3; if (numberCellsToMerge == 0) continue; + var title = course.Homeworks[i].Title; + var publicationDate = course.Homeworks[i].PublicationDate; + var tags = course.Homeworks[i].Tags.Where(t => !string.IsNullOrWhiteSpace(t)).ToList(); + var isTest = tags.Contains(HomeworkTags.Test); + var tagsStr = $" ({tags.Select(GetTagLabel).Join(", ")})"; + worksheet.Cells[position.Row, position.Column].Value - = $"h/w {i + 1}: {course.Homeworks[i].Title}, {course.Homeworks[i].PublicationDate:dd.MM}"; + = $"h/w {i + 1}: {title}, {publicationDate:dd.MM}{(tags.Count > 0 ? tagsStr : "")}"; worksheet.Cells[position.Row, position.Column, position.Row, position.Column + numberCellsToMerge - 1] .Merge = true; + if (isTest) + { + var range = worksheet.Cells[ + position.Row, position.Column, position.Row + 2, position.Column + numberCellsToMerge - 1]; + range.Style.Fill.PatternType = ExcelFillStyle.Solid; + range.Style.Fill.BackgroundColor.SetColor(TestHeaderIntColor); + range.Style.Font.Color.SetColor(Color.White); + } + position.Column += numberCellsToMerge; AddBorderedSeparationColumn(worksheet, position, heightInCells, separationColumnWidth); } @@ -155,7 +190,7 @@ private static void AddTasksHeaders(ExcelWorksheet worksheet, CourseDTO course, } } - private static void AddMinMaxCntHeadersWithBottomBorder(ExcelWorksheet worksheet, CourseDTO course, + private static void AddRatingHeadersWithBottomBorder(ExcelWorksheet worksheet, CourseDTO course, Position position) { for (var i = 0; i < course.Homeworks.Length; ++i) @@ -165,9 +200,9 @@ private static void AddMinMaxCntHeadersWithBottomBorder(ExcelWorksheet worksheet for (var j = position.Column; j < position.Column + lengthInCells; j += 3) { - worksheet.Cells[position.Row, j].Value = "min"; - worksheet.Cells[position.Row, j + 1].Value = "max"; - worksheet.Cells[position.Row, j + 2].Value = "cnt"; + worksheet.Cells[position.Row, j].Value = "оценка"; + worksheet.Cells[position.Row, j + 1].Value = "макс. балл"; + worksheet.Cells[position.Row, j + 2].Value = "попытки"; worksheet.Cells[position.Row, j, position.Row, j + 2].Style.Border.Bottom.Style = BorderStyle; } @@ -176,7 +211,7 @@ private static void AddMinMaxCntHeadersWithBottomBorder(ExcelWorksheet worksheet } } - private static (int, int) AddTasksMaxRatingInfo( + private static (int MaxRatingForHw, int MaxRatingForTests) AddTasksMaxRatingInfo( ExcelWorksheet worksheet, CourseDTO course, int heightInCells, @@ -194,8 +229,7 @@ private static (int, int) AddTasksMaxRatingInfo( { var maxRating = course.Homeworks[i].Tasks[j].MaxRating; var isTest = course.Homeworks[i].Tasks[j].Tags.Contains(HomeworkTags.Test); - if (isTest) maxRatingForTests += maxRating; - else maxRatingForHw += maxRating; + var isBonus = course.Homeworks[i].Tasks[j].Tags.Contains(HomeworkTags.BonusTask); for (var k = firstMaxFieldPosition.Row; k <= heightInCells; ++k) { @@ -203,6 +237,9 @@ private static (int, int) AddTasksMaxRatingInfo( } firstMaxFieldPosition.Column += 3; + if (isBonus) continue; + if (isTest) maxRatingForTests += maxRating; + else maxRatingForHw += maxRating; } ++firstMaxFieldPosition.Column; @@ -211,7 +248,7 @@ private static (int, int) AddTasksMaxRatingInfo( return (maxRatingForHw, maxRatingForTests); } - private static List<(int, int)> AddCourseMatesInfo( + private static List<(int HwRating, int TestRating)> AddCourseMatesInfo( CourseDTO course, ExcelWorksheet worksheet, List courseMatesModels, @@ -237,20 +274,20 @@ private static (int, int) AddTasksMaxRatingInfo( var solutions = allSolutions .Where(solution => solution.State == SolutionState.Rated || solution.State == SolutionState.Final); - var min = solutions.Any() ? solutions.Last().Rating : 0; - var cnt = solutions.Count(); - worksheet.Cells[position.Row, position.Column].Value = min; - worksheet.Cells[position.Row, position.Column + 2].Value = cnt; - if (cnt != allSolutions.Count) + var current = solutions.Any() ? solutions.Last().Rating : 0; + var count = solutions.Count(); + worksheet.Cells[position.Row, position.Column].Value = current; + worksheet.Cells[position.Row, position.Column + 2].Value = count; + if (count != allSolutions.Count) { - worksheet.Cells[position.Row, position.Column + 2].Style.Fill.PatternType = - ExcelFillStyle.Solid; - worksheet.Cells[position.Row, position.Column + 2].Style.Fill.BackgroundColor.SetColor( - BlueIntArgbColor.Alpha, BlueIntArgbColor.Red, BlueIntArgbColor.Green, BlueIntArgbColor.Blue); + worksheet.Cells[position.Row, position.Column + 2] + .Style.Fill.PatternType = ExcelFillStyle.Solid; + worksheet.Cells[position.Row, position.Column + 2] + .Style.Fill.BackgroundColor.SetColor(BlueIntColor); } - if (isTest) testRating += min; - else hwRating += min; + if (isTest) testRating += current; + else hwRating += current; position.Column += 3; } @@ -267,7 +304,7 @@ private static (int, int) AddTasksMaxRatingInfo( private static int AddSummary(ExcelWorksheet worksheet, int maxRatingForHw, int maxRatingForTests, - List<(int, int)> totalRatings, + List<(int HwRating, int TestRating)> totalRatings, int heightInCells, int separationColumnWidth) { @@ -278,20 +315,18 @@ private static int AddSummary(ExcelWorksheet worksheet, if (hasTests) { worksheet.Cells[1, 2].Insert(eShiftTypeInsert.EntireColumn); - worksheet.Cells[1, 2].Value = "Summary"; - worksheet.Cells[2, 2].Value = $"Test ({maxRatingForTests})"; + worksheet.Cells[1, 2].Value = "Итоговые баллы"; + worksheet.Cells[2, 2].Value = $"КР ({maxRatingForTests})"; worksheet.Cells[2, 2, 3, 2].Merge = true; - worksheet.Cells[3, 2].Style.Border.Bottom.Style = BorderStyle; - worksheet.Cells[4, 2, 4 + totalRatings.Count - 1, 2].FillList(totalRatings.Select(p => p.Item2)); + worksheet.Cells[4, 2, 4 + totalRatings.Count - 1, 2].FillList(totalRatings.Select(p => p.TestRating)); } if (hasHomework) { worksheet.Cells[1, 2].Insert(eShiftTypeInsert.EntireColumn); - worksheet.Cells[1, 2].Value = "Summary"; - worksheet.Cells[2, 2].Value = $"HW ({maxRatingForHw})"; + worksheet.Cells[1, 2].Value = "Итоговые баллы"; + worksheet.Cells[2, 2].Value = $"ДЗ ({maxRatingForHw})"; worksheet.Cells[2, 2, 3, 2].Merge = true; - worksheet.Cells[3, 2].Style.Border.Bottom.Style = BorderStyle; - worksheet.Cells[4, 2, 4 + totalRatings.Count - 1, 2].FillList(totalRatings.Select(p => p.Item1)); + worksheet.Cells[4, 2, 4 + totalRatings.Count - 1, 2].FillList(totalRatings.Select(p => p.HwRating)); } var cellsToMerge = (hasHomework ? 1 : 0) + (hasTests ? 1 : 0); @@ -301,6 +336,7 @@ private static int AddSummary(ExcelWorksheet worksheet, worksheet.Cells[1, 2 + cellsToMerge].Insert(eShiftTypeInsert.EntireColumn); AddBorderedSeparationColumn( worksheet, new Position(1, 2 + cellsToMerge), heightInCells, separationColumnWidth); + worksheet.Cells[2, 2, 3, 1 + cellsToMerge].Style.Border.Bottom.Style = BorderStyle; } return cellsToMerge; From b532fb88eba3a2fbb8c24f2ac2f1c8b2f63f1441 Mon Sep 17 00:00:00 2001 From: bygu4 Date: Sun, 4 May 2025 18:04:07 +0300 Subject: [PATCH 38/58] update yandex application credentials --- hwproj.front/.env | 6 +++--- hwproj.front/src/components/Solutions/ExportToYandex.tsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/hwproj.front/.env b/hwproj.front/.env index bce7a9822..0bf7058d8 100644 --- a/hwproj.front/.env +++ b/hwproj.front/.env @@ -2,7 +2,7 @@ VITE_BASE_PATH=http://localhost:5000 VITE_YANDEX_METRICA_ID=101061418 VITE_YANDEX_APPLICATION_NAME=hwproj-spreadsheets-dev -VITE_YANDEX_CLIENT_ID=8545882a5f454277b0e32678e29a44d6 -VITE_YANDEX_CLIENT_SECRET=cba6bdaa445e47ad912786cbd36cc36b -VITE_YANDEX_AUTHORIZATION_TOKEN=y0__xCEp86vBhi0wDcgndnNgBMU-uahzwhSdazLYTCyz0Xx5DWHKA +VITE_YANDEX_CLIENT_ID=07640102bafa409b85f3c66cea6114f5 +VITE_YANDEX_CLIENT_SECRET=3a9b2c6cbfb34c5d8c4430ff716f8486 +VITE_YANDEX_AUTHORIZATION_TOKEN=y0__xCEp86vBhiBwzcgopv5gRM89dGLIc_uw4VAa6uH7XBYPalrgg WDS_SOCKET_PORT=0 diff --git a/hwproj.front/src/components/Solutions/ExportToYandex.tsx b/hwproj.front/src/components/Solutions/ExportToYandex.tsx index 93c48a5b8..ca2d372cb 100644 --- a/hwproj.front/src/components/Solutions/ExportToYandex.tsx +++ b/hwproj.front/src/components/Solutions/ExportToYandex.tsx @@ -150,8 +150,8 @@ const ExportToYandex: FC = (props: ExportToYandexProps) => }
From 2c6a3e80edd24e7b924eadf64de88e8b1218b218 Mon Sep 17 00:00:00 2001 From: bygu4 Date: Sun, 4 May 2025 18:59:30 +0300 Subject: [PATCH 39/58] correct redirection from yandex auth --- hwproj.front/src/App.tsx | 2 +- hwproj.front/src/components/Courses/Course.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/hwproj.front/src/App.tsx b/hwproj.front/src/App.tsx index d5df5b77c..e8ab9305e 100644 --- a/hwproj.front/src/App.tsx +++ b/hwproj.front/src/App.tsx @@ -117,6 +117,7 @@ class App extends Component<{ navigate: any }, AppState> { }/> }/> }/> + }/> }/> }/> @@ -127,7 +128,6 @@ class App extends Component<{ navigate: any }, AppState> { }/> }/> }/> - }/> }/>
diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index 9e7942300..4b7a78e02 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -87,10 +87,9 @@ const Course: React.FC = () => { const {courseId, tab} = useParams() const [searchParams] = useSearchParams() - const isFromYandex = courseId === undefined + const isFromYandex = !courseId || courseId === "yandex" const validatedCourseId = isFromYandex ? getLastViewedCourseId() : courseId - const navigate = useNavigate() const {enqueueSnackbar} = useSnackbar() From 9aa916910b0f50eca48c7cc85af8ed31a012ca8b Mon Sep 17 00:00:00 2001 From: bygu4 Date: Sun, 4 May 2025 20:34:45 +0300 Subject: [PATCH 40/58] refactor coloring font in GoogleService --- .../ExportServices/GoogleService.cs | 82 ++++++++++++------- .../TableGenerators/ExcelGenerator.cs | 30 ++++--- 2 files changed, 70 insertions(+), 42 deletions(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs index 65868dd15..d85b4b282 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs @@ -25,15 +25,6 @@ public GoogleService(SheetsService internalGoogleSheetsService) private static int SeparationColumnPixelWidth { get; set; } = 20; - - private static Color WhiteColor { get; set; } = new Color() - { - Alpha = 1, - Red = 1, - Green = 1, - Blue = 1, - }; - public async Task Export( CourseDTO course, IOrderedEnumerable statistics, @@ -163,6 +154,7 @@ private static (ValueRange ValueRange, string Range, BatchUpdateSpreadsheetReque var range = worksheet.Dimension.LocalAddress; var headersFieldEndAddress = string.Empty; + var whiteForegroundAddresses = new List(); var blueCellsAddresses = new List(); var grayCellsAddresses = new List(); var testHeaderCellsAddresses = new List(); @@ -183,8 +175,12 @@ private static (ValueRange ValueRange, string Range, BatchUpdateSpreadsheetReque { headersFieldEndAddress = cell.LocalAddress; } + if (cell.Style.Font.Color.Rgb == ExcelGenerator.WhiteArgbColor) + { + whiteForegroundAddresses.Add(cell.LocalAddress); + } - if (cell.Style.Fill.BackgroundColor.Rgb == ExcelGenerator.BlueArgbColor) + if (cell.Style.Fill.BackgroundColor.Rgb == ExcelGenerator.CyanArgbColor) { blueCellsAddresses.Add(cell.LocalAddress); } @@ -221,10 +217,10 @@ private static (ValueRange ValueRange, string Range, BatchUpdateSpreadsheetReque var batchUpdateRequest = new BatchUpdateSpreadsheetRequest(); batchUpdateRequest.Requests = new List(); AddClearStylesRequest(batchUpdateRequest, worksheet, sheetId, range); - AddColouredCellsRequests(batchUpdateRequest, worksheet, sheetId, blueCellsAddresses, ExcelGenerator.BlueFloatColor); - AddColouredCellsRequests(batchUpdateRequest, worksheet, sheetId, grayCellsAddresses, ExcelGenerator.GrayFloatColor); - AddColouredCellsRequests( - batchUpdateRequest, worksheet, sheetId, testHeaderCellsAddresses, ExcelGenerator.TestHeaderFloatColor, WhiteColor); + AddColoredCellsRequests(batchUpdateRequest, worksheet, sheetId, blueCellsAddresses, ExcelGenerator.CyanFloatColor); + AddColoredCellsRequests(batchUpdateRequest, worksheet, sheetId, grayCellsAddresses, ExcelGenerator.GrayFloatColor); + AddColoredCellsRequests(batchUpdateRequest, worksheet, sheetId, testHeaderCellsAddresses, ExcelGenerator.TestHeaderFloatColor); + AddColoredFontRequest(batchUpdateRequest, worksheet, sheetId, whiteForegroundAddresses, ExcelGenerator.WhiteFloatColor); AddUpdateCellsWidthRequest(batchUpdateRequest, worksheet, sheetId, grayCellsAddresses, SeparationColumnPixelWidth); AddCellsFormattingRequest(batchUpdateRequest, worksheet, sheetId, range); AddHeadersFormattingRequest(batchUpdateRequest, worksheet, sheetId, $"{sheetName}!A1:{headersFieldEndAddress}"); @@ -391,19 +387,18 @@ private static void AddUpdateCellsWidthRequest( } } - private static void AddColouredCellsRequests( + private static void AddColoredCellsRequests( BatchUpdateSpreadsheetRequest batchUpdateRequest, ExcelWorksheet worksheet, int sheetId, - List colouredCellsAddresses, - (float Alpha, float Red, float Green, float Blue) fillColor, - Color? fontColor = null) + List coloredCellsAddresses, + (float Alpha, float Red, float Green, float Blue) fillColor) { - for (var i = 0; i < colouredCellsAddresses.Count; ++i) + for (var i = 0; i < coloredCellsAddresses.Count; ++i) { - var cellAddress = colouredCellsAddresses[i]; - var colorInRedRequest = new RepeatCellRequest(); - colorInRedRequest.Range = FillGridRange(worksheet, cellAddress, sheetId); + var cellAddress = coloredCellsAddresses[i]; + var colorCellRequest = new RepeatCellRequest(); + colorCellRequest.Range = FillGridRange(worksheet, cellAddress, sheetId); var cell = new CellData(); cell.UserEnteredFormat = new CellFormat(); cell.UserEnteredFormat.BackgroundColor = new Color() @@ -414,18 +409,43 @@ private static void AddColouredCellsRequests( Blue = fillColor.Blue, }; - if (fontColor != null) + colorCellRequest.Fields = $"userEnteredFormat(backgroundColor)"; + colorCellRequest.Cell = cell; + + var request = new Request(); + request.RepeatCell = colorCellRequest; + batchUpdateRequest.Requests.Add(request); + } + } + + private static void AddColoredFontRequest( + BatchUpdateSpreadsheetRequest batchUpdateRequest, + ExcelWorksheet worksheet, + int sheetId, + List cellsAddresses, + (float Alpha, float Red, float Green, float Blue) fontColor) + { + for (var i = 0; i < cellsAddresses.Count; ++i) + { + var cellAddress = cellsAddresses[i]; + var colorFontRequest = new RepeatCellRequest(); + colorFontRequest.Range = FillGridRange(worksheet, cellAddress, sheetId); + var cell = new CellData(); + cell.UserEnteredFormat = new CellFormat(); + cell.UserEnteredFormat.TextFormat = new TextFormat(); + cell.UserEnteredFormat.TextFormat.ForegroundColor = new Color() { - cell.UserEnteredFormat.TextFormat = new TextFormat(); - cell.UserEnteredFormat.TextFormat.ForegroundColor = fontColor; - } + Alpha = fontColor.Alpha, + Red = fontColor.Red, + Green = fontColor.Green, + Blue = fontColor.Blue, + }; - colorInRedRequest.Fields = - $"userEnteredFormat(backgroundColor{(fontColor != null ? ",textFormat.foregroundColor" : "")})"; - colorInRedRequest.Cell = cell; + colorFontRequest.Fields = $"userEnteredFormat(textFormat.foregroundColor)"; + colorFontRequest.Cell = cell; var request = new Request(); - request.RepeatCell = colorInRedRequest; + request.RepeatCell = colorFontRequest; batchUpdateRequest.Requests.Add(request); } } @@ -445,4 +465,4 @@ private static void AddColouredCellsRequests( } } } -} \ No newline at end of file +} diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs b/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs index 84aa8aac3..f2d00df67 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/TableGenerators/ExcelGenerator.cs @@ -28,17 +28,25 @@ public static class ExcelGenerator public static int FontSize { get; set; } = 11; /// - /// Shade of blue used in the reports. + /// Color for font to use in test headers. /// - private static Color BlueIntColor { get; set; } = Color.FromArgb(255, 0, 255, 255); - public static string BlueArgbColor { get; set; } = "FF00FFFF"; - public static (float Alpha, float Red, float Green, float Blue) BlueFloatColor { get; set; } = + private static Color WhiteColor { get; set; } = Color.White; + public static string WhiteArgbColor = "FFFFFFFF"; + public static (float Alpha, float Red, float Green, float Blue) WhiteFloatColor { get; set; } = + (1, 1, 1, 1); + + /// + /// Cyan color used to indicate unrated solutions. + /// + private static Color CyanColor { get; set; } = Color.Cyan; + public static string CyanArgbColor { get; set; } = "FF00FFFF"; + public static (float Alpha, float Red, float Green, float Blue) CyanFloatColor { get; set; } = (1, 0, 1, 1); /// - /// Shade of gray used in the reports. + /// Gray color used with separation columns. /// - private static Color GrayIntColor { get; set; } = Color.FromArgb(255, 80, 80, 80); + private static Color GrayColor { get; set; } = Color.FromArgb(255, 80, 80, 80); public static string GrayArgbColor { get; set; } = "FF505050"; public static (float Alpha, float Red, float Green, float Blue) GrayFloatColor { get; set; } = (1, (float)0.3137, (float)0.3137, (float)0.3137); @@ -46,7 +54,7 @@ public static class ExcelGenerator /// /// Header color for tests. /// - private static Color TestHeaderIntColor { get; set; } = Color.FromArgb(255, 63, 81, 181); + private static Color TestHeaderColor { get; set; } = Color.FromArgb(255, 63, 81, 181); public static string TestHeaderArgbColor = "FF3F51B5"; public static (float Alpha, float Red, float Green, float Blue) TestHeaderFloatColor { get; set; } = (1, (float)0.2471, (float)0.3176, (float)0.7098); @@ -126,7 +134,7 @@ private static void AddBorderedSeparationColumn(ExcelWorksheet worksheet, Positi { var range = worksheet.Cells[1, position.Column, heightInCells, position.Column]; range.Style.Fill.PatternType = ExcelFillStyle.Solid; - range.Style.Fill.BackgroundColor.SetColor(GrayIntColor); + range.Style.Fill.BackgroundColor.SetColor(GrayColor); worksheet.Column(position.Column).Width = columnWidth; ++position.Column; } @@ -154,8 +162,8 @@ private static void AddHomeworksHeaders(ExcelWorksheet worksheet, CourseDTO cour var range = worksheet.Cells[ position.Row, position.Column, position.Row + 2, position.Column + numberCellsToMerge - 1]; range.Style.Fill.PatternType = ExcelFillStyle.Solid; - range.Style.Fill.BackgroundColor.SetColor(TestHeaderIntColor); - range.Style.Font.Color.SetColor(Color.White); + range.Style.Fill.BackgroundColor.SetColor(TestHeaderColor); + range.Style.Font.Color.SetColor(WhiteColor); } position.Column += numberCellsToMerge; @@ -283,7 +291,7 @@ private static (int MaxRatingForHw, int MaxRatingForTests) AddTasksMaxRatingInfo worksheet.Cells[position.Row, position.Column + 2] .Style.Fill.PatternType = ExcelFillStyle.Solid; worksheet.Cells[position.Row, position.Column + 2] - .Style.Fill.BackgroundColor.SetColor(BlueIntColor); + .Style.Fill.BackgroundColor.SetColor(CyanColor); } if (isTest) testRating += current; From bf2ca093028dbefcef26db39aa24eea899dd65bb Mon Sep 17 00:00:00 2001 From: bygu4 Date: Fri, 9 May 2025 00:32:56 +0300 Subject: [PATCH 41/58] add save stats action menu, refactor --- hwproj.front/package-lock.json | 26 +++ hwproj.front/package.json | 1 + .../src/components/Courses/StatsMenu.tsx | 213 ++++++++++++++++++ .../src/components/Courses/StudentStats.tsx | 111 ++------- .../src/components/Courses/YandexLogo.svg | 16 ++ .../src/components/Solutions/SaveStats.tsx | 130 ----------- .../src/components/Solutions/YandexLogo.svg | 1 - 7 files changed, 269 insertions(+), 229 deletions(-) create mode 100644 hwproj.front/src/components/Courses/StatsMenu.tsx create mode 100644 hwproj.front/src/components/Courses/YandexLogo.svg delete mode 100644 hwproj.front/src/components/Solutions/SaveStats.tsx delete mode 100644 hwproj.front/src/components/Solutions/YandexLogo.svg diff --git a/hwproj.front/package-lock.json b/hwproj.front/package-lock.json index 37a13e471..ae2dfc708 100644 --- a/hwproj.front/package-lock.json +++ b/hwproj.front/package-lock.json @@ -42,6 +42,7 @@ "isomorphic-fetch": "^3.0.0", "jwt-decode": "^3.1.2", "lowdb": "^1.0.0", + "mui-nested-menu": "^4.0.1", "notistack": "^3.0.2", "portable-fetch": "^3.0.0", "qrcode.react": "^4.1.0", @@ -19921,6 +19922,31 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mui-nested-menu": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/mui-nested-menu/-/mui-nested-menu-4.0.1.tgz", + "integrity": "sha512-o/UaG3oXvHI+phKZzTJdX/fAqgJXQC5xjo/KjMrJq8XtShs+n+JmVYCqD6ATIyoTamEt7+5LAjcIy4iyARcKdg==", + "license": "MIT", + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material": "^5.0.0 || ^6.0.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, "node_modules/nan": { "version": "2.22.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", diff --git a/hwproj.front/package.json b/hwproj.front/package.json index d09d9ae66..6a4bf5d84 100644 --- a/hwproj.front/package.json +++ b/hwproj.front/package.json @@ -38,6 +38,7 @@ "isomorphic-fetch": "^3.0.0", "jwt-decode": "^3.1.2", "lowdb": "^1.0.0", + "mui-nested-menu": "^4.0.1", "notistack": "^3.0.2", "portable-fetch": "^3.0.0", "qrcode.react": "^4.1.0", diff --git a/hwproj.front/src/components/Courses/StatsMenu.tsx b/hwproj.front/src/components/Courses/StatsMenu.tsx new file mode 100644 index 000000000..3d2a87daa --- /dev/null +++ b/hwproj.front/src/components/Courses/StatsMenu.tsx @@ -0,0 +1,213 @@ +import {FC, useState, useEffect} from "react"; +import { + Button, + Menu, + MenuItem, + ListItemIcon, + ListItemText, + Dialog, + DialogTitle, + DialogContent} +from "@mui/material"; +import {NestedMenuItem} from "mui-nested-menu"; +import {Download, ShowChart} from "@mui/icons-material"; +import {useNavigate} from "react-router-dom"; +import DownloadStats from "../Solutions/DownloadStats"; +import ExportToGoogle from "../Solutions/ExportToGoogle"; +import ExportToYandex from "../Solutions/ExportToYandex"; +import SaveIcon from '@mui/icons-material/Save'; +import GoogleIcon from '@mui/icons-material/Google'; +import YandexLogo from './YandexLogo.svg'; + +enum SaveStatsAction { + Download, + ShareWithGoogle, + ShareWithYandex, +} + +const actions = [SaveStatsAction.Download, SaveStatsAction.ShareWithGoogle, SaveStatsAction.ShareWithYandex] + +interface StatsMenuProps { + courseId: number | undefined; + userId: string; + yandexCode: string | null; + onActionOpening: () => void; + onActionClosing: () => void; +} + +interface StatsMenuState { + anchorEl: HTMLElement | null; + saveStatsAction: SaveStatsAction | null; +} + +const StatsMenu: FC = props => { + const [menuState, setMenuState] = useState({ + anchorEl: null, + saveStatsAction: null, + }) + + const {anchorEl, saveStatsAction} = menuState + const showMenu = anchorEl !== null + + useEffect(() => { + if (saveStatsAction !== null) + props.onActionOpening() + else + props.onActionClosing() + }, [saveStatsAction]); + + const navigate = useNavigate(); + + const goToCharts = () => { + navigate(`/statistics/${props.courseId}/charts`) + } + + const handleOpen = (event: React.MouseEvent) => + setMenuState ({ + anchorEl: event.currentTarget, + saveStatsAction: null, + }) + + const handleClose = () => + setMenuState ({ + anchorEl: null, + saveStatsAction: null, + }) + + const handleSelectAction = (action: SaveStatsAction | null) => + setMenuState({ + anchorEl: null, + saveStatsAction: action, + }) + + const getActionIcon = (action: SaveStatsAction | null) => { + switch (action) { + case SaveStatsAction.Download: + return + case SaveStatsAction.ShareWithGoogle: + return + case SaveStatsAction.ShareWithYandex: + return Y + default: + return null + } + } + + const getActionLabel = (action: SaveStatsAction | null) => { + switch (action) { + case SaveStatsAction.Download: + return "На диск" + case SaveStatsAction.ShareWithGoogle: + return "В Google Docs" + case SaveStatsAction.ShareWithYandex: + return "На Яндекс Диск" + default: + return "" + } + } + + const getActionTitle = (action: SaveStatsAction | null) => { + switch (action) { + case SaveStatsAction.Download: + return "Сохранить на диск" + case SaveStatsAction.ShareWithGoogle: + return "Отправить в Google Docs" + case SaveStatsAction.ShareWithYandex: + return "Отправить на Яндекс Диск" + default: + return "" + } + } + + const getActionContent = (action: SaveStatsAction | null) => { + switch (action) { + case SaveStatsAction.Download: + return + case SaveStatsAction.ShareWithGoogle: + return + case SaveStatsAction.ShareWithYandex: + return + default: + return null + } + } + + return ( +
+ + + + + + + + Графики успеваемости + + + + + + } + renderLabel={() => + + Выгрузить таблицу + + } + > + {actions.map(action => + handleSelectAction(action)}> + + {getActionIcon(action)} + + + {getActionLabel(action)} + + + )} + + + + + {getActionTitle(saveStatsAction)} + + + {getActionContent(saveStatsAction)} + + +
+ ) +} + +export default StatsMenu; diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index a3e525af2..6e122fa58 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -1,15 +1,13 @@ -import React, {FC, useEffect, useState} from "react"; +import {useEffect, useState, CSSProperties} from "react"; import {CourseViewModel, HomeworkViewModel, StatisticsCourseMatesModel} from "../../api/"; -import {useNavigate, useParams} from 'react-router-dom'; import {Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@material-ui/core"; import StudentStatsCell from "../Tasks/StudentStatsCell"; -import {Alert, Button, Chip, Typography, Menu, MenuItem, ListItemIcon, ListItemText, Popover} from "@mui/material"; +import {Alert, Chip, Typography} from "@mui/material"; import {grey} from "@material-ui/core/colors"; import StudentStatsUtils from "../../services/StudentStatsUtils"; -import {MoreVert, Download, ShowChart} from "@mui/icons-material"; import {BonusTag, DefaultTags, TestTag} from "../Common/HomeworkTags"; import Lodash from "lodash" -import SaveStats from "components/Solutions/SaveStats"; +import StatsMenu from "./StatsMenu"; interface IStudentStatsProps { course: CourseViewModel; @@ -32,11 +30,6 @@ const StudentStats: React.FC = (props) => { searched: "", isSaveStatsActionOpened: false }); - const {courseId} = useParams(); - const navigate = useNavigate(); - const goToCharts = () => { - navigate(`/statistics/${courseId}/charts`) - } const {searched, isSaveStatsActionOpened} = state @@ -70,7 +63,7 @@ const StudentStats: React.FC = (props) => { color: "white", } - const homeworkStyles = (homeworks: HomeworkViewModel[], idx: number): React.CSSProperties | undefined => { + const homeworkStyles = (homeworks: HomeworkViewModel[], idx: number): CSSProperties | undefined => { if (homeworks[idx].tags?.includes(TestTag)) return testHomeworkStyle if (idx !== 0 && homeworks[idx - 1].tags?.includes(TestTag)) @@ -103,92 +96,6 @@ const StudentStats: React.FC = (props) => { const hasHomeworks = homeworksMaxSum > 0 const hasTests = testsMaxSum > 0 - const StatsMenu: FC = () => { - const [menuState, setMenuState] = useState<{ - anchorEl: null | HTMLElement; - popoverAnchorEl: null | HTMLElement; - }>({anchorEl: null, popoverAnchorEl: null}) - - const {anchorEl, popoverAnchorEl} = menuState - const openMenu = Boolean(anchorEl) - const openPopover = Boolean(popoverAnchorEl) - - const handleOpenMenu = (event: React.MouseEvent) => { - setMenuState ({ - anchorEl: event.currentTarget, - popoverAnchorEl: null, - }) - } - - const handleClose = () => { - setMenuState ({ - anchorEl: null, - popoverAnchorEl: null, - }) - } - - const handleOpenPopover = (event: React.MouseEvent) => { - setMenuState (prevState => ({ - ...prevState, - popoverAnchorEl: event.currentTarget, - })) - } - - return ( -
- - - - - - - - Графики успеваемости - - - - - - - - Выгрузить таблицу - - - setSearched({searched, isSaveStatsActionOpened: true})} - onActionClosing={() => setSearched({searched, isSaveStatsActionOpened: false})} - /> - - - -
- ) - } - return (
{searched && @@ -233,7 +140,15 @@ const StudentStats: React.FC = (props) => { - {solutions.length > 0 && } + {solutions.length > 0 && + setSearched({searched, isSaveStatsActionOpened: true})} + onActionClosing={() => setSearched({searched, isSaveStatsActionOpened: false})} + /> + } {hasHomeworks && + + +Created with Fabric.js 5.2.4 + + + + + + + + + + + + \ No newline at end of file diff --git a/hwproj.front/src/components/Solutions/SaveStats.tsx b/hwproj.front/src/components/Solutions/SaveStats.tsx deleted file mode 100644 index f34f1ce4e..000000000 --- a/hwproj.front/src/components/Solutions/SaveStats.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import {FC, useState, useEffect} from "react"; -import {Box} from "@mui/material"; -import GetAppIcon from '@material-ui/icons/GetApp'; -import SpeedDial from '@mui/material/SpeedDial'; -import SaveIcon from '@mui/icons-material/Save'; -import SpeedDialAction from '@mui/material/SpeedDialAction'; -import ExportToGoogle from "components/Solutions/ExportToGoogle"; -import ExportToYandex from "components/Solutions/ExportToYandex"; -import DownloadStats from "components/Solutions/DownloadStats"; -import YandexLogo from './YandexLogo.svg'; -import GoogleIcon from '@mui/icons-material/Google'; - -interface SaveStatsProps { - courseId : number | undefined; - userId : string; - yandexCode: string | null; - onActionOpening: () => void; - onActionClosing: () => void; -} - -enum SpeedDialView { - Collapsed, - Expanded, - Download, - ShareWithGoogle, - ShareWithYandex, -} - -interface SaveStatsState { - selectedView : SpeedDialView; -} - -const SaveStats: FC = (props : SaveStatsProps) => { - const [state, setSelectedView] = useState({ - selectedView: props.yandexCode === null ? SpeedDialView.Collapsed : SpeedDialView.ShareWithYandex, - }) - - const {selectedView} = state - - useEffect(() => { - if (selectedView === SpeedDialView.Download || - selectedView === SpeedDialView.ShareWithGoogle || - selectedView === SpeedDialView.ShareWithYandex) { - props.onActionOpening() - return - } - props.onActionClosing() - }, [selectedView]); - - const handleSpeedDialItemClick = (operation : string) => { - switch ( operation ) { - case 'download': - setSelectedView({selectedView: SpeedDialView.Download}); - break; - case 'shareWithGoogle': - setSelectedView({selectedView: SpeedDialView.ShareWithGoogle}); - break; - case 'shareWithYandex': - setSelectedView({selectedView: SpeedDialView.ShareWithYandex}); - break; - default: - break; - } - } - - const handleCancellation = () => setSelectedView({selectedView: SpeedDialView.Collapsed}); - - const handleChangingView = () => - setSelectedView({selectedView: selectedView === SpeedDialView.Collapsed ? - SpeedDialView.Expanded : SpeedDialView.Collapsed - }) - - const actions = [ - { icon: , name: 'Сохранить', operation: 'download' }, - { icon: , name: 'Отправить в Google', operation: 'shareWithGoogle' }, - // Icon by Icons8 ("https://icons8.com") - { icon: Y, name: 'Отправить в Яндекс', operation: 'shareWithYandex' }, - ]; - - return ( -
- {(selectedView === SpeedDialView.Collapsed || selectedView === SpeedDialView.Expanded) && - - } - direction="right" - onClickCapture={handleChangingView} - sx={{ '& .MuiFab-primary': { width: 45, height: 45 } }} - open={selectedView !== SpeedDialView.Collapsed} - > - {actions.map((action) => ( - { - handleSpeedDialItemClick(action.operation) - }} - /> - ))} - - - } - {selectedView === SpeedDialView.Download && - handleCancellation()} - /> - } - {selectedView === SpeedDialView.ShareWithGoogle && - handleCancellation()} - /> - } - {selectedView === SpeedDialView.ShareWithYandex && - handleCancellation()} - userCode={props.yandexCode} - /> - } -
- ) -} -export default SaveStats; diff --git a/hwproj.front/src/components/Solutions/YandexLogo.svg b/hwproj.front/src/components/Solutions/YandexLogo.svg deleted file mode 100644 index 25a7ddddd..000000000 --- a/hwproj.front/src/components/Solutions/YandexLogo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From 92433425a1b93885ab5e78f8a30fdbd8d69521d9 Mon Sep 17 00:00:00 2001 From: bygu4 Date: Fri, 9 May 2025 13:27:40 +0300 Subject: [PATCH 42/58] update action dialogs --- .../src/components/Courses/StatsMenu.tsx | 16 +- .../components/Solutions/DownloadStats.tsx | 78 +++++----- .../components/Solutions/ExportToGoogle.tsx | 129 ++++++++-------- .../components/Solutions/ExportToYandex.tsx | 139 +++++++++--------- 4 files changed, 177 insertions(+), 185 deletions(-) diff --git a/hwproj.front/src/components/Courses/StatsMenu.tsx b/hwproj.front/src/components/Courses/StatsMenu.tsx index 3d2a87daa..f11d5cda9 100644 --- a/hwproj.front/src/components/Courses/StatsMenu.tsx +++ b/hwproj.front/src/components/Courses/StatsMenu.tsx @@ -1,4 +1,4 @@ -import {FC, useState, useEffect} from "react"; +import { FC, useState, useEffect } from "react"; import { Button, Menu, @@ -9,9 +9,9 @@ import { DialogTitle, DialogContent} from "@mui/material"; -import {NestedMenuItem} from "mui-nested-menu"; -import {Download, ShowChart} from "@mui/icons-material"; -import {useNavigate} from "react-router-dom"; +import { NestedMenuItem } from "mui-nested-menu"; +import { Download, ShowChart } from "@mui/icons-material"; +import { useNavigate } from "react-router-dom"; import DownloadStats from "../Solutions/DownloadStats"; import ExportToGoogle from "../Solutions/ExportToGoogle"; import ExportToYandex from "../Solutions/ExportToYandex"; @@ -43,7 +43,7 @@ interface StatsMenuState { const StatsMenu: FC = props => { const [menuState, setMenuState] = useState({ anchorEl: null, - saveStatsAction: null, + saveStatsAction: props.yandexCode !== null ? SaveStatsAction.ShareWithYandex : null, }) const {anchorEl, saveStatsAction} = menuState @@ -146,7 +146,7 @@ const StatsMenu: FC = props => { } return ( -
+
- + + Загрузить + - - + ) } -export default DownloadStats; \ No newline at end of file + +export default DownloadStats; diff --git a/hwproj.front/src/components/Solutions/ExportToGoogle.tsx b/hwproj.front/src/components/Solutions/ExportToGoogle.tsx index eafbdfe99..6fff318a2 100644 --- a/hwproj.front/src/components/Solutions/ExportToGoogle.tsx +++ b/hwproj.front/src/components/Solutions/ExportToGoogle.tsx @@ -1,8 +1,9 @@ import { FC, useState } from "react"; -import { Alert, Box, Button, CircularProgress, Grid, MenuItem, Select, TextField } from "@mui/material"; +import { Alert, Button, Grid, MenuItem, Select, TextField } from "@mui/material"; import apiSingleton from "../../api/ApiSingleton"; import { green, red } from "@material-ui/core/colors"; import { StringArrayResult } from "@/api"; +import { LoadingButton } from "@mui/lab"; enum LoadingStatus { None, @@ -27,7 +28,7 @@ interface ExportToGoogleState { const ExportToGoogle: FC = (props: ExportToGoogleProps) => { const [state, setState] = useState({ - url: '', + url: "", selectedSheet: 0, googleSheetTitles: undefined, loadingStatus: LoadingStatus.None, @@ -37,8 +38,10 @@ const ExportToGoogle: FC = (props: ExportToGoogleProps) => const {url, googleSheetTitles, selectedSheet, loadingStatus, error } = state const handleGoogleDocUrlChange = async (value: string) => { - const titles = await apiSingleton.statisticsApi.statisticsGetSheetTitles(value) - setState(prevState => ({ ...prevState, url: value, googleSheetTitles: titles })); + const titles = value + ? await apiSingleton.statisticsApi.statisticsGetSheetTitles(value) + : undefined + setState(prevState => ({ ...prevState, url: value, googleSheetTitles: titles })) } const getGoogleSheetName = () => { @@ -56,45 +59,53 @@ const ExportToGoogle: FC = (props: ExportToGoogleProps) => }), }; - return - - {(googleSheetTitles && !googleSheetTitles.succeeded && - - {googleSheetTitles!.errors![0]} - ) - || - (loadingStatus === LoadingStatus.Error && - - {error} - ) - || - ( - Для загрузки таблицы необходимо разрешить доступ на редактирование по ссылке для Google Sheets + return ( + + + {(googleSheetTitles && !googleSheetTitles.succeeded && + + {googleSheetTitles!.errors![0]} ) - } - - - - - handleGoogleDocUrlChange(event.target.value) - } - /> + || + (loadingStatus === LoadingStatus.Error && + + {error} + ) + || + ( + Для загрузки таблицы необходимо разрешить доступ на редактирование по ссылке для Google Sheets + ) + } - {googleSheetTitles && googleSheetTitles.value && googleSheetTitles.value.length > 0 && - - } - {googleSheetTitles && googleSheetTitles.succeeded && - - - {loadingStatus === LoadingStatus.Loading && ( - - )} - - } - - + - + ) } + export default ExportToGoogle; diff --git a/hwproj.front/src/components/Solutions/ExportToYandex.tsx b/hwproj.front/src/components/Solutions/ExportToYandex.tsx index ca2d372cb..afa33f7f4 100644 --- a/hwproj.front/src/components/Solutions/ExportToYandex.tsx +++ b/hwproj.front/src/components/Solutions/ExportToYandex.tsx @@ -1,8 +1,9 @@ import { FC, useState } from "react"; import { useEffect } from 'react'; -import { Alert, Box, Button, CircularProgress, Grid, Link, TextField } from "@mui/material"; +import { Alert, Button, Grid, Link, TextField } from "@mui/material"; import apiSingleton from "../../api/ApiSingleton"; import { green, red } from "@material-ui/core/colors"; +import { LoadingButton } from "@mui/lab"; enum LoadingStatus { None, @@ -127,86 +128,80 @@ const ExportToYandex: FC = (props: ExportToYandexProps) => }), }; - return - {userToken === null && - - {!isAuthorizationError && - - - Для загрузки таблицы необходимо пройти авторизацию.{' '} - - Начать авторизацию - - - - } - {isAuthorizationError && - - Авторизация не пройдена. Попробуйте еще раз{' '} + return userToken === null ? ( + + {!isAuthorizationError && + + + Для загрузки таблицы необходимо пройти авторизацию.{' '} Начать авторизацию - } - - + } + {isAuthorizationError && + + Авторизация не пройдена. Попробуйте еще раз{' '} + + Начать авторизацию + + + } + + - } - {userToken !== null && - + + ) : ( + + + + Авторизация успешно пройдена. Файл будет загружен на диск по адресу + "Приложения/{import.meta.env.VITE_YANDEX_APPLICATION_NAME}/{fileName}.xlsx" + + + - - Авторизация успешно пройдена. Файл будет загружен на диск по адресу - "Приложения/{import.meta.env.VITE_YANDEX_APPLICATION_NAME}/{fileName}.xlsx" - + { + event.persist() + setState((prevState) => + ({...prevState, fileName: event.target.value, loadingStatus: LoadingStatus.None}) + ) + }} + /> - - - { - event.persist() - setState((prevState) => - ({...prevState, fileName: event.target.value, loadingStatus: LoadingStatus.None}))}} - /> - - - - - {loadingStatus === LoadingStatus.Loading && ( - - )} - - - - - + + { + setState((prevState) => ({...prevState, loadingStatus: LoadingStatus.Loading})) + handleExportClick() + }} + > + Сохранить + + + + - } - + + ) } + export default ExportToYandex; From 4ed022b025dd3bba5e15e8c6858917f79284d360 Mon Sep 17 00:00:00 2001 From: bygu4 Date: Fri, 9 May 2025 15:53:29 +0300 Subject: [PATCH 43/58] update error handling --- .../ExportServices/GoogleService.cs | 16 +++++++++-- .../components/Solutions/DownloadStats.tsx | 15 ++++++---- .../components/Solutions/ExportToGoogle.tsx | 23 +++++++++------ .../components/Solutions/ExportToYandex.tsx | 28 +++++++++---------- 4 files changed, 52 insertions(+), 30 deletions(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs index d85b4b282..640781eb3 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/ExportServices/GoogleService.cs @@ -11,6 +11,8 @@ using HwProj.Models.Result; using OfficeOpenXml; using OfficeOpenXml.Style; +using Google; +using System.Net; namespace HwProj.APIGateway.API.ExportServices { @@ -88,9 +90,19 @@ public async Task> GetSheetTitles(string sheetUrl) var spreadsheet = await _internalGoogleSheetsService.Spreadsheets.Get(spreadsheetId).ExecuteAsync(); return Result.Success(spreadsheet.Sheets.Select(t => t.Properties.Title).ToArray()); } - catch (Exception ex) + catch (GoogleApiException ex) { - return Result.Failed($"Ошибка при обращении к Google Sheets: {ex.Message}"); + var message = $"Ошибка при обращении к Google Sheets: {ex.Message}"; + if (ex.Error.Code == (int)HttpStatusCode.NotFound) + { + message = "Таблица не найдена, проверьте корректность ссылки"; + } + else if (ex.Error.Code == (int)HttpStatusCode.Forbidden) + { + message = "Нет прав не редактирование таблицы, проверьте настройки доступа"; + } + + return Result.Failed(message); } } diff --git a/hwproj.front/src/components/Solutions/DownloadStats.tsx b/hwproj.front/src/components/Solutions/DownloadStats.tsx index 429b52913..bd17b87e6 100644 --- a/hwproj.front/src/components/Solutions/DownloadStats.tsx +++ b/hwproj.front/src/components/Solutions/DownloadStats.tsx @@ -32,11 +32,16 @@ const DownloadStats: FC = (props: DownloadStatsProps) => { return ( - { - event.persist(); - setFileName(event.target.value); - }}/> + { + event.persist(); + setFileName(event.target.value); + }} + /> = (props: ExportToGoogleProps) => const {url, googleSheetTitles, selectedSheet, loadingStatus, error } = state - const handleGoogleDocUrlChange = async (value: string) => { - const titles = value - ? await apiSingleton.statisticsApi.statisticsGetSheetTitles(value) - : undefined - setState(prevState => ({ ...prevState, url: value, googleSheetTitles: titles })) + const handleGoogleDocUrlChange = (value: string) => { + setState(prevState => ({ ...prevState, url: value })) + if (value) + apiSingleton.statisticsApi.statisticsGetSheetTitles(value) + .then(response => setState(prevState => ({ ...prevState, googleSheetTitles: response }))) } const getGoogleSheetName = () => { @@ -79,10 +79,15 @@ const ExportToGoogle: FC = (props: ExportToGoogleProps) => - - handleGoogleDocUrlChange(event.target.value) - } + { + event.persist() + handleGoogleDocUrlChange(event.target.value) + }} /> {googleSheetTitles && googleSheetTitles.value && googleSheetTitles.value.length > 0 && diff --git a/hwproj.front/src/components/Solutions/ExportToYandex.tsx b/hwproj.front/src/components/Solutions/ExportToYandex.tsx index afa33f7f4..dce2495f8 100644 --- a/hwproj.front/src/components/Solutions/ExportToYandex.tsx +++ b/hwproj.front/src/components/Solutions/ExportToYandex.tsx @@ -130,24 +130,24 @@ const ExportToYandex: FC = (props: ExportToYandexProps) => return userToken === null ? ( - {!isAuthorizationError && - + + {!isAuthorizationError && Для загрузки таблицы необходимо пройти авторизацию.{' '} Начать авторизацию - - } - {isAuthorizationError && - - Авторизация не пройдена. Попробуйте еще раз{' '} - - Начать авторизацию - - - } + } + {isAuthorizationError && + + Авторизация не пройдена. Попробуйте еще раз{' '} + + Начать авторизацию + + + } + ) : ( - + Авторизация успешно пройдена. Файл будет загружен на диск по адресу "Приложения/{import.meta.env.VITE_YANDEX_APPLICATION_NAME}/{fileName}.xlsx" - + = (props: ExportToYandexProps) => color="primary" type="button" sx={buttonSx} + style={{ marginRight: 8 }} loading={loadingStatus === LoadingStatus.Loading} onClick={() => { setState((prevState) => ({...prevState, loadingStatus: LoadingStatus.Loading})) @@ -192,8 +194,6 @@ const ExportToYandex: FC = (props: ExportToYandexProps) => > Сохранить - -
) diff --git a/hwproj.front/src/components/Solutions/DownloadStats.tsx b/hwproj.front/src/components/Solutions/DownloadStats.tsx index 61613fb94..57039beee 100644 --- a/hwproj.front/src/components/Solutions/DownloadStats.tsx +++ b/hwproj.front/src/components/Solutions/DownloadStats.tsx @@ -1,5 +1,5 @@ import { FC, useState } from "react"; -import { Button, Grid, TextField } from "@mui/material"; +import { Button, DialogActions, DialogContent, Grid, TextField } from "@mui/material"; import apiSingleton from "../../api/ApiSingleton"; import { LoadingButton } from "@mui/lab"; @@ -30,37 +30,39 @@ const DownloadStats: FC = (props: DownloadStatsProps) => { } return ( - - - { - event.persist(); - setFileName(event.target.value); - }} - /> - - - - Загрузить - - - - + + + + { + event.persist(); + setFileName(event.target.value); + }} + /> + + + + Загрузить + + + + + + + ) } diff --git a/hwproj.front/src/components/Solutions/ExportToGoogle.tsx b/hwproj.front/src/components/Solutions/ExportToGoogle.tsx index 03ca119eb..2359ca6e2 100644 --- a/hwproj.front/src/components/Solutions/ExportToGoogle.tsx +++ b/hwproj.front/src/components/Solutions/ExportToGoogle.tsx @@ -1,5 +1,15 @@ import { FC, useState } from "react"; -import { Alert, Button, Grid, MenuItem, Select, TextField } from "@mui/material"; +import { + Alert, + Button, + DialogActions, + DialogContent, + DialogContentText, + Grid, + MenuItem, + Select, + TextField, +} from "@mui/material"; import apiSingleton from "../../api/ApiSingleton"; import { green, red } from "@material-ui/core/colors"; import { StringArrayResult } from "@/api"; @@ -62,58 +72,60 @@ const ExportToGoogle: FC = (props: ExportToGoogleProps) => }; return ( - - - {(googleSheetTitles && !googleSheetTitles.succeeded && - - {googleSheetTitles!.errors![0]} - ) - || - (loadingStatus === LoadingStatus.Error && + + + + {(googleSheetTitles && !googleSheetTitles.succeeded && + + {googleSheetTitles!.errors![0]} + + ) || (loadingStatus === LoadingStatus.Error && {error} - ) - || - ( - Для загрузки таблицы необходимо разрешить доступ на редактирование по ссылке для Google Sheets - ) - } - - - - { - event.persist() - handleGoogleDocUrlChange(event.target.value) - }} - /> + + ) || ( + + Для загрузки таблицы необходимо разрешить доступ + на редактирование по ссылке для Google Sheets + + )} - {googleSheetTitles && googleSheetTitles.value && googleSheetTitles.value.length > 0 && + + + - + label="Ссылка на Google Sheets" + value={url} + onChange={event => { + event.persist() + handleGoogleDocUrlChange(event.target.value) + }} + /> - } - - {googleSheetTitles && googleSheetTitles.succeeded && + {googleSheetTitles && googleSheetTitles.value && googleSheetTitles.value.length > 0 && + + + + } + + {googleSheetTitles && googleSheetTitles.succeeded && + { setState((prevState) => ({...prevState, loadingStatus: LoadingStatus.Loading})) @@ -134,14 +146,16 @@ const ExportToGoogle: FC = (props: ExportToGoogleProps) => > Сохранить - } + + } + - - + + ) } diff --git a/hwproj.front/src/components/Solutions/ExportToYandex.tsx b/hwproj.front/src/components/Solutions/ExportToYandex.tsx index dfe079f17..54054046b 100644 --- a/hwproj.front/src/components/Solutions/ExportToYandex.tsx +++ b/hwproj.front/src/components/Solutions/ExportToYandex.tsx @@ -1,6 +1,14 @@ -import { FC, useState } from "react"; -import { useEffect } from 'react'; -import { Alert, Button, Grid, Link, TextField } from "@mui/material"; +import { FC, useState, useEffect } from "react"; +import { + Alert, + Button, + DialogActions, + DialogContent, + DialogContentText, + Grid, + Link, + TextField, +} from "@mui/material"; import apiSingleton from "../../api/ApiSingleton"; import { green, red } from "@material-ui/core/colors"; import { LoadingButton } from "@mui/lab"; @@ -129,43 +137,47 @@ const ExportToYandex: FC = (props: ExportToYandexProps) => }; return userToken === null ? ( - - - {!isAuthorizationError && - - Для загрузки таблицы необходимо пройти{" "} - - авторизацию - - - } - {isAuthorizationError && - - Авторизация не пройдена. Попробуйте{" "} - - еще раз - - - } - - - - - + + + + {isAuthorizationError ? ( + + Авторизация не пройдена. Попробуйте{" "} + + еще раз + + + ) : ( + + Для загрузки таблицы необходимо пройти{" "} + + авторизацию + + + )} + + + + + + + + ) : ( - - - - Авторизация успешно пройдена. Файл будет загружен на диск по адресу - "Приложения/{import.meta.env.VITE_YANDEX_APPLICATION_NAME}/{fileName}.xlsx" - - - - + + + + + Авторизация успешно пройдена. Файл будет загружен на диск по адресу + "Приложения/{import.meta.env.VITE_YANDEX_APPLICATION_NAME}/{fileName}.xlsx" + + + + + = (props: ExportToYandexProps) => variant="text" color="primary" type="button" - sx={buttonSx} - style={{ marginRight: 8 }} + sx={buttonSx} loading={loadingStatus === LoadingStatus.Loading} onClick={() => { setState((prevState) => ({...prevState, loadingStatus: LoadingStatus.Loading})) @@ -194,13 +205,15 @@ const ExportToYandex: FC = (props: ExportToYandexProps) => > Сохранить + + - - + + ) } From 13bd4ccc0d6494206859dce8013320a4d19f0e05 Mon Sep 17 00:00:00 2001 From: bygu4 Date: Sat, 10 May 2025 17:29:39 +0300 Subject: [PATCH 48/58] correct spacing in dialog, use TextField with sheet select --- hwproj.front/src/components/Solutions/DownloadStats.tsx | 2 +- hwproj.front/src/components/Solutions/ExportToGoogle.tsx | 8 ++++---- hwproj.front/src/components/Solutions/ExportToYandex.tsx | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/hwproj.front/src/components/Solutions/DownloadStats.tsx b/hwproj.front/src/components/Solutions/DownloadStats.tsx index 57039beee..b921ca549 100644 --- a/hwproj.front/src/components/Solutions/DownloadStats.tsx +++ b/hwproj.front/src/components/Solutions/DownloadStats.tsx @@ -31,7 +31,7 @@ const DownloadStats: FC = (props: DownloadStatsProps) => { return ( - + = (props: ExportToGoogleProps) => )} - + = (props: ExportToGoogleProps) => {googleSheetTitles && googleSheetTitles.value && googleSheetTitles.value.length > 0 && - + } diff --git a/hwproj.front/src/components/Solutions/ExportToYandex.tsx b/hwproj.front/src/components/Solutions/ExportToYandex.tsx index 54054046b..842af8ad5 100644 --- a/hwproj.front/src/components/Solutions/ExportToYandex.tsx +++ b/hwproj.front/src/components/Solutions/ExportToYandex.tsx @@ -157,7 +157,7 @@ const ExportToYandex: FC = (props: ExportToYandexProps) => )} - +
) } diff --git a/hwproj.front/src/components/Solutions/DownloadStats.tsx b/hwproj.front/src/components/Solutions/DownloadStats.tsx index cf6107bd3..578a41a08 100644 --- a/hwproj.front/src/components/Solutions/DownloadStats.tsx +++ b/hwproj.front/src/components/Solutions/DownloadStats.tsx @@ -1,59 +1,43 @@ -import { FC, useState } from "react"; -import { Button, DialogActions, DialogContent, Grid } from "@mui/material"; +import { FC, useEffect } from "react"; +import { useSnackbar } from "notistack"; import apiSingleton from "../../api/ApiSingleton"; -import { LoadingButton } from "@mui/lab"; import Utils from "@/services/Utils"; interface DownloadStatsProps { courseId: number | undefined userId: string - onCancellation: () => void + onClose: () => void } const DownloadStats: FC = (props: DownloadStatsProps) => { - const [loading, setLoading] = useState(false) + const {courseId, userId, onClose} = props + const {enqueueSnackbar} = useSnackbar() - const handleFileDownloading = () => { - const statsDatetime = Utils.toStringForFileName(new Date()) - setLoading(true) - apiSingleton.statisticsApi.statisticsGetFile(props.courseId, props.userId, "Лист 1") - .then((response) => response.blob()) - .then((blob) => { - const fileName = `StatsReport_${statsDatetime}` - const url = window.URL.createObjectURL(new Blob([blob])); - const link = document.createElement("a"); - link.href = url; - link.setAttribute("download", `${fileName}.xlsx`); - document.body.appendChild(link); - link.click(); - link.parentNode!.removeChild(link); - }) - .finally(() => setLoading(false)) - } + useEffect(() => { + const downloadStats = async () => { + try { + const statsDatetime = Utils.toStringForFileName(new Date()) + const response = await apiSingleton.statisticsApi.statisticsGetFile(courseId, userId, "Лист 1") + const blob = await response.blob() + const fileName = `StatsReport_${statsDatetime}` + const url = window.URL.createObjectURL(new Blob([blob])); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", `${fileName}.xlsx`); + document.body.appendChild(link); + link.click(); + link.parentNode!.removeChild(link); + } catch (e) { + console.error("Ошибка при загрузке статистики:", e) + enqueueSnackbar("Не удалось загрузить файл со статистикой, попробуйте позже", {variant: "error"}) + } + } - return ( - - - - - Скачать - - - - - - - - ) + downloadStats() + onClose() + }) + + return null } export default DownloadStats; diff --git a/hwproj.front/src/components/Solutions/ExportToGoogle.tsx b/hwproj.front/src/components/Solutions/ExportToGoogle.tsx index 5e5dec29f..8ca6c84d5 100644 --- a/hwproj.front/src/components/Solutions/ExportToGoogle.tsx +++ b/hwproj.front/src/components/Solutions/ExportToGoogle.tsx @@ -3,9 +3,11 @@ import { Alert, Button, CircularProgress, + Dialog, DialogActions, DialogContent, DialogContentText, + DialogTitle, Grid, MenuItem, TextField, @@ -24,8 +26,8 @@ enum LoadingStatus { interface ExportToGoogleProps { courseId: number | undefined - userId: string - onCancellation: () => void + open: boolean + onClose: () => void } interface ExportToGoogleState { @@ -38,6 +40,8 @@ interface ExportToGoogleState { } const ExportToGoogle: FC = (props: ExportToGoogleProps) => { + const {courseId, open, onClose} = props + const [state, setState] = useState({ url: "", googleSheetTitles: undefined, @@ -82,94 +86,99 @@ const ExportToGoogle: FC = (props: ExportToGoogleProps) => }; return ( - - - - {(googleSheetTitles && !googleSheetTitles.succeeded && - - {googleSheetTitles!.errors![0]} - - ) || (exportStatus === LoadingStatus.Error && - - {error} - - ) || ( - - Для загрузки таблицы необходимо разрешить доступ - на редактирование по ссылке для Google Sheets - - )} - - - - - { - event.persist() - handleGoogleDocUrlChange(event.target.value) - }} - /> - - {loadingSheets && + + + Выгрузить таблицу в Google Docs + + + - + {(googleSheetTitles && !googleSheetTitles.succeeded && + + {googleSheetTitles!.errors![0]} + + ) || (exportStatus === LoadingStatus.Error && + + {error} + + ) || ( + + Для загрузки таблицы необходимо разрешить доступ + на редактирование по ссылке для Google Sheets + + )} - } - {!loadingSheets && googleSheetTitles && googleSheetTitles.value && googleSheetTitles.value.length > 0 && - + + + setState(prevState => ({ ...prevState, selectedSheet: +v.target.value }))} - > - {googleSheetTitles.value.map((title, i) => {title})} - + label="Ссылка на Google Sheets" + value={url} + onChange={event => { + event.persist() + handleGoogleDocUrlChange(event.target.value) + }} + /> - } - {!loadingSheets && googleSheetTitles && googleSheetTitles.succeeded && - - { - setState((prevState) => ({...prevState, exportStatus: LoadingStatus.Loading})) - const result = await apiSingleton.statisticsApi.statisticsExportToGoogleSheets( - props.courseId, - url, - getGoogleSheetName()) - setState((prevState) => - ({...prevState, - exportStatus: result.succeeded ? LoadingStatus.Success : LoadingStatus.Error, - error: result.errors === undefined - || result.errors === null - || result.errors.length === 0 - ? null : result.errors[0] - })) + {loadingSheets && + + + + } + {!loadingSheets && googleSheetTitles && googleSheetTitles.value && googleSheetTitles.value.length > 0 && + + setState(prevState => ({ ...prevState, selectedSheet: +v.target.value }))} + > + {googleSheetTitles.value.map((title, i) => {title})} + + + } + {!loadingSheets && googleSheetTitles && googleSheetTitles.succeeded && + + { + setState((prevState) => ({...prevState, exportStatus: LoadingStatus.Loading})) + const result = await apiSingleton.statisticsApi.statisticsExportToGoogleSheets( + courseId, + url, + getGoogleSheetName()) + setState((prevState) => + ({...prevState, + exportStatus: result.succeeded ? LoadingStatus.Success : LoadingStatus.Error, + error: result.errors === undefined + || result.errors === null + || result.errors.length === 0 + ? null : result.errors[0] + })) + } } - } - > - Сохранить - + > + Сохранить + + + } + + - } - - - - - + + + ) } diff --git a/hwproj.front/src/components/Solutions/ExportToYandex.tsx b/hwproj.front/src/components/Solutions/ExportToYandex.tsx index 31ad7e902..1b153fe12 100644 --- a/hwproj.front/src/components/Solutions/ExportToYandex.tsx +++ b/hwproj.front/src/components/Solutions/ExportToYandex.tsx @@ -2,9 +2,11 @@ import { Alert, Button, + Dialog, DialogActions, DialogContent, DialogContentText, + DialogTitle, Grid, Link, TextField, @@ -28,8 +30,9 @@ interface LocalStorageKey { interface ExportToYandexProps { courseId: number | undefined userId: string - onCancellation: () => void userCode: string | null + open: boolean + onClose: () => void } interface ExportToYandexState { @@ -40,10 +43,12 @@ interface ExportToYandexState { } const ExportToYandex: FC = (props: ExportToYandexProps) => { + const {courseId, userId, userCode, open, onClose} = props + const [state, setState] = useState({ fileName: "", userToken: localStorage.getItem( - JSON.stringify({name: "yandexAccessToken", userId: `${props.userId}`})), + JSON.stringify({name: "yandexAccessToken", userId: `${userId}`})), loadingStatus: LoadingStatus.None, isAuthorizationError: false, }) @@ -79,9 +84,9 @@ const ExportToYandex: FC = (props: ExportToYandexProps) => const setCurrentState = async () => { - if (userToken === null && props.userCode !== null) + if (userToken === null && userCode !== null) { - const token = await setUserYandexToken(props.userCode, props.userId) + const token = await setUserYandexToken(userCode, userId) setState((prevState) => ({...prevState, userToken: token === 'error' ? null : token, isAuthorizationError: token === 'error'})) } @@ -103,7 +108,7 @@ const ExportToYandex: FC = (props: ExportToYandexProps) => if (response.status >= 200 && response.status < 300) { const jsonResponse = await response.json(); const url = jsonResponse.href; - const fileData = await apiSingleton.statisticsApi.statisticsGetFile(props.courseId, props.userId, "Лист 1"); + const fileData = await apiSingleton.statisticsApi.statisticsGetFile(courseId, userId, "Лист 1"); const data = await fileData.blob(); const fileExportResponse = await fetch(url, { @@ -136,84 +141,91 @@ const ExportToYandex: FC = (props: ExportToYandexProps) => }), }; - return userToken === null ? ( - - - - {isAuthorizationError ? ( - - Авторизация не пройдена. Попробуйте{" "} - - еще раз - - - ) : ( - - Для загрузки таблицы необходимо пройти{" "} - - авторизацию - - - )} - - - - - - - - - ) : ( - - - - - Авторизация успешно пройдена. Файл будет загружен на диск по адресу - "Приложения/{import.meta.env.VITE_YANDEX_APPLICATION_NAME}/{fileName}.xlsx" - - - - - - { - event.persist() - setState((prevState) => - ({...prevState, fileName: event.target.value, loadingStatus: LoadingStatus.None}) - ) - }} - /> - - - { - setState((prevState) => ({...prevState, loadingStatus: LoadingStatus.Loading})) - handleExportClick() - }} - > - Сохранить - - - - - - - + return ( + + + Выгрузить таблицу на Яндекс Диск + + {userToken === null ? ( + + + + {isAuthorizationError ? ( + + Авторизация не пройдена. Попробуйте{" "} + + еще раз + + + ) : ( + + Для загрузки таблицы необходимо пройти{" "} + + авторизацию + + + )} + + + + + + + + + ) : ( + + + + + Авторизация успешно пройдена. Файл будет загружен на диск по адресу + "Приложения/{import.meta.env.VITE_YANDEX_APPLICATION_NAME}/{fileName}.xlsx" + + + + + + { + event.persist() + setState((prevState) => + ({...prevState, fileName: event.target.value, loadingStatus: LoadingStatus.None}) + ) + }} + /> + + + { + setState((prevState) => ({...prevState, loadingStatus: LoadingStatus.Loading})) + handleExportClick() + }} + > + Сохранить + + + + + + + + )} + ) } From 44bf2c204851225f64848c49f3765e269bc81311 Mon Sep 17 00:00:00 2001 From: bygu4 Date: Mon, 26 May 2025 17:38:35 +0300 Subject: [PATCH 56/58] correct sheets service configuration on missing google credentials --- HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs index 947b4aa30..556f4dc9b 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs @@ -19,6 +19,7 @@ using IStudentsInfo; using StudentsInfo; using Newtonsoft.Json.Linq; +using System; namespace HwProj.APIGateway.API { @@ -94,7 +95,14 @@ private static JToken Serialize(IConfigurationSection configurationSection) private static SheetsService ConfigureGoogleSheets(IConfigurationSection _sheetsConfiguration) { var jsonObject = Serialize(_sheetsConfiguration); - var credential = GoogleCredential.FromJson(jsonObject.ToString()).CreateScoped(SheetsService.Scope.Spreadsheets); + GoogleCredential? credential = null; + + try + { + credential = GoogleCredential.FromJson(jsonObject.ToString()) + .CreateScoped(SheetsService.Scope.Spreadsheets); + } + catch (Exception) {} return new SheetsService(new BaseClientService.Initializer { From debc4b9f1ee65244e60d68543d3528b59245a86e Mon Sep 17 00:00:00 2001 From: bygu4 Date: Sun, 8 Jun 2025 16:44:28 +0300 Subject: [PATCH 57/58] update api client --- hwproj.front/src/api/api.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index a2743bc05..136573ff3 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -1732,20 +1732,6 @@ export interface ScopeDTO { */ courseUnitId?: number; } -/** - * - * @export - * @interface SheetUrl - */ -export interface SheetUrl { - /** - * - * @type {string} - * @memberof SheetUrl - */ - url?: string; -} - /** * * @export From 483c0e68d668811eb154830be9a0a7dd08eaa4c7 Mon Sep 17 00:00:00 2001 From: bygu4 Date: Sun, 8 Jun 2025 17:16:01 +0300 Subject: [PATCH 58/58] use validatedCourseId --- .../HwProj.CoursesService.Tests/CoursesServiceTests.cs | 4 ++-- hwproj.front/src/components/Courses/Course.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.Tests/CoursesServiceTests.cs b/HwProj.CoursesService/HwProj.CoursesService.Tests/CoursesServiceTests.cs index 85ddd92e9..c8bca21fe 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.Tests/CoursesServiceTests.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.Tests/CoursesServiceTests.cs @@ -179,7 +179,7 @@ public async Task NullTaskPropertiesAfterShouldBeInheritedFromHomework() var homeworkResult = await client.AddHomeworkToCourse(homework, courseId); - var actualResult = (await client.GetHomework(homeworkResult.Value.Id)).Tasks.FirstOrDefault(); + var actualResult = (await client.GetHomework(homeworkResult.Value.Id)).Value.Tasks.FirstOrDefault(); homeworkResult.Succeeded.Should().BeTrue(); homeworkResult.Errors.Should().BeNull(); @@ -345,7 +345,7 @@ public async Task AddTaskByMentorNotFromThisCourseShouldReturnFailedResult() var homeworkFromDb = await client.GetHomework(homeworkResult.Value.Id); addHomeworkResult.Succeeded.Should().BeFalse(); - homeworkFromDb.Tasks.Should().BeEmpty(); + homeworkFromDb.Value.Tasks.Should().BeEmpty(); } [Test] diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index 4b7a78e02..24c416c00 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -161,7 +161,7 @@ const Course: React.FC = () => { let delay = 1000; // Начальная задержка 1 сек const scopeDto: ScopeDTO = { - courseId: +courseId!, + courseId: +validatedCourseId!, courseUnitType: CourseUnitType.Homework, courseUnitId: homeworkId } @@ -338,7 +338,7 @@ const Course: React.FC = () => { useEffect(() => { ApiSingleton.statisticsApi.statisticsGetCourseStatistics(+validatedCourseId!) .then(res => setStudentSolutions(res)) - }, [courseId]) + }, [validatedCourseId]) useEffect(() => changeTab(tab || "homeworks"), [tab, validatedCourseId, isFound])