Index: openacs-4/packages/proctoring-support/proctoring-support.info
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/proctoring-support/proctoring-support.info,v
diff -u -r1.1.2.28 -r1.1.2.29
--- openacs-4/packages/proctoring-support/proctoring-support.info	14 Feb 2022 16:45:52 -0000	1.1.2.28
+++ openacs-4/packages/proctoring-support/proctoring-support.info	17 Feb 2022 18:28:26 -0000	1.1.2.29
@@ -10,7 +10,7 @@
     <inherit-templates-p>f</inherit-templates-p>
     <auto-mount>proctoring</auto-mount>
 
-    <version name="3.2.2" url="http://openacs.org/repository/download/apm/proctoring-support-3.2.2.apm">
+    <version name="3.3.2" url="http://openacs.org/repository/download/apm/proctoring-support-3.3.2.apm">
         <owner url="mailto:apisano@wu.ac.at">Antonio Pisano</owner>
         <summary>Set of tools to implement proctoring of user interaction</summary>
         <vendor url="https://www.wu.ac.at/">Wirtschaftsuniversität Wien</vendor>
@@ -21,7 +21,7 @@
 No real UI is provided by the package itself. Other packages must integrate the provided includes.</description>
         <maturity>0</maturity>
 
-        <provides url="proctoring-support" version="3.2.2"/>
+        <provides url="proctoring-support" version="3.3.2"/>
         <requires url="acs-templating" version="5.10.0"/>
         <requires url="xotcl-core" version="5.10.0"/>
 
Index: openacs-4/packages/proctoring-support/catalog/proctoring-support.de_DE.ISO-8859-1.xml
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/proctoring-support/catalog/proctoring-support.de_DE.ISO-8859-1.xml,v
diff -u -r1.1.2.16 -r1.1.2.17
--- openacs-4/packages/proctoring-support/catalog/proctoring-support.de_DE.ISO-8859-1.xml	30 Aug 2021 15:12:00 -0000	1.1.2.16
+++ openacs-4/packages/proctoring-support/catalog/proctoring-support.de_DE.ISO-8859-1.xml	17 Feb 2022 18:28:26 -0000	1.1.2.17
@@ -97,6 +97,8 @@
 	&lt;li&gt;die Pr�fung mit dieser Best�tigung entgegennehme, d.h. es erfolgt eine Beurteilung und dieser Antritt wird auf die Gesamtzahl der Wiederholungen angerechnet.&lt;/li&gt;
 &lt;/ul&gt;</msg>
   <msg key="Examination_Statement">Pr�fungserkl�rung</msg>
+  <msg key="flag_artifact_label">Zur �berpr�fung markieren</msg>
+  <msg key="flagged_label">Markiert</msg>
   <msg key="grant_access_to_camera_msg">Erlauben Sie den Zugriff auf Ihre Kamera, wenn Ihr Browser Sie dazu auffordert. Haben Sie den Zugriff erlaubt, wird Ihnen eine Vorschau angezeigt. Stellen Sie sicher, dass ihr Gesicht im Vorschaubild gut zu erkennen ist</msg>
   <msg key="grant_access_to_camera_title">Zugriff auf Ihre Kamera erlauben</msg>
   <msg key="grant_access_to_desktop_msg">Sie werden von Ihrem Browser aufgefordert, den Zugriff auf Ihren Bildschirm zu erlauben. Bitte w�hlen Sie zuerst den gesamten Bildschirm aus und best�tigen Sie die Erlaubnis �ber den Button &#34;Teilen&#34;. Ihnen wird eine Vorschau Ihres Bildschirms angezeigt.</msg>
@@ -109,6 +111,7 @@
   <msg key="microphone_volume_is_too_low">Die Tonaufnahme des Mikrofons ist unzureichend. M�gliche Ursachen sind:&lt;ul&gt;&lt;li&gt;&lt;b&gt;Die Lautst�rke des Mikrofons ist zu leise eingestellt&lt;/b&gt;: Schalten Sie die Eingangslautst�rke lauter. (Windows) �ffnen Sie die Systemsteuerung Ihres Computers und gehen Sie zu\&#34;Sound\&#34; | \&#34;Eingabe\&#34; | \&#34;Ger�teeigenschaften\&#34;. Passen Sie hier die Lautst�rke an. (macOS)Klicken Sie auf \&#34;Systemeinstellungen\&#34; | \&#34;Ton\&#34; und �ffnen Sie den Reiter \&#34;Eingabe\&#34;. Passen Sie bei Ihrem Mikrofon die Eingangslautst�rke an.&lt;/li&gt;&lt;li&gt;&lt;b&gt;Das Mikrofon ist stummgeschaltet&lt;/b&gt;: Heben Sie die Stummschaltung auf. Kontrollieren Sie die entsprechenden Tastaturbefehle und alle hardwareseitigen Einstellungen wie z.B. die Stummschaltung direkt in den Soundeinstellungen oder am Mikrofon/Headset selbst.&lt;/li&gt;&lt;/ul&gt;</msg>
   <msg key="missing_stream_message">Die Online-Aufsicht wurde beendet, ist f�r diese Pr�fung aber noch aktiviert. Bitte schlie�en Sie Google Chrome, um die Pr�fungsumgebung ganz zu verlassen, oder starten Sie die automatisierte Online-Aufsicht wieder mit einem Klick auf \&#34;OK\&#34;. Damit betreten Sie die Pr�fungsumgebung erneut.&lt;br&gt;&lt;br&gt;Wenn Sie �ber dieses Problem melden, inkludieren Sie bitte folgenden Text: </msg>
   <msg key="mobile_devices_are_unsupported">Mobile Ger�te werden nicht unterst�tzt.</msg>
+  <msg key="not_reviewed_label">Nicht �berpr�ft</msg>
   <msg key="OnlineExam">Online-Pr�fung</msg>
   <msg key="Preview">Vorschau</msg>
   <msg key="preview_help_text">Falls aktiviert, wird den Benutzern w�hrend der Online-Aufsichtssitzung eine Vorschau der aufgezeichneten Eingaben angezeigt.</msg>
