Skip to content

Commit

Permalink
Improving visualization of plagiarism similarities over multiple files.
Browse files Browse the repository at this point in the history
  • Loading branch information
krulis-martin committed Dec 31, 2023
1 parent 0fa1927 commit fe3bbb0
Show file tree
Hide file tree
Showing 12 changed files with 439 additions and 253 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
.activeRow {
background-color: #eee;
}
.activeRow {
background-color: #eee;
}
371 changes: 214 additions & 157 deletions src/components/Solutions/PlagiarismCodeBox/PlagiarismCodeBox.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';

import DateTime from '../../widgets/DateTime';
import GroupsNameContainer from '../../../containers/GroupsNameContainer';
import { CodeFileIcon } from '../../icons';

const FileSelectionTableRow = ({ file, idx, selected = false, selectFile }) => (
<tr
className={selected ? 'table-primary' : 'clickable'}
onClick={!selected && selectFile ? () => selectFile(idx) : null}>
<td className="text-nowrap shrink-col">
<CodeFileIcon className="text-muted" gapLeft gapRight />
</td>
<td>
<code>
{file.solutionFile.name}
{file.fileEntry ? `/${file.fileEntry}` : ''}
</code>
</td>
<td>
{file.fragments && (
<FormattedMessage
id="app.solutionPlagiarisms.selectPlagiarismFileModal.fragments"
defaultMessage="({fragments} {fragments, plural, one {fragment} other {fragments}})"
values={{ fragments: file.fragments.length }}
/>
)}
</td>
<td>
<FormattedMessage id="app.solutionPlagiarisms.selectPlagiarismFileModal.fromSolution" defaultMessage="solution" />{' '}
<strong>#{file.solution.attemptIndex}</strong>
</td>
<td>
(<DateTime unixts={file.solution.createdAt} />)
</td>
<td className="small">
<GroupsNameContainer groupId={file.groupId} admins />
</td>
</tr>
);

FileSelectionTableRow.propTypes = {
file: PropTypes.object.isRequired,
idx: PropTypes.number.isRequired,
selected: PropTypes.bool,
selectFile: PropTypes.func,
};

export default FileSelectionTableRow;
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import { defaultMemoize } from 'reselect';

import PlagiarismCodeBox from '../PlagiarismCodeBox';
import SourceCodeBox from '../SourceCodeBox';
import DateTime from '../../widgets/DateTime';
import Button from '../../widgets/TheButton';
import Button, { TheButtonGroup } from '../../widgets/TheButton';
import Box from '../../widgets/Box';
import InsetPanel from '../../widgets/InsetPanel';
import ResourceRenderer from '../../helpers/ResourceRenderer';
import GroupsNameContainer from '../../../containers/GroupsNameContainer';
import { CloseIcon, CodeFileIcon, LoadingIcon } from '../../icons';
import Icon, { CodeCompareIcon, CloseIcon, LoadingIcon } from '../../icons';
import FileSelectionTableRow from './FileSelectionTableRow';

