Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: show student score in first position #2314

Merged
merged 11 commits into from
Nov 29, 2023
4 changes: 2 additions & 2 deletions client/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ module.exports = {
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:jest-dom/recommended',
'plugin:testing-library/dom',
'plugin:jest/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
Expand Down Expand Up @@ -38,7 +37,8 @@ module.exports = {
{
// Enable eslint-plugin-testing-library rules or preset only for matching testing files
files: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'],
extends: ['plugin:testing-library/react'],
excludedFiles: 'specs/**/*',
extends: ['plugin:testing-library/react', 'plugin:testing-library/dom'],
},
],
settings: {
Expand Down
2 changes: 1 addition & 1 deletion client/specs/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const url = process.env.BASE_URL || 'http://localhost:3000';
test.beforeEach(async ({ page }) => {
await page.context().clearCookies();
await page.goto(url);
await page.locator('.ant-btn-primary').click();
await page.getByRole('button', { name: /sign up with gitHub/i }).click();
await page.waitForSelector('.profile');
});

Expand Down
70 changes: 70 additions & 0 deletions client/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15248,6 +15248,43 @@ export const StudentsScoreApiAxiosParamCreator = function (configuration?: Confi



setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};

return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {number} courseId
* @param {string} githubId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getStudentScore: async (courseId: number, githubId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'courseId' is not null or undefined
assertParamExists('getStudentScore', 'courseId', courseId)
// verify required parameter 'githubId' is not null or undefined
assertParamExists('getStudentScore', 'githubId', githubId)
const localVarPath = `/course/{courseId}/students/score/{githubId}`
.replace(`{${"courseId"}}`, encodeURIComponent(String(courseId)))
.replace(`{${"githubId"}}`, encodeURIComponent(String(githubId)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}

const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;



setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
Expand Down Expand Up @@ -15286,6 +15323,17 @@ export const StudentsScoreApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getScore(activeOnly, orderBy, orderDirection, current, pageSize, courseId, githubId, name, mentorGithubId, cityName, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {number} courseId
* @param {string} githubId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getStudentScore(courseId: number, githubId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ScoreStudentDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getStudentScore(courseId, githubId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};

Expand Down Expand Up @@ -15314,6 +15362,16 @@ export const StudentsScoreApiFactory = function (configuration?: Configuration,
getScore(activeOnly: string, orderBy: 'rank' | 'totalScore' | 'crossCheckScore' | 'githubId' | 'name' | 'cityName' | 'mentor' | 'totalScoreChangeDate' | 'repositoryLastActivityDate', orderDirection: 'asc' | 'null' | 'desc', current: string, pageSize: string, courseId: number, githubId?: string, name?: string, mentorGithubId?: string, cityName?: string, options?: any): AxiosPromise<ScoreDto> {
return localVarFp.getScore(activeOnly, orderBy, orderDirection, current, pageSize, courseId, githubId, name, mentorGithubId, cityName, options).then((request) => request(axios, basePath));
},
/**
*
* @param {number} courseId
* @param {string} githubId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getStudentScore(courseId: number, githubId: string, options?: any): AxiosPromise<ScoreStudentDto> {
return localVarFp.getStudentScore(courseId, githubId, options).then((request) => request(axios, basePath));
},
};
};

Expand Down Expand Up @@ -15343,6 +15401,18 @@ export class StudentsScoreApi extends BaseAPI {
public getScore(activeOnly: string, orderBy: 'rank' | 'totalScore' | 'crossCheckScore' | 'githubId' | 'name' | 'cityName' | 'mentor' | 'totalScoreChangeDate' | 'repositoryLastActivityDate', orderDirection: 'asc' | 'null' | 'desc', current: string, pageSize: string, courseId: number, githubId?: string, name?: string, mentorGithubId?: string, cityName?: string, options?: AxiosRequestConfig) {
return StudentsScoreApiFp(this.configuration).getScore(activeOnly, orderBy, orderDirection, current, pageSize, courseId, githubId, name, mentorGithubId, cityName, options).then((request) => request(this.axios, this.basePath));
}

/**
*
* @param {number} courseId
* @param {string} githubId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof StudentsScoreApi
*/
public getStudentScore(courseId: number, githubId: string, options?: AxiosRequestConfig) {
return StudentsScoreApiFp(this.configuration).getStudentScore(courseId, githubId, options).then((request) => request(this.axios, this.basePath));
}
}


Expand Down
26 changes: 26 additions & 0 deletions client/src/modules/Score/components/ScoreTable/Summary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Table } from 'antd';
import { ColumnType } from 'antd/lib/table';
import { ScoreStudentDto } from 'api';
import get from 'lodash/get';

type SummaryProps = {
visibleColumns: ColumnType<ScoreStudentDto>[];
studentScore: ScoreStudentDto;
};

export const Summary = ({ visibleColumns, studentScore }: SummaryProps) => {
return (
<Table.Summary.Row>
{/* the table has a hidden first column */}
<Table.Summary.Cell index={0} />
{visibleColumns.map(({ dataIndex, render }, index) => {
const value = get(studentScore, dataIndex as string | string[], null);
return (
<Table.Summary.Cell key={index} index={index + 1}>
{render ? render(value, studentScore, index + 1) : value}
</Table.Summary.Cell>
);
})}
</Table.Summary.Row>
);
};
44 changes: 32 additions & 12 deletions client/src/modules/Score/components/ScoreTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { CoursePageProps } from 'services/models';
import { IPaginationInfo } from 'common/types/pagination';
import { ScoreOrder, ScoreTableFilters } from 'modules/Score/hooks/types';
import useWindowDimensions from 'utils/useWindowDimensions';
import { Summary } from './Summary';

type Props = CoursePageProps & {
onLoading: (value: boolean) => void;
Expand Down Expand Up @@ -55,6 +56,8 @@ export function ScoreTable(props: Props) {
filter: { activeOnly: true },
order: { field: 'rank', order: 'ascend' },
});
const [studentScore, setStudentScore] = useState<ScoreStudentDto | null>(null);

const recentlyAppliedFilters = useRef<null | Record<string, FilterValue | null>>(null);

const [notVisibleColumns = [], setNotVisibleColumns] = useLocalStorage<string[]>('notVisibleColumns');
Expand All @@ -66,13 +69,8 @@ export function ScoreTable(props: Props) {
filters: ScoreTableFiltersModified,
order: TableScoreOrder,
) => {
try {
props.onLoading(true);
const data = await getPagedData(pagination as IPaginationInfo, filters as ScoreTableFilters, order as ScoreOrder);
setStudents({ ...students, content: data.content, pagination: data.pagination });
} finally {
props.onLoading(false);
}
const data = await getPagedData(pagination as IPaginationInfo, filters as ScoreTableFilters, order as ScoreOrder);
setStudents({ ...students, content: data.content, pagination: data.pagination });
};

const loadInitialData = useCallback(async () => {
Expand All @@ -94,8 +92,9 @@ export function ScoreTable(props: Props) {
filters = { ...filters, name } as ScoreTableFilters;
}

const [courseScore, courseTasks] = await Promise.all([
const [courseScore, studentCourseScore, courseTasks] = await Promise.all([
courseService.getCourseScore(students.pagination, filters, students.order),
courseService.getStudentCourseScore(props.session?.githubId as string),
courseTasksApi.getCourseTasks(props.course.id),
]);
const sortedTasks = courseTasks.data
Expand All @@ -105,6 +104,7 @@ export function ScoreTable(props: Props) {
isVisible: !notVisibleColumns.includes(String(task.id)),
}));
setStudents({ ...students, content: courseScore.content, pagination: courseScore.pagination });
setStudentScore(studentCourseScore);
setCourseTasks(sortedTasks);
setColumns(
getColumns({
Expand Down Expand Up @@ -161,7 +161,7 @@ export function ScoreTable(props: Props) {
return null;
}

const handleChange: TableProps<ScoreStudentDto>['onChange'] = (pagination, filters, sorter, { action }) => {
const handleChange: TableProps<ScoreStudentDto>['onChange'] = async (pagination, filters, sorter, { action }) => {
// Dirty hack to prevent sort request with old filters on Enter key in filter modal search input
// This is known issue please, see https://github.com/ant-design/ant-design/issues/37334
// TODO: Remove this hack after fix in antd
Expand All @@ -172,21 +172,41 @@ export function ScoreTable(props: Props) {
if (action === 'sort' && recentlyAppliedFilters.current) {
filters = recentlyAppliedFilters.current;
}
getCourseScore(pagination, filters, sorter);

try {
props.onLoading(true);
const [studentCourseScore] = await Promise.all([
courseService.getStudentCourseScore(props.session?.githubId as string),
getCourseScore(pagination, filters, sorter),
]);
setStudentScore(studentCourseScore);
} finally {
props.onLoading(false);
}
};

const visibleColumns = getVisibleColumns(columns);
const isSummaryShown = students.content.length > 0 && studentScore;

return (
<>
<Table<ScoreStudentDto>
className="table-score"
showHeader
scroll={{ x: getTableWidth(getVisibleColumns(columns).length), y: 'calc(95vh - 320px)' }}
scroll={{ x: getTableWidth(visibleColumns.length), y: 'calc(95vh - 320px)' }}
pagination={{ ...students.pagination, showTotal: total => `Total ${total} students` }}
rowKey="githubId"
rowClassName={record => (!record.isActive ? 'rs-table-row-disabled' : '')}
dataSource={students.content}
summary={() =>
isSummaryShown && (
<Table.Summary fixed="top">
<Summary studentScore={studentScore} visibleColumns={visibleColumns} />
</Table.Summary>
)
}
onChange={handleChange}
columns={getVisibleColumns(columns)}
columns={visibleColumns}
rowSelection={{
selectedRowKeys: state,
onChange: (_, selectedRows) => {
Expand Down
10 changes: 5 additions & 5 deletions client/src/modules/Score/pages/ScorePage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Row, Switch, Typography } from 'antd';
import { Row, Space, Switch, Typography } from 'antd';
import { CourseNoAccess } from 'modules/Course/components/CourseNoAccess';
import { CoursePageLayout } from 'components/CoursePageLayout';
import { ExportCsvButton } from 'modules/Score/components/ExportCsvButton';
Expand Down Expand Up @@ -34,11 +34,11 @@ export function ScorePage() {
<CourseNoAccess />
) : (
<CoursePageLayout showCourseName course={course} title="Score" githubId={session.githubId} loading={loading}>
<Row style={{ margin: '8px 0' }} justify="space-between">
<div>
<span style={{ display: 'inline-block', lineHeight: '24px' }}>Active Students Only</span>{' '}
<Row style={{ margin: '8px 0', gap: 8 }} justify="space-between">
<Space>
<Text>Active Students Only</Text>
<Switch checked={activeOnly} onChange={handleActiveOnlyChange} />
</div>
</Space>
<Text mark>Total score and position is updated every day at 04:00 GMT+3</Text>
<ExportCsvButton enabled={csvEnabled} onClick={handleExportCsv} />
</Row>
Expand Down
5 changes: 5 additions & 0 deletions client/src/services/course.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,11 @@ export class CourseService {
return result.data;
}

async getStudentCourseScore(githubId: string) {
const result = await studentsScoreApi.getStudentScore(this.courseId, githubId);
return result.data;
}

async postStudentScore(githubId: string, courseTaskId: number, data: PostScore) {
await this.axios.post(`/student/${githubId}/task/${courseTaskId}/result`, data);
}
Expand Down
11 changes: 10 additions & 1 deletion nestjs/src/courses/score/score.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { DEFAULT_CACHE_TTL } from 'src/constants';

import { ScoreQueryDto, OrderDirection, OrderField } from './dto/score-query.dto';
import { ScoreService } from './score.service';
import { ScoreDto } from './dto/score.dto';
import { ScoreDto, ScoreStudentDto } from './dto/score.dto';

@Controller('course/:courseId/students/score')
@ApiTags('students score')
Expand Down Expand Up @@ -36,4 +36,13 @@ export class ScoreController {

return score;
}

@Get('/:githubId')
@UseGuards(DefaultGuard, CourseGuard)
@ApiOperation({ operationId: 'getStudentScore' })
@ApiOkResponse({ type: ScoreStudentDto })
public async getStudentScore(@Param('courseId', ParseIntPipe) courseId: number, @Param('githubId') githubId: string) {
const studentScore = await this.scoreService.getStudentScore({ githubId, courseId });
return studentScore;
}
}
Loading
Loading