Course update permission check and warnings
This commit is contained in:
parent
795473a580
commit
0d793fce8f
7 changed files with 274 additions and 11 deletions
|
@ -73,3 +73,47 @@ export function format_date(d,short){
|
||||||
year: 'numeric', month: monthformat, day: 'numeric'
|
year: 'numeric', month: monthformat, day: 'numeric'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides standardized information about the period between two dates
|
||||||
|
* As
|
||||||
|
* @param {Date|string} first First day of the span
|
||||||
|
* @param {Date|string} last Last day of the span
|
||||||
|
* @returns {Object} Object containing formatted start and end dates and span information
|
||||||
|
*/
|
||||||
|
export function datespaninfo(first,last){
|
||||||
|
if(!(first instanceof Date)){ first = new Date(first);}
|
||||||
|
if(!(last instanceof Date)){ last = new Date(last);}
|
||||||
|
|
||||||
|
// Make sure the end date is at the very end of the day and the start date at the very beginning
|
||||||
|
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)/(24*60*60*1000)); // Add one millisecond to offset the 999 ms
|
||||||
|
const years = Math.floor(dayspan/365); // Yes, we ignore leap years/leap days
|
||||||
|
const ydaysleft = dayspan % 365;
|
||||||
|
|
||||||
|
const weeks = Math.floor(ydaysleft/7);
|
||||||
|
const wdaysleft = ydaysleft % 7;
|
||||||
|
|
||||||
|
return {
|
||||||
|
first: first,
|
||||||
|
last: last,
|
||||||
|
totaldays: dayspan,
|
||||||
|
years: years,
|
||||||
|
weeks: weeks,
|
||||||
|
days: wdaysleft,
|
||||||
|
formatted: {
|
||||||
|
first: format_date(first),
|
||||||
|
last: format_date(last),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {SimpleLine} from "./simpleline";
|
||||||
import {call} from 'core/ajax';
|
import {call} from 'core/ajax';
|
||||||
import notification from 'core/notification';
|
import notification from 'core/notification';
|
||||||
import {get_strings} from 'core/str';
|
import {get_strings} from 'core/str';
|
||||||
import {load_stringkeys, load_strings, format_date} from './string-helper';
|
import {load_stringkeys, load_strings, format_date, datespaninfo} from './string-helper';
|
||||||
import {objCopy,transportItem} from './studyplan-processor';
|
import {objCopy,transportItem} from './studyplan-processor';
|
||||||
import Debugger from './debugger';
|
import Debugger from './debugger';
|
||||||
import {download,upload} from './downloader';
|
import {download,upload} from './downloader';
|
||||||
|
@ -148,6 +148,26 @@ export default {
|
||||||
startdate: 'studyplan_startdate',
|
startdate: 'studyplan_startdate',
|
||||||
enddate: 'studyplan_enddate',
|
enddate: 'studyplan_enddate',
|
||||||
},
|
},
|
||||||
|
course_timing: {
|
||||||
|
title: 'course_timing_title',
|
||||||
|
desc: 'course_timing_desc',
|
||||||
|
question: 'course_timing_question',
|
||||||
|
warning: 'course_timing_warning',
|
||||||
|
course: 'course$core',
|
||||||
|
period: 'period',
|
||||||
|
yes: 'yes$core',
|
||||||
|
no: 'no$core',
|
||||||
|
duration: 'duration',
|
||||||
|
years: 'years$core',
|
||||||
|
year: 'year$core',
|
||||||
|
weeks: 'weeks$core',
|
||||||
|
week: 'week$core',
|
||||||
|
days: 'days$core',
|
||||||
|
day: 'day$core',
|
||||||
|
rememberchoice: 'course_timing_rememberchoice',
|
||||||
|
hidewarning: 'course_timing_hidewarning,'
|
||||||
|
|
||||||
|
},
|
||||||
studyplan_associate: {
|
studyplan_associate: {
|
||||||
associations: 'associations',
|
associations: 'associations',
|
||||||
associated_cohorts: 'associated_cohorts',
|
associated_cohorts: 'associated_cohorts',
|
||||||
|
@ -1526,6 +1546,7 @@ export default {
|
||||||
:line="line"
|
:line="line"
|
||||||
:plan="value"
|
:plan="value"
|
||||||
:page="page"
|
:page="page"
|
||||||
|
:period="page.perioddesc[index-1]"
|
||||||
:layer="layeridx-1"
|
:layer="layeridx-1"
|
||||||
:class="'t-studyline ' + ((lineindex%2==0)?' odd ':' even ')
|
:class="'t-studyline ' + ((lineindex%2==0)?' odd ':' even ')
|
||||||
+ ((lineindex==0 && layeridx==1)?' first ':' ')
|
+ ((lineindex==0 && layeridx==1)?' first ':' ')
|
||||||
|
@ -1787,7 +1808,14 @@ export default {
|
||||||
type: Object, // Studyplan data
|
type: Object, // Studyplan data
|
||||||
default(){ return null;},
|
default(){ return null;},
|
||||||
},
|
},
|
||||||
|
page: {
|
||||||
|
type: Object, // Studyplan data
|
||||||
|
default(){ return null;},
|
||||||
|
},
|
||||||
|
period: {
|
||||||
|
type: Object, // Studyplan data
|
||||||
|
default(){ return null;},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const self=this;
|
const self=this;
|
||||||
|
@ -1807,6 +1835,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
slotkey(){
|
||||||
|
return `${this.type}'-'${this.line.id}-${this.slotindex}-${this.layer}`;
|
||||||
|
},
|
||||||
item(){
|
item(){
|
||||||
for(const ix in this.value){
|
for(const ix in this.value){
|
||||||
const itm = this.value[ix];
|
const itm = this.value[ix];
|
||||||
|
@ -1832,10 +1863,18 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
text: strings.course_timing,
|
||||||
resizeListener: null,
|
resizeListener: null,
|
||||||
hover: {
|
hover: {
|
||||||
component:null,
|
component:null,
|
||||||
type: null,
|
type: null,
|
||||||
|
},
|
||||||
|
datechanger: {
|
||||||
|
coursespan: null,
|
||||||
|
periodspan: null,
|
||||||
|
default: false,
|
||||||
|
defaultchoice: false,
|
||||||
|
hidewarn: false,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -1861,15 +1900,18 @@ export default {
|
||||||
const self = this;
|
const self = this;
|
||||||
if(self.dragacceptitem().includes(event.type)) {
|
if(self.dragacceptitem().includes(event.type)) {
|
||||||
let item = event.data;
|
let item = event.data;
|
||||||
|
|
||||||
// Perform layer update - set this slot and layer here
|
// Perform layer update - set this slot and layer here
|
||||||
self.relocateStudyItem(item).done(()=>{
|
self.relocateStudyItem(item).done(()=>{
|
||||||
item.layer = this.layer;
|
item.layer = this.layer;
|
||||||
item.slot = this.slotindex;
|
item.slot = this.slotindex;
|
||||||
self.value.push(item);
|
self.value.push(item);
|
||||||
self.$emit("input",self.value);
|
self.$emit("input",self.value);
|
||||||
|
self.validate_course_period();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if(self.dragacceptcomponent().includes(event.type) ){
|
else if(self.dragacceptcomponent().includes(event.type) ){
|
||||||
|
|
||||||
if(event.type == "course"){
|
if(event.type == "course"){
|
||||||
call([{
|
call([{
|
||||||
methodname: 'local_treestudyplan_add_studyitem',
|
methodname: 'local_treestudyplan_add_studyitem',
|
||||||
|
@ -1892,6 +1934,7 @@ export default {
|
||||||
self.relocateStudyItem(item).done(()=>{
|
self.relocateStudyItem(item).done(()=>{
|
||||||
self.value.push(item);
|
self.value.push(item);
|
||||||
self.$emit("input",self.value);
|
self.$emit("input",self.value);
|
||||||
|
self.validate_course_period();
|
||||||
});
|
});
|
||||||
}).fail(notification.exception);
|
}).fail(notification.exception);
|
||||||
}
|
}
|
||||||
|
@ -1944,7 +1987,58 @@ export default {
|
||||||
this.hover.component = null;
|
this.hover.component = null;
|
||||||
this.hover.type = null;
|
this.hover.type = null;
|
||||||
},
|
},
|
||||||
|
validate_course_period() {
|
||||||
|
const self = this;
|
||||||
|
debug.info("Validating course and period",self.item,self.period);
|
||||||
|
if(self.item && self.item.type == 'course'){
|
||||||
|
self.datechanger.coursespan = datespaninfo(self.item.course.startdate,self.item.course.enddate);
|
||||||
|
self.datechanger.periodspan = datespaninfo(self.period.startdate,self.period.enddate);
|
||||||
|
if( self.datechanger.coursespan.first != self.datechanger.periodspan.first
|
||||||
|
|| self.datechanger.coursespan.last != self.datechanger.periodspan.last ){
|
||||||
|
debug.info("Course timing does not match period timing");
|
||||||
|
|
||||||
|
if(self.item.course.canupdatecourse){
|
||||||
|
if(!self.datechanger.default){
|
||||||
|
// Periods do not match, pop up the date change request
|
||||||
|
this.$bvModal.show("t-course-date-matching-"+this.slotkey);
|
||||||
|
} else if (self.datechanger.defaultvalue){
|
||||||
|
// go for it without asking
|
||||||
|
self.change_course_period();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// user is not able to change course timing - show a warning
|
||||||
|
if(!self.datechanger.hidewarn){
|
||||||
|
this.$bvModal.show("t-course-date-warning-"+this.slotkey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
debug.info("Course timing matches period",self.datechanger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
change_course_period() {
|
||||||
|
const self=this;
|
||||||
|
return call([{
|
||||||
|
methodname: 'local_treestudyplan_course_period_timing',
|
||||||
|
args: { page_id: self.page_id,
|
||||||
|
period: self.period.period,
|
||||||
|
course_id: this.item.course.id,
|
||||||
|
}
|
||||||
|
}])[0].fail(notification.exception);
|
||||||
|
},
|
||||||
|
format_duration(dsi){
|
||||||
|
let s = "";
|
||||||
|
if(dsi.years == 1){ s += `1 ${this.text.year}, `;}
|
||||||
|
else if(dsi.years > 1){ s += `${dsi.years} ${this.text.years}, `;}
|
||||||
|
if(dsi.weeks == 1){ s += `1 ${this.text.week}, `;}
|
||||||
|
else if(dsi.weeks > 1){ s += `${dsi.weeks} ${this.text.weeks}, `;}
|
||||||
|
if(dsi.days == 1){ s += `1 ${this.text.day}, `;}
|
||||||
|
else if(dsi.days > 1){ s += `${dsi.days} ${this.text.days}, `;}
|
||||||
|
|
||||||
|
return s.toLocaleLowerCase();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div :class="'t-studyline-slot '+type + ' t-studyline-slot-'+slotindex + ' ' + ((slotindex==0)?' t-studyline-firstcolumn ':' ')"
|
<div :class="'t-studyline-slot '+type + ' t-studyline-slot-'+slotindex + ' ' + ((slotindex==0)?' t-studyline-firstcolumn ':' ')"
|
||||||
|
@ -1987,8 +2081,77 @@ export default {
|
||||||
class="t-slot-item feedback"
|
class="t-slot-item feedback"
|
||||||
:key="hover.type">--{{ hover.type }}--</div
|
:key="hover.type">--{{ hover.type }}--</div
|
||||||
></template
|
></template
|
||||||
></drop
|
></drop>
|
||||||
></div>
|
<b-modal
|
||||||
|
:id="'t-course-date-matching-'+this.slotkey"
|
||||||
|
size="lg"
|
||||||
|
:title="text.title"
|
||||||
|
@ok="change_course_period"
|
||||||
|
:ok-title="text.yes"
|
||||||
|
cancel-disabled
|
||||||
|
>
|
||||||
|
<b-container v-if="datechanger.coursespan && datechanger.periodspan && item && item.course">
|
||||||
|
<b-row><b-col cols="12">{{ text.desc }}</b-col></b-row>
|
||||||
|
<b-row><b-col cols="12"><div class="generalbox alert alert-warning">{{ text.question }}</div></b-col></b-row>
|
||||||
|
<b-row>
|
||||||
|
<b-col cols="6">
|
||||||
|
<h3> {{ text.course }} </h3>
|
||||||
|
<p><b>{{ item.course.fullname }}</b></p>
|
||||||
|
<p><b>{{ item.course.shortname }}</b></p>
|
||||||
|
<p>{{ datechanger.coursespan.formatted.first}} - {{ datechanger.coursespan.formatted.last}}</p>
|
||||||
|
<p><b>{{ text.duration }}</b><br>
|
||||||
|
{{ format_duration(datechanger.coursespan)}}</p>
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="6">
|
||||||
|
<h3> {{ text.period }} </h3>
|
||||||
|
<p><b>{{ period.fullname }}</b></p>
|
||||||
|
<p><b>{{ period.shortname }}</b></p>
|
||||||
|
<p>{{ datechanger.periodspan.formatted.first}} - {{ datechanger.periodspan.formatted.last}}</p>
|
||||||
|
<p><b>{{ text.duration }}</b><br>
|
||||||
|
{{ format_duration(datechanger.periodspan)}}</p>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
<b-row><b-col cols="12">
|
||||||
|
<b-form-checkbox type="checkbox" v-model="datechanger.default">{{ text.rememberchoice }}</b-form-checkbox>
|
||||||
|
</b-col></b-row>
|
||||||
|
</b-container>
|
||||||
|
</b-modal>
|
||||||
|
<b-modal
|
||||||
|
:id="'t-course-date-warning-'+this.slotkey"
|
||||||
|
size="lg"
|
||||||
|
ok-variant="danger"
|
||||||
|
:title="text.title"
|
||||||
|
:ok-title="text.yes"
|
||||||
|
:cancel-title="text.no"
|
||||||
|
cancel-variant="primary"
|
||||||
|
>
|
||||||
|
<b-container v-if="datechanger.coursespan && datechanger.periodspan && item && item.course">
|
||||||
|
<b-row><b-col cols="12">{{ text.desc }}</b-col></b-row>
|
||||||
|
<b-row><b-col cols="12"><div class="generalbox alert alert-warning">{{ text.warning }}</div></b-col></b-row>
|
||||||
|
<b-row>
|
||||||
|
<b-col cols="6">
|
||||||
|
<h3> {{ text.course }} </h3>
|
||||||
|
<p><b>{{ item.course.fullname }}</b></p>
|
||||||
|
<p><b>{{ item.course.shortname }}</b></p>
|
||||||
|
<p>{{ datechanger.coursespan.formatted.first}} - {{ datechanger.coursespan.formatted.last}}</p>
|
||||||
|
<p><b>{{ text.duration }}</b><br>
|
||||||
|
{{ format_duration(datechanger.coursespan)}}</p>
|
||||||
|
</b-col>
|
||||||
|
<b-col cols=>"6">
|
||||||
|
<h3> {{ text.period }} </h3>
|
||||||
|
<p><b>{{ period.fullname }}</b></p>
|
||||||
|
<p><b>{{ period.shortname }}</b></p>
|
||||||
|
<p>{{ datechanger.periodspan.formatted.first}} - {{ datechanger.periodspan.formatted.last}}</p>
|
||||||
|
<p><b>{{ text.duration }}</b><br>
|
||||||
|
{{ format_duration(datechanger.periodspan)}}</p>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
<b-row><b-col cols="12">
|
||||||
|
<b-form-checkbox type="checkbox" v-model="datechanger.hidewarn">{{ text.hidewarning }}</b-form-checkbox>
|
||||||
|
</b-col></b-row>
|
||||||
|
</b-container>
|
||||||
|
</b-modal>
|
||||||
|
</div>
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -175,7 +175,7 @@ export default {
|
||||||
>{{ value.fullname }}<br>
|
>{{ value.fullname }}<br>
|
||||||
<span class="s-studyline-header-period-datespan">
|
<span class="s-studyline-header-period-datespan">
|
||||||
<span class="date">{{ startdate }}</span> - <span class="date">{{ enddate }}</span>
|
<span class="date">{{ startdate }}</span> - <span class="date">{{ enddate }}</span>
|
||||||
<span>
|
</span>
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
<slot></slot
|
<slot></slot
|
||||||
><p class="s-studyline-header-period-datespan small">
|
><p class="s-studyline-header-period-datespan small">
|
||||||
|
|
|
@ -178,6 +178,7 @@ class courseinfo {
|
||||||
"startdate" => new \external_value(PARAM_TEXT, 'Course start date'),
|
"startdate" => new \external_value(PARAM_TEXT, 'Course start date'),
|
||||||
"enddate" => new \external_value(PARAM_TEXT, 'Course end date'),
|
"enddate" => new \external_value(PARAM_TEXT, 'Course end date'),
|
||||||
"amteacher" => new \external_value(PARAM_BOOL, 'Requesting user is teacher in this course'),
|
"amteacher" => new \external_value(PARAM_BOOL, 'Requesting user is teacher in this course'),
|
||||||
|
"canupdatecourse" => new \external_value(PARAM_BOOL, "If the current user can update this course"),
|
||||||
"canselectgradables" => new \external_value(PARAM_BOOL, 'Requesting user can change selected gradables'),
|
"canselectgradables" => new \external_value(PARAM_BOOL, 'Requesting user can change selected gradables'),
|
||||||
"tag" => new \external_value(PARAM_TEXT, 'Tag'),
|
"tag" => new \external_value(PARAM_TEXT, 'Tag'),
|
||||||
], 'referenced course information',$value);
|
], 'referenced course information',$value);
|
||||||
|
@ -200,6 +201,7 @@ class courseinfo {
|
||||||
'startdate' => date("Y-m-d",$this->course->startdate,),
|
'startdate' => date("Y-m-d",$this->course->startdate,),
|
||||||
'enddate' => date("Y-m-d",$this->course->enddate),
|
'enddate' => date("Y-m-d",$this->course->enddate),
|
||||||
'amteacher' => $this->amTeacher(),
|
'amteacher' => $this->amTeacher(),
|
||||||
|
'canupdatecourse' => \has_capability("moodle/course:update",$this->coursecontext),
|
||||||
'canselectgradables' => $this->iCanSelectGradables(),
|
'canselectgradables' => $this->iCanSelectGradables(),
|
||||||
'tag' => "Editormodel",
|
'tag' => "Editormodel",
|
||||||
'grades' => [],
|
'grades' => [],
|
||||||
|
|
|
@ -1179,6 +1179,44 @@ class studyplanservice extends \external_api
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/************************
|
||||||
|
* *
|
||||||
|
* edit_period *
|
||||||
|
* *
|
||||||
|
************************/
|
||||||
|
|
||||||
|
public static function course_period_timing_parameters()
|
||||||
|
{
|
||||||
|
return new \external_function_parameters( [
|
||||||
|
"page_id" => new \external_value(PARAM_INT, 'Studyplan page id'),
|
||||||
|
"period" => new \external_value(PARAM_INT, 'Period number within page'),
|
||||||
|
"course_id"=> new \external_value(PARAM_INT, 'Id of course to adjust dates for'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function course_period_timing_returns()
|
||||||
|
{
|
||||||
|
return success::structure();
|
||||||
|
}
|
||||||
|
public static function course_period_timing($page_id, $period, $course_id){
|
||||||
|
$page = studyplanpage::findById($page_id);
|
||||||
|
$course = \get_course($course_id);
|
||||||
|
$coursecontext = \context_course::instance($course_id);
|
||||||
|
// Check for studyplan edit permissions
|
||||||
|
webservicehelper::require_capabilities(self::CAP_EDIT,$page->studyplan()->context());
|
||||||
|
|
||||||
|
if(webservicehelper::has_capabilities("moodle/course:update",$coursecontext)){
|
||||||
|
//TODO: Actually perform the timing changes, while also updating the module times
|
||||||
|
// Like what happens on a course "reset"
|
||||||
|
|
||||||
|
|
||||||
|
return success::success()->model();
|
||||||
|
} else {
|
||||||
|
// probably should return a nice message
|
||||||
|
return success::fail("You do not have date change permissions on this course")->model();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -291,3 +291,11 @@ $string["badgeissuedstats"] = "Issuing progress";
|
||||||
|
|
||||||
$string["period_default_fullname"] = 'Period {$a}';
|
$string["period_default_fullname"] = 'Period {$a}';
|
||||||
$string["period_default_shortname"] = 'P{$a}';
|
$string["period_default_shortname"] = 'P{$a}';
|
||||||
|
$string["course_timing_title"] = 'Course timing does not match period timing';
|
||||||
|
$string["course_timing_desc"] = 'The start and end date of the course you are dropping into this period does not match the start and end date of the period.';
|
||||||
|
$string["course_timing_question"] = 'Do you want to update the course\'s start and end time to match that of the period?';
|
||||||
|
$string["course_timing_warning"] = 'You do not have permission to automatically update this course start and end date. Automatic timing update not available';
|
||||||
|
$string["period"] = 'Period';
|
||||||
|
$string["duration"] = 'Duration';
|
||||||
|
$string["course_timing_rememberchoice"] = 'Remember my choice for future date mismatches';
|
||||||
|
$string["course_timing_hidewarning"] = 'Hide this warning next time';
|
|
@ -294,3 +294,11 @@ $string["badgeissuedstats"] = "Voortgang van uitgifte";
|
||||||
|
|
||||||
$string["period_default_fullname"] = 'Periode {$a}';
|
$string["period_default_fullname"] = 'Periode {$a}';
|
||||||
$string["period_default_shortname"] = 'P{$a}';
|
$string["period_default_shortname"] = 'P{$a}';
|
||||||
|
$string["course_timing_title"] = 'Cursustiming en periodetiming komen niet overeen';
|
||||||
|
$string["course_timing_desc"] = 'De start- en einddatum van de cursus, komen niet overen met die van de periode waarin je hem hebt gedropt.';
|
||||||
|
$string["course_timing_question"] = 'Wil je de start- en eindtijd van de cursus aanpassen naar doe van de periode?';
|
||||||
|
$string["course_timing_warning"] = 'Je hebt geen rechten om de start- en eindtijd van deze cursus aan te passen. Aanpassen van cursus naar periodetiming is niet beschikbaar.';
|
||||||
|
$string["period"] = 'Periode';
|
||||||
|
$string["duration"] = 'Duur';
|
||||||
|
$string["course_timing_rememberchoice'"] = 'Onthoud mijn keuze voor toekomstige mismatches tussen cursus en periode';
|
||||||
|
$string["course_timing_hidewarning"] = 'Hide this warning next time';
|
Loading…
Reference in a new issue