@@ -117,7 +120,12 @@
   <msg key="Record_camera">Kamera aufnehmen</msg>
   <msg key="Record_desktop">Desktop aufnehmen</msg>
   <msg key="recordings">Aufzeichnungen</msg>
+  <msg key="reviewed_label">�berpr�ft</msg>
   <msg key="Start_date">Anfangsdatum</msg>
+  <msg key="unflag_artifact_label">Als OK best�tigen</msg>
+  <msg key="unflagged_label">Best�tigt OK</msg>
+  <msg key="user_has_flagged_this_artifact_msg">Dieses Objekt wurde zur �berpr�fung vorgemerkt</msg>
+  <msg key="user_has_unflagged_this_artifact_msg">Dieses Objekt wurde best�tigt</msg>
   <msg key="user_photo">Foto</msg>
   <msg key="wizard_finish">Abschlie�en</msg>
   <msg key="wrong_display_surface_selected">Der Desktop kann nicht aufgezeichnet werden, da Sie den falschen Bildschirmausschnitt ausgew�hlt haben. W�hlen Sie \&#34;gesamter Bildschirm\&#34; aus, klicken Sie auf den Bildschirm und w�hlen Sie dann \&#34;Teilen\&#34;.</msg>
Index: openacs-4/packages/proctoring-support/catalog/proctoring-support.en_US.ISO-8859-1.xml
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/proctoring-support/catalog/proctoring-support.en_US.ISO-8859-1.xml,v
diff -u -r1.1.2.20 -r1.1.2.21
--- openacs-4/packages/proctoring-support/catalog/proctoring-support.en_US.ISO-8859-1.xml	30 Aug 2021 15:12:00 -0000	1.1.2.20
+++ openacs-4/packages/proctoring-support/catalog/proctoring-support.en_US.ISO-8859-1.xml	17 Feb 2022 18:28:26 -0000	1.1.2.21
@@ -99,6 +99,8 @@
 	&lt;li&gt;I accept receipt of this examination, i.e. I acknowledge that the exam will be graded and will count towards my total number of examination attempts&lt;/li&gt;
 &lt;/ul&gt;</msg>
   <msg key="Examination_Statement">Examination Statement</msg>
+  <msg key="flag_artifact_label">Flag for review</msg>
+  <msg key="flagged_label">Flagged</msg>
   <msg key="grant_access_to_camera_msg">If your browser prompts you to give access to the camera, please do so. Once permission has been granted, a preview image will appear. Please make sure your face is clearly visible in the picture before proceeding.</msg>
   <msg key="grant_access_to_camera_title">Grant access to your camera</msg>
   <msg key="grant_access_to_desktop_msg">You are being prompted to give access to your desktop. Please first select &#34;Your Entire Screen&#34; and grant the access thereafter by clicking the button &#34;Share&#34;. On success you should see a preview of your desktop on the page.</msg>
@@ -112,6 +114,7 @@
   <msg key="microphone_volume_is_too_low">The microphone&#39;s sound recording is insufficient. Possible causes are:&lt;ul&gt;&lt;li&gt;&lt;b&gt;The volume of the microphone is set too low&lt;/b&gt;: Turn up the input volume. (Windows) Open your computer&#39;s control panel and go to \&#34;Sound\&#34; | \&#34;Input\&#34; | \&#34;Device properties\&#34;. Adjust the volume here. (macOS) Click on \&#34;System Preferences\&#34; | \&#34;Sound\&#34; and open the \&#34;Input\&#34; tab.Adjust the input volume on your microphone.&lt;/li&gt;&lt;li&gt;&lt;b&gt;The microphone is muted&lt;/b&gt;: Unmute it. Check the corresponding keyboard commands and all hardware settings such as the mute function directly in the sound settings or on the microphone / headset itself.&lt;/li&gt;&lt;/ul&gt;</msg>
   <msg key="missing_stream_message">Online Supervision has ended. However, it is still activated for this exam. Please close Google Chrome to completely exit the exam environment. Alternatively, you can restart the automated online supervision: By clicking \&#34;OK\&#34;, you are re-entering the exam environment.&lt;br&gt;&lt;br&gt;When reporting this issue, please mention the following message: </msg>
   <msg key="mobile_devices_are_unsupported">Mobile devices are unsupported.</msg>
+  <msg key="not_reviewed_label">Not reviewed</msg>
   <msg key="OnlineExam">Online Exam</msg>
   <msg key="Preview">Preview</msg>
   <msg key="preview_help_text">If enabled, a preview of recorded input will be displayed to users during the online supervision session.</msg>
@@ -122,12 +125,17 @@
   <msg key="recordings">Recordings</msg>
   <msg key="request_failed">Request to the server has failed. Retry in 10s...</msg>
   <msg key="request_timeout">Request to the server timed out. Retry in 10s...</msg>
+  <msg key="reviewed_label">Reviewed</msg>
   <msg key="Safe_Exam_Browser">Safe Exam Browser</msg>
   <msg key="Safe_Exam_Browser_File">Safe Exam Browser .seb file</msg>
   <msg key="Safe_Exam_Browser_File_help_text">.seb file generated via the Safe Exam Browser configuration tool that holds the restriction applied to this exam session.</msg>
   <msg key="Safe_Exam_Browser_Key">Safe Exam Browser key</msg>
   <msg key="Safe_Exam_Browser_Key_help_text">Encryption key generated via the Safe Exam Browser configuration tool that will be used to ensure students are applying supplied configuration to their exam session.</msg>
   <msg key="Start_date">Start date</msg>
