- @events.timestamp_pretty@
+ @events.timestamp@
@@ -165,7 +165,16 @@
function createEvent(e) {
var event = template.cloneNode(true);
var title = event.querySelector("[name=title]");
- var timestamp = new Date(e.timestamp * 1000).toISOString().substring(0, 19).replace("T", " ");
+ var timestamp = new Date(e.timestamp * 1000);
+ // Compute the local ISO date. toISOString would
+ // return the UTC time...
+ timestamp =
+ (timestamp.getFullYear() + '').padStart(4, '0') + '-' +
+ ((timestamp.getMonth() + 1) + '').padStart(2, '0') + '-' +
+ (timestamp.getDate() + '').padStart(2, '0') + ' ' +
+ (timestamp.getHours() + '').padStart(2, '0') + ':' +
+ (timestamp.getMinutes() + '').padStart(2, '0') + ':' +
+ (timestamp.getSeconds() + '').padStart(2, '0');
title.textContent = timestamp;
eventList.appendChild(event);
return event;
Index: openacs-4/packages/proctoring-support/lib/proctoring-display.tcl
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/proctoring-support/lib/Attic/proctoring-display.tcl,v
diff -u -N -r1.1.2.5 -r1.1.2.6
--- openacs-4/packages/proctoring-support/lib/proctoring-display.tcl 11 Jun 2021 14:53:15 -0000 1.1.2.5
+++ openacs-4/packages/proctoring-support/lib/proctoring-display.tcl 10 Feb 2022 09:32:58 -0000 1.1.2.6
@@ -73,6 +73,11 @@
if {$delete_p && [llength $user_id] >= 1} {
foreach u $user_id {
+ ::xo::dc dml -prepare {integer integer} delete_artifacts {
+ delete from proctoring_object_artifacts
+ where object_id = :object_id
+ and user_id = :u
+ }
set folder [::proctoring::folder \
-object_id $object_id -user_id $u]
file delete -force -- $folder
@@ -106,58 +111,64 @@
set portrait_url [export_vars -base "/shared/portrait-bits.tcl" {user_id {size x200}}]
set back_url $base_url
- set camera_pics [lsort -increasing -dictionary \
- [glob -nocomplain -directory $folder camera-image-*.*]]
- set desktop_pics [lsort -increasing -dictionary \
- [glob -nocomplain -directory $folder desktop-image-*.*]]
- set rows [list]
- foreach camera_pic $camera_pics desktop_pic $desktop_pics {
- set row [dict create \
- audio_url ""]
- if {$camera_pic ne ""} {
- set camera_pic [file tail $camera_pic]
- regexp {^camera-image-(\d+)\.\w+$} $camera_pic m camera_timestamp
- dict set row camera_url [export_vars -base $user_url {{file $camera_pic}}]
- } else {
- dict set row camera_url ""
- }
+ db_multirow events get_artifacts {
+ select camera.file as camera_url,
+ desktop.file as desktop_url,
+ coalesce(camera.timestamp,
+ desktop.timestamp) as timestamp,
+ null as audio_url
+ from (select timestamp,
+ file,
+ rank() over (
+ partition by object_id, user_id
+ order by timestamp asc
+ ) as order
+ from proctoring_object_artifacts
+ where object_id = :object_id
+ and user_id = :user_id
+ and type = 'image'
+ and name = 'camera') camera
+ join
+ (select timestamp,
+ file,
+ rank() over (
+ partition by object_id, user_id
+ order by timestamp asc
+ ) as order
+ from proctoring_object_artifacts
+ where object_id = :object_id
+ and user_id = :user_id
+ and type = 'image'
+ and name = 'desktop') desktop
+ on camera.order = desktop.order
- if {$desktop_pic ne ""} {
- set desktop_pic [file tail $desktop_pic]
- regexp {^desktop-image-(\d+)\.\w+$} $desktop_pic m desktop_timestamp
- dict set row desktop_url [export_vars -base $user_url {{file $desktop_pic}}]
- } else {
- dict set row desktop_url ""
- }
+ union
- if {[info exists camera_timestamp]} {
- set timestamp $camera_timestamp
- } else {
- set timestamp $desktop_timestamp
- }
- dict set row timestamp $timestamp
- dict set row timestamp_pretty [clock format $timestamp -format "%y-%m-%d %H:%M:%S"]
+ select null as camera_url,
+ null as desktop_url,
+ timestamp,
+ file as audio_url
+ from proctoring_object_artifacts
+ where object_id = :object_id
+ and user_id = :user_id
+ and type = 'audio'
- lappend rows $row
+ order by timestamp asc
+ } {
+ if {$camera_url ne ""} {
+ set camera_url [file tail $camera_url]
+ set camera_url [export_vars -base $user_url {{file $camera_url}}]
+ }
+ if {$desktop_url ne ""} {
+ set desktop_url [file tail $desktop_url]
+ set desktop_url [export_vars -base $user_url {{file $desktop_url}}]
+ }
+ if {$audio_url ne ""} {
+ set audio_url [file tail $audio_url]
+ set audio_url [export_vars -base $user_url {{file $audio_url}}]
+ }
}
-
- set audios [glob -nocomplain -directory $folder *-audio-*.*]
- foreach audio $audios {
- set row [dict create]
- set audio [file tail $audio]
- regexp {^\w+-audio-(\d+)\.\w+$} $audio m timestamp
- dict set row audio_url [export_vars -base $user_url {{file $audio}}]
- dict set row camera_url ""
- dict set row desktop_url ""
- dict set row timestamp $timestamp
- dict set row timestamp_pretty [clock format $timestamp -format "%y-%m-%d %H:%M:%S"]
-
- lappend rows $row
- }
-
- template::util::list_to_multirow events $rows
- template::multirow sort events timestamp
}
} else {
# List of proctored users
@@ -169,32 +180,34 @@
set delete_confirm [_ xowiki.delete_all_confirm]
if {$delete_p} {
+ ::xo::dc dml -prepare integer delete_artifacts {
+ delete from proctoring_object_artifacts
+ where object_id = :object_id
+ }
file delete -force -- $folder
ad_returnredirect $base_url
ad_script_abort
}
- set rows [list]
- foreach user_folder [glob -type d -nocomplain -directory $folder *] {
- set row [dict create]
- set proctored_user_id [file tail $user_folder]
- dict set row user_id $proctored_user_id
+ db_multirow -extend {
+ student_id
+ proctoring_url
+ portrait_url
+ filter
+ } -unclobber users get_users {
+ select distinct a.user_id,
+ p.first_names,
+ p.last_name
+ from proctoring_object_artifacts a,
+ persons p
+ where object_id = :object_id
+ and a.user_id = p.person_id
+ order by last_name asc, first_names asc
+ } {
+ set student_id [::party::email -party_id $user_id]
- set user [acs_user::get -user_id $proctored_user_id]
- if {$user eq ""} {continue}
-
- set first_names [dict get $user first_names]
- set last_name [dict get $user last_name]
-
- dict set row user_id $proctored_user_id
- dict set row first_names $first_names
- dict set row last_name $last_name
- dict set row proctoring_url [export_vars -no_base_encode -base $base_url {{user_id $proctored_user_id} {object_id $object_id}}]
- dict set row portrait_url /shared/portrait-bits.tcl?user_id=$proctored_user_id
- dict set row filter [string tolower "$last_name $first_names"]
- lappend rows $row
+ set proctoring_url [export_vars -no_base_encode -base $base_url { user_id object_id }]
+ set portrait_url /shared/portrait-bits.tcl?user_id=$user_id
+ set filter [string tolower "$last_name $first_names $student_id"]
}
- template::util::list_to_multirow users $rows
- template::multirow sort users first_names
- template::multirow sort users last_name
}
Index: openacs-4/packages/proctoring-support/lib/proctoring-upload.tcl
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/proctoring-support/lib/Attic/proctoring-upload.tcl,v
diff -u -N -r1.1.2.12 -r1.1.2.13
--- openacs-4/packages/proctoring-support/lib/proctoring-upload.tcl 9 Feb 2022 15:09:51 -0000 1.1.2.12
+++ openacs-4/packages/proctoring-support/lib/proctoring-upload.tcl 10 Feb 2022 09:32:58 -0000 1.1.2.13
@@ -80,6 +80,16 @@
file mkdir -- $proctoring_dir
file rename -force -- ${file.tmpfile} $file_path
+ # Create an entry in the database for the file we have just
+ # collected, so that we can further enrich it with metadata in
+ # later postprocessing phases.
+ ::xo::dc dml -prepare {integer integer integer text text text} init_artifact {
+ insert into proctoring_object_artifacts
+ (object_id, user_id, timestamp, name, type, file)
+ values
+ (:object_id, :user_id, to_timestamp(:timestamp), :name, :type, :file_path)
+ }
+
# Notify a websocket about the upload so that e.g. a UI can be updated
# in real time.
if {$notify_p} {
Index: openacs-4/packages/proctoring-support/sql/postgresql/proctoring-support-create.sql
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/proctoring-support/sql/postgresql/Attic/proctoring-support-create.sql,v
diff -u -N -r1.1.2.6 -r1.1.2.7
--- openacs-4/packages/proctoring-support/sql/postgresql/proctoring-support-create.sql 6 Aug 2021 11:31:12 -0000 1.1.2.6
+++ openacs-4/packages/proctoring-support/sql/postgresql/proctoring-support-create.sql 10 Feb 2022 09:32:58 -0000 1.1.2.7
@@ -66,4 +66,49 @@
-- access to this exam
);
+--
+-- The proctoring_object_artifacts table is meant to store information
+-- for each file collected during proctoring and provide a technical
+-- space to store additional metadata coming from e.g. postprocessing
+-- happening at a later phase.
+-- The metadata column is a JSON so that the particular type of
+-- information we store is flexible and can depend on the different
+-- kind of file or even be extended by downstream integrations.
+--
+create table proctoring_object_artifacts (
+ artifact_id serial primary key,
+ -- we might have referenced the proctoring_objects table rather
+ -- than acs_objects, but this makes the data model more
+ -- flexible for those integrations that do not store the
+ -- proctoring configuration in proctoring_objects (e.g. xowf)
+ object_id integer references acs_objects(object_id) on delete cascade,
+ user_id integer references users(user_id) on delete cascade,
+ timestamp timestamp not null default current_timestamp,
+ name text not null,
+ type text not null,
+ file text not null,
+ metadata jsonb
+);
+
+create index proctoring_object_artifacts_object_id_idx on
+ proctoring_object_artifacts(object_id);
+
+create index proctoring_object_artifacts_user_id_idx on
+ proctoring_object_artifacts(user_id);
+
+create index proctoring_object_artifacts_timestamp_idx on
+ proctoring_object_artifacts(timestamp);
+
+create index proctoring_object_artifacts_name_idx on
+ proctoring_object_artifacts(name);
+
+create index proctoring_object_artifacts_type_idx on
+ proctoring_object_artifacts(type);
+
+create index proctoring_object_artifacts_file_idx on
+ proctoring_object_artifacts(file);
+
+create unique index proctoring_object_artifacts_un_idx on
+ proctoring_object_artifacts(object_id, user_id, timestamp, name, type);
+
end;
Index: openacs-4/packages/proctoring-support/sql/postgresql/proctoring-support-drop.sql
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/proctoring-support/sql/postgresql/Attic/proctoring-support-drop.sql,v
diff -u -N -r1.1.2.2 -r1.1.2.3
--- openacs-4/packages/proctoring-support/sql/postgresql/proctoring-support-drop.sql 9 Jun 2021 15:55:54 -0000 1.1.2.2
+++ openacs-4/packages/proctoring-support/sql/postgresql/proctoring-support-drop.sql 10 Feb 2022 09:32:58 -0000 1.1.2.3
@@ -2,3 +2,4 @@
drop table proctoring_objects;
drop table proctoring_examination_statement_acceptance;
drop table proctoring_safe_exam_browser_conf;
+drop table proctoring_object_artifacts;
Index: openacs-4/packages/proctoring-support/sql/postgresql/upgrade/upgrade-3.0.0-3.1.0.sql
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/proctoring-support/sql/postgresql/upgrade/Attic/upgrade-3.0.0-3.1.0.sql,v
diff -u -N
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ openacs-4/packages/proctoring-support/sql/postgresql/upgrade/upgrade-3.0.0-3.1.0.sql 10 Feb 2022 09:32:58 -0000 1.1.2.1
@@ -0,0 +1,48 @@
+begin;
+
+--
+-- The proctoring_object_artifacts table is meant to store information
+-- for each file collected during proctoring and provide a technical
+-- space to store additional metadata coming from e.g. postprocessing
+-- happening at a later phase.
+-- The metadata column is a JSON so that the particular type of
+-- information we store is flexible and can depend on the different
+-- kind of file or even be extended by downstream integrations.
+--
+create table if not exists proctoring_object_artifacts (
+ artifact_id serial primary key,
+ -- we might have referenced the proctoring_objects table rather
+ -- than acs_objects, but this makes the data model more
+ -- flexible for those integrations that do not store the
+ -- proctoring configuration in proctoring_objects (e.g. xowf)
+ object_id integer references acs_objects(object_id) on delete cascade,
+ user_id integer references users(user_id) on delete cascade,
+ timestamp timestamp not null default current_timestamp,
+ name text not null,
+ type text not null,
+ file text not null,
+ metadata jsonb
+);
+
+create index if not exists proctoring_object_artifacts_object_id_idx on
+ proctoring_object_artifacts(object_id);
+
+create index if not exists proctoring_object_artifacts_user_id_idx on
+ proctoring_object_artifacts(user_id);
+
+create index if not exists proctoring_object_artifacts_timestamp_idx on
+ proctoring_object_artifacts(timestamp);
+
+create index if not exists proctoring_object_artifacts_name_idx on
+ proctoring_object_artifacts(name);
+
+create index if not exists proctoring_object_artifacts_type_idx on
+ proctoring_object_artifacts(type);
+
+create index if not exists proctoring_object_artifacts_file_idx on
+ proctoring_object_artifacts(file);
+
+create unique index if not exists proctoring_object_artifacts_un_idx on
+ proctoring_object_artifacts(object_id, user_id, timestamp, name, type);
+
+end;
Index: openacs-4/packages/proctoring-support/tcl/apm-callback-procs.tcl
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/proctoring-support/tcl/Attic/apm-callback-procs.tcl,v
diff -u -N
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ openacs-4/packages/proctoring-support/tcl/apm-callback-procs.tcl 10 Feb 2022 09:32:59 -0000 1.1.2.1
@@ -0,0 +1,113 @@
+ad_library {
+
+ APM callbacks for the proctoring-support package.
+
+}
+
+namespace eval proctoring {}
+namespace eval proctoring::apm {}
+
+ad_proc -private ::proctoring::apm::after_upgrade {
+ {-from_version_name:required}
+ {-to_version_name:required}
+} {
+ Upgrade logic
+} {
+ apm_upgrade_logic \
+ -from_version_name $from_version_name \
+ -to_version_name $to_version_name \
+ -spec {
+ 3.0.0 3.1.0 {
+ ::proctoring::apm::upgrade_to_3_1_0 -apm
+ }
+ }
+}
+
+ad_proc -private ::proctoring::apm::upgrade_to_3_1_0 {
+ -apm:boolean
+} {
+ Version 3.1.0 introduced an actual table in the datamodel to store
+ proctoring artifacts. Go into the proctoring folder and generate a
+ database entry for each picture that respects the format used so
+ far.
+} {
+ # Go in the proctoring folder...
+ set object_folders [glob \
+ -nocomplain \
+ -directory [acs_root_dir]/proctoring/ \
+ -type d *]
+
+ set msg "::proctoring::apm::upgrade_to_3_1_0 START\n"
+ append msg "Creating entries in the artifacts table. [llength $object_folders] to inspect..."
+ ns_log warning $msg
+ if {$apm_p} {
+ apm_ns_write_callback $msg
+ }
+
+ # ...for each object folder...
+ foreach object_folder $object_folders {
+ set object_id [file tail $object_folder]
+ if {![string is integer -strict $object_id]} {
+ continue
+ }
+
+ set user_folders [glob \
+ -nocomplain \
+ -directory $object_folder \
+ -type d *]
+
+ set msg "...object '$object_id' has [llength $user_folders] user folders..."
+ ns_log warning $msg
+ if {$apm_p} {
+ apm_ns_write_callback $msg
+ }
+
+ # ...foreach user folder...
+ foreach user_folder $user_folders {
+ set user_id [file tail $user_folder]
+ if {![string is integer -strict $user_id]} {
+ continue
+ }
+
+ set files [glob \
+ -nocomplain \
+ -directory $user_folder \
+ -type f *]
+ set msg "......for object '$object_id', user '$user_id' has [llength $files] files..."
+ ns_log warning $msg
+ if {$apm_p} {
+ apm_ns_write_callback $msg
+ }
+
+ # ...foreach file...
+ foreach f $files {
+ if {[regexp {^(\w+)-(\w+)-(\d+)\.\w+$} [file tail $f] m name type timestamp]} {
+ set msg ".........object '$object_id', user '$user_id' storing file '$f' in the artifacts table..."
+ # If the file respects the naming convention
+ # upheld so far, store the information in the
+ # database.
+ ::xo::dc dml -prepare integer,integer,integer,text,text,text,integer,integer init_artifact {
+ insert into proctoring_object_artifacts
+ (object_id, user_id, timestamp, name, type, file)
+ select :object_id, :user_id, to_timestamp(:timestamp), :name, :type, :f
+ from dual
+ where exists (select 1 from acs_objects where object_id = :object_id)
+ and exists (select 1 from users where user_id = :user_id)
+ }
+ } else {
+ set msg ".........object '$object_id', user '$user_id' file '$f' does not respect the naming convention."
+ }
+ ns_log warning $msg
+ if {$apm_p} {
+ apm_ns_write_callback $msg
+ }
+ }
+ }
+ }
+
+ set msg "::proctoring::apm::upgrade_to_3_1_0 FINISH\n"
+ ns_log warning $msg
+ if {$apm_p} {
+ apm_ns_write_callback $msg
+ }
+}
Index: openacs-4/packages/proctoring-support/www/upload.tcl
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/proctoring-support/www/Attic/upload.tcl,v
diff -u -N -r1.1.2.4 -r1.1.2.5
--- openacs-4/packages/proctoring-support/www/upload.tcl 21 Nov 2021 20:26:07 -0000 1.1.2.4
+++ openacs-4/packages/proctoring-support/www/upload.tcl 10 Feb 2022 09:32:59 -0000 1.1.2.5
@@ -7,7 +7,7 @@
file
file.tmpfile
{check_active_p:boolean true}
- {notify_p:boolean false}
+ {notify_p:boolean true}
{record_p:boolean true}
}
#ns_log notice "UPLOAD called with notify_p $notify_p record_p $record_p"