From ac378988bf416d086d8cc5e8701ba6b6031eab24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kruli=C5=A1?= Date: Tue, 5 May 2026 23:34:54 +0200 Subject: [PATCH 1/2] Changing labels, captions, and texts related to exam lock types. --- .../GroupExamPending/GroupExamPending.js | 75 ++++---------- .../Groups/GroupExamStatus/GroupExamStatus.js | 57 +++-------- .../Groups/GroupExamsTable/GroupExamsTable.js | 9 +- .../Groups/helpers/groupExamMessages.js | 99 +++++++++++++++++++ src/locales/cs.json | 32 +++--- src/locales/en.json | 34 ++++--- src/redux/modules/groups.js | 2 +- 7 files changed, 178 insertions(+), 130 deletions(-) create mode 100644 src/components/Groups/helpers/groupExamMessages.js diff --git a/src/components/Groups/GroupExamPending/GroupExamPending.js b/src/components/Groups/GroupExamPending/GroupExamPending.js index 7b279ec7c..756b28796 100644 --- a/src/components/Groups/GroupExamPending/GroupExamPending.js +++ b/src/components/Groups/GroupExamPending/GroupExamPending.js @@ -10,6 +10,13 @@ import DateTime from '../../widgets/DateTime'; import Explanation from '../../widgets/Explanation'; import { isStudentRole } from '../../helpers/usersRoles.js'; +import { + LOCK_TYPE, + LOCK_TITLE, + LOCK_EXPLANATION, + STUDENT_INFO, + LOCKED_STUDENT_INFO, +} from '../helpers/groupExamMessages.js'; import withLinks from '../../../helpers/withLinks.js'; const REFRESH_INTERVAL = 1; // [s] @@ -51,9 +58,9 @@ class GroupExamPending extends Component { render() { const { id, - privateData: { examBegin, examEnd, examLockStrict }, + privateData: { examBegin, examEnd, examLockType }, currentUser: { - privateData: { groupLock, isGroupLockStrict, ipLock, role }, + privateData: { groupLock, groupLockType, ipLock, role }, }, links: { GROUP_ASSIGNMENTS_URI_FACTORY }, } = this.props; @@ -106,17 +113,7 @@ class GroupExamPending extends Component { id="app.groupExams.lockedStudentInfo" defaultMessage="You may now see and submit solutions to exam assignments." />{' '} - {isGroupLockStrict ? ( - - ) : ( - - )} + {LOCKED_STUDENT_INFO[groupLockType] || ''} {ipLock && ( @@ -150,17 +147,7 @@ class GroupExamPending extends Component { id="app.groupExams.studentInfo" defaultMessage="You need to lock yourself in to see the exam assignments. When locked, your actions will be restricted to your current IP address." />{' '} - {examLockStrict ? ( - - ) : ( - - )} + {STUDENT_INFO[examLockType] || ''}