+  <msg key="unflag_artifact_label">Confirm OK</msg>
+  <msg key="unflagged_label">Confirmed OK</msg>
+  <msg key="user_has_flagged_this_artifact_msg">This artifact has been flagged for revision</msg>
+  <msg key="user_has_unflagged_this_artifact_msg">This artifact has been confirmed OK</msg>
   <msg key="user_photo">Photo</msg>
   <msg key="wizard_finish">Finish</msg>
   <msg key="wrong_display_surface_selected">The screen cannot be recorded because you selected the wrong screen area. Select \&#34;Your Entire Screen\&#34; by clicking on the screen and then on \&#34;Share\&#34;.</msg>
Index: openacs-4/packages/proctoring-support/catalog/proctoring-support.it_IT.ISO-8859-1.xml
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/proctoring-support/catalog/proctoring-support.it_IT.ISO-8859-1.xml,v
diff -u -r1.1.2.3 -r1.1.2.4
--- openacs-4/packages/proctoring-support/catalog/proctoring-support.it_IT.ISO-8859-1.xml	14 Feb 2022 16:45:52 -0000	1.1.2.3
+++ openacs-4/packages/proctoring-support/catalog/proctoring-support.it_IT.ISO-8859-1.xml	17 Feb 2022 18:28:26 -0000	1.1.2.4
@@ -20,6 +20,8 @@
   <msg key="Exam_mode">Modalit� esame</msg>
   <msg key="Exam_mode_message">&lt;h4&gt;Dichiarazione d&#39;esame: Informazioni importanti per gli esami online&lt;/h4&gt; &lt;p&gt; &lt;/p&gt; &lt;h5&gt;1) Partecipazione&lt;/h5&gt; &lt;p&gt;Senza eccezioni, solo gli studenti che sono ufficialmente iscritti al corso e/o all&#39;esame potranno sostenere l&#39;esame. &lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;L&#39;esame sar� valutato solo se tutte e 3 le seguenti condizioni sono state soddisfatte:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Ha caricato una foto che soddisfa i criteri necessari per confermare la sua identit�, se tale requisito di identificazione � stato precedentemente annunciato per l&#39;esame in questione dalla persona responsabile dell&#39;esame. &lt;/li&gt; &lt;li&gt;La supervisione automatica  ha funzionato correttamente durante l&#39;esame, se tale requisito � stato precedentemente annunciato per questo esame&lt;/li&gt; &lt;li&gt;Ha confermato di aver letto e compreso questa dichiarazione d&#39;esame&lt;/li&gt; &lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt; &lt;p&gt;Con la conferma della dichiarazione d&#39;esame, lei conferma anche la ricezione dell&#39;esame. Se ha ricevuto l&#39;esame ma non ha soddisfatto il requisito dell&#39;identit� e/o la supervisione automatica online non ha funzionato correttamente durante l&#39;esame, l&#39;esame sar� dichiarato NULLO e sar� considerato un tentativo di esame. Se non conferma la dichiarazione d&#39;esame, non le sar� dato accesso all&#39;esame. Questo significa che l&#39;esame non sar� valutato e il tentativo d&#39;esame non sar� conteggiato. &lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;h5&gt;2) Requisiti tecnici&lt;/h5&gt; &lt;p&gt;� vostra responsabilit� assicurarvi che non sarete disturbati durante l&#39;esame e che tutti i requisiti tecnici (come precedentemente annunciato) siano soddisfatti (vedi &#34;Informazioni su questo esame online&#34; e/o &#34;Lista di controllo tecnico&#34; nel vostro ambiente d&#39;esame online). L&#39;Universit� non pu� garantire il completamento dell&#39;esame senza problemi per ogni studente che lavora sul suo dispositivo elettronico individuale. &lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;h5&gt;3) Iniziare e terminare/interrompere l&#39;esame&lt;/h5&gt; &lt;p&gt;Con la conferma di aver letto e compreso questa dichiarazione d&#39;esame, lei conferma anche di aver ricevuto l&#39;esame, e il tentativo verr� conteggiato. L&#39;esame sar� valutato e conteggiato nel numero totale di tentativi d&#39;esame consentiti. Questo vale anche se si termina l&#39;esame prematuramente o non si presenta l&#39;esame completato. &lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;Se siete costretti a terminare prematuramente l&#39;esame o a interromperlo a causa di difficolt� tecniche (per esempio, perdita della connessione internet), contattate immediatamente la persona responsabile dell&#39;esame. Per farlo, si prega di utilizzare il canale di comunicazione precedentemente annunciato a tale scopo dalla persona responsabile dell&#39;esame. Quando segnalate la fine/interruzione del vostro esame, assicuratevi di includere le seguenti informazioni:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Il vostro numero ID dello studente&lt;/li&gt; &lt;li&gt;L&#39;ora esatta della fine/interruzione&lt;/li&gt; &lt;li&gt;Screenshot del messaggio di errore, se applicabile&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Se siete in grado di risolvere il problema e continuare l&#39;esame, segnalatelo con il messaggio &#34;Esame ripreso&#34;. Per poter inviare tali segnalazioni in caso di problemi tecnici, si raccomanda di scaricare le applicazioni appropriate sui propri dispositivi mobili prima dell&#39;inizio dell&#39;esame. In questo modo, potrete per esempio segnalare la fine/interruzione dell&#39;esame anche se la vostra connessione Wi-Fi non funziona. &lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;Nei casi in cui lei segnala l&#39;interruzione dell&#39;esame a causa di problemi tecnici indipendenti dalla sua volont�, come regola generale, l&#39;esame non viene valutato e il tentativo d&#39;esame non viene conteggiato. Se si desidera che l&#39;esame venga comunque valutato, si prega di contattare la persona responsabile dell&#39;esame immediatamente dopo l&#39;esame e richiedere che l&#39;esame venga valutato. L&#39;esame pu� essere valutato solo se tutti i requisiti applicabili per la valutazione sono soddisfatti (ad esempio, la supervisione automatica online ha funzionato correttamente, se applicabile). Saranno valutate solo le parti dell&#39;esame che sono state completate senza problemi.&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;h5&gt;4) Imbroglio e conferma dell&#39;identit�&lt;/h5&gt; &lt;p&gt;Ogni tentativo di imbrogliare durante l&#39;esame (ad esempio con il cellulare, consultando materiali proibiti, consultando altre persone) comporter� che l&#39;esame sia dichiarato NULLO e il tentativo di esame conteggiato. Vi sar� inoltre impedito di registrarvi nuovamente per ripetere l&#39;esame per un periodo di 4 mesi a partire dalla data dell&#39;esame. &lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;Far fare l&#39;esame a qualcun altro al posto vostro pu� costituire un reato penale. Tali casi di imbroglio saranno denunciati alla Procura della Repubblica, senza eccezioni.&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt; &lt;p&gt;Se annunciato in anticipo dalla persona responsabile dell&#39;esame, la supervisione automatizzata dell&#39;esame online sar� condotta per tutta la durata dell&#39;esame. Questo significa che lei e il suo schermo saranno monitorati da una telecamera e da un microfono (video e audio) durante tutto l&#39;esame, e l&#39;esaminatore potr� vedere e sentire le registrazioni. Appena inizia l&#39;esame, dovr� dare al suo browser il permesso di accedere al suo schermo, alla webcam e al microfono. Qualsiasi tentativo di manipolare la supervisione dell&#39;esame online sar� considerato un tentativo di imbroglio.&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;h5&gt;5) Aiuti consentiti&lt;/h5&gt; &lt;p&gt;Quando si partecipa a questo esame, si possono usare solo gli aiuti esplicitamente elencati o non esplicitamente vietati nelle informazioni su questo esame specifico sotto &#34;Esame online&#34; dalla persona responsabile dell&#39;esame. Di regola, nessun altro pu� essere presente nella stessa stanza con lei mentre lavora all&#39;esame. &lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;&lt;b&gt;Riconoscimento delle informazioni sulla ricezione e il completamento dell&#39;esame &lt;/b&gt;&lt;/p&gt; &lt;p&gt; Confermo che&lt;/p&gt; &lt;ul&gt; &lt;li&gt;ho letto e compreso le informazioni fornite sopra sull&#39;esame scritto online&lt;/li&gt; &lt;li&gt;sono in possesso di tutti i requisiti per sostenere questo esame&lt;/li&gt; &lt;li&gt;completer� l&#39;esame autonomamente e senza consultare alcun ausilio proibito&lt;/li&gt; &lt;li&gt;ho letto e compreso le informazioni su questo esame specifico sotto &#34;Esame online&#34;&lt;/li&gt; &lt;li&gt;accetto di sostenere questo esame, e accetto che l&#39;esame sia valutato e conti come uno dei tentativi a me consentiti.&lt;/li&gt; &lt;/ul&gt;</msg>
   <msg key="Examination_Statement">Dichiarazione d&#39;esame</msg>
