Skip to content

Commit fd2d37b

Browse files
committed
feat: simple yandex support is implemented
1 parent 1c21ff5 commit fd2d37b

File tree

11 files changed

+549
-141
lines changed

11 files changed

+549
-141
lines changed

HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Linq;
1+
using System.IO;
2+
using System.Linq;
23
using System.Net;
34
using System.Threading.Tasks;
45
using HwProj.APIGateway.API.ExportServices;
@@ -9,6 +10,7 @@
910
using HwProj.Models.Result;
1011
using HwProj.SolutionsService.Client;
1112
using Microsoft.AspNetCore.Mvc;
13+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
1214

1315
namespace HwProj.APIGateway.API.Controllers
1416
{

HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Google.Apis.Services;
44
using Google.Apis.Sheets.v4;
55
using HwProj.APIGateway.API.ExportServices;
6+
using HwProj.APIGateway.API.Models;
67
using HwProj.AuthService.Client;
78
using HwProj.CoursesService.Client;
89
using HwProj.NotificationsService.Client;
@@ -14,6 +15,8 @@
1415
using Microsoft.Extensions.DependencyInjection;
1516
using Microsoft.IdentityModel.Tokens;
1617
using HwProj.Utils.Authorization;
18+
using Microsoft.EntityFrameworkCore;
19+
1720

1821
namespace HwProj.APIGateway.API
1922
{
@@ -28,6 +31,7 @@ public Startup(IConfiguration configuration)
2831

2932
public void ConfigureServices(IServiceCollection services)
3033
{
34+
services.AddCors();
3135
services.ConfigureHwProjServices("API Gateway");
3236

3337
const string authenticationProviderKey = "GatewayKey";

hwproj.front/src/App.tsx

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class App extends Component<AppProps, AppState> {
4141
this.state = {
4242
loggedIn: ApiSingleton.authService.isLoggedIn(),
4343
isLecturer: ApiSingleton.authService.isLecturer(),
44-
newNotificationsCount: 0
44+
newNotificationsCount: 0,
4545
};
4646
}
4747

@@ -71,6 +71,52 @@ class App extends Component<AppProps, AppState> {
7171
this.props.history.push("/login");
7272
}
7373

74+
updatedLastViewedCourseId = (courseId : string) =>
75+
{
76+
sessionStorage.setItem("courseId", courseId)
77+
}
78+
79+
getLastCourseId = () =>
80+
{
81+
const sessionStorageCourseId = sessionStorage.getItem("courseId");
82+
return sessionStorageCourseId === null ? "-1" : sessionStorageCourseId;
83+
}
84+
85+
getUserYandexToken = () =>
86+
{
87+
const linkWithConfirmationCode = window.location.href;
88+
const regExp : RegExp = new RegExp("yandex\\?code=(.*)", "g");
89+
const confirmationCode = regExp.exec(linkWithConfirmationCode)![1];
90+
const fetchBody = `grant_type=authorization_code&code=${confirmationCode}&client_id=49a5c4e9d2744ff5b0161a017d1ecd05&client_secret=184c6646ff62417fa2e942a18023881a`;
91+
interface ExchangeConfirmationCodeRequest {
92+
access_token: string
93+
}
94+
fetch(`https://oauth.yandex.ru/token`, {
95+
method: "post",
96+
headers: {
97+
'Content-Type': 'application/x-www-form-urlencoded; Charset=utf-8',
98+
'Host': 'https://oauth.yandex.ru/'
99+
},
100+
body: fetchBody
101+
})
102+
.then( async (response) => {
103+
if (response.status === 200) {
104+
const jsonResponse = await response.json();
105+
const token = jsonResponse.access_token;
106+
const userId = await ApiSingleton.accountApi.apiAccountGetUserDataGet()
107+
.then((data) => {
108+
const userData = data.userData;
109+
if (userData !== undefined) {
110+
return userData.userId
111+
}
112+
})
113+
if (token !== null && userId !== undefined)
114+
{
115+
localStorage.setItem(`yandexAccessToken=${userId}`, token);
116+
}
117+
}
118+
});
119+
}
74120

75121
render() {
76122
return (
@@ -88,7 +134,15 @@ class App extends Component<AppProps, AppState> {
88134
<Route exact path="/courses" component={Courses}/>
89135
<Route exact path="/profile/:id" component={Workspace}/>
90136
<Route exact path="/create_course" component={CreateCourse}/>
91-
<Route exact path="/courses/:id" component={Course}/>
137+
<Route exact path="/courses/:id"
138+
render={(props) =>
139+
<Course
140+
{...props}
141+
id={props.match.params.id}
142+
onSet={this.updatedLastViewedCourseId}
143+
isFromRedirect={false}
144+
redirectHandler={this.getUserYandexToken}
145+
/>}/>
92146
<Route exact path="/courses/:courseId/edit" component={EditCourse}/>
93147
<Route exact path="/homework/:homeworkId/edit" component={EditHomework}/>
94148
<Route exact path="/task/:taskId/edit" component={EditTask}/>
@@ -104,6 +158,17 @@ class App extends Component<AppProps, AppState> {
104158
path="/register"
105159
render={(props) => <Register {...props} onLogin={this.login}/>}
106160
/>
161+
<Route
162+
exact
163+
path="/yandex"
164+
render={(props) => <Course
165+
{...props}
166+
id={this.getLastCourseId()}
167+
onSet={this.updatedLastViewedCourseId}
168+
isFromRedirect={true}
169+
redirectHandler={this.getUserYandexToken}
170+
/>}
171+
/>
107172
<Route exact path={"*"} component={WrongPath}/>
108173
</Switch>
109174
</>

hwproj.front/src/components/Courses/Course.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ interface ICourseState {
3232

3333
interface ICourseProps {
3434
id: string;
35+
onSet: (courseId: string) => void;
36+
isFromRedirect: boolean;
37+
redirectHandler: () => void;
3538
}
3639

3740
const styles = makeStyles(() => ({
@@ -41,8 +44,8 @@ const styles = makeStyles(() => ({
4144
},
4245
}))
4346

44-
const Course: React.FC<RouteComponentProps<ICourseProps>> = (props) => {
45-
const courseId = props.match.params.id
47+
const Course: React.FC<ICourseProps> = (props) => {
48+
const courseId = props.id
4649
const classes = styles()
4750

4851
const [courseState, setCourseState] = useState<ICourseState>({
@@ -76,7 +79,12 @@ const Course: React.FC<RouteComponentProps<ICourseProps>> = (props) => {
7679
}
7780

7881
useEffect(() => {
79-
setCurrentState()
82+
setCurrentState();
83+
props.onSet(courseId);
84+
if (props.isFromRedirect)
85+
{
86+
props.redirectHandler();
87+
}
8088
}, [])
8189

8290
const joinCourse = async () => {

hwproj.front/src/components/Courses/StudentStats.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {CourseViewModel, HomeworkViewModel, StatisticsCourseMatesModel} from "..
33
import {Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@material-ui/core";
44
import StudentStatsCell from "../Tasks/StudentStatsCell";
55
import {Alert, Grid} from "@mui/material";
6-
import LoadStatsToGoogleDoc from "components/Solutions/LoadStatsToGoogleDoc";
6+
import SaveStats from "components/Solutions/SaveStats";
77

88
interface IStudentStatsProps {
99
course: CourseViewModel;
@@ -97,7 +97,7 @@ class StudentStats extends React.Component<IStudentStatsProps, IStudentStatsStat
9797
</Table>
9898
</TableContainer>
9999
<div style={{marginTop: 15}}>
100-
<LoadStatsToGoogleDoc
100+
<SaveStats
101101
courseId={this.props.course.id}
102102
userId={this.props.userId}
103103
/>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React, { FC, useState } from "react";
2+
import { ResultString } from "../../api";
3+
import { Alert, Box, Button, CircularProgress, Grid, MenuItem, Select, TextField } from "@mui/material";
4+
import apiSingleton from "../../api/ApiSingleton";
5+
6+
interface DownloadStatsProps {
7+
courseId: number | undefined
8+
userId: string
9+
onCancellation: () => void
10+
}
11+
12+
interface DownloadStatsState {
13+
fileName: string,
14+
}
15+
16+
const DownloadStats: FC<DownloadStatsProps> = (props: DownloadStatsProps) => {
17+
const [state, setState] = useState<DownloadStatsState>({
18+
fileName: "",
19+
})
20+
21+
const { fileName } = state
22+
23+
const handleFileDownloading = (promise : Promise<Response>, fileName: string) => {
24+
promise.then((response) => response.blob())
25+
.then((blob) => {
26+
const url = window.URL.createObjectURL(new Blob([blob]));
27+
const link = document.createElement('a');
28+
link.href = url;
29+
link.setAttribute('download', `${fileName}.xlsx`);
30+
document.body.appendChild(link);
31+
link.click();
32+
link.parentNode!.removeChild(link);
33+
})
34+
}
35+
36+
return <Grid container spacing={1} style={{ marginTop: 15 }}>
37+
<Grid container item spacing={1} alignItems={"center"}>
38+
<Grid item xs={5}>
39+
<TextField size={"small"} fullWidth label={"Название файла"} value={fileName}
40+
onChange={event => {
41+
event.persist();
42+
setState({fileName: event.target.value});
43+
}} />
44+
</Grid>
45+
<Grid item>
46+
<Box sx={{ m: 1, position: 'relative' }}>
47+
<Button variant="text" color="primary" type="button"
48+
onClick={() => {
49+
const fileData = apiSingleton.statisticsApi.apiStatisticsGetFileGet(
50+
props.courseId, props.userId, "Лист 1"
51+
);
52+
handleFileDownloading(fileData, fileName);
53+
}}>
54+
Загрузить
55+
</Button>
56+
</Box>
57+
</Grid>
58+
<Grid item>
59+
<Button variant="text" color="primary" type="button"
60+
onClick={props.onCancellation}>
61+
Отмена
62+
</Button>
63+
</Grid>
64+
</Grid>
65+
</Grid>
66+
}
67+
export default DownloadStats;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import React, {FC, FunctionComponent, FunctionComponentElement, useState} from "react";
2+
import { ResultString } from "../../api";
3+
import { ResultExternalService } from "../../api";
4+
import { Alert, Box, Button, CircularProgress, Grid, MenuItem, Select, TextField } from "@mui/material";
5+
import apiSingleton from "../../api/ApiSingleton";
6+
import ExportToGoogle from "components/Solutions/ExportToGoogle";
7+
import ExportToYandex from "components/Solutions/ExportToYandex";
8+
9+
interface ExportStatsProps {
10+
courseId: number | undefined
11+
userId: string
12+
onCancellation: () => void
13+
}
14+
15+
interface ExportStatsState {
16+
url: string,
17+
googleSheetTitles: ResultString | undefined,
18+
selectedSheet: number
19+
selectedService: ResultExternalService.ValueEnum | undefined
20+
}
21+
22+
const ExportStats: FC<ExportStatsProps> = (props: ExportStatsProps) => {
23+
const [state, setState] = useState<ExportStatsState>({
24+
selectedSheet: 0,
25+
url: "",
26+
googleSheetTitles: undefined,
27+
selectedService: undefined
28+
})
29+
30+
const { url, googleSheetTitles, selectedSheet, selectedService } = state
31+
32+
const servicesComponents = new Map<ResultExternalService.ValueEnum, FunctionComponentElement<any>>([
33+
[ResultExternalService.ValueEnum.NUMBER_0,
34+
<ExportToGoogle
35+
courseId={props.courseId}
36+
userId={props.userId}
37+
url={url}
38+
onUrlChange={(value: string) => handleUrlChange(value)}
39+
onCancellation={() => props.onCancellation()}
40+
/>],
41+
[ResultExternalService.ValueEnum.NUMBER_1,
42+
<ExportToYandex
43+
courseId={props.courseId}
44+
userId={props.userId}
45+
url={url}
46+
onUrlChange={(value: string) => handleUrlChange(value)}
47+
onCancellation={() => props.onCancellation()}
48+
/>],
49+
]);
50+
51+
const handleUrlChange = async (value: string) => {
52+
const service = await apiSingleton.statisticsApi.apiStatisticsProcessLinkPost(value);
53+
setState(prevState => ({ ...prevState, url: value, selectedService: service.succeeded ? service.value : undefined }));
54+
}
55+
56+
const getSelectedServiceComponent = () => {
57+
if (selectedService === undefined || !servicesComponents.has(selectedService))
58+
{
59+
return (
60+
<Grid container spacing={1} style={{ marginTop: 15 }}>
61+
<Grid item>
62+
{(url &&
63+
<Alert severity="error">
64+
Сервис не распознан. Поддерживаемые сервисы: Google Docs, Yandex
65+
</Alert>)
66+
||
67+
<Alert severity="info" variant={"standard"}>
68+
Поддерживаемые сервисы: Google Docs, Яндекс Диск
69+
</Alert>}
70+
</Grid>
71+
<Grid container item spacing={1} xs={12} alignItems={"center"}>
72+
<Grid item xs={5}>
73+
<TextField fullWidth size={"small"} label={"Ссылка на сервис"} value={url}
74+
onChange={event => {
75+
const newUrl = event.target.value
76+
handleUrlChange(newUrl)
77+
}} />
78+
</Grid>
79+
<Grid item>
80+
<Button variant="text" color="primary" type="button"
81+
onClick={props.onCancellation}>
82+
Отмена
83+
</Button>
84+
</Grid>
85+
</Grid>
86+
</Grid>
87+
)
88+
}
89+
90+
return servicesComponents.get(selectedService);
91+
}
92+
93+
return (
94+
<div>
95+
{getSelectedServiceComponent()}
96+
</div>
97+
)
98+
}
99+
export default ExportStats;

0 commit comments

Comments
 (0)