@@ -213,37 +200,15 @@ class GroupExamPending extends Component { - : + : - - {examLockStrict ? ( - - ) : ( - - )} - - - ) : ( - - ) - }> - {examLockStrict ? ( - - ) : ( - - )} - + {LOCK_TYPE[examLockType] || '???'} + {LOCK_EXPLANATION[examLockType] ? ( + + {LOCK_EXPLANATION[examLockType] || ''} + + ) : null} @@ -260,14 +225,14 @@ GroupExamPending.propTypes = { privateData: PropTypes.shape({ examBegin: PropTypes.number, examEnd: PropTypes.number, - examLockStrict: PropTypes.bool, + examLockType: PropTypes.string, }).isRequired, archived: PropTypes.bool, currentUser: PropTypes.shape({ privateData: PropTypes.shape({ ipLock: PropTypes.string, groupLock: PropTypes.string, - isGroupLockStrict: PropTypes.bool, + groupLockType: PropTypes.string, role: PropTypes.string, }).isRequired, }), diff --git a/src/components/Groups/GroupExamStatus/GroupExamStatus.js b/src/components/Groups/GroupExamStatus/GroupExamStatus.js index 4a7a6ade2..b8347fc42 100644 --- a/src/components/Groups/GroupExamStatus/GroupExamStatus.js +++ b/src/components/Groups/GroupExamStatus/GroupExamStatus.js @@ -17,6 +17,7 @@ import { getErrorMessage } from '../../../locales/apiErrorMessages.js'; import { isStudentRole } from '../../helpers/usersRoles.js'; import { hasPermissions, shallowCompare } from '../../../helpers/common.js'; +import { LOCK_TYPE, LOCK_TITLE, LOCK_EXPLANATION, LOCKED_STUDENT_INFO } from '../helpers/groupExamMessages.js'; const REFRESH_INTERVAL = 1; // [s] @@ -122,7 +123,7 @@ class GroupExamStatus extends Component { pending, removeExamPeriod, currentUser: { - privateData: { ipLock, groupLock, isGroupLockStrict, role }, + privateData: { ipLock, groupLock, groupLockType, role }, }, } = this.props; const isStudent = isStudentRole(role); @@ -174,37 +175,15 @@ class GroupExamStatus extends Component { {!isStudent && ( - : + : - - {group.privateData.examLockStrict ? ( - - ) : ( - - )} - - - ) : ( - - ) - }> - {group.privateData.examLockStrict ? ( - - ) : ( - - )} - + {LOCK_TYPE[group.privateData.examLockType] || '???'} + {LOCK_EXPLANATION[group.privateData.examLockType] ? ( + + {LOCK_EXPLANATION[group.privateData.examLockType] || ''} + + ) : null} )} @@ -321,17 +300,7 @@ class GroupExamStatus extends Component { id="app.groupExams.lockedStudentInfo" defaultMessage="You may now see and submit solutions to exam assignments." />{' '} - {isGroupLockStrict ? ( - - ) : ( - - )} + {LOCKED_STUDENT_INFO[groupLockType] || ''} ) : ( @@ -379,7 +348,7 @@ class GroupExamStatus extends Component { ? prepareExamInitValues( group.privateData.examBegin, group.privateData.examEnd, - group.privateData.examLockStrict || false + group.privateData.examLockType || 'visible' ) : prepareExamInitValues() } @@ -404,14 +373,14 @@ GroupExamStatus.propTypes = { privateData: PropTypes.shape({ examBegin: PropTypes.number, examEnd: PropTypes.number, - examLockStrict: PropTypes.bool, + examLockType: PropTypes.string, }).isRequired, }).isRequired, currentUser: PropTypes.shape({ privateData: PropTypes.shape({ ipLock: PropTypes.string, groupLock: PropTypes.string, - isGroupLockStrict: PropTypes.bool, + groupLockType: PropTypes.string, role: PropTypes.string, }).isRequired, }).isRequired, diff --git a/src/components/Groups/GroupExamsTable/GroupExamsTable.js b/src/components/Groups/GroupExamsTable/GroupExamsTable.js index d91ddbcfb..7e69099a3 100644 --- a/src/components/Groups/GroupExamsTable/GroupExamsTable.js +++ b/src/components/Groups/GroupExamsTable/GroupExamsTable.js @@ -8,6 +8,7 @@ import { lruMemoize } from 'reselect'; import DateTime from '../../widgets/DateTime'; import Button from '../../widgets/TheButton'; import { VisibleIcon } from '../../icons'; +import { LOCK_TYPE } from '../helpers/groupExamMessages.js'; const sortExams = lruMemoize(exams => { const sorted = [...exams]; @@ -43,13 +44,7 @@ const GroupExamsTable = ({ exams = null, selected = null, linkFactory = null }) - - {exam.strict ? ( - - ) : ( - - )} - + {LOCK_TYPE[exam.type] || '???'} {linkFactory && ( diff --git a/src/components/Groups/helpers/groupExamMessages.js b/src/components/Groups/helpers/groupExamMessages.js new file mode 100644 index 000000000..28e72fcb4 --- /dev/null +++ b/src/components/Groups/helpers/groupExamMessages.js @@ -0,0 +1,99 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; + +export const LOCK_TYPE = { + restricted: , + accepted: , + reviewed: , + visible: , +}; + +export const LOCK_TITLE = { + restricted: , + accepted: , + reviewed: ( + + ), + visible: , +}; + +export const LOCK_EXPLANATION = { + restricted: ( + + ), + accepted: ( + + ), + reviewed: ( + + ), + visible: ( + + ), +}; + +export const STUDENT_INFO = { + restricted: ( + + ), + accepted: ( + + ), + reviewed: ( + + ), + visible: ( + + ), +}; + +export const LOCKED_STUDENT_INFO = { + restricted: ( + + ), + accepted: ( + + ), + reviewed: ( + + ), + visible: ( + + ), +}; diff --git a/src/locales/cs.json b/src/locales/cs.json index def3d9a76..51df36a1a 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -1117,26 +1117,36 @@ "app.groupExams.ipLockInfo": "Vaše akce budou příjmány pouze z IP adresy [{ipLock}] dokud budete v uzamčeném režimu. Kontaktujte vašeho zkoušejícího pokud potřebujete změnit svoje umístění.", "app.groupExams.ipLocked": "IP zamčena", "app.groupExams.listBoxTitle": "Předchozí zkoušky", - "app.groupExams.lockRegular": "běžný", - "app.groupExams.lockRegularExplanation": "Studenti, kteří se účastní zkoušky, budou moci číst data z ostatních skupin (a tedy i použít části dříve odevzdaných řešení).", - "app.groupExams.lockRegularTitle": "Běžný zámek", - "app.groupExams.lockStrict": "striktní", - "app.groupExams.lockStrictExplanation": "Studenti, kteří se účastní zkoušky, nebudou moct přistupovat do jiných skupin ani v režimu pro čtení (jsou tedy odříznuti od jejich dříve odevzdaných řešení).", - "app.groupExams.lockStrictTitle": "Striktní zámek", + "app.groupExams.lock.accepted": "akceptované", + "app.groupExams.lock.restricted": "nepřístupné", + "app.groupExams.lock.reviewed": "revidované", + "app.groupExams.lock.visible": "viditelné", + "app.groupExams.lockExplanation.accepted": "Studenti, kteří se účastní zkoušky, budou moci přistupovat pouze k řešením, která jsou v ostatních skupinách označena jako akceptovaná (v režimu pro čtení).", + "app.groupExams.lockExplanation.restricted": "Studenti, kteří se účastní zkoušky, nebudou moct přistupovat do jiných skupin ani v režimu pro čtení (jsou tedy odříznuti od jejich dříve odevzdaných řešení).", + "app.groupExams.lockExplanation.reviewed": "Studenti, kteří se účastní zkoušky, budou moci přistupovat k revidovaným a akceptovaným řešením v jiných skupinách v režimu pro čtení.", + "app.groupExams.lockExplanation.visible": "Studenti, kteří se účastní zkoušky, budou moci číst všechna data z ostatních skupin (a tedy i použít části dříve odevzdaných řešení).", "app.groupExams.lockStudentButton": "Zamknout se", + "app.groupExams.lockTitle.accepted": "Akceptovaná řešení", + "app.groupExams.lockTitle.restricted": "Nepřístupné", + "app.groupExams.lockTitle.reviewed": "Revidovaná a akceptovaná řešení", + "app.groupExams.lockTitle.visible": "Vše viditelné (pro čtení)", + "app.groupExams.lockType": "Omezení přístupu", "app.groupExams.lockedElsewhere": "Právě jste zamčený ve zkouškovém režimu v jiné skupině (tuto skupinu můžete procházet pouze v režimu ke čtení).", "app.groupExams.lockedStudentInfo": "Nyní můžete vidět zkouškové úlohy a odevzdávat u nich řešení.", - "app.groupExams.lockedStudentInfoRegular": "K ostatním skupinám můžete přistupovat pouze v režimu pro čtení dokud jste v uzamčeném režimu.", - "app.groupExams.lockedStudentInfoStrict": "K ostatním skupinám nemáte přístup dokud jste v uzamčeném režimu.", - "app.groupExams.locking": "Typ zámku", + "app.groupExams.lockedStudentInfo.accepted": "V ostatních skupinách můžete přistupovat pouze k akceptovaným řešením v režimu pro čtení dokud jste uzamčen v probíhající zkoušce.", + "app.groupExams.lockedStudentInfo.restricted": "K ostatním skupinám nemáte přístup dokud jste uzamčen v probíhající zkoušce.", + "app.groupExams.lockedStudentInfo.reviewed": "V ostatních skupinách můžete přistupovat pouze k revidovaným a akceptovaným řešením v režimu pro čtení dokud jste uzamčen v probíhající zkoušce.", + "app.groupExams.lockedStudentInfo.visible": "K ostatním skupinám můžete přistupovat pouze v režimu pro čtení dokud jste v uzamčen v probíhající zkoušce.", "app.groupExams.locksBoxTitle": "Zaznamenané události zamykání studentů", "app.groupExams.noExam": "V tuto chvíli není naplánovaná žádná zkouška", "app.groupExams.pending.studentLockedTitle": "Jste uzamčeném režimu pro probíhající zkoušku", "app.groupExams.pending.teacherInfo": "V tuto chvíli jsou zkouškové úlohy viditelné pouze studentům, kteří se uzamkli ve skupině.", "app.groupExams.remainingStudentsBoxTitle": "Zbývající studenti (dosud nezamčení)", "app.groupExams.studentInfo": "Musíte se uzamknout ve skupině, abyste mohl(a) vidět zkouškové úlohy. Po čas uzamčení je komunikace s vámi omezena na vaši aktuální IP adresu.", - "app.groupExams.studentInfoRegular": "Navíc budete moct přistupovat k ostatním skupinám pouze v režimu pro čtení dokud budete v uzamčeném režimu.", - "app.groupExams.studentInfoStrict": "Navíc nebudete moct přistupovat k ostatním skupinám dokud budete v uzamčeném režimu.", + "app.groupExams.studentInfo.accepted": "Navíc budete moct přistupovat k akceptovaným řešením v jiných skupinách v režimu pro čtení dokud budete v uzamčeném režimu.", + "app.groupExams.studentInfo.restricted": "Navíc nebudete moct přistupovat k ostatním skupinám dokud budete v uzamčeném režimu.", + "app.groupExams.studentInfo.reviewed": "Navíc budete moct přistupovat k revidovaným a akceptovaným řešením v jiných skupinách v režimu pro čtení dokud budete v uzamčeném režimu.", + "app.groupExams.studentInfo.visible": "Navíc budete moct přistupovat k ostatním skupinám pouze v režimu pro čtení dokud budete v uzamčeném režimu.", "app.groupExams.studentsBoxTitle": "Studenti účastnící se zkoušky", "app.groupExams.studentsCount": "({count, plural, one {# student} =2 {# studenti} =3 {# studenti} =4 {# studenti} other {# studentů}})", "app.groupExams.timeAccuracyWarning": "Lokální hodiny na vašem systému musí být dostatečně seřízené, jinak nemusí tato komponenta fungovat zcela správně.", diff --git a/src/locales/en.json b/src/locales/en.json index 0800c992f..1bea12cec 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1117,26 +1117,36 @@ "app.groupExams.ipLockInfo": "Your actions are restricted to IP address [{ipLock}] until the exam lock expires. Contact your exam supervisor if you require relocation.", "app.groupExams.ipLocked": "IP locked", "app.groupExams.listBoxTitle": "Previous exams", - "app.groupExams.lockRegular": "regular", - "app.groupExams.lockRegularExplanation": "Users taking the exam will be able to access other groups in read-only mode (for instance to utilize pieces of previously submitted code).", - "app.groupExams.lockRegularTitle": "Regular lock", - "app.groupExams.lockStrict": "strict", - "app.groupExams.lockStrictExplanation": "Users taking the exam will not be allowed to access any other group, not even for reading (so that are cut of source codes they submitted before the exam).", - "app.groupExams.lockStrictTitle": "Strict lock", + "app.groupExams.lock.accepted": "accepted", + "app.groupExams.lock.restricted": "restricted", + "app.groupExams.lock.reviewed": "reviewed", + "app.groupExams.lock.visible": "visible", + "app.groupExams.lockExplanation.accepted": "Users taking the exam will be allowed to access solutions marked as accepted in other groups in read-only mode.", + "app.groupExams.lockExplanation.restricted": "Users taking the exam will not be allowed to access any other group, not even for reading (so that are cut of source codes they submitted before the exam).", + "app.groupExams.lockExplanation.reviewed": "Users taking the exam will be allowed to access reviewed and accepted solutions in other groups in read-only mode.", + "app.groupExams.lockExplanation.visible": "Users taking the exam will be able to see everything in all other groups in read-only mode (for instance to utilize pieces of previously submitted code).", "app.groupExams.lockStudentButton": "Lock In", + "app.groupExams.lockTitle.accepted": "Accepted solutions", + "app.groupExams.lockTitle.restricted": "Restricted access", + "app.groupExams.lockTitle.reviewed": "Reviewed and accepted solutions", + "app.groupExams.lockTitle.visible": "All visible (read-only)", + "app.groupExams.lockType": "Access limit", "app.groupExams.lockedElsewhere": "You are already locked for an exam in a different group (you can see this group in a read-only mode now).", "app.groupExams.lockedStudentInfo": "You may now see and submit solutions to exam assignments.", - "app.groupExams.lockedStudentInfoRegular": "You may access other groups in read-only mode until the exam lock expires.", - "app.groupExams.lockedStudentInfoStrict": "You may not access any other groups until the exam lock expires.", - "app.groupExams.locking": "Lock type", + "app.groupExams.lockedStudentInfo.accepted": "You may access accepted solutions in other groups in read-only mode until the exam lock expires.", + "app.groupExams.lockedStudentInfo.restricted": "You may not access any other groups until the exam lock expires.", + "app.groupExams.lockedStudentInfo.reviewed": "You may access accepted and reviewed solutions in other groups in read-only mode until the exam lock expires.", + "app.groupExams.lockedStudentInfo.visible": "You may access other groups in read-only mode until the exam lock expires.", "app.groupExams.locksBoxTitle": "Recorded student locking events", "app.groupExams.noExam": "There is currently no exam scheduled", "app.groupExams.pending.studentLockedTitle": "You are locked in for an exam", "app.groupExams.pending.teacherInfo": "The exam assignments are currently visible only to students who have lock themselves in the group.", "app.groupExams.remainingStudentsBoxTitle": "Remaining students (not locked yet)", "app.groupExams.studentInfo": "You need to lock yourself in to see the exam assignments. When locked, your actions will be restricted to your current IP address.", - "app.groupExams.studentInfoRegular": "Furthermore, you will be able to access other groups in a read-only mode until the exam lock expires.", - "app.groupExams.studentInfoStrict": "Furthermore, you will not be able to access other groups until the exam lock expires.", + "app.groupExams.studentInfo.accepted": "Furthermore, you will be able to access accepted solutions in other groups in a read-only mode until the exam lock expires.", + "app.groupExams.studentInfo.restricted": "Furthermore, you will not be able to access other groups until the exam lock expires.", + "app.groupExams.studentInfo.reviewed": "Furthermore, you will be able to access accepted and reviewed solutions in other groups in a read-only mode until the exam lock expires.", + "app.groupExams.studentInfo.visible": "Furthermore, you will be able to access other groups in a read-only mode until the exam lock expires.", "app.groupExams.studentsBoxTitle": "Participating students", "app.groupExams.studentsCount": "({count, plural, one {# student} other {# students}})", "app.groupExams.timeAccuracyWarning": "Your local system clock should be sufficiently synchronized or this component may not work properly.", @@ -2187,4 +2197,4 @@ "recodex-judge-shuffle-all": "Unordered-tokens-and-rows judge", "recodex-judge-shuffle-newline": "Unordered-tokens judge (ignoring ends of lines)", "recodex-judge-shuffle-rows": "Unordered-rows judge" -} +} \ No newline at end of file diff --git a/src/redux/modules/groups.js b/src/redux/modules/groups.js index 2abc134e5..d4f9dc44e 100644 --- a/src/redux/modules/groups.js +++ b/src/redux/modules/groups.js @@ -379,7 +379,7 @@ const reducer = handleActions( .deleteIn(['resources', groupId, 'pending-exam-period']) .setIn(['resources', groupId, 'data', 'privateData', 'examBegin'], null) .setIn(['resources', groupId, 'data', 'privateData', 'examEnd'], null) - .setIn(['resources', groupId, 'data', 'privateData', 'examLockStrict'], null), + .setIn(['resources', groupId, 'data', 'privateData', 'examLockType'], null), [additionalActionTypes.REMOVE_EXAM_PERIOD_REJECTED]: (state, { meta: { groupId } }) => state.deleteIn(['resources', groupId, 'pending-exam-period']), From fdb1c6a384adf100295d3a88e5a3633b1d4892ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kruli=C5=A1?= Date: Mon, 11 May 2026 15:11:53 +0200 Subject: [PATCH 2/2] Updating exam form to use lock type instead of strict flag. Adjusting lock info on Assignment detail page. --- .../GroupExamPending/GroupExamPending.js | 2 + .../Groups/GroupExamStatus/GroupExamStatus.js | 4 +- .../Groups/helpers/groupExamMessages.js | 30 ++++++++++ src/components/forms/ExamForm/ExamForm.js | 58 ++++++++++--------- src/components/forms/Fields/RadioField.js | 2 +- src/locales/cs.json | 7 ++- src/locales/en.json | 7 ++- src/pages/Assignment/Assignment.js | 35 +++++++++-- src/pages/GroupExams/GroupExams.js | 4 +- src/redux/modules/groups.js | 6 +- 10 files changed, 112 insertions(+), 43 deletions(-) diff --git a/src/components/Groups/GroupExamPending/GroupExamPending.js b/src/components/Groups/GroupExamPending/GroupExamPending.js index 756b28796..a605de205 100644 --- a/src/components/Groups/GroupExamPending/GroupExamPending.js +++ b/src/components/Groups/GroupExamPending/GroupExamPending.js @@ -16,6 +16,7 @@ import { LOCK_EXPLANATION, STUDENT_INFO, LOCKED_STUDENT_INFO, + LOCKED_ELSEWHERE_STUDENT_INFO, } from '../helpers/groupExamMessages.js'; import withLinks from '../../../helpers/withLinks.js'; @@ -73,6 +74,7 @@ class GroupExamPending extends Component { id="app.groupExams.lockedElsewhere" defaultMessage="You are already locked for an exam in a different group (you can see this group in a read-only mode now)." /> + {LOCKED_ELSEWHERE_STUDENT_INFO[groupLockType] ? <> {LOCKED_ELSEWHERE_STUDENT_INFO[groupLockType]} : null}{' '} diff --git a/src/components/Groups/GroupExamStatus/GroupExamStatus.js b/src/components/Groups/GroupExamStatus/GroupExamStatus.js index b8347fc42..a9bf59280 100644 --- a/src/components/Groups/GroupExamStatus/GroupExamStatus.js +++ b/src/components/Groups/GroupExamStatus/GroupExamStatus.js @@ -49,9 +49,9 @@ class GroupExamStatus extends Component { }; examFormSubmit = data => { - const { begin, end, strict } = transformExamData(data); + const { begin, end, type } = transformExamData(data); const { examInProgress } = this.state; - return this.props.setExamPeriod(examInProgress ? null : begin, end, examInProgress ? null : strict).then(res => { + return this.props.setExamPeriod(examInProgress ? null : begin, end, examInProgress ? null : type).then(res => { this.examModalClose(); return Promise.resolve(res); }); diff --git a/src/components/Groups/helpers/groupExamMessages.js b/src/components/Groups/helpers/groupExamMessages.js index 28e72fcb4..de5da4e85 100644 --- a/src/components/Groups/helpers/groupExamMessages.js +++ b/src/components/Groups/helpers/groupExamMessages.js @@ -97,3 +97,33 @@ export const LOCKED_STUDENT_INFO = { /> ), }; + +export const LOCKED_ELSEWHERE_STUDENT_INFO = { + accepted: ( + + ), + reviewed: ( + + ), +}; + +export const STUDENT_SOLUTIONS_RESTRICTIONS = { + accepted: ( + + ), + reviewed: ( + + ), +}; diff --git a/src/components/forms/ExamForm/ExamForm.js b/src/components/forms/ExamForm/ExamForm.js index c1e92114c..d959b138b 100644 --- a/src/components/forms/ExamForm/ExamForm.js +++ b/src/components/forms/ExamForm/ExamForm.js @@ -10,8 +10,18 @@ import Button, { TheButtonGroup } from '../../widgets/TheButton'; import Explanation from '../../widgets/Explanation'; import SubmitButton from '../SubmitButton'; import { CloseIcon, SendIcon } from '../../icons'; +import { TextField, CheckboxField, RadioField, DatetimeField } from '../Fields'; +import { LOCK_TITLE, LOCK_EXPLANATION } from '../../Groups/helpers/groupExamMessages.js'; -import { TextField, CheckboxField, DatetimeField } from '../Fields'; +const TYPE_OPTIONS = ['visible', 'reviewed', 'accepted', 'restricted'].map(key => ({ + key, + name: ( + <> + {LOCK_TITLE[key]} + {LOCK_EXPLANATION[key]} + + ), +})); export const secondsToTime = seconds => { if (seconds < 0) { @@ -34,16 +44,16 @@ export const timeToSeconds = timeStr => { return !isNaN(hours) && !isNaN(minutes) ? hours * 3600 + minutes * 60 : null; }; -export const prepareInitValues = (begin = null, end = null, strict = false) => ({ +export const prepareInitValues = (begin = null, end = null, type = 'visible') => ({ beginImmediately: false, endRelative: false, begin: begin ? moment.unix(begin) : moment().add(2, 'hour').startOf('hour'), end: end ? moment.unix(end) : begin ? moment.unix(begin).add(2, 'hour') : moment().add(4, 'hour').startOf('hour'), - strict, + type, length: begin && end ? secondsToTime(end - begin) : '2:00', }); -export const transformSubmittedData = ({ beginImmediately, endRelative, begin, end, length, strict = false }) => { +export const transformSubmittedData = ({ beginImmediately, endRelative, begin, end, length, type = 'visible' }) => { const beginTs = beginImmediately ? Math.ceil(Date.now() / 1000) : moment.isMoment(begin) ? begin.unix() : null; const endTs = endRelative ? beginTs && timeToSeconds(length) @@ -52,7 +62,7 @@ export const transformSubmittedData = ({ beginImmediately, endRelative, begin, e : moment.isMoment(end) ? end.unix() : null; - return { begin: beginTs, end: endTs, strict }; + return { begin: beginTs, end: endTs, type }; }; const ExamForm = ({ @@ -158,32 +168,28 @@ const ExamForm = ({
- - - - - - - } - /> +
+ +
+
)} - - {error && dirty && {error}} + {error && dirty && ( + + + {error} + + + )} + +
+ -
diff --git a/src/locales/cs.json b/src/locales/cs.json index 51df36a1a..1bb6c3e5e 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -745,9 +745,8 @@ "app.examForm.errors.length": "Délka musí být v platném formátu a větší než nula.", "app.examForm.errors.tooLongExam": "Zkouška nesmí trvat déle než 24 hodin.", "app.examForm.length": "Délka [h:mm]:", + "app.examForm.lockTypeTitle": "Úroveň omezení přístupu pro zamčené studenty", "app.examForm.saveExam": "Uložit zkoušku", - "app.examForm.strict": "Striktní zámek", - "app.examForm.strictLockExplanation": "Během zkoušky se studenti musí zamknout ve skupině, přičemž se jim omezí přístup k ostatním skupinám. V případě běžného zámku budou ostatní skupiny přístupné pouze pro čtení. Striktní zámek zakáže přístup do ostatních skupin zcela. Pokud si nepřejete, aby studenti mohli používat části zdrojových kódu dříve odevzdaných řešení, zvolte striktní zámek.", "app.exercise.addReferenceSolutionDetailed": "Referenční řešení můžete vytvořit na hlavní stránce úlohy.", "app.exercise.admins": "Administrátoři", "app.exercise.admins.explanation": "Administrátoři mají stejná práva jako autor úlohy, ale nejsou zobrazováni v seznamech ani nejsou použiti při filtrování úloh.", @@ -1132,6 +1131,8 @@ "app.groupExams.lockTitle.visible": "Vše viditelné (pro čtení)", "app.groupExams.lockType": "Omezení přístupu", "app.groupExams.lockedElsewhere": "Právě jste zamčený ve zkouškovém režimu v jiné skupině (tuto skupinu můžete procházet pouze v režimu ke čtení).", + "app.groupExams.lockedElsewhereStudentInfo.accepted": "Váš přístup je omezen pouze na akceptovaná řešení.", + "app.groupExams.lockedElsewhereStudentInfo.reviewed": "Váš přístup je omezen pouze na revidovaná a akceptovaná řešení.", "app.groupExams.lockedStudentInfo": "Nyní můžete vidět zkouškové úlohy a odevzdávat u nich řešení.", "app.groupExams.lockedStudentInfo.accepted": "V ostatních skupinách můžete přistupovat pouze k akceptovaným řešením v režimu pro čtení dokud jste uzamčen v probíhající zkoušce.", "app.groupExams.lockedStudentInfo.restricted": "K ostatním skupinám nemáte přístup dokud jste uzamčen v probíhající zkoušce.", @@ -1147,6 +1148,8 @@ "app.groupExams.studentInfo.restricted": "Navíc nebudete moct přistupovat k ostatním skupinám dokud budete v uzamčeném režimu.", "app.groupExams.studentInfo.reviewed": "Navíc budete moct přistupovat k revidovaným a akceptovaným řešením v jiných skupinách v režimu pro čtení dokud budete v uzamčeném režimu.", "app.groupExams.studentInfo.visible": "Navíc budete moct přistupovat k ostatním skupinám pouze v režimu pro čtení dokud budete v uzamčeném režimu.", + "app.groupExams.studentSolutionsRestrictions.accepted": "zobazují se pouze akceptovaná řešení", + "app.groupExams.studentSolutionsRestrictions.reviewed": "zobazují se pouze akceptovaná a revidovaná řešení", "app.groupExams.studentsBoxTitle": "Studenti účastnící se zkoušky", "app.groupExams.studentsCount": "({count, plural, one {# student} =2 {# studenti} =3 {# studenti} =4 {# studenti} other {# studentů}})", "app.groupExams.timeAccuracyWarning": "Lokální hodiny na vašem systému musí být dostatečně seřízené, jinak nemusí tato komponenta fungovat zcela správně.", diff --git a/src/locales/en.json b/src/locales/en.json index 1bea12cec..b1fbec71d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -745,9 +745,8 @@ "app.examForm.errors.length": "The length must be in valid format and not zero.", "app.examForm.errors.tooLongExam": "The exam must not be longer than 24 hours.", "app.examForm.length": "Length [h:mm]:", + "app.examForm.lockTypeTitle": "Access limitations for locked students", "app.examForm.saveExam": "Save Exam", - "app.examForm.strict": "Strict lock", - "app.examForm.strictLockExplanation": "During the exam, students will be required to lock themselves in the group. When locked, access to all other groups is restricted. In case of regular locks, other groups are read-only. If the lock is strict, the groups may not be accessed at all. Use strict locking when the students are to be prevented from utilizing pieced of previously submitted code.", "app.exercise.addReferenceSolutionDetailed": "A reference solution can be added on the exercise detail page.", "app.exercise.admins": "Administrators", "app.exercise.admins.explanation": "The administrators have the same permissions as the author towards the exercise, but they are not explicitly mentioned in listings or used in search filters.", @@ -1132,6 +1131,8 @@ "app.groupExams.lockTitle.visible": "All visible (read-only)", "app.groupExams.lockType": "Access limit", "app.groupExams.lockedElsewhere": "You are already locked for an exam in a different group (you can see this group in a read-only mode now).", + "app.groupExams.lockedElsewhereStudentInfo.accepted": "Your access is limited to accepted solutions only.", + "app.groupExams.lockedElsewhereStudentInfo.reviewed": "Your access is limited to accepted and reviewed solutions only.", "app.groupExams.lockedStudentInfo": "You may now see and submit solutions to exam assignments.", "app.groupExams.lockedStudentInfo.accepted": "You may access accepted solutions in other groups in read-only mode until the exam lock expires.", "app.groupExams.lockedStudentInfo.restricted": "You may not access any other groups until the exam lock expires.", @@ -1147,6 +1148,8 @@ "app.groupExams.studentInfo.restricted": "Furthermore, you will not be able to access other groups until the exam lock expires.", "app.groupExams.studentInfo.reviewed": "Furthermore, you will be able to access accepted and reviewed solutions in other groups in a read-only mode until the exam lock expires.", "app.groupExams.studentInfo.visible": "Furthermore, you will be able to access other groups in a read-only mode until the exam lock expires.", + "app.groupExams.studentSolutionsRestrictions.accepted": "showing only accepted solutions", + "app.groupExams.studentSolutionsRestrictions.reviewed": "showing only accepted and reviewed solutions", "app.groupExams.studentsBoxTitle": "Participating students", "app.groupExams.studentsCount": "({count, plural, one {# student} other {# students}})", "app.groupExams.timeAccuracyWarning": "Your local system clock should be sufficiently synchronized or this component may not work properly.", diff --git a/src/pages/Assignment/Assignment.js b/src/pages/Assignment/Assignment.js index 620d4ae8d..1a79abca7 100644 --- a/src/pages/Assignment/Assignment.js +++ b/src/pages/Assignment/Assignment.js @@ -8,8 +8,10 @@ import { Col, Row } from 'react-bootstrap'; import Box from '../../components/widgets/Box'; import Callout from '../../components/widgets/Callout'; import OptionalPopoverWrapper from '../../components/widgets/OptionalPopoverWrapper'; +import GroupExamPending from '../../components/Groups/GroupExamPending'; import { fetchAssignmentIfNeeded, syncWithExercise, SYNC_OPTIONS_ALL } from '../../redux/modules/assignments.js'; +import { fetchGroupIfNeeded } from '../../redux/modules/groups.js'; import { canSubmit } from '../../redux/modules/canSubmit.js'; import { init, @@ -38,13 +40,14 @@ import { } from '../../redux/selectors/solutions.js'; import { loggedUserIsStudentOfSelector } from '../../redux/selectors/usersGroups.js'; import { loggedInUserSelector } from '../../redux/selectors/users.js'; +import { groupSelector } from '../../redux/selectors/groups.js'; import Page from '../../components/layout/Page'; import { AssignmentNavigation } from '../../components/layout/Navigation'; import ResourceRenderer from '../../components/helpers/ResourceRenderer'; import FetchManyResourceRenderer from '../../components/helpers/FetchManyResourceRenderer'; import AssignmentDetails from '../../components/Assignments/Assignment/AssignmentDetails'; -import Icon, { AssignmentIcon } from '../../components/icons'; +import Icon, { AssignmentIcon, WarningIcon } from '../../components/icons'; import LocalizedTexts from '../../components/helpers/LocalizedTexts'; import SubmitSolutionButton from '../../components/Assignments/SubmitSolutionButton'; import SubmitSolutionContainer from '../../containers/SubmitSolutionContainer'; @@ -55,6 +58,7 @@ import CommentThreadContainer from '../../containers/CommentThreadContainer'; import LoadingSolutionsTable from '../../components/Assignments/SolutionsTable/LoadingSolutionsTable.js'; import FailedLoadingSolutionsTable from '../../components/Assignments/SolutionsTable/FailedLoadingSolutionsTable.js'; import { isStudentLocked } from '../../components/helpers/exams.js'; +import { STUDENT_SOLUTIONS_RESTRICTIONS } from '../../components/Groups/helpers/groupExamMessages.js'; import { hasPermissions } from '../../helpers/common.js'; const getReason = ({ lockedReason }, locale) => @@ -65,7 +69,9 @@ const getReason = ({ lockedReason }, locale) => class Assignment extends Component { static loadAsync = ({ assignmentId }, dispatch, { userId }) => Promise.all([ - dispatch(fetchAssignmentIfNeeded(assignmentId)), + dispatch(fetchAssignmentIfNeeded(assignmentId)) + .then(res => res.value) + .then(assignment => dispatch(fetchGroupIfNeeded(assignment.groupId))), dispatch(fetchRuntimeEnvironments()), dispatch(canSubmit(assignmentId)), dispatch(fetchUsersSolutions(userId, assignmentId)), @@ -108,6 +114,7 @@ class Assignment extends Component { fetchManyStatus, assignmentSolversLoading, assignmentSolverSelector, + getGroup, intl: { locale }, } = this.props; @@ -133,6 +140,10 @@ class Assignment extends Component { } /> + + {group => group.privateData && } + + {assignment.exerciseId && hasPermissions(assignment, 'update') && ( )} @@ -155,8 +166,8 @@ class Assignment extends Component { )} - - {(canSubmitObj, ...runtimes) => ( + + {(canSubmitObj, group, ...runtimes) => ( } + title={ + <> + + {currentUser?.privateData?.groupLock && + STUDENT_SOLUTIONS_RESTRICTIONS[currentUser?.privateData?.groupLockType] && ( + + + {STUDENT_SOLUTIONS_RESTRICTIONS[currentUser?.privateData?.groupLockType]} + + + )} + + } collapsable isOpen noPadding @@ -314,6 +337,7 @@ Assignment.propTypes = { fetchManyStatus: PropTypes.string, assignmentSolversLoading: PropTypes.bool, assignmentSolverSelector: PropTypes.func.isRequired, + getGroup: PropTypes.func.isRequired, reloadCanSubmit: PropTypes.func.isRequired, reloadSolvers: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -336,6 +360,7 @@ export default injectIntl( fetchManyStatus: fetchManyUserSolutionsStatus(state)(userId || loggedInUserId, assignmentId), assignmentSolversLoading: isAssignmentSolversLoading(state), assignmentSolverSelector: getAssignmentSolverSelector(state), + getGroup: id => groupSelector(state, id), }; }, (dispatch, { params: { assignmentId } }) => ({ diff --git a/src/pages/GroupExams/GroupExams.js b/src/pages/GroupExams/GroupExams.js index 22334bf93..231a4786c 100644 --- a/src/pages/GroupExams/GroupExams.js +++ b/src/pages/GroupExams/GroupExams.js @@ -295,8 +295,8 @@ export default withLinks( loadGroupExamLocks: () => dispatch(fetchGroupExamLocksIfNeeded(groupId, examId)), reload: () => dispatch(fetchGroup(groupId)), addNotification: (...args) => dispatch(addNotification(...args)), - setExamPeriod: (begin, end, strict) => - dispatch(setExamPeriod(groupId, begin, end, strict)).then(() => + setExamPeriod: (begin, end, type) => + dispatch(setExamPeriod(groupId, begin, end, type)).then(() => GroupExams.loadAsync({ groupId, examId }, dispatch) ), removeExamPeriod: () => diff --git a/src/redux/modules/groups.js b/src/redux/modules/groups.js index d4f9dc44e..bddf885d1 100644 --- a/src/redux/modules/groups.js +++ b/src/redux/modules/groups.js @@ -186,10 +186,10 @@ export const setExamFlag = (groupId, value = true) => meta: { groupId }, }); -export const setExamPeriod = (groupId, begin, end = null, strict = undefined) => { +export const setExamPeriod = (groupId, begin, end = null, type = undefined) => { const body = { begin, end }; - if (strict !== undefined && strict !== null) { - body.strict = strict; + if (type !== undefined && type !== null) { + body.type = type; } return createApiAction({ type: additionalActionTypes.SET_EXAM_PERIOD,