+  <msg key="flag_artifact_label">Segna per la revisione</msg>
+  <msg key="flagged_label">Segnati</msg>
   <msg key="grant_access_to_camera_msg">Se il browser chiede accesso alla telecamera, ti preghiamo di accettare. Una volta che il permesso � stato concesso, apparir� una anteprima. Assicurati che il tuo volto sia chiaramente visibile nel riquadro prima di procedere.</msg>
   <msg key="grant_access_to_camera_title">Concedi l&#39;accesso alla tua telecamera</msg>
   <msg key="grant_access_to_desktop_msg">Ti viene chiesto di dare accesso al tuo desktop. Seleziona &#34;Schermo intero&#34; e concedi l&#39;accesso cliccando il tasto &#34;Condividi&#34;. Al termine dovresti vedere una anteprima del tuo desktop sulla pagina.</msg>
@@ -33,6 +35,7 @@
   <msg key="microphone_volume_is_too_low">Il suono del microfono � insufficiente. Le possibili cause sono:&lt;ul&gt;&lt;li&gt;&lt;b&gt;Il volume del microfono � troppo basso&lt;/b&gt;: Alza il volume di ingresso. (Windows) Apri il pannello di controllo del tuo computer e vai su \&#34;Suono\&#34; | \&#34;Ingresso\&#34;. | \&#34;Propriet� dei dispositivi\&#34;. Regola qui il volume. (macOS) Clicca su \&#34;Preferenze di sistema\&#34; | (macOS) Clicca su \&#34;Preferenze di sistema\&#34;, \&#34;Suono\&#34; e apri la scheda \&#34;Ingresso\&#34;. Regola il volume del tuo microfono.&lt;/li&gt;&lt;li&gt;&lt;b&gt;Il microfono � silenziato&lt;/b&gt;: Disattivarlo. Controlla i comandi da tastiera corrispondenti e tutte le impostazioni hardware come la funzione mute direttamente nelle impostazioni audio o sul microfono/cuffia stessa.&lt;/li&gt;&lt;/ul&gt;</msg>
   <msg key="missing_stream_message">La supervisione online � terminata. Tuttavia, � ancora attiva per questo esame. Si prega di chiudere Google Chrome per uscire completamente dall&#39;ambiente d&#39;esame. In alternativa, puoi riavviare la supervisione online automatica: Cliccando su \&#34;OK\&#34;, stai rientrando nell&#39;ambiente d&#39;esame.&lt;br&gt;&lt;br&gt;Nel segnalare questo problema, menziona il seguente messaggio:</msg>
   <msg key="mobile_devices_are_unsupported">I dispositivi movili non sono supportati.</msg>
+  <msg key="not_reviewed_label">Non verificati</msg>
   <msg key="OnlineExam">Esame Online</msg>
   <msg key="Preview">Anteprima</msg>
   <msg key="preview_help_text">Se abilitato, una anteprima dell&#39;input registrato sar� mostrata all&#39;utente durante la sessione di supervisione online.</msg>