/**
* Construct an object { fileIdx => fragmentsArray } where the fragments array holds only
Expand All @@ -28,7 +28,7 @@ const getRemainingFragments = defaultMemoize((files, selected) => {
});

class PlagiarismCodeBoxWithSelector extends Component {
state = { selectedFile: 0, dialogOpen: false };
state = { selectedFile: 0, dialogOpen: false, switchTo: null };

componentDidUpdate(prevProps) {
if (
Expand All @@ -39,17 +39,36 @@ class PlagiarismCodeBoxWithSelector extends Component {
}
}

openDialog = ev => {
this.setState({ dialogOpen: true });
ev.stopPropagation();
openDialog = (ev, switchToRaw = null) => {
// restrict to which files a switch is possible
let switchTo =
switchToRaw &&
switchToRaw.filter(id => id !== this.state.selectedFile && id < this.props.selectedPlagiarismSource.files.length);
if (switchTo && switchTo.length === 0) {
switchTo = null;
}

this.setState({ dialogOpen: true, switchTo });

if (ev) {
ev.stopPropagation();
}
};

closeDialog = () => this.setState({ dialogOpen: false });

selectFile = selectedFile => this.setState({ selectedFile, dialogOpen: false });

render() {
const { file, solutionId, selectedPlagiarismSource = null, download, fileContentsSelector } = this.props;
const {
file,
solutionId,
selectedPlagiarismSource = null,
download,
fileContentsSelector,
authorId = null,
sourceAuthorId = null,
} = this.props;
const sourceContents =
selectedPlagiarismSource &&
selectedPlagiarismSource.files.map(file => file && fileContentsSelector(file.solutionFile.id, file.fileEntry));
Expand All @@ -75,65 +94,97 @@ class PlagiarismCodeBoxWithSelector extends Component {
<>
<PlagiarismCodeBox
{...file}
authorId={authorId}
sourceAuthorId={sourceAuthorId}
solutionId={solutionId}
download={download}
fileContentsSelector={fileContentsSelector}
selectedPlagiarismFile={selectedPlagiarismSource.files[this.state.selectedFile]}
selectPlagiarismFile={selectedPlagiarismSource.files.length > 1 ? this.selectFile : null}
openSelectFileDialog={selectedPlagiarismSource.files.length > 1 ? this.openDialog : null}
sourceFilesCount={selectedPlagiarismSource.files.length}
similarity={selectedPlagiarismSource.similarity}
remainingFragments={getRemainingFragments(selectedPlagiarismSource.files, this.state.selectedFile)}
/>

{selectedPlagiarismSource.files.length > 1 && (
<Modal show={this.state.dialogOpen} backdrop="static" onHide={this.closeDialog} size="xl">
<Modal.Header closeButton>
<Modal.Title>
<FormattedMessage
id="app.solutionPlagiarisms.selectPlagiarismFileModal.title"
defaultMessage="Select one of the possible source files to be compared"
/>
{this.state.selectedFile !== null && this.state.switchTo ? (
<FormattedMessage
id="app.solutionPlagiarisms.selectPlagiarismFileModal.titleSwitch"
defaultMessage="Change the displayed source file (on the right)"
/>
) : (
<FormattedMessage
id="app.solutionPlagiarisms.selectPlagiarismFileModal.title"
defaultMessage="Select one of the possible source files to be compared (on the right)"
/>
)}
</Modal.Title>
</Modal.Header>
<Modal.Body className="p-0">
{this.state.selectedFile !== null && this.state.switchTo && (
<InsetPanel className="m-1">
<FormattedMessage
id="app.solutionPlagiarisms.selectPlagiarismFileModal.switchToExplain"
defaultMessage="The area you wish to visualize is not covered by the selected source file. You may switch to another file that covers it."
/>
</InsetPanel>
)}

<Table hover className="m-0">
<tbody>
{selectedPlagiarismSource.files.map((file, idx) => (
<tr
key={file.id}
className={this.state.selectedFile === idx ? 'table-primary' : 'clickable'}
onClick={this.state.selectedFile !== idx ? () => this.selectFile(idx) : null}>
<td className="text-nowrap shrink-col">
<CodeFileIcon className="text-muted" gapLeft gapRight />
</td>
<td>
<code>
{file.solutionFile.name}
{file.fileEntry ? `/${file.fileEntry}` : ''}
</code>
</td>
<td>
<FormattedMessage
id="app.solutionPlagiarisms.selectPlagiarismFileModal.fromSolution"
defaultMessage="solution"
/>{' '}
<strong>#{file.solution.attemptIndex}</strong>
</td>
<td>
(<DateTime unixts={file.solution.createdAt} />)
</td>
<td className="small">
<GroupsNameContainer groupId={file.groupId} admins />
{this.state.selectedFile !== null && this.state.switchTo ? (
<tbody>
<FileSelectionTableRow
file={selectedPlagiarismSource.files[this.state.selectedFile]}
idx={this.state.selectedFile}
selected
/>
<tr>
<td colSpan={7} className="text-center larger p-3 text-success">
<Icon icon={['far', 'circle-down']} />
</td>
</tr>
))}
</tbody>
{this.state.switchTo.map(idx => (
<FileSelectionTableRow
key={idx}
file={selectedPlagiarismSource.files[idx]}
idx={idx}
selected={this.state.selectedFile === idx}
selectFile={this.selectFile}
/>
))}
</tbody>
) : (
<tbody>
{selectedPlagiarismSource.files.map((file, idx) => (
<FileSelectionTableRow
key={idx}
file={file}
idx={idx}
selected={this.state.selectedFile === idx}
selectFile={this.selectFile}
/>
))}
</tbody>
)}
</Table>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={this.closeDialog}>
<CloseIcon gapRight />
<FormattedMessage id="generic.close" defaultMessage="Close" />
</Button>
<TheButtonGroup>
{this.state.selectedFile !== null && this.state.switchTo && this.state.switchTo.length === 1 && (
<Button variant="success" onClick={() => this.selectFile(this.state.switchTo[0])}>
<CodeCompareIcon gapRight />
<FormattedMessage id="generic.change" defaultMessage="Change" />
</Button>
)}
<Button variant="secondary" onClick={this.closeDialog}>
<CloseIcon gapRight />
<FormattedMessage id="generic.close" defaultMessage="Close" />
</Button>
</TheButtonGroup>
</Modal.Footer>
</Modal>
)}
Expand Down Expand Up @@ -169,6 +220,8 @@ PlagiarismCodeBoxWithSelector.propTypes = {
download: PropTypes.func,
fileContentsSelector: PropTypes.func,
selectedPlagiarismSource: PropTypes.object.isRequired,
authorId: PropTypes.string,
sourceAuthorId: PropTypes.string,
};

export default PlagiarismCodeBoxWithSelector;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';

import { identity } from '../../../helpers/common';
import styles from './CodeFragmentSelector.less';

// Process fragments and generate a list of start+end markers sorted by offset.
Expand All @@ -26,6 +25,7 @@ const _fragment = (content, startOffset, endOffset, refs, selectedFragment) => {

// click and double click data passed to the respective handlers
// double click works only if there are no signle click actions (as secondary fragments)
const identity = x => x !== undefined && x !== null;
const clickData = refsArr.map(idx => refs[idx].clickData).filter(identity);
const doubleClickData = refsArr.map(idx => refs[idx].doubleClickData).filter(identity);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,11 @@ pre.codeFragments {
}

span.secondary.overlap {
background-color: #ede0fe;
background-color: #fe7;
background-color: #e6d4fd;
}

span.secondary.overlapMore {
background-color: #e6d4fd;
background-color: #fd4;
background-color: #dac0fb;
}

span.selected, span.secondary.selected {
Expand Down
6 changes: 3 additions & 3 deletions src/components/helpers/LocalizedTexts/LocalizedTexts.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
.tab-pane img {
max-width: 100%;
}
.tab-pane img {
max-width: 100%;
}
4 changes: 3 additions & 1 deletion src/components/widgets/Box/Box.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class Box extends Component {
const {
id = null,
title,
flexTitle = false,
description = null,
type = 'light',
solid = false,
Expand All @@ -80,7 +81,7 @@ class Box extends Component {
[className]: className.length > 0,
})}>
<Card.Header onClick={this.toggleDetails}>
<Card.Title>
<Card.Title className={flexTitle ? 'd-flex justify-content-between float-none' : null}>
{title}

<span className="whenTargetted text-warning">
Expand Down Expand Up @@ -129,6 +130,7 @@ class Box extends Component {
Box.propTypes = {
id: PropTypes.string,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
flexTitle: PropTypes.bool,
description: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
type: PropTypes.string,
isOpen: PropTypes.bool,
Expand Down
12 changes: 10 additions & 2 deletions src/locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1751,9 +1751,13 @@
"app.solutionFiles.title": "Odevzdané soubory",
"app.solutionFiles.total": "Celkem:",
"app.solutionPlagiarisms.changeSelectedSource": "změnit vybraný zdroj",
"app.solutionPlagiarisms.displayedOnRight": "zobrazeno napravo",
"app.solutionPlagiarisms.noMatchesForFile": "nebyly nalezeny podobnosti s tímto souborem",
"app.solutionPlagiarisms.selectPlagiarismFileModal.fragments": "({fragments} {fragments, plural, one {fragment} =2 {fragmenty} =3 {fragmenty} =4 {fragmenty} other {fragmentů}})",
"app.solutionPlagiarisms.selectPlagiarismFileModal.fromSolution": "řešení",
"app.solutionPlagiarisms.selectPlagiarismFileModal.title": "Vyberte jeden z možných zdrojových souborů k porovnání",
"app.solutionPlagiarisms.selectPlagiarismFileModal.switchToExplain": "Oblast, kterou si přejete vybrat, není pokrytá aktuálně vybraným souborem. Nyní můžete změnit zobrazení na zdrojový soubor, který oblast pokrývá.",
"app.solutionPlagiarisms.selectPlagiarismFileModal.title": "Vyberte jeden z možných zdrojových souborů k porovnání (napravo)",
"app.solutionPlagiarisms.selectPlagiarismFileModal.titleSwitch": "Změnit zobrazovaný zdrojový soubor (napravo)",
"app.solutionPlagiarisms.selectSourceTable.avg": "průměr",
"app.solutionPlagiarisms.selectSourceTable.files": "{count} {count, plural, one {soubor} =2 {soubory} =3 {soubory} =4 {soubory} other {souborů}}",
"app.solutionPlagiarisms.selectSourceTable.max": "maximum",
Expand All @@ -1766,6 +1770,7 @@
"app.solutionReviewIcon.tooltip.issues": "Recenzent vám zanechal {issues} {issues, plural, one {připomínku} =2 {připomínky} =3 {připomínky} =4 {připomínky} other {připomínek}} k vyřešení. Prosíme, opravte {issues, plural, one {ji} other {je}} v následujícím odevzdaném řešení.",
"app.solutionReviewIcon.tooltip.noIssues": "Nebyly vytvořeny žádné připomínky k vyřešení.",
"app.solutionReviewIcon.tooltip.startedAt": "Revize byla zahájena v {started} a zatím nebyla dokončena.",
"app.solutionSourceCodes.adjustMappingFiles": "{count} {count, plural, one {podobný soubor} =2 {podobné soubory} =3 {podobné soubory} =4 {podobné soubory} other {podobných souborů}}",
"app.solutionSourceCodes.adjustMappingTooltip": "Změnit, který soubor z druhého řešení bude porovnán s tímto souborem.",
"app.solutionSourceCodes.cancelDiffButton": "Vypnout srovnávací režim",
"app.solutionSourceCodes.codeReviewsAbout": "Zde můžete provést revizi odevzdaných zdrojových souborů a komentovat jednotlivé řádky kódu. Jakmile zahájite revizi, můžete začít přidávat komentáře dvojklikem na požadované řádky kódu. Autor řešení uvidí komentáře, až když revizi uzavřete. Komentáře nejsou viditelné ve srovávacím režimu.",
Expand All @@ -1775,6 +1780,7 @@
"app.solutionSourceCodes.diffModal.tabRecentSolutions": "Nedávno navštívené",
"app.solutionSourceCodes.diffModal.tabUserSolutions": "Uživatelská řešení",
"app.solutionSourceCodes.diffModal.title": "Porovnat dvě řešení a zobrazit rozdíly",
"app.solutionSourceCodes.fullWidthTooltip": "Povolit celou šířku slouců, i když celková šíře boxu překročí šířku obrazovky.",
"app.solutionSourceCodes.isBeingComparedWith": "... je porovnáváno s ...",
"app.solutionSourceCodes.left": "Nalevo",
"app.solutionSourceCodes.malformedTooltip": "Tento soubor neobsahuje běžný text v kódování UTF-8, takže není možne jej zobrazit jako zdrojový kód.",
Expand All @@ -1783,6 +1789,7 @@
"app.solutionSourceCodes.mappingModal.resetButton": "Výchozí mapování",
"app.solutionSourceCodes.mappingModal.title": "Upravit mapování porovnávaných souborů",
"app.solutionSourceCodes.noDiffWithFile": "žádný odpovídající soubor pro porovnání nebyl nalezen",
"app.solutionSourceCodes.restrictWidthTooltip": "Omezit šířky sloupců, aby každý zabíral polovinu obrazovky.",
"app.solutionSourceCodes.reviewClosedAuthorInfoNoIssues": "Vaše řešení bylo revidováno a nejsou k němu vedeny žádné připomínky.",
"app.solutionSourceCodes.reviewClosedAuthorInfoWithIssues": "Vaše řešení bylo revidováno. Máte celkem {issues} {issues, plural, one {připomínku} =2 {připomínky} =3 {připomínky} =4 {připomínky} other {připomínek}} k vyřešení.",
"app.solutionSourceCodes.reviewClosedInfo": "Autor řešení nyní vidí komentáře revize. Komentáře můžete stále upravovat, ale každá změna bude oznámena autorovi formou emailové notifikace. Pokud si přejete udělat zásadní změny, znovu otevřete revizi, proveďte je, a opět revizi zavřete.",
Expand Down Expand Up @@ -1968,6 +1975,7 @@
"generic.attempt": "Pokus",
"generic.author": "Autor",
"generic.cancel": "Zrušit",
"generic.change": "Změnit",
"generic.clearAll": "Zrušit vše",
"generic.close": "Zavřít",
"generic.copyToClipboard": "Zkopírovat do schránky",
Expand Down Expand Up @@ -2050,4 +2058,4 @@
"recodex-judge-shuffle-all": "Sudí neuspořádaných tokenů a řádků",
"recodex-judge-shuffle-newline": "Sudí neuspořádaných tokenů (ignorující konce řádků)",
"recodex-judge-shuffle-rows": "Sudí neuspořádaných řádků"
}
}

0 comments on commit fe3bbb0

Please sign in to comment.