diff --git a/src/locales/cs.json b/src/locales/cs.json
index 26f6e87b8..c5bff10ab 100644
--- a/src/locales/cs.json
+++ b/src/locales/cs.json
@@ -161,6 +161,7 @@
"app.assignmentStats.noSolutions": "Momentálně zde nejsou žádná odevzdaná řešení.",
"app.assignmentStats.onlyBestSolutionsCheckbox": "Pouze nejlepší řešení",
"app.assignmentStats.pendingReviews": "V tuto chvíli {count, plural, one {je otevřena} =2 {jsou otevřeny} =3 {jsou otevřeny} =4 {jsou otevřeny} other {je otevřeno}} {count} {count, plural, one {revize} =2 {revize} =3 {revize} =4 {revize} other {revizí}} ze všech řešení vybrané úlohy. Nezapomeňte, že autoři úloh vidí vaše komentáře zdrojových kódů až poté, co jsou příslušné revize uzavřeny.",
+ "app.assignmentStats.plagiarismsDetected": "V seznamu {count, plural, one {je zobrazeno} =2 {jsou zobrazena} =3 {jsou zobrazena} =4 {jsou zobrazena} other {je zobrazeno}} {count} řešení, {count, plural, one {ke kterému} other {ke kterým}} byla nelezena podobná řešení. V takových případech se může jednat o plagiáty.",
"app.assignmentStats.title": "Všechna řešení zadané úlohy",
"app.assignments.deadline": "Termín odevzdání",
"app.assignments.deleteAllButton": "Smazat",
@@ -1664,6 +1665,7 @@
"app.solution.submittedAt": "Odevzdáno",
"app.solution.submittedBeforeFirstDeadline": "Řešení bylo odevzdáno včas před termínem",
"app.solution.submittedBeforeSecondDeadline": "Řešení bylo odevzdáno po prvním termínu, ale stále ještě před druhým termínem",
+ "app.solution.suspectedPlagiarismWarning": "Byla detekována podobná řešení, toto řešení by mohlo být plagiátem. Detialy naleznete na stránce 'Podobnosti'.",
"app.solution.title": "Řešení",
"app.solutionArchiveInfoBox.description": "Všechny soubory v ZIPu",
"app.solutionDetail.comments.additionalSwitchNote": "(autor tohoto řešení a vedoucí této skupiny)",
diff --git a/src/locales/en.json b/src/locales/en.json
index 8fd7194d8..97ff0a6fb 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -161,6 +161,7 @@
"app.assignmentStats.noSolutions": "There are currently no submitted solutions.",
"app.assignmentStats.onlyBestSolutionsCheckbox": "Best solutions only",
"app.assignmentStats.pendingReviews": "There {count, plural, one {is} other {are}} {count} pending {count, plural, one {review} other {reviews}} among the solutions of the selected assignment. Remember that the review comments are visible to the author after a review is closed.",
+ "app.assignmentStats.plagiarismsDetected": "There {count, plural, one {is} other {are}} {count} {count, plural, one {solution} other {solutions}} with detected similarities. Such solutions may be plagiarisms.",
"app.assignmentStats.title": "All Submissions of The Assignment",
"app.assignments.deadline": "Deadline",
"app.assignments.deleteAllButton": "Delete",
@@ -1664,6 +1665,7 @@
"app.solution.submittedAt": "Submitted at",
"app.solution.submittedBeforeFirstDeadline": "The solution was submitted before the deadline",
"app.solution.submittedBeforeSecondDeadline": "The solution was submitted after the first but still before the second deadline",
+ "app.solution.suspectedPlagiarismWarning": "Similar solutions have been detected, the solution is suspected of being a plagiarism. Details can be found on 'Similarities' page.",
"app.solution.title": "The Solution",
"app.solutionArchiveInfoBox.description": "All files in a ZIP archive",
"app.solutionDetail.comments.additionalSwitchNote": "(author of the solution and supervisors of this group)",
diff --git a/src/pages/AssignmentStats/AssignmentStats.js b/src/pages/AssignmentStats/AssignmentStats.js
index df4a7a03e..43d768c53 100644
--- a/src/pages/AssignmentStats/AssignmentStats.js
+++ b/src/pages/AssignmentStats/AssignmentStats.js
@@ -21,6 +21,7 @@ import Icon, {
DetailIcon,
CodeFileIcon,
LoadingIcon,
+ PlagiarismIcon,
ResultsIcon,
UserIcon,
} from '../../components/icons';
@@ -262,6 +263,13 @@ const getPendingReviewSolutions = defaultMemoize(assignmentSolutions =>
.filter(solution => solution && solution.review && solution.review.startedAt && !solution.review.closedAt)
);
+const getPlagiarisms = defaultMemoize(assignmentSolutions =>
+ assignmentSolutions
+ .toArray()
+ .map(getJsData)
+ .filter(solution => solution && solution.plagiarism)
+);
+
const localStorageStateKey = 'AssignmentStats.state';
class AssignmentStats extends Component {
@@ -351,6 +359,7 @@ class AssignmentStats extends Component {
} = this.props;
const pendingReviews = getPendingReviewSolutions(assignmentSolutions);
+ const plagiarisms = getPlagiarisms(assignmentSolutions);
return (
+ {plagiarisms && plagiarisms.length > 0 && (
+ }>
+
+
+ )}
+
{pendingReviews && pendingReviews.length > 0 && (
diff --git a/src/pages/GroupUserSolutions/GroupUserSolutions.js b/src/pages/GroupUserSolutions/GroupUserSolutions.js
index 282179f78..fd0d3aafd 100644
--- a/src/pages/GroupUserSolutions/GroupUserSolutions.js
+++ b/src/pages/GroupUserSolutions/GroupUserSolutions.js
@@ -13,7 +13,14 @@ import ReviewSolutionContainer from '../../containers/ReviewSolutionContainer';
import Page from '../../components/layout/Page';
import { GroupNavigation } from '../../components/layout/Navigation';
-import Icon, { AssignmentIcon, DetailIcon, CodeFileIcon, LoadingIcon, UserIcon } from '../../components/icons';
+import Icon, {
+ AssignmentIcon,
+ DetailIcon,
+ CodeFileIcon,
+ LoadingIcon,
+ PlagiarismIcon,
+ UserIcon,
+} from '../../components/icons';
import SolutionTableRowIcons from '../../components/Assignments/SolutionsTable/SolutionTableRowIcons';
import Points from '../../components/Assignments/SolutionsTable/Points';
import SolutionsTable from '../../components/Assignments/SolutionsTable';
@@ -302,6 +309,25 @@ const getPendingReviewSolutions = defaultMemoize((assignments, getAssignmentSolu
: []
);
+const getPlagiarisms = defaultMemoize((assignments, getAssignmentSolutions) =>
+ assignments
+ ? assignments
+ .toArray()
+ .map(getJsData)
+ .filter(identity)
+ .reduce(
+ (acc, assignment) => [
+ ...acc,
+ ...getAssignmentSolutions(assignment.id)
+ .toArray()
+ .map(getJsData)
+ .filter(solution => solution && solution.plagiarism),
+ ],
+ []
+ )
+ : []
+);
+
const localStorageStateKey = 'GroupUserSolutions.state';
class GroupUserSolutions extends Component {
@@ -379,6 +405,7 @@ class GroupUserSolutions extends Component {
} = this.props;
const pendingReviews = getPendingReviewSolutions(assignments, getAssignmentSolutions);
+ const plagiarisms = getPlagiarisms(assignments, getAssignmentSolutions);
return (
+ {plagiarisms && plagiarisms.length > 0 && (
+ }>
+
+
+ )}
+
{pendingReviews && pendingReviews.length > 0 && (
diff --git a/src/pages/Solution/Solution.js b/src/pages/Solution/Solution.js
index 5ea120c52..9d10097c0 100644
--- a/src/pages/Solution/Solution.js
+++ b/src/pages/Solution/Solution.js
@@ -45,7 +45,7 @@ import { assignmentSubmissionScoreConfigSelector } from '../../redux/selectors/e
import { registerSolutionVisit } from '../../components/Solutions/RecentlyVisited/functions';
import { hasPermissions } from '../../helpers/common';
-import { SolutionResultsIcon, WarningIcon } from '../../components/icons';
+import { PlagiarismIcon, SolutionResultsIcon, WarningIcon } from '../../components/icons';
const assignmentHasRuntime = defaultMemoize(
(assignment, runtimeId) =>
@@ -117,7 +117,7 @@ class Solution extends Component {
userId={solution.authorId}
groupId={assignment.groupId}
attemptIndex={solution.attemptIndex}
- plagiarism={Boolean(solution.plagiarism)}
+ plagiarism={Boolean(solution.plagiarism) && hasPermissions(solution, 'viewDetectedPlagiarisms')}
canViewSolutions={hasPermissions(assignment, 'viewAssignmentSolutions')}
canViewExercise={
hasPermissions(
@@ -128,6 +128,15 @@ class Solution extends Component {
canViewUserProfile={hasPermissions(assignment, 'viewAssignmentSolutions')}
/>
+ {solution.plagiarism && hasPermissions(solution, 'viewDetectedPlagiarisms') && (
+ }>
+
+
+ )}
+
{(hasPermissions(solution, 'setFlag') ||
hasPermissions(solution, 'review') ||
hasPermissions(assignment, 'resubmitSubmissions')) && (
diff --git a/src/pages/SolutionPlagiarisms/SolutionPlagiarisms.js b/src/pages/SolutionPlagiarisms/SolutionPlagiarisms.js
index 502026c76..ccc14af0b 100644
--- a/src/pages/SolutionPlagiarisms/SolutionPlagiarisms.js
+++ b/src/pages/SolutionPlagiarisms/SolutionPlagiarisms.js
@@ -175,7 +175,7 @@ class SolutionPlagiarisms extends Component {
userId={solution.authorId}
groupId={assignment.groupId}
attemptIndex={solution.attemptIndex}
- plagiarism={Boolean(solution.plagiarism)}
+ plagiarism={Boolean(solution.plagiarism) && hasPermissions(solution, 'viewDetectedPlagiarisms')}
canViewSolutions={hasPermissions(assignment, 'viewAssignmentSolutions')}
canViewExercise={
hasPermissions(
diff --git a/src/pages/SolutionSourceCodes/SolutionSourceCodes.js b/src/pages/SolutionSourceCodes/SolutionSourceCodes.js
index bab55a155..6c0603fe8 100644
--- a/src/pages/SolutionSourceCodes/SolutionSourceCodes.js
+++ b/src/pages/SolutionSourceCodes/SolutionSourceCodes.js
@@ -16,6 +16,7 @@ import Button, { TheButtonGroup } from '../../components/widgets/TheButton';
import {
CircleIcon,
CodeCompareIcon,
+ PlagiarismIcon,
RefreshIcon,
SolutionResultsIcon,
StopIcon,
@@ -255,7 +256,7 @@ class SolutionSourceCodes extends Component {
userId={solution.authorId}
groupId={assignment.groupId}
attemptIndex={solution.attemptIndex}
- plagiarism={Boolean(solution.plagiarism)}
+ plagiarism={Boolean(solution.plagiarism) && hasPermissions(solution, 'viewDetectedPlagiarisms')}
canViewSolutions={hasPermissions(assignment, 'viewAssignmentSolutions')}
canViewExercise={
hasPermissions(
@@ -272,6 +273,7 @@ class SolutionSourceCodes extends Component {
) : null
}
/>
+
{diffMode && (
<>
@@ -290,7 +292,9 @@ class SolutionSourceCodes extends Component {
userId={secondSolution.authorId}
groupId={secondAssignment.groupId}
attemptIndex={secondSolution.attemptIndex}
- plagiarism={Boolean(secondSolution.plagiarism)}
+ plagiarism={
+ Boolean(secondSolution.plagiarism) && hasPermissions(secondSolution, 'viewDetectedPlagiarisms')
+ }
canViewSolutions={hasPermissions(secondAssignment, 'viewAssignmentSolutions')}
canViewExercise={
hasPermissions(
@@ -310,6 +314,15 @@ class SolutionSourceCodes extends Component {
{isSupervisorRole(effectiveRole) && (
<>
+ {solution.plagiarism && hasPermissions(solution, 'viewDetectedPlagiarisms') && (
+ }>
+
+
+ )}
+
{!diffMode && (