@@ -43,12 +46,17 @@
   <msg key="recordings">Registrazioni</msg>
   <msg key="request_failed">La richiesta al server � fallita. Nuovo tentativo in 10s...</msg>
   <msg key="request_timeout">La richiesta al server � andata in timeout. Nuovo tentativo in 10s...</msg>
+  <msg key="reviewed">Verificati</msg>
   <msg key="Safe_Exam_Browser">Safe Exam Browser</msg>
   <msg key="Safe_Exam_Browser_File">File .seb per Safe Exam Browser</msg>
   <msg key="Safe_Exam_Browser_File_help_text">File .seb generato tramite lo strumento di configurazione del Safe Exam Browser che contiene i vincoli applicati a questa sessione d&#39;esame.</msg>
   <msg key="Safe_Exam_Browser_Key">Chiave per Safe Exam Browser</msg>
   <msg key="Safe_Exam_Browser_Key_help_text">Chiave di cifratura generata tramite lo strumento di configurazione del Safe Exam Browser che sar� usata per verificare che gli studenti stiano davvero applicando la configurazione specificata alla loro sessione d&#39;esame.</msg>
   <msg key="Start_date">Data inizio</msg>
+  <msg key="unflag_artifact_label">Segna come OK</msg>
+  <msg key="unflagged_label">Confermati OK</msg>
+  <msg key="user_has_flagged_this_artifact_msg">Questo artefatto � stato segnalato per la revisione</msg>
+  <msg key="user_has_unflagged_this_artifact_msg">Questo artefatto � stato confermato OK</msg>
   <msg key="user_photo">Foto</msg>
   <msg key="wizard_finish">Fine</msg>
   <msg key="wrong_display_surface_selected">Lo schermo non pu� essere registrato perch� hai selezionato l&#39;area dello schermo sbagliata. Seleziona \&#34;Tutto lo schermo\&#34; cliccando sullo schermo e poi su \&#34;Condividi\&#34;.</msg>
Index: openacs-4/packages/proctoring-support/lib/proctoring-display.adp
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/proctoring-support/lib/proctoring-display.adp,v
diff -u -r1.1.2.5 -r1.1.2.6
--- openacs-4/packages/proctoring-support/lib/proctoring-display.adp	15 Feb 2022 15:14:18 -0000	1.1.2.5
+++ openacs-4/packages/proctoring-support/lib/proctoring-display.adp	17 Feb 2022 18:28:26 -0000	1.1.2.6
@@ -3,7 +3,159 @@
     <property name="doc(title)">#proctoring-support.Proctoring#</property>
 </if>
 
+<style>
+/* The Modal (background) */
+.modal {
+    display: none; /* Hidden by default */
+    position: fixed; /* Stay in place */
+    z-index: 1; /* Sit on top */
+    padding-top: 100px; /* Location of the box */
+    left: 0;
+    top: 0;
+    width: 100%; /* Full width */
+    height: 100%; /* Full height */
+    overflow: auto; /* Enable scroll if needed */
+    background-color: rgb(0,0,0); /* Fallback color */
+    background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
+}
+
+/* Modal Content */
+.modal-content {
+    background-color: #fefefe;
+    margin: auto;
+    padding: 20px;
+    border: 1px solid #888;
+    width: 80%;
+}
+
+/* The Close Button */
+.close {
+    color: #aaaaaa;
+    float: right;
+    font-size: 28px;
+    font-weight: bold;
+}
+
+.close:hover,
+.close:focus {
+    color: #000;
+    text-decoration: none;
+    cursor: pointer;
+}
+
+/* Responsive events list */
+
+* {
+    box-sizing: border-box;
+}
+
+.flex-container {
+    display: flex;
+    flex-wrap: wrap;
+    padding: 10px;
+    margin-bottom:10px;
+    border: 2px #eee solid;
+    border-radius: 8px;
+}
+
+.flex-container.flagged {
+    border-color: #a94442;
+}
+
+[name='revision'].flagged {
+    color: #a94442;
+}
+
+.flex-container img {
+    max-width:100%;
+}
+
+[class*="flex-"] {
+    flex: 100%;
+    margin-bottom: 10px;
+}
+
+@media only screen and (min-width: 768px) {
+  /* For desktop: */
+  .flex-1 {flex: 8.33%;}
+  .flex-2 {flex: 16.66%;}
+  .flex-3 {flex: 25%;}
+  .flex-4 {flex: 33.33%;}
+  .flex-5 {flex: 41.66%;}
+  .flex-6 {flex: 50%;}
+  .flex-7 {flex: 58.33%;}
+  .flex-8 {flex: 66.66%;}
+  .flex-9 {flex: 75%;}
+  .flex-10 {flex: 83.33%;}
+  .flex-11 {flex: 91.66%;}
+  .flex-12 {flex: 100%;}
+}
+
+</style>
+
+<!-- The Modal -->
+<div id="modal" class="modal">
+  <!-- Modal content -->
+  <div class="modal-content">
+    <span class="close">&times;</span>
+    <form>
+      <input name="artifact_id" type="hidden">
+      <div class="form-group">
+        <label for="comment">#acs-subsite.Comment#</label>
+        <textarea id="comment" name="comment" class="form-control" required></textarea>
+      </div>
+      <div class="checkbox">
+        <label>
+          <input name="flag" type="checkbox">#proctoring-support.flag_artifact_label#
+        </label>
+      </div>
+      <button type="submit" class="btn btn-default">#acs-kernel.common_Save#</button>
+    </form>
+  </div>
+</div>
+
 <script <if @::__csp_nonce@ not nil>nonce="@::__csp_nonce@"</if>>
+
+    // Get references to the modal
+    var modal = document.getElementById("modal");
+    var modalIdElement = modal.querySelector('[name="artifact_id"]');
+
+    document.querySelector("#modal form").addEventListener('submit', function (e) {
+        e.preventDefault();
+        var request = new XMLHttpRequest();
+        var form = this;
+        request.addEventListener("loadend", function () {
+            if (this.status === 200) {
+                updateArtifactComments(modalIdElement.value, this.response);
+                form.reset();
+                closeModal();
+            } else {
+                alert(this.response);
+            }
+        });
+        request.open("POST", '@proctoring_url@/review');
+        request.send(new FormData(form));
+    });
+
+    function closeModal() {
+        modal.style.display = "none";
+    }
+    function openReview(e) {
+        var artifactId = this.getAttribute('data-artifact-id');
+        modalIdElement.value = artifactId;
+        modal.style.display = "block";
+    };
+
+    // When the user clicks on <span> (x), close the modal
+    document.querySelector('#modal .close').addEventListener('click', closeModal);
+
+    // When the user clicks anywhere outside of the modal, close it
+    window.addEventListener('click', function(event) {
+        if (event.target == modal) {
+            closeModal();
+        }
+    });
+
     function initWS(URL, onMessage) {
         if ("WebSocket" in window) {
             websocket = new WebSocket(URL);
@@ -123,35 +275,264 @@
         <p><a href="@back_url@" class="btn btn-default">#xowiki.back#</a></p>
 
         <h3 style="margin-top:1em;">#proctoring-support.recordings#</h3>
-
-        <ul class="list-group" id="event-list">
+        <div class="radio">
+           <label><input type="radio" name="only" value="reviewed">#proctoring-support.reviewed_label#</label>
+           <label><input type="radio" name="only" value="not-reviewed">#proctoring-support.not_reviewed_label#</label>
+        </div>
+        <div class="radio">
+          <label><input type="radio" name="only" value="flagged">#proctoring-support.flagged_label#</label>
+          <label><input type="radio" name="only" value="unflagged">#proctoring-support.unflagged_label#</label>
+        </div>
+        <div class="radio">
+          <label><input type="radio" name="only" value="all" checked>#acs-kernel.common_All#</label>
+        </div>
+        <div>
+          <span id="total-shown">@total@</span>/<span id="total">@total@</span>
+        </div>
+        <script <if @::__csp_nonce@ not nil>nonce="@::__csp_nonce@"</if>>
+          var radioFilters = document.querySelectorAll("[name='only']");
+          function hideFiltered() {
+              var total = 0;
+              var totalShown = 0;
+              var filterValue = 'all';
+              for (r of radioFilters) {
+                  if (r.checked) {
+                      filterValue = r.value;
+                  }
+              }
+              for (e of document.querySelectorAll("#event-list [name='data']")) {
+                  if (filterValue === 'all') {
+                      e.style.display = null;
+                  } else if (filterValue === 'flagged') {
+                      e.style.display = e.classList.contains('flagged') ? null : 'none';
+                  } else if (filterValue === 'unflagged') {
+                      e.style.display = e.classList.contains('flagged') ? 'none': null;
+                  } else if (filterValue === 'reviewed') {
+                      e.style.display = e.querySelector("[data-msg]") ? null : 'none';
+                  } else if (filterValue === 'not-reviewed') {
+                      e.style.display = e.querySelector("[data-msg]") ? 'none' : null;
+                  }
+                  total++;
+                  if (!e.style.display) {
+                      totalShown++;
+                  }
+              }
+              document.querySelector("#total").textContent = total;
+              document.querySelector("#total-shown").textContent = totalShown;
+          }
+          for (r of radioFilters) {
+              r.addEventListener('change', function(e) {
+                  if (this.checked) {
+                      hideFiltered();
+                  }
+              });
+          }
+        </script>
+        <div id="event-list">
           <multiple name="events">
-            <li class="list-group-item" >
-              <h3 name="title">@events.timestamp@</h3>
-
-              <div name="data">
-                <if @events.camera_url@ ne "">
-                  <span name="camera"><img class="lazy" data-src="@events.camera_url@"></span>
-                </if>
-                <if @events.desktop_url@ ne "">
-                  <span name="desktop"><img class="lazy" data-src="@events.desktop_url@"></span>
-                </if>
-                <if @events.audio_url@ ne "">
-                  <span name="audio"><audio class="lazy" data-src="@events.audio_url@" controls></span>
-                </if>
-                <script <if @::__csp_nonce@ not nil>nonce="@::__csp_nonce@"</if>>
-                </script>
+            <div name="data" class="flex-container">
+              <div class="flex-12">
+                <h3 name="title">@events.timestamp@</h3>
               </div>
-            </li>
+              <div class="flex-12">
+                <button class="flag-all btn btn-danger"
+                        data-artifact-id="@events.artifact_id@">
+                  #proctoring-support.flag_artifact_label#
+                </button>
+                <button class="unflag-all btn btn-success"
+                        data-artifact-id="@events.artifact_id@">
+                  #proctoring-support.unflag_artifact_label#
+                </button>
+              </div>
+              <if @events.camera_url@ ne "">
+                <span name="camera" class="flex-3">
+                  <img class="lazy" data-src="@events.camera_url@">
+                </span>
+              </if>
+              <if @events.desktop_url@ ne "">
+                <span name="desktop" class="flex-9">
+                  <img class="lazy" data-src="@events.desktop_url@">
+                </span>
+              </if>
+              <if @events.audio_url@ ne "">
+                <span name="audio" class="flex-12">
+                  <audio class="lazy" data-src="@events.audio_url@" controls></audio>
+                </span>
+              </if>
+              <div class="flex-12" name="revisions" data-revisions="@events.revisions@">
+                <div name="revision" class="flex-12" style="display:none;">
+                  <button class="delete btn btn-warning"
+                          data-artifact-id="@events.artifact_id@">
+                    #acs-kernel.common_Delete#
+                  </button>
+                  <span name="timestamp"></span>
+                  -
+                  <span name="author"></span>
+                  :
+                  <span name="comment"></span>
+                </div>
+                <button class="comment btn btn-default"
+                        data-artifact-id="@events.artifact_id@">
+                  #acs-subsite.Comment#
+                </button>
+              </div>
+            </div>
           </multiple>
-        </ul>
+        </div>
+        <script <if @::__csp_nonce@ not nil>nonce="@::__csp_nonce@"</if>>
+          function updateArtifactComments(artifactId, data) {
+              var button = document.
+                  querySelector(".comment[data-artifact-id='" + artifactId + "']");
+              if (button) {
+                  var revisions = button.parentElement;
+                  revisions.setAttribute("data-revisions", data);
+                  renderArtifactComments(revisions);
+              }
+          }
+          function flag(e, flag=true) {
+              e.preventDefault();
+              var artifactId = e.target.getAttribute('data-artifact-id');
+              var request = new XMLHttpRequest();
+              request.addEventListener("loadend", function () {
+                  if (this.status === 200) {
+                      updateArtifactComments(artifactId, this.response);
+                  } else {
+                      alert(this.response);
+                  }
+              });
+              var formData = new FormData();
+              formData.append('artifact_id', artifactId);
+              formData.append('flag', flag);
+              request.open("POST", '@proctoring_url@/review');
+              request.send(formData);
+          }
+          function unFlag(e) {
+              flag(e, false);
+          }
+          function deleteArtifactComment(e) {
+              e.preventDefault();
+              if (!confirm(`#acs-templating.Are_you_sure#`)) {
+                  return
+              }
+              var deleteButton = e.target;
+              var artifactId = deleteButton.getAttribute('data-artifact-id');
+              var record = deleteButton.getAttribute("data-record");
+              var request = new XMLHttpRequest();
+              request.addEventListener("loadend", function () {
+                  if (this.status === 200) {
+                      updateArtifactComments(artifactId, this.response);
+                  } else {
+                      alert(this.response);
+                  }
+              });
+              var formData = new FormData();
+              formData.append('artifact_id', artifactId);
+              formData.append('deleted_record', record);
+              request.open("POST", '@proctoring_url@/review');
+              request.send(formData);
+          }
+          function renderArtifactComments(e) {
+              // Cleanup
+              for (c of e.querySelectorAll("div[name='revision'][data-msg]")) {
+                  e.removeChild(c);
+              }
+              var revisions = e.getAttribute('data-revisions');
+              var isFlagged = false;
+              if (revisions !== '') {
+                  for (r of JSON.parse(decodeURIComponent(revisions))) {
+
+                      var revision = e.firstElementChild.cloneNode(true);
+                      revision.style.display = null;
+                      revision.setAttribute("data-msg", true);
+
+                      var timestamp = revision.querySelector("[name='timestamp']");
+                      timestamp.style.display = null;
+                      timestamp.textContent = r.timestamp;
+
+                      var author = revision.querySelector("[name='author']");
+                      author.style.display = null;
+                      author.textContent = r.author;
+
+                      var comment = revision.querySelector("[name='comment']");
+                      comment.style.display = null;
+                      comment.textContent = r.comment;
+
+                      if (r.flag !== 'false') {
+                          isFlagged = true;
+                      }
+
+                      var deleteButton = revision.querySelector(".delete");
+                      deleteButton.setAttribute("data-record", JSON.stringify(r));
+                      deleteButton.addEventListener("click", deleteArtifactComment);
+
+                      e.insertBefore(revision, e.lastElementChild);
+                  }
+              }
+              if (isFlagged) {
+                  e.parentElement.classList.add("flagged");
+              } else {
+                  e.parentElement.classList.remove("flagged");
+              }
+              hideFiltered();
+          }
+          for (e of document.querySelectorAll("[name='revisions']")) {
+              renderArtifactComments(e);
+          }
+        </script>
         <div id="template" style="display:none;">
-          <li class="list-group-item" >
-            <h3 name="title"></h3>
-            <div name="data"></div>
-          </li>
+          <div name="data" class="flex-container">
+            <div class="flex-12">
+              <h3 name="title"></h3>
+            </div>
+            <div class="flex-12">
+              <button class="flag-all btn btn-danger"
+                      data-artifact-id="">
+                #proctoring-support.flag_artifact_label#
+              </button>
+              <button class="unflag-all btn btn-success"
+                      data-artifact-id="">
+                #proctoring-support.unflag_artifact_label#
+              </button>
+            </div>
+            <span name="camera" class="flex-3">
+              <img class="lazy" data-src="">
+            </span>
+            <span name="desktop" class="flex-9">
+              <img class="lazy" data-src="">
+            </span>
+            <span name="audio" class="flex-12">
+              <audio class="lazy" data-src="" controls></audio>
+            </span>
+            <div class="flex-12" name="revisions" data-revisions="">
+              <div name="revision" class="flex-12" style="display:none;">
+                <button class="delete btn btn-warning">
+                  #acs-kernel.common_Delete#
+                </button>
+                <span name="timestamp"></span>
+                -
+                <span name="author"></span>
+                :
+                <span name="comment"></span>
+              </div>
+              <button class="comment btn btn-default"
+                      style="display:none;"
+                      data-artifact-id="">#acs-subsite.Comment#</button>
+            </div>
+          </div>
         </div>
         <script <if @::__csp_nonce@ not nil>nonce="@::__csp_nonce@"</if>>
+           function setEventButtonHandlers(element) {
+               for (e of element.querySelectorAll(".comment")) {
+                   e.addEventListener('click', openReview);
+               }
+               for (e of element.querySelectorAll(".flag-all")) {
+                   e.addEventListener('click', flag);
+               }
+               for (e of element.querySelectorAll(".unflag-all")) {
+                   e.addEventListener('click', unFlag);
+               }
+           }
+           setEventButtonHandlers(document);
            initWS("@ws_url;literal@", function(e) {
                var template = document.querySelector("#template").firstElementChild;
                var eventList = document.querySelector("#event-list");
@@ -166,6 +547,9 @@
                }
                function createEvent(e) {
                    var event = template.cloneNode(true);
+                   for (s of event.querySelectorAll("span")) {
+                       s.style.display = 'none';
+                   }
                    var title = event.querySelector("[name=title]");
                    var timestamp = new Date(e.timestamp * 1000);
                    // Compute the local ISO date. toISOString would
@@ -178,46 +562,47 @@
                        (timestamp.getMinutes() + '').padStart(2, '0') + ':' +
                        (timestamp.getSeconds() + '').padStart(2, '0');
                    title.textContent = timestamp;
+                   var button = event.querySelector(".comment");
+                   button.addEventListener('click', openReview);
                    eventList.appendChild(event);
                    return event;
                }
-               function appendImage(e) {
+               function appendEvent(e) {
                    var event;
-                   if (lastEvent != null &&
-                       lastEvent.querySelector("[name='" + e.name + "']") == null &&
-                       lastEvent.querySelector("[name='audio']") == null) {
+                   if (e.type !== 'audio' &&
+                       lastEvent &&
+                       lastEvent.querySelector("[name='" + e.name + "']")?.style.display === 'none' &&
+                       lastEvent.querySelector("[name='audio']")?.style.display === 'none') {
                        event = lastEvent;
                    } else {
                        event = createEvent(e);
                    }
-                   var span = document.createElement("span");
-                   span.setAttribute("name", e.name);
-                   var img = document.createElement("img");
-                   img.setAttribute("class", "lazy");
-                   img.setAttribute("data-src", getFileURL(e));
-                   imageObserver.observe(img);
-                   span.appendChild(img);
-                   event.querySelector("[name='data']").appendChild(span);
-               }
-               function appendAudio(e) {
-                   var event = createEvent(e);
-                   var span = document.createElement("span");
-                   span.setAttribute("name", "audio");
-                   var audio = document.createElement("audio");
-                   audio.controls = true;
-                   audio.setAttribute("class", "lazy");
-                   audio.setAttribute("data-src", getFileURL(e));
-                   imageObserver.observe(audio);
-                   span.appendChild(audio);
-                   event.querySelector("[name='data']").appendChild(span);
-               }
 
-               var event = JSON.parse(e.data);
-               if (event.type == "audio") {
-                   appendAudio(event);
-               } else {
-                   appendImage(event);
+                   var buttons = event.querySelectorAll("[data-artifact-id]");
+                   var id = buttons[0].getAttribute("data-artifact-id");
+                   if (id === '' || e.name === 'camera') {
+                       for (b of buttons) {
+                           b.setAttribute("data-artifact-id", e.id);
+                           b.style.display = null;
+                       }
+                       setEventButtonHandlers(event);
+                   }
+
+                   var place, element;
+                   if (e.type === 'audio') {
+                       place = event.querySelector("[name='audio']");
+                       element = place.querySelector("audio");
+                       element.setAttribute("data-src", getFileURL(e));
+                   } else {
+                       place = event.querySelector("[name='" + e.name + "']");
+                       element = place.querySelector("img");
+                       element.setAttribute("data-src", getFileURL(e));
+                   }
+                   place.style.display = null;
+                   imageObserver.observe(element);
                }
+
+               appendEvent(JSON.parse(e.data));
            });
         </script>
     </if>
Index: openacs-4/packages/proctoring-support/lib/proctoring-display.tcl
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/proctoring-support/lib/proctoring-display.tcl,v
diff -u -r1.1.2.12 -r1.1.2.13
--- openacs-4/packages/proctoring-support/lib/proctoring-display.tcl	15 Feb 2022 14:36:28 -0000	1.1.2.12
+++ openacs-4/packages/proctoring-support/lib/proctoring-display.tcl	17 Feb 2022 18:28:26 -0000	1.1.2.13
@@ -124,30 +124,37 @@
         # their own pace. The rows so produced are then sorted by
         # timestamp, so that the "events" are chronologically sorted.
         db_multirow events get_artifacts {
-            select camera.file as camera_url,
+            select coalesce(camera.artifact_id,
+                            desktop.artifact_id) as artifact_id,
+                   camera.file as camera_url,
                    desktop.file as desktop_url,
                    coalesce(camera.timestamp,
                             desktop.timestamp) as timestamp,
-                   null as audio_url
-              from (select timestamp,
+                   null as audio_url,
+                   camera.revisions || desktop.revisions as revisions
+            from (select artifact_id,
+                           timestamp,
                            file,
                            rank() over (
                                         partition by object_id, user_id
                                         order by timestamp asc
-                                         ) as order
-                      from proctoring_object_artifacts
+                                         ) as order,
+                           coalesce(metadata->'revisions', '[]') as revisions
+                  from proctoring_object_artifacts
                     where object_id = :object_id
                     and user_id = :user_id
                     and type = 'image'
                     and name = 'camera') camera
                    join
-                   (select timestamp,
+                   (select artifact_id,
+                           timestamp,
                            file,
                            rank() over (
                                         partition by object_id, user_id
                                         order by timestamp asc
-                                         ) as order
-                      from proctoring_object_artifacts
+                                         ) as order,
+                           coalesce(metadata->'revisions', '[]') as revisions
+                     from proctoring_object_artifacts
                     where object_id = :object_id
                     and user_id = :user_id
                     and type = 'image'
@@ -156,10 +163,12 @@
 
             union
 
-            select null as camera_url,
+            select artifact_id,
+                   null as camera_url,
                    null as desktop_url,
                    timestamp,
-                   file as audio_url
+                   file as audio_url,
+                   metadata->'revisions' as revisions
               from proctoring_object_artifacts
              where object_id = :object_id
                and user_id = :user_id
@@ -180,6 +189,8 @@
                 set audio_url [export_vars -base $user_url {{file $audio_url}}]
             }
         }
+
+        set total [::template::multirow size events]
     }
 } else {
     set folder [::proctoring::folder \
Fisheye: Tag 1.1 refers to a dead (removed) revision in file `openacs-4/packages/proctoring-support/www/review.tcl'.
Fisheye: No comparison available.  Pass `N' to diff?