Skip to content

Commit b63d725

Browse files
committed
Improving visualization of locked students on the Exam page.
1 parent fc4113b commit b63d725

File tree

9 files changed

+196
-62
lines changed

9 files changed

+196
-62
lines changed

src/components/Groups/LockedStudentsTable/LockedStudentsTable.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,18 @@ const LockedStudentsTable = ({ groupId, lockedStudents, currentUser, intl: { loc
1717
lockedStudents && lockedStudents.length > 0 ? (
1818
<Table className="m-0" hover>
1919
<tbody>
20-
{sortStudents(lockedStudents).map(student => (
20+
{sortStudents(lockedStudents, locale).map(student => (
2121
<tr key={student.id}>
2222
<td>
2323
<UsersName
2424
{...student}
2525
currentUserId={currentUser.id}
26-
showEmail="full"
26+
showEmail="icon"
2727
showExternalIdentifiers
2828
listItem
2929
/>
3030
</td>
31+
<td>{student.privateData?.ipLock && <code>{student.privateData.ipLock}</code>}</td>
3132
<td className="text-end">
3233
<ExamUnlockButtonContainer groupId={groupId} userId={student.id} size="xs" />
3334
</td>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { FormattedMessage, injectIntl } from 'react-intl';
4+
import { Table } from 'react-bootstrap';
5+
import { lruMemoize } from 'reselect';
6+
7+
import UsersName from '../../Users/UsersName';
8+
import DateTime from '../../widgets/DateTime/DateTime';
9+
import Icon from '../../icons';
10+
import { createUserNameComparator } from '../../helpers/users.js';
11+
12+
const sortStudents = lruMemoize((notLockedStudents, locale) => {
13+
const sorted = [...notLockedStudents];
14+
return sorted.sort(createUserNameComparator(locale));
15+
});
16+
17+
const NotLockedStudentsTable = ({ notLockedStudents, currentUser, intl: { locale } }) =>
18+
notLockedStudents && notLockedStudents.length > 0 ? (
19+
<Table className="m-0" hover>
20+
<tbody>
21+
{sortStudents(notLockedStudents, locale).map(student => (
22+
<tr key={student.id}>
23+
<td>
24+
<UsersName
25+
{...student}
26+
currentUserId={currentUser.id}
27+
showEmail="icon"
28+
showExternalIdentifiers
29+
listItem
30+
/>
31+
</td>
32+
<td className="text-nowrap shrink-col small text-body-secondary align-middle">
33+
{student.privateData?.lastAuthenticationAt && (
34+
<>
35+
<Icon
36+
icon="right-to-bracket"
37+
gapRight
38+
tooltipId={student.id}
39+
tooltipPlacement="bottom"
40+
tooltip={
41+
<FormattedMessage
42+
id="app.notLockedStudentsTable.lastAuthentication"
43+
defaultMessage="Last time the user was authenticated"
44+
/>
45+
}
46+
/>
47+
<DateTime unixts={student.privateData.lastAuthenticationAt} showSeconds />
48+
</>
49+
)}
50+
</td>
51+
</tr>
52+
))}
53+
</tbody>
54+
</Table>
55+
) : (
56+
<div className="text-center text-body-secondary p-4">
57+
<em>
58+
<FormattedMessage
59+
id="app.notLockedStudentsTable.noNotLockedStudents"
60+
defaultMessage="There are no remaining unlocked students."
61+
/>
62+
</em>
63+
</div>
64+
);
65+
66+
NotLockedStudentsTable.propTypes = {
67+
notLockedStudents: PropTypes.array,
68+
currentUser: PropTypes.shape({
69+
id: PropTypes.string.isRequired,
70+
}),
71+
intl: PropTypes.object.isRequired,
72+
};
73+
74+
export default injectIntl(NotLockedStudentsTable);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import NotLockedStudentsTable from './NotLockedStudentsTable.js';
2+
export default NotLockedStudentsTable;

src/components/buttons/ExamUnlockButton/ExamUnlockButton.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl';
44
import { UnlockIcon, LoadingIcon } from '../../icons';
55
import Button from '../../widgets/TheButton';
66

7-
const ExamUnlockButton = ({ pending, disabled = false, unlockUserForExam, ...props }) => (
7+
const ExamUnlockButton = ({ pending = false, disabled = false, unlockUserForExam, ...props }) => (
88
<Button
99
{...props}
1010
variant={disabled ? 'secondary' : 'warning'}
@@ -16,7 +16,7 @@ const ExamUnlockButton = ({ pending, disabled = false, unlockUserForExam, ...pro
1616
);
1717

1818
ExamUnlockButton.propTypes = {
19-
pending: PropTypes.bool.isRequired,
19+
pending: PropTypes.bool,
2020
disabled: PropTypes.bool,
2121
unlockUserForExam: PropTypes.func.isRequired,
2222
};

src/containers/ExamUnlockButtonContainer/ExamUnlockButtonContainer.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { lruMemoize } from 'reselect';
77
import ExamUnlockButton from '../../components/buttons/ExamUnlockButton';
88
import { unlockStudentFromExam } from '../../redux/modules/groups.js';
99
import { groupPendingUserUnlock } from '../../redux/selectors/groups.js';
10-
import { loggedInUserSelector } from '../../redux/selectors/users.js';
1110
import { getErrorMessage } from '../../locales/apiErrorMessages.js';
1211
import { addNotification } from '../../redux/modules/notifications.js';
1312

@@ -45,7 +44,6 @@ ExamUnlockButtonContainer.propTypes = {
4544

4645
const mapStateToProps = (state, { groupId }) => ({
4746
pending: groupPendingUserUnlock(state, groupId),
48-
currentUser: loggedInUserSelector(state),
4947
});
5048

5149
const mapDispatchToProps = (dispatch, { groupId, userId }) => ({

src/locales/cs.json

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,6 @@
106106
"app.assignExerciseButton.isBroken": "Rozbitá",
107107
"app.assignExerciseButton.isLocked": "Zamčená",
108108
"app.assignExerciseButton.noRefSolutions": "Neověřená",
109-
"app.assignmentStatusIcon.failed": "Žádné správné řešení nebylo zatím odevzdáno.",
110-
"app.assignmentStatusIcon.inProgress": "Řešení úlohy je právě vyhodnocováno.",
111-
"app.assignmentStatusIcon.isBestSolution": "Toto je nejlepší řešení, které autor dosud odevzdal.",
112-
"app.assignmentStatusIcon.ok": "Úloha byla úspěšně splněna.",
113-
"app.assignmentStatusIcon.solutionMissingSubmission": "Řešení nebylo odevzdáno k vyhodnocení pravděpodobně díky chybě. Budete ho muset odevzdat znovu.",
114109
"app.assignment.assignedAtExplanation": "Čas zadání úlohy ve skupině. Tento čas se nutně nemusí shodovat s časem, kdy byla úloha nastavena jako viditelná studentům.",
115110
"app.assignment.correctnessThreshold": "Hranice správnosti",
116111
"app.assignment.correctnessThresholdExplanation": "Minimální požadovaná správnost řešení, za kterou je možné udělit body. Řešení, která nedosahují této hranice, nedostanou žádné body.",
@@ -184,6 +179,11 @@
184179
"app.assignmentSolutions.viewModes.reviewRequests": "Žádosti o revize",
185180
"app.assignmentSolutions.viewModes.reviewed": "Pouze revidovaná řešení",
186181
"app.assignmentSolutions.viewModesTitle": "Vyberte způsob zobrazení řešení",
182+
"app.assignmentStatusIcon.failed": "Žádné správné řešení nebylo zatím odevzdáno.",
183+
"app.assignmentStatusIcon.inProgress": "Řešení úlohy je právě vyhodnocováno.",
184+
"app.assignmentStatusIcon.isBestSolution": "Toto je nejlepší řešení, které autor dosud odevzdal.",
185+
"app.assignmentStatusIcon.ok": "Úloha byla úspěšně splněna.",
186+
"app.assignmentStatusIcon.solutionMissingSubmission": "Řešení nebylo odevzdáno k vyhodnocení pravděpodobně díky chybě. Budete ho muset odevzdat znovu.",
187187
"app.assignments.deadline": "Termín odevzdání",
188188
"app.assignments.deleteAllButton": "Smazat",
189189
"app.assignments.deleteAllButtonConfirm": "Opravdu si přejete smazat všechny vybrané zadané úlohy?",
@@ -374,14 +374,14 @@
374374
"app.editAssignmentForm.visibility.visible": "Viditelná studentům",
375375
"app.editAssignmentForm.visibility.visibleFrom": "Bude viditelná od daného data",
376376
"app.editAssignmentForm.visibleFrom": "Datum zveřejnění:",
377+
"app.editAssignmentForm.warnings.alreadyAssigned": "Úloha je v této skupině již zadaná.",
378+
"app.editAssignmentForm.warnings.alreadyAssignedGlobal": "Úloha již byla zadána do některých vybraných skupin. Po potvrzení formuláře bude tedy v některých skupinách úloha zadána vícekrát.",
379+
"app.editAssignmentForm.warnings.canViewJudgeLogs": "Povolení zveřejnění logů sudího studentům sebou nese určitá rizika. U jednoduchých úloh může např. dojít k tomu, že studenti využijí tento kanál, aby získali vstupní data a očekávané výstupy jednotlivých testů a vyrobili triviální řešení, které má správné odpovědi přímo v kódu. Používejte tuto volbu uváživě.",
377380
"app.editAssignmentForm.warnings.chooseFirstDeadlineBeforeSecondDeadline": "Před nastavením druhého termínu odevzdání je nutné zvolit první termín.",
378381
"app.editAssignmentForm.warnings.interpolationGoesToZero": "Limit dosáhne nuly ještě před druhým deadline (viz graf).",
379382
"app.editAssignmentForm.warnings.pointsAreTheSame": "Oba bodové limity jsou stejné, takže není důvod používat nastavení se dvěma termíny odevzdání.",
380383
"app.editAssignmentForm.warnings.secondLimitIsGreaterThanFirstLimit": "Limit je větší než první limit, což je poněkud neobvyklé.",
381384
"app.editAssignmentForm.warnings.secondLimitIsZero": "Limit je nula, takže není důvod používat nastavení se dvěma termíny odevzdání.",
382-
"app.editAssignmentForm.warnings.alreadyAssigned": "Úloha je v této skupině již zadaná.",
383-
"app.editAssignmentForm.warnings.alreadyAssignedGlobal": "Úloha již byla zadána do některých vybraných skupin. Po potvrzení formuláře bude tedy v některých skupinách úloha zadána vícekrát.",
384-
"app.editAssignmentForm.warnings.canViewJudgeLogs": "Povolení zveřejnění logů sudího studentům sebou nese určitá rizika. U jednoduchých úloh může např. dojít k tomu, že studenti využijí tento kanál, aby získali vstupní data a očekávané výstupy jednotlivých testů a vyrobili triviální řešení, které má správné odpovědi přímo v kódu. Používejte tuto volbu uváživě.",
385385
"app.editEnvironmentConfig.duplicateVariable": "Duplicitní jméno proměnné.",
386386
"app.editEnvironmentConfig.noRuntimeSelected": "Aby bylo možné pokračovat se zbytkem konfigurace, nejprve je třeba nastavit běhové prostředí.",
387387
"app.editEnvironmentConfig.noVariables": "Zatím nejsou nastavené žádné proměnné...",
@@ -1121,10 +1121,12 @@
11211121
"app.groupExams.noExam": "V tuto chvíli není naplánovaná žádná zkouška",
11221122
"app.groupExams.pending.studentLockedTitle": "Jste uzamčeném režimu pro probíhající zkoušku",
11231123
"app.groupExams.pending.teacherInfo": "V tuto chvíli jsou zkouškové úlohy viditelné pouze studentům, kteří se uzamkli ve skupině.",
1124+
"app.groupExams.remainingStudentsBoxTitle": "Zbývající studenti (dosud nezamčení)",
11241125
"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.",
11251126
"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.",
11261127
"app.groupExams.studentInfoStrict": "Navíc nebudete moct přistupovat k ostatním skupinám dokud budete v uzamčeném režimu.",
11271128
"app.groupExams.studentsBoxTitle": "Studenti účastnící se zkoušky",
1129+
"app.groupExams.studentsCount": "({count, plural, one {# student} =2 {# studenti} =3 {# studenti} =4 {# studenti} other {# studentů}})",
11281130
"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ě.",
11291131
"app.groupExams.title": "Zkouškové termíny skupiny",
11301132
"app.groupExams.unlockStudentButton": "Odemknout",
@@ -1325,6 +1327,8 @@
13251327
"app.notFound.description": "Oops, tohle jste pravděpodobně nehledali.",
13261328
"app.notFound.text": "Buď jste do adresního řádku dostali špatné (případně staré) URL, nebo máme chybu v generátoru odkazů.",
13271329
"app.notFound.title": "Stránka nenalezena",
1330+
"app.notLockedStudentsTable.lastAuthentication": "Kdy byl uživatel naposledy autenitzován",
1331+
"app.notLockedStudentsTable.noNotLockedStudents": "Nezbývají žádní nezamčení studenti.",
13281332
"app.notifications.hideAll": "Pouze nové notifikace",
13291333
"app.notifications.showAll": "Zobrazit {count, plural, one {jednu starou notifikaci} two {dvě staré notifikace} other {# starých notifikací}}",
13301334
"app.notifications.title": "{count, plural, =0 {Nemáte žádnou novou notifikaci} one {Máte novou notifikaci} =2 {Máte dvě nové notifikace} =3 {Máte tři nové notifikace} =4 {Máte čtyři nové notifikace} other {Máte # nových notifikací}}",
@@ -1763,6 +1767,7 @@
17631767
"app.sidebar.menu.admin.messages": "Systémové zprávy",
17641768
"app.sidebar.menu.admin.server": "Správa serveru",
17651769
"app.sidebar.menu.admin.sis": "SIS integrace [stará]",
1770+
"app.sidebar.menu.admin.title": "Administrátor",
17661771
"app.sidebar.menu.admin.users": "Uživatelé",
17671772
"app.sidebar.menu.archive": "Archiv",
17681773
"app.sidebar.menu.createAccount": "Zaregistrovat se",
@@ -2022,26 +2027,25 @@
20222027
"app.submissions.testResultsTable.statusSkipped": "Přeskočeno",
20232028
"app.submissions.testResultsTable.timeExceeded": "Naměřený čas spuštění",
20242029
"app.submissions.testResultsTable.wallTimeExplain": "Reálný čas (tzv. 'wall-time' měřený systémovými hodinami)",
2025-
"app.submitSolution.instructions": "Musíte připojit alespoň jeden soubor se zdrojovým kódem a počkat, než jsou všechny soubory nahrány na server. Pokud nastane problém při uploadu některých soborů, nahrajte je znovu nebo soubory odeberte. Jméno souboru NESMÍ OBSAHOVAT žádné nestandardní znaky (například v kódování UTF-8).",
2026-
"app.submitSolution.submitFailed": "Odevzdané řešení bylo aktivně odmítnuto. Toto je obvykle způsobeno nahráním souborů se jménem obsahujícím nestandardní znaky nebo špatnou koncovkou. Pokud nemůžete odevzdat řešení bez zjevného důvodu, kontaktujte prosím svého vyučujícího.",
20272030
"app.submitButton.invalid": "Nějaký vstup není validní",
20282031
"app.submitButton.lastError.title": "Nastala chyba",
20292032
"app.submitRefSolution.noteLabel": "Popis referenčního řešení",
20302033
"app.submitRefSolution.title": "Vytvořit referenční řešení",
20312034
"app.submitSolution.emptyNoteSubmitConfirm": "Popis referenčního řešení je prázdný. Je nanejvýš vhodné, aby referenční řešení byla opatřena relevantním popiskem. Opravdu si přejete pokračovat v odevzdání?",
20322035
"app.submitSolution.emptyNoteWarning": "Popis referenčního řešení je prázdný. Je nanejvýš vhodné, aby referenční řešení byla opatřena relevantním popiskem.",
20332036
"app.submitSolution.entryPoint": "Vyberte vstupní bod (zaváděcí soubor vaší aplikace):",
2037+
"app.submitSolution.instructions": "Musíte připojit alespoň jeden soubor se zdrojovým kódem a počkat, než jsou všechny soubory nahrány na server. Pokud nastane problém při uploadu některých soborů, nahrajte je znovu nebo soubory odeberte. Jméno souboru NESMÍ OBSAHOVAT žádné nestandardní znaky (například v kódování UTF-8).",
20342038
"app.submitSolution.limitsExceeded": "Byly překročeny povolené limity souborů",
20352039
"app.submitSolution.limitsExceededCount": "Nemůžete odevzdat více než {limit} {limit, plural, one {soubor} =2 {soubory} =3 {soubory} =4 {soubory} other {souborů}}.",
20362040
"app.submitSolution.limitsExceededSize": "Celková velikost všech odevzdaných souborů nesmí překročit {limit} KiB.",
20372041
"app.submitSolution.linkToWiki": "Vyberte běhové prostředí, pro které je určeno vaše řešení. Více informací o běhových prostředích naleznete na naší <a>wiki stránce</a>.",
20382042
"app.submitSolution.noEnvironments": "Nahrané soubory neodpovídají žádnému povolenému běhovému prostředí.",
20392043
"app.submitSolution.noteLabel": "Poznámka pro vás a vašeho vyučujícího:",
20402044
"app.submitSolution.runtimeEnvironment": "Vyberte běhové prostředí (programovací jazyk):",
2045+
"app.submitSolution.submitFailed": "Odevzdané řešení bylo aktivně odmítnuto. Toto je obvykle způsobeno nahráním souborů se jménem obsahujícím nestandardní znaky nebo špatnou koncovkou. Pokud nemůžete odevzdat řešení bez zjevného důvodu, kontaktujte prosím svého vyučujícího.",
20412046
"app.submitSolution.title": "Odevzdat nové řešení",
20422047
"app.submitSolution.uploadFilesFirst": "Nejprve nahrajte vaše soubory...",
20432048
"app.submitSolution.validating": "Předběžná kontrola nahraných souborů...",
2044-
"app.sidebar.menu.admin.title": "Administrátor",
20452049
"app.supplementaryFiles.cannotDeleteExplain": "Soubor nelze smazat protože je použit v konfiguraci.",
20462050
"app.supplementaryFiles.deleteConfirm": "Opravdu si přejete tento soubor smazat? Tato operace nemůže být vrácena.",
20472051
"app.supplementaryFilesTable.description": "Soubory úlohy jsou soubory, které mohou být použity v nastavení úlohy (jako vstupní soubory, vzorové výstupní soubory, přídavné soubory pro kompilaci, vlastní sudí, ...).",
@@ -2050,10 +2054,10 @@
20502054
"app.systemMessages.failed": "Nepodařilo se načíst seznam systémových zpráv",
20512055
"app.systemMessages.listTitle": "Systémové zprávy",
20522056
"app.systemMessages.loading": "Načítám seznam systémových zpráv...",
2057+
"app.systemMessages.markUnread": "Zobrazit všechny zprávy (označit jako nepřečtené)",
20532058
"app.systemMessages.newSystemMessage": "Nová systémová zpráva",
20542059
"app.systemMessages.title": "Systémové zprávy",
20552060
"app.systemMessages.titleLong": "Máte {totalMessagesCount, number} {totalMessagesCount, plural, one {aktivní zprávu} =2 {aktivní zprávy} =3 {aktivní zprávy} =4 {aktivní zprávy} other {aktivních zpráv}} (z toho {unreadCount, number} {unreadCount, plural, one {nepřečtenou} =2 {nepřečtené} =3 {nepřečtené} =4 {nepřečtené} other {nepřečtených}})",
2056-
"app.systemMessages.markUnread": "Zobrazit všechny zprávy (označit jako nepřečtené)",
20572061
"app.systemMessagesList.noMessages": "Nyní zde nejsou žádné systémové zprávy.",
20582062
"app.systemMessagesList.showAll": "Zobrazit všechny zprávy (včetně expirovaných)",
20592063
"app.systemMessagesList.text": "Text",
@@ -2110,8 +2114,8 @@
21102114
"app.users.takeOver": "Přihlásit jako",
21112115
"app.users.title": "Seznam všech uživatelů",
21122116
"app.users.userCreatedAt": "Uživatel vytvořen",
2113-
"app.usersName.notVerified.title": "Tento účet nemá ověřenou e-mailovou adresu.",
21142117
"app.usersName.notVerified.description": "Tento uživatel si neověřil svou e-mailovou adresu přes aktivační odkaz, který mu byl na tuto adresu zaslán.",
2118+
"app.usersName.notVerified.title": "Tento účet nemá ověřenou e-mailovou adresu.",
21152119
"diff": "Sudí binárních dat",
21162120
"generic.accept": "Akceptovat",
21172121
"generic.accessDenied": "Nemáte oprávnění vidět tuto stránku. Pokud došlo k přesměrování na tuto stránku po kliknutí na zdánlivě legitimní odkaz nebo tlačítko, prosíme nahlaste chybu.",

0 commit comments

Comments
 (0)