Replaced student email by last course access time

This commit is contained in:
PMKuipers 2024-02-24 16:09:47 +01:00
parent 6b616c0a6a
commit 8f2673c4a4
10 changed files with 127 additions and 41 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,3 @@
define("local_treestudyplan/util/date-helper",["exports"],(function(_exports){function format_date(d,short){d instanceof Date||(d=new Date(d));let monthformat="short";return!0===short?monthformat="numeric":!1===short&&(monthformat="long"),d.toLocaleDateString(document.documentElement.lang,{year:"numeric",month:monthformat,day:"numeric"})}function studyplanDates(plan){let earliestStart=null,latestEnd=null,openEnded=!1;for(const ix in plan.pages){const page=plan.pages[ix],s=new Date(page.startdate);if(page.enddate||(openEnded=!0),(!earliestStart||s<earliestStart)&&(earliestStart=s),page.enddate){const e=new Date(page.enddate);(!latestEnd||e>latestEnd)&&(latestEnd=e)}}return{start:earliestStart,end:openEnded?null:latestEnd}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.add_days=function(datestr,days){const date=new Date(datestr);return function(date){const d=new Date(date);let month=""+(d.getMonth()+1),day=""+d.getDate();const year=d.getFullYear();month.length<2&&(month="0"+month);day.length<2&&(day="0"+day);return[year,month,day].join("-")}(new Date(date.getTime()+864e5*days))},_exports.datespaninfo=function(first,last){first instanceof Date||(first=new Date(first));last instanceof Date||(last=new Date(last));first.setHours(0),first.setMinutes(0),first.setSeconds(0),first.setMilliseconds(0),last.setHours(23),last.setMinutes(59),last.setSeconds(59),last.setMilliseconds(999);const dayspan=Math.round((last-first+1)/864e5),years=Math.floor(dayspan/365),ydaysleft=dayspan%365,weeks=Math.floor(ydaysleft/7);return{first:first,last:last,totaldays:dayspan,years:years,weeks:weeks,days:ydaysleft%7,formatted:{first:format_date(first),last:format_date(last)}}},_exports.format_date=format_date,_exports.studyplanDates=studyplanDates,_exports.studyplanPageTiming=function(page){const now=(new Date).getTime(),start=new Date(page.startdate),end=page.enddate?new Date(page.enddate):null;return start<now?end&&now>end?"past":"present":"future"},_exports.studyplanTiming=function(plan){const now=(new Date).getTime(),dates=studyplanDates(plan);return dates.start<now?dates.end&&now>dates.end?"past":"present":"future"}})); define("local_treestudyplan/util/date-helper",["exports"],(function(_exports){function format_date(d,short){d instanceof Date||(d=new Date(d));let monthformat="short";return!0===short?monthformat="numeric":!1===short&&(monthformat="long"),d.toLocaleDateString(document.documentElement.lang,{year:"numeric",month:monthformat,day:"numeric"})}function studyplanDates(plan){let earliestStart=null,latestEnd=null,openEnded=!1;for(const ix in plan.pages){const page=plan.pages[ix],s=new Date(page.startdate);if(page.enddate||(openEnded=!0),(!earliestStart||s<earliestStart)&&(earliestStart=s),page.enddate){const e=new Date(page.enddate);(!latestEnd||e>latestEnd)&&(latestEnd=e)}}return{start:earliestStart,end:openEnded?null:latestEnd}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.add_days=function(datestr,days){const date=new Date(datestr);return function(date){const d=new Date(date);let month=""+(d.getMonth()+1),day=""+d.getDate();const year=d.getFullYear();month.length<2&&(month="0"+month);day.length<2&&(day="0"+day);return[year,month,day].join("-")}(new Date(date.getTime()+864e5*days))},_exports.datespaninfo=function(first,last){first instanceof Date||(first=new Date(first));last instanceof Date||(last=new Date(last));first.setHours(0),first.setMinutes(0),first.setSeconds(0),first.setMilliseconds(0),last.setHours(23),last.setMinutes(59),last.setSeconds(59),last.setMilliseconds(999);const dayspan=Math.round((last-first+1)/864e5),years=Math.floor(dayspan/365),ydaysleft=dayspan%365,weeks=Math.floor(ydaysleft/7);return{first:first,last:last,totaldays:dayspan,years:years,weeks:weeks,days:ydaysleft%7,formatted:{first:format_date(first),last:format_date(last)}}},_exports.format_date=format_date,_exports.format_datetime=function(d,short){d instanceof Date||(d=new Date(d));let monthformat="short";!0===short?monthformat="numeric":!1===short&&(monthformat="long");return d.toLocaleDateString(document.documentElement.lang,{year:"numeric",month:monthformat,day:"numeric"})+" "+d.toLocaleTimeString(document.documentElement.lang,{timeStyle:"short"})},_exports.studyplanDates=studyplanDates,_exports.studyplanPageTiming=function(page){const now=(new Date).getTime(),start=new Date(page.startdate),end=page.enddate?new Date(page.enddate):null;return start<now?end&&now>end?"past":"present":"future"},_exports.studyplanTiming=function(plan){const now=(new Date).getTime(),dates=studyplanDates(plan);return dates.start<now?dates.end&&now>dates.end?"past":"present":"future"}}));
//# sourceMappingURL=date-helper.min.js.map //# sourceMappingURL=date-helper.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -13,6 +13,7 @@ import Debugger from './util/debugger';
import Config from 'core/config'; import Config from 'core/config';
import TSComponents from './treestudyplan-components'; import TSComponents from './treestudyplan-components';
import FitTextVue from './util/fittext-vue'; import FitTextVue from './util/fittext-vue';
import {format_datetime} from "./util/date-helper";
const debug = new Debugger("treestudyplan-viewer"); const debug = new Debugger("treestudyplan-viewer");
@ -121,7 +122,8 @@ export default {
students: 'students@core', students: 'students@core',
firstname: 'firstname@core', firstname: 'firstname@core',
lastname: 'lastname@core', lastname: 'lastname@core',
email: 'email@core' email: 'email@core',
lastaccess: 'lastaccess@core',
}, },
studentresults: { studentresults: {
completion_incomplete: "completion_incomplete", completion_incomplete: "completion_incomplete",
@ -132,6 +134,7 @@ export default {
completion_good: "completion_good", completion_good: "completion_good",
completion_excellent: "completion_excellent", completion_excellent: "completion_excellent",
student_not_tracked: "student_not_tracked", student_not_tracked: "student_not_tracked",
never: "never@core",
} }
}); });
@ -166,12 +169,13 @@ export default {
}; };
}, },
created() { created() {
this.loadStudents();
}, },
watch:{ watch:{
structure: { structure: {
immediate: true, immediate: true,
handler (structure) { handler (structure) {
this.loadStudents(); // reload the student list
// (Re)build expansion info structure // (Re)build expansion info structure
let firstperiod = true; let firstperiod = true;
for (const period of structure.periods) { for (const period of structure.periods) {
@ -224,7 +228,7 @@ export default {
computed: { computed: {
sortedstudents(){ sortedstudents(){
const self=this; const self=this;
// Probably should make a deep copy for purity's sake, but this works just as well. // Probably could make a deep copy for purity's sake, but this works just as well and is probably more efficient.
const students = this.students; const students = this.students;
for (const group of this.students) { for (const group of this.students) {
group.users.sort((a,b) => { group.users.sort((a,b) => {
@ -234,7 +238,13 @@ export default {
d = b; d = b;
e = a; e = a;
} }
return String(d[this.sorting.header]).localeCompare(String(e[this.sorting.header])); if (this.sorting.header == "lastaccess") {
const dvalue = (d[this.sorting.header]?d[this.sorting.header]:0);
const evalue = (e[this.sorting.header]?e[this.sorting.header]:0);
return dvalue - evalue;
} else {
return String(d[this.sorting.header]).localeCompare(String(e[this.sorting.header]));
}
}); });
} }
@ -342,7 +352,7 @@ export default {
:style="'--resultColCount: '+resultColCount+';'"> :style="'--resultColCount: '+resultColCount+';'">
<colgroup class="q-col-studentinfo"> <colgroup class="q-col-studentinfo">
<col class="q-name"></col> <col class="q-name"></col>
<col class="q-email"></col> <col class="q-lastaccess"></col>
</colgroup> </colgroup>
<colgroup class="q-col-resultinfo"> <colgroup class="q-col-resultinfo">
<col v-for="n in resultColCount"></col> <col v-for="n in resultColCount"></col>
@ -475,11 +485,11 @@ export default {
template: ` template: `
<thead class='q-header'> <thead class='q-header'>
<tr> <!-- period heading --> <tr> <!-- period heading -->
<th rowspan='3' colspan='2' class='q-studentinfo q-generic'><span>{{text.students}}</span></th> <th rowspan='4' colspan='2' class='q-studentinfo q-generic'><span>{{text.students}}</span></th>
<th v-for="p in structure.periods" <th v-for="p in structure.periods"
:class="'q-period-heading '+ ((expansion.periods[p.period.id].expanded)?'expanded':'collapsed')" :class="'q-period-heading '+ ((expansion.periods[p.period.id].expanded)?'expanded':'collapsed')"
:colspan='colspanPeriod(p)' :colspan='colspanPeriod(p)'
:rowspan='(expansion.periods[p.period.id].expanded && p.lines.length > 0)?1:4' :rowspan='(expansion.periods[p.period.id].expanded && p.lines.length > 0)?1:5'
><span class="q-wrap"><a v-if='(p.lines.length > 0)' href='#' @click.prevent="togglePeriod(p.period)" ><span class="q-wrap"><a v-if='(p.lines.length > 0)' href='#' @click.prevent="togglePeriod(p.period)"
><i v-if="expansion.periods[p.period.id].expanded" ><i v-if="expansion.periods[p.period.id].expanded"
class='q-chevron fa fa-minus'></i class='q-chevron fa fa-minus'></i
@ -494,7 +504,7 @@ export default {
<th v-for="l in p.lines" <th v-for="l in p.lines"
:class="'q-line-heading ' + ((expansion.lines[p.period.id][l.line.id].expanded)?'expanded':'collapsed')" :class="'q-line-heading ' + ((expansion.lines[p.period.id][l.line.id].expanded)?'expanded':'collapsed')"
:colspan="colspanLine(p,l)" :colspan="colspanLine(p,l)"
:rowspan='(expansion.lines[p.period.id][l.line.id].expanded)?1:3' :rowspan='(expansion.lines[p.period.id][l.line.id].expanded)?1:4'
><span class="q-wrap"><fittext vertical maxsize="18pt" ><span class="q-wrap"><fittext vertical maxsize="18pt"
><span class='q-label' ><span class='q-label'
:title="l.line.shortname" :title="l.line.shortname"
@ -513,7 +523,7 @@ export default {
<th v-for="item in l.items" <th v-for="item in l.items"
:class="'q-item-heading ' + ((expansion.items[item.id].expanded)?'expanded':'collapsed')" :class="'q-item-heading ' + ((expansion.items[item.id].expanded)?'expanded':'collapsed')"
:colspan="colspanItem(item)" :colspan="colspanItem(item)"
:rowspan='(expansion.items[item.id].expanded)?1:2' :rowspan='(expansion.items[item.id].expanded)?1:3'
><span class="q-wrap"><a href='#' ><span class="q-wrap"><a href='#'
@click.prevent="toggleItem(item)" @click.prevent="toggleItem(item)"
><i v-if="expansion.items[item.id].expanded" ><i v-if="expansion.items[item.id].expanded"
@ -537,32 +547,16 @@ export default {
</template> </template>
</tr> </tr>
<tr> <!-- condition heading --> <tr> <!-- condition heading -->
<th class="q-studentinfo q-name">
<fittext maxsize="12pt"
><a href="#" @click.prevent="toggleSort('firstname')">{{text.firstname}}</a
><i v-if="sorting.header=='firstname' && sorting.asc" class='fa fa-sort-asc fa-fw'></i
><i v-else-if="sorting.header=='firstname' && !sorting.asc" class='fa fa-sort-desc fa-fw'></i>
/ <a href="#" @click.prevent="toggleSort('lastname')">{{text.lastname}}</a
><i v-if="sorting.header=='lastname' && sorting.asc" class='fa fa-sort-asc fa-fw'></i
><i v-else-if="sorting.header=='lastname' && !sorting.asc" class='fa fa-sort-desc fa-fw'></i
></fittext>
</th>
<th class="q-studentinfo q-email">
<fittext maxsize="12pt"
><a href="#" @click.prevent="toggleSort('email')">{{text.email}}</a
><i v-if="sorting.header=='email' && sorting.asc" class='fa fa-sort-asc fa-fw'></i
><i v-else-if="sorting.header=='email' && !sorting.asc" class='fa fa-sort-desc fa-fw'></i
></fittext>
</th>
<template v-for="p in structure.periods"> <template v-for="p in structure.periods">
<template v-if="expansion.periods[p.period.id].expanded"> <template v-if="expansion.periods[p.period.id].expanded">
<template v-for="l in p.lines"> <template v-for="l in p.lines">
<template v-if="expansion.lines[p.period.id][l.line.id].expanded"> <template v-if="expansion.lines[p.period.id][l.line.id].expanded">
<template v-for="item in l.items"> <template v-for="item in l.items">
<template v-if="expansion.items[item.id].expanded"> <template v-if="expansion.items[item.id].expanded">
<th class='q-condition-heading overall' <th class='q-condition-heading overall' rowspan="2"
><span class='q-wrap'>{{ text.overall }}</span></th> ><span class='q-wrap'>{{ text.overall }}</span></th>
<th v-for="c in conditions(item)" <th v-for="c in conditions(item)"
rowspan="2"
class='q-condition-heading' class='q-condition-heading'
><span class="q-wrap"><fittext vertical maxsize="14pt"><a class='q-label q-condition-label' ><span class="q-wrap"><fittext vertical maxsize="14pt"><a class='q-label q-condition-label'
:title="c.tooltip" href="#" @click.prevent :title="c.tooltip" href="#" @click.prevent
@ -577,6 +571,25 @@ export default {
</template> </template>
</template> </template>
</tr> </tr>
<tr> <!-- student info heading -->
<th class="q-studentinfo q-name">
<fittext maxsize="12pt"
><a href="#" @click.prevent="toggleSort('firstname')">{{text.firstname}}</a
><i v-if="sorting.header=='firstname' && sorting.asc" class='fa fa-sort-asc fa-fw'></i
><i v-else-if="sorting.header=='firstname' && !sorting.asc" class='fa fa-sort-desc fa-fw'></i>
/ <a href="#" @click.prevent="toggleSort('lastname')">{{text.lastname}}</a
><i v-if="sorting.header=='lastname' && sorting.asc" class='fa fa-sort-asc fa-fw'></i
><i v-else-if="sorting.header=='lastname' && !sorting.asc" class='fa fa-sort-desc fa-fw'></i
></fittext>
</th>
<th class="q-studentinfo q-email">
<fittext maxsize="12pt"
><a href="#" @click.prevent="toggleSort('lastaccess')">{{text.lastaccess}}</a
><i v-if="sorting.header=='lastaccess' && sorting.asc" class='fa fa-sort-asc fa-fw'></i
><i v-else-if="sorting.header=='lastaccess' && !sorting.asc" class='fa fa-sort-desc fa-fw'></i
></fittext>
</th>
</tr>
</thead> </thead>
`, `,
}); });
@ -678,6 +691,13 @@ export default {
}; };
}, },
computed: { computed: {
lastaccess() {
if (this.student.lastaccess) {
return format_datetime(this.student.lastaccess*1000); // Takes date in milliseconds
} else {
return this.text.never;
}
}
}, },
methods: { methods: {
useritems(line) { useritems(line) {
@ -705,7 +725,7 @@ export default {
template: ` template: `
<tr :class="'q-student-results userrow ' + (even?'even':'odd')"> <tr :class="'q-student-results userrow ' + (even?'even':'odd')">
<td class='q-studentinfo q-name'><fittext maxsize="12pt">{{student.firstname}} {{student.lastname}}</fittext></td> <td class='q-studentinfo q-name'><fittext maxsize="12pt">{{student.firstname}} {{student.lastname}}</fittext></td>
<td class='q-studentinfo q-email'><fittext maxsize="12pt">{{student.email}}</fittext></td> <td class='q-studentinfo q-email'><fittext maxsize="12pt">{{lastaccess}}</fittext></td>
<template v-for="p in structure.periods"> <template v-for="p in structure.periods">
<template v-if="expansion.periods[p.period.id].expanded && p.lines.length > 0"> <template v-if="expansion.periods[p.period.id].expanded && p.lines.length > 0">
<template v-for="l in p.lines"> <template v-for="l in p.lines">

View File

@ -20,6 +20,30 @@ export function format_date(d,short){
}); });
} }
/**
* Format a date according to localized custom
* @param {Date|string} d The date to convert
* @param {boolean} short Short format (default false)
* @returns {string}
*/
export function format_datetime(d,short){
if(!(d instanceof Date)){
d = new Date(d);
}
let monthformat = "short";
if(short === true){
monthformat = "numeric";
} else if (short === false) {
monthformat = "long";
}
return d.toLocaleDateString(document.documentElement.lang,{
year: 'numeric', month: monthformat, day: 'numeric'
})+" "+d.toLocaleTimeString(document.documentElement.lang,{
timeStyle: "short",
});
}
/** /**
* Provides standardized information about the period between two dates * Provides standardized information about the period between two dates
* As * As

View File

@ -53,6 +53,7 @@ class associationservice extends \external_api {
"lastname" => new \external_value(PARAM_TEXT, 'last name'), "lastname" => new \external_value(PARAM_TEXT, 'last name'),
"idnumber" => new \external_value(PARAM_TEXT, 'id number'), "idnumber" => new \external_value(PARAM_TEXT, 'id number'),
"email" => new \external_value(PARAM_TEXT, 'email address'), "email" => new \external_value(PARAM_TEXT, 'email address'),
"lastaccess" => new \external_value(PARAM_INT, 'id of last access this user had to any course in the studyplan', VALUE_OPTIONAL),
]); ]);
} }
@ -125,6 +126,28 @@ class associationservice extends \external_api {
} }
public static function user_lastaccess($userid,$studyplanid=null) {
global $DB;
if (!empty($studyplanid)) {
$lasql = "SELECT MAX(a.timeaccess) FROM {user_lastaccess} a
INNER JOIN {local_treestudyplan_item} i ON i.course_id = a.courseid
INNER JOIN {local_treestudyplan_line} l ON l.id = i.line_id
INNER JOIN {local_treestudyplan_page} p ON l.page_id = p.id
WHERE a.userid = :userid AND p.studyplan_id = :studyplanid";
$lastaccess = $DB->get_field_sql($lasql,["userid" => $userid, "studyplanid" => $studyplanid]);
debug::write("Got lastaccess '{$lastaccess}' for user {$userid} in plan {$studyplanid}");
} else {
$lasql = "SELECT MAX(a.timeaccess) FROM {user_lastaccess} a
WHERE a.userid = :userid";
$lastaccess = $DB->get_field_sql($lasql,["userid" => $userid]);
debug::write("Got lastaccess '{$lastaccess}' for user {$userid} in any course");
}
return $lastaccess;
}
/** /**
* Parameter description for webservice function list_cohort * Parameter description for webservice function list_cohort
*/ */
@ -454,9 +477,17 @@ class associationservice extends \external_api {
ORDER BY u.lastname, u.firstname"; ORDER BY u.lastname, u.firstname";
$rs = $DB->get_recordset_sql($sql, ['studyplan_id' => $studyplanid]); $rs = $DB->get_recordset_sql($sql, ['studyplan_id' => $studyplanid]);
/*
ID: 30
page: 33
plan: 28
*/
$users = []; $users = [];
foreach ($rs as $u) { foreach ($rs as $u) {
$users[] = self::make_user_model($u); $user = self::make_user_model($u);
$user["lastaccess"] = self::user_lastaccess($u->id,$studyplanid);
$users[] = $user;
} }
$rs->close(); $rs->close();
self::sortusermodels($users); self::sortusermodels($users);
@ -601,7 +632,9 @@ class associationservice extends \external_api {
$rs = $DB->get_recordset_sql($sql, ["cohortid" => $c->id]); $rs = $DB->get_recordset_sql($sql, ["cohortid" => $c->id]);
foreach ($rs as $u) { foreach ($rs as $u) {
$users[] = self::make_user_model($u); $user = self::make_user_model($u);
$user["lastaccess"] = self::user_lastaccess($u->id,$studyplanid);
$users[] = $user;
} }
$rs->close(); $rs->close();

View File

@ -1586,8 +1586,11 @@ body.path-local-treestudyplan .editmode-switch-form > * {
table-layout: fixed; table-layout: fixed;
width: calc(24rem + var(--resultColCount) * 4rem); width: calc(24rem + var(--resultColCount) * 4rem);
} }
.path-local-treestudyplan table.q-studyplanreport colgroup.q-col-studentinfo col { .path-local-treestudyplan table.q-studyplanreport colgroup.q-col-studentinfo col.q-name {
width: 12rem; width: 14rem;
}
.path-local-treestudyplan table.q-studyplanreport colgroup.q-col-studentinfo col.q-lastaccess {
width: 10rem;
} }
.path-local-treestudyplan table.q-studyplanreport colgroup.q-col-resultinfo col { .path-local-treestudyplan table.q-studyplanreport colgroup.q-col-resultinfo col {
width: 4rem; width: 4rem;

View File

@ -9,8 +9,11 @@
table-layout: fixed; table-layout: fixed;
width: calc((2 * 12rem) + (var(--resultColCount) * 4rem)); width: calc((2 * 12rem) + (var(--resultColCount) * 4rem));
colgroup.q-col-studentinfo { colgroup.q-col-studentinfo {
col { col.q-name {
width: 12rem; width: 14rem;
}
col.q-lastaccess {
width: 10rem;
} }
} }
colgroup.q-col-resultinfo { colgroup.q-col-resultinfo {

View File

@ -1586,8 +1586,11 @@ body.path-local-treestudyplan .editmode-switch-form > * {
table-layout: fixed; table-layout: fixed;
width: calc(24rem + var(--resultColCount) * 4rem); width: calc(24rem + var(--resultColCount) * 4rem);
} }
.path-local-treestudyplan table.q-studyplanreport colgroup.q-col-studentinfo col { .path-local-treestudyplan table.q-studyplanreport colgroup.q-col-studentinfo col.q-name {
width: 12rem; width: 14rem;
}
.path-local-treestudyplan table.q-studyplanreport colgroup.q-col-studentinfo col.q-lastaccess {
width: 10rem;
} }
.path-local-treestudyplan table.q-studyplanreport colgroup.q-col-resultinfo col { .path-local-treestudyplan table.q-studyplanreport colgroup.q-col-resultinfo col {
width: 4rem; width: 4rem;