Skip to content

Commit effe5fa

Browse files
committed
WIP: settings view on the UI should now work for all setting types.
1 parent 6f0e647 commit effe5fa

File tree

7 files changed

+130
-32
lines changed

7 files changed

+130
-32
lines changed

conf/course_settings.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -401,8 +401,8 @@
401401
student answer, e.g. 1 if student input is sin(pi/2). If this is set to false, e.g.
402402
to save space in the response area, the student can still see their evaluated answer
403403
by hovering the mouse pointer over the typeset version of their answer.
404-
type: text
405-
default_value: ''
404+
type: boolean
405+
default_value: false
406406
-
407407
setting_name: use_base_10_log
408408
category: problem

lib/DB/Schema/ResultSet/Course.pm

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -450,8 +450,6 @@ A single course setting as either a hashref or a C<DBIx::Class::ResultSet::Cours
450450
451451
=cut
452452

453-
use Data::Dumper;
454-
455453
sub updateCourseSetting ($self, %args) {
456454
my $course = $self->getCourse(info => getCourseInfo($args{info}), as_result_set => 1);
457455

@@ -466,13 +464,13 @@ sub updateCourseSetting ($self, %args) {
466464
my $params = {
467465
course_id => $course->course_id,
468466
setting_id => $global_setting->{setting_id},
469-
value => { value => $args{params}->{value} }
467+
value => { value => $args{params}{value} }
470468
};
471469

472470
# remove the following fields before checking for valid settings:
473-
for (qw/setting_id course_id/) { delete $global_setting->{$_}; }
471+
delete $global_setting->{$_} for (qw/setting_id course_id/);
474472

475-
isValidSetting($global_setting, $params->{value}->{value});
473+
isValidSetting($global_setting, $params->{value}{value});
476474

477475
# The course_id must be deleted to ensure it is written to the database correctly.
478476
delete $params->{course_id} if defined($params->{course_id});

lib/WeBWorK3.pm

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,10 @@ sub problemRoutes ($app, $course_routes) {
217217
}
218218

219219
sub settingsRoutes ($self, $course_routes) {
220-
$self->routes->get('/webwork3/api/global-settings')->requires(authenticated => 1)->to('Settings#getGlobalSettings');
221-
$self->routes->get('/webwork3/api/global-setting/:setting_id')->requires(authenticated => 1)
222-
->to('Settings#getGlobalSetting');
220+
my $global_settings = $self->routes->any('/webwork3/api/global-settings')->requires(authenticated => 1);
221+
$global_settings->get('/')->to('Settings#getGlobalSettings');
222+
$global_settings->get('/:setting_id')->to('Settings#getGlobalSetting');
223+
$global_settings->post('/check-timezone')->to('Settings#checkTimeZone');
223224
$course_routes->get('/settings')->to('Settings#getCourseSettings');
224225
$course_routes->put('/settings/:setting_id')->to('Settings#updateCourseSetting');
225226
$course_routes->delete('/settings/:setting_id')->to('Settings#deleteCourseSetting');

lib/WeBWorK3/Controller/Settings.pm

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ package WeBWorK3::Controller::Settings;
33
use warnings;
44
use strict;
55

6+
use Mojo::JSON qw/true false/;
7+
use DateTime::TimeZone;
8+
use Try::Tiny;
9+
610
=head1 Description
711
812
These are the methods that call the database for course settings
@@ -61,4 +65,13 @@ sub deleteCourseSetting ($c) {
6165
return;
6266
}
6367

68+
sub checkTimeZone ($c) {
69+
try {
70+
DateTime::TimeZone->new(name => $c->req->json->{timezone});
71+
$c->render(json => {valid_timezone => true });
72+
} catch {
73+
$c->render(json => {valid_timezone => false });
74+
};
75+
}
76+
6477
1;

lib/WeBWorK3/Utils/Settings.pm

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ This checks if the setting given the type, value and list of options (if needed)
5656
=cut
5757

5858
sub isValidSetting ($setting, $value = undef) {
59-
return 0 if !defined $setting->{type};
59+
DB::Exception::ParametersNeeded->throw(
60+
message => 'The field \'type\' must be defined for the setting'
61+
) unless defined $setting->{type};
6062

6163
# If $value is not passed in, use the default_value for the setting
6264
my $val = $value // $setting->{default_value};
@@ -161,11 +163,12 @@ sub validateMultilist ($setting, $value) {
161163
DB::Exception::InvalidCourseFieldType->throw(
162164
message => "The options field for the type multilist in $setting->{setting_name} is missing ")
163165
unless defined($setting->{options});
166+
164167
DB::Exception::InvalidCourseFieldType->throw(
165168
message => "The options field for $setting->{setting_name} is not an ARRAYREF")
166169
unless ref($setting->{options}) eq 'ARRAY';
167170

168-
my @diff = array_minus(@{ $setting->{options} }, @$value);
171+
my @diff = array_minus(@$value, @{ $setting->{options} });
169172
throw DB::Exception::InvalidCourseFieldType->throw(
170173
message => "The values for $setting->{setting_name} must be a subset of the options field")
171174
unless scalar(@diff) == 0;
Lines changed: 92 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,55 @@
11
<template>
22
<tr>
33
<td width="60%">{{ setting.description }}
4-
<q-icon v-if="setting.doc" name="help" class="q-ml-md">
5-
<q-tooltip class="text-body2">
6-
{{ setting.doc }}
7-
</q-tooltip>
8-
</q-icon>
4+
<q-icon v-if="setting.doc" name="help" size="sm" color="primary"
5+
class="q-ml-md" @click="show_help = !show_help"
6+
/>
97
</td>
108
<td width="40%">
119
<input-with-blur
1210
outlined dense
13-
v-if="setting?.type === 'text' || setting?.type === 'timezone'"
14-
v-model="course_setting.value"
11+
v-if="setting?.type === 'text'"
12+
v-model="setting_value"
13+
/>
14+
<input-with-blur
15+
outlined dense
16+
v-if="setting?.type === 'decimal'"
17+
v-model.number="setting_value"
18+
type="number"
1519
/>
16-
<q-select v-if="setting.type === 'list'" v-model="option" :options="options" />
20+
<input-with-blur
21+
outlined dense
22+
v-if="setting?.type === 'timezone'"
23+
v-model="setting_value"
24+
:error="!valid_timezone"
25+
error-message="This is not a valid timezone"
26+
/>
27+
<q-select v-if="setting.type === 'list'" v-model="option_value" :options="options" />
28+
<q-select v-if="setting.type === 'multilist'" multiple v-model="multilist_value" :options="options" />
1729
<input-with-blur
1830
v-if="setting.type === 'time_duration'"
19-
v-model="course_setting.value"
31+
v-model="setting_value"
32+
lazy-rules
2033
:rules="[checkTimeDuration]"
2134
/>
22-
<q-toggle v-if="setting.type === 'boolean'" v-model="course_setting.value" />
23-
<q-input v-if="setting.type === 'integer'" v-model="course_setting.value" :rules="[checkInt]" />
35+
<q-toggle v-if="setting.type === 'boolean'" v-model="setting_value" />
36+
<q-input v-if="setting.type === 'int'" v-model.number="setting_value" type="number" :rules="[checkInt]" />
2437
</td>
2538
</tr>
39+
<tr v-if="setting.doc && show_help"><td class="helptext" colspan="2"><div v-html="setting.doc" /></td></tr>
2640
</template>
2741

2842
<script setup lang="ts">
2943
import { defineProps, ref, watch } from 'vue';
3044
import { useQuasar } from 'quasar';
31-
import { CourseSetting, OptionType } from 'src/common/models/settings';
45+
import { CourseSetting, OptionType, SettingValueType } from 'src/common/models/settings';
3246
3347
import InputWithBlur from 'src/components/common/InputWithBlur.vue';
3448
import { logger } from 'src/boot/logger';
3549
3650
import { useSettingsStore } from 'src/stores/settings';
3751
import { isTimeDuration } from 'src/common/models/parsers';
52+
import { api } from 'src/boot/axios';
3853
3954
const props = defineProps<{
4055
setting: CourseSetting
@@ -44,22 +59,43 @@ const $q = useQuasar();
4459
const settings = useSettingsStore();
4560
4661
const course_setting = ref(props.setting.clone());
62+
// used for text input/toggles
63+
const setting_value = ref<SettingValueType>();
64+
if (['int', 'decimal', 'text', 'boolean', 'time_duration', 'timezone'].includes(course_setting.value.type)) {
65+
setting_value.value = course_setting.value.value;
66+
}
67+
// Used for type list and multilist
68+
const option_value = ref<OptionType>({ value: '', label: '' });
69+
const multilist_value = ref<OptionType[]>([]);
70+
const options = ref<OptionType[]>([]);
71+
72+
// Determine if the help in settings.doc is shown.
73+
const show_help = ref(false);
4774
48-
const option = ref<OptionType>({ value: '', label: '' });
49-
const options = ref<Array<OptionType>>([]);
75+
const checkInt = (val: string) => Number.isInteger(val) || 'This must be an integer.';
76+
const checkTimeDuration = (val: string) => isTimeDuration(val) || 'This must be a time duration.';
5077
51-
const checkInt = (val: string) => Number.isInteger(val) ? true : 'This must be an integer.';
52-
const checkTimeDuration = (val: string) => isTimeDuration(val) ? true : 'This must be a time duration.';
78+
const valid_timezone = ref(true);
5379
80+
// These are for type list/multilist
5481
if (course_setting.value.options) {
5582
options.value = course_setting.value.options.map((opt: string | OptionType) =>
5683
typeof opt === 'string' ? { label: opt, value: opt } : opt
5784
);
58-
const v = options.value.find((opt: OptionType) => opt.value === course_setting.value.value);
59-
option.value = v || { value: '', label: '' };
85+
}
86+
// Extract the option_value for type list
87+
if (course_setting.value.type === 'list') {
88+
option_value.value = options.value.find((opt: OptionType) => opt.value === course_setting.value.value) ||
89+
{ value: '', label: '' };
90+
}
91+
92+
// Extract the multilist_value for type list
93+
if (course_setting.value.type === 'multilist') {
94+
multilist_value.value = options.value
95+
.filter((opt: OptionType) => (course_setting.value.value as string[]).includes(opt.value));
6096
}
6197
62-
watch(() => course_setting.value.value, async () => {
98+
const updateCourseSetting = async () => {
6399
try {
64100
await settings.updateCourseSetting(course_setting.value as CourseSetting);
65101
const msg = `The setting '${course_setting.value.setting_name}' was updated successfully`;
@@ -72,5 +108,42 @@ watch(() => course_setting.value.value, async () => {
72108
$q.notify({ message: err as string, color: 'red' });
73109
logger.error(`[CourseSettings/updateCourseSetting]: ${err as string}`);
74110
}
111+
};
112+
113+
watch(() => setting_value.value, async () => {
114+
if (setting_value.value) {
115+
if (course_setting.value.type === 'timezone') {
116+
// Check for valid timezone on the server.
117+
const response = await api.post('/global-settings/check-timezone',
118+
{ timezone: setting_value.value });
119+
valid_timezone.value = (response.data as { valid_timezone: boolean }).valid_timezone;
120+
if (!valid_timezone.value) return;
121+
}
122+
course_setting.value.value = setting_value.value;
123+
await updateCourseSetting();
124+
}
125+
});
126+
127+
watch(() => option_value.value, async () => {
128+
if (option_value.value) {
129+
course_setting.value.value = option_value.value.value;
130+
await updateCourseSetting();
131+
}
132+
});
133+
134+
watch(() => multilist_value.value, async () => {
135+
if (multilist_value.value) {
136+
course_setting.value.value = multilist_value.value.map(opt => opt.value);
137+
await updateCourseSetting();
138+
}
75139
});
76140
</script>
141+
142+
<style lang="scss" scoped>
143+
.helptext {
144+
border: 1px solid black;
145+
border-radius: 5px;
146+
padding: 5px 0px;
147+
background-color: lightyellow;
148+
}
149+
</style>

t/mojolicious/015_course_settings.t

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ for my $setting (@$global_settings_from_db) {
6060
is_deeply($global_settings_from_db, $global_settings_from_file, 'test that the global settings are correct.');
6161

6262
# get a single global/default setting
63-
$t->get_ok('/webwork3/api/global-setting/1')->content_type_is('application/json;charset=UTF-8')->status_is(200)
63+
$t->get_ok('/webwork3/api/global-settings/1')->content_type_is('application/json;charset=UTF-8')->status_is(200)
6464
->json_is('/setting_name' => $global_settings_from_file->[0]->{setting_name})
6565
->json_is('/default_value' => $global_settings_from_file->[0]->{default_value})
6666
->json_is('/description' => $global_settings_from_file->[0]->{description});
@@ -105,4 +105,14 @@ $t->put_ok(
105105
$t->delete_ok("/webwork3/api/courses/4/settings/$reduced_scoring->{setting_id}")
106106
->content_type_is('application/json;charset=UTF-8')->status_is(200)->json_is('/value' => 0.5);
107107

108+
# Check for valid and invalid timezones
109+
110+
$t->post_ok('/webwork3/api/global-settings/check-timezone' => json => {timezone => 'America/Chicago'})
111+
->content_type_is('application/json;charset=UTF-8')->status_is(200)
112+
->json_is('/valid_timezone' => true);
113+
114+
$t->post_ok('/webwork3/api/global-settings/check-timezone' => json => {timezone => 'Amrica/Chicago'})
115+
->status_is(200)->json_is('/valid_timezone' => false);
116+
117+
108118
done_testing;

0 commit comments

Comments
 (0)