Skip to content

Commit c71dcad

Browse files
committed
WIP: use non-neg ints in db for time_durations and human readability on UI.
1 parent 5b61b26 commit c71dcad

File tree

10 files changed

+210
-13
lines changed

10 files changed

+210
-13
lines changed

lib/DB/Utils.pm

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ no warnings qw/experimental::signatures/;
88
require Exporter;
99
use base qw(Exporter);
1010
our @EXPORT_OK = qw/getCourseInfo getUserInfo getSetInfo updateAllFields updatePermissions
11-
getPoolInfo getProblemInfo getPoolProblemInfo getSettingInfo removeLoginParams/;
11+
getPoolInfo getProblemInfo getPoolProblemInfo getSettingInfo removeLoginParams
12+
convertTimeDuration/;
1213

1314
use Clone qw/clone/;
14-
use List::Util qw/first/;
1515
use Scalar::Util qw/reftype/;
1616
use YAML::XS qw/LoadFile/;
1717

@@ -193,4 +193,27 @@ sub updatePermissions ($ww3_conf, $role_perm_file) {
193193
return;
194194
}
195195

196+
=pod
197+
=head2 convertTimeDuration
198+
199+
This subroutine converts time durations stored as a string in human-readable format
200+
to a number of seconds.
201+
202+
=cut
203+
204+
sub convertTimeDuration ($time_duration) {
205+
if ($time_duration =~ /^(\d+)\s(sec)s?$/) {
206+
return $1;
207+
} elsif ($time_duration =~ /^(\d+)\s(min(ute)?)s?$/) {
208+
return $1*60;
209+
} elsif ($time_duration =~ /^(\d+)\s(h(ou)?r)s?$/) {
210+
return $1*60*60;
211+
} elsif ($time_duration =~ /^(\d+)\s(day)s?$/) {
212+
return $1*60*60*24;
213+
} elsif ($time_duration =~ /^(\d+)\s(week)s?$/) {
214+
return $1*60*60*24*7;
215+
}
216+
}
217+
218+
196219
1;

lib/WeBWorK3/Utils/Settings.pm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ sub isValidSetting ($setting, $value = undef) {
103103
} elsif ($setting->{type} eq 'time_duration') {
104104
DB::Exception::InvalidCourseFieldType->throw(
105105
message => qq/The variable $setting->{setting_name} has value $val and must be a time duration/)
106-
unless isTimeDuration($val);
106+
unless $val =~ /^\d+$/;
107107
} elsif ($setting->{type} eq 'timezone') {
108108
# try to make a new timeZone. If the name isn't valid an 'Invalid offset:' will be thrown.
109109
DateTime::TimeZone->new(name => $val);

src/common/models/parsers.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ export const mail_re = /^[\w.]+@([a-zA-Z_.]+)+\.[a-zA-Z]{2,9}$/;
9999
export const username_re = /^[_a-zA-Z]([a-zA-Z._0-9])+$/;
100100
export const time_re = /^([01][0-9]|2[0-3]):[0-5]\d$/;
101101
// Update this for localization
102-
export const time_duration_re = /^(\d+)\s(sec|second|min|minute|day|week|hr|hour)s?$/i;
102+
// This a regexp for time durations separated by commas.
103+
export const time_duration_re = /^(((\d+)\s(sec|second|min|minute|day|week|hr|hour)s?),?\s?)+$/i;
103104

104105
// Checking functions
105106

@@ -110,7 +111,7 @@ export const isValidEmail = (v: string) => mail_re.test(v);
110111
export const isTimeDuration = (v: string) => time_duration_re.test(v);
111112
export const isTime = (v: string) => time_re.test(v);
112113

113-
// Parsing functionis
114+
// Parsing functions
114115

115116
export function parseNonNegInt(val: string | number) {
116117
if (isNonNegInt(val)) return parseInt(`${val}`);
@@ -162,3 +163,53 @@ export function parseString(_value: string | number | boolean) {
162163
return _value;
163164
}
164165
}
166+
167+
/**
168+
* Converts a time_duration type setting to a human-readable one.
169+
* TODO: use localization for this.
170+
* @params td - time duration in seconds.
171+
*/
172+
173+
export const humanReadableTimeDuration = (td: number): string => {
174+
const times = {
175+
week: Math.floor(td / 604800),
176+
day: Math.floor(td % 604800 / 86400),
177+
hour: Math.floor(td % 86400 / 3600),
178+
min: Math.floor(td % 3600 / 60),
179+
sec: td % 60
180+
};
181+
182+
return Object.entries(times).reduce((prev: string, [key, value]) => prev +
183+
// if the time value is non zero, and there is already something in prev, add a comma
184+
(prev != '' && value > 0 ? ', ' : '') +
185+
// pluralize.
186+
(value > 0 ? `${value} ${key}${value === 1 ? '' : 's'}` : ''), '');
187+
};
188+
189+
/**
190+
* Convert a time_duration as a string (possibility separated by commas) to a number of seconds.
191+
*/
192+
193+
export const convertTimeDuration = (dur: string): number => {
194+
const times = dur.split(/,\s/);
195+
let time_duration = 0;
196+
times.forEach(t => {
197+
const match_sec = /^(\d+)\s(sec(ond)?)s?$/.exec(t);
198+
const match_min = /^(\d+)\s(min(ute)?)s?$/.exec(t);
199+
const match_hr = /^(\d+)\s(h(ou)?r)s?$/.exec(t);
200+
const match_day = /^(\d+)\s(day)s?$/.exec(t);
201+
const match_week = /^(\d+)\s(week)s?$/.exec(t);
202+
if (match_sec) {
203+
time_duration += parseInt(match_sec[0]);
204+
} else if (match_min) {
205+
time_duration += parseInt(match_min[0]) * 60;
206+
} else if (match_hr) {
207+
time_duration += parseInt(match_hr[0]) * 3600;
208+
} else if (match_day) {
209+
time_duration += parseInt(match_day[0]) * 86400;
210+
} else if (match_week) {
211+
time_duration += parseInt(match_week[0]) * 604800;
212+
}
213+
});
214+
return time_duration;
215+
};

src/common/models/settings.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/* These are related to Course Settings */
33

44
import { Model } from '.';
5-
import { isTime, isTimeDuration } from './parsers';
5+
import { isNonNegInt, isTime } from './parsers';
66

77
export enum SettingType {
88
int = 'int',
@@ -132,7 +132,7 @@ const validSettingValue = (setting: GlobalSetting | CourseSetting, v: SettingVal
132132
case SettingType.text: return typeof(v) === 'string';
133133
case SettingType.boolean: return typeof(v) === 'boolean';
134134
case SettingType.time: return typeof(v) === 'string' && isTime(v);
135-
case SettingType.time_duration: return typeof(v) === 'string' && isTimeDuration(v);
135+
case SettingType.time_duration: return typeof(v) === 'number' && isNonNegInt(v);
136136
case SettingType.timezone: return typeof(v) === 'string';
137137
default: return false;
138138

src/components/instructor/SingleSetting.vue

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
<q-select v-if="setting.type === 'multilist'" multiple v-model="multilist_value" :options="options" />
2929
<input-with-blur
3030
v-if="setting.type === 'time_duration'"
31-
v-model="setting_value"
31+
v-model="time_duration_value"
3232
lazy-rules
3333
:rules="[checkTimeDuration]"
3434
/>
@@ -48,7 +48,7 @@ import InputWithBlur from 'src/components/common/InputWithBlur.vue';
4848
import { logger } from 'src/boot/logger';
4949
5050
import { useSettingsStore } from 'src/stores/settings';
51-
import { isTimeDuration } from 'src/common/models/parsers';
51+
import { convertTimeDuration, humanReadableTimeDuration, isTimeDuration } from 'src/common/models/parsers';
5252
import { api } from 'src/boot/axios';
5353
5454
const props = defineProps<{
@@ -69,6 +69,9 @@ const option_value = ref<OptionType>({ value: '', label: '' });
6969
const multilist_value = ref<OptionType[]>([]);
7070
const options = ref<OptionType[]>([]);
7171
72+
// Needed for time_duration
73+
const time_duration_value = ref('');
74+
7275
// Determine if the help in settings.doc is shown.
7376
const show_help = ref(false);
7477
@@ -77,6 +80,11 @@ const checkTimeDuration = (val: string) => isTimeDuration(val) || 'This must be
7780
7881
const valid_timezone = ref(true);
7982
83+
// Convert the time_duration to human readable format:
84+
if (course_setting.value.type === 'time_duration') {
85+
time_duration_value.value = humanReadableTimeDuration(course_setting.value.value as number);
86+
}
87+
8088
// These are for type list/multilist
8189
if (course_setting.value.options) {
8290
options.value = course_setting.value.options.map((opt: string | OptionType) =>
@@ -137,6 +145,13 @@ watch(() => multilist_value.value, async () => {
137145
await updateCourseSetting();
138146
}
139147
});
148+
149+
watch(() => time_duration_value.value, async () => {
150+
if (time_duration_value.value) {
151+
course_setting.value.value = convertTimeDuration(time_duration_value.value);
152+
await updateCourseSetting();
153+
}
154+
});
140155
</script>
141156

142157
<style lang="scss" scoped>

t/db/002_course_settings.t

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use DB::Schema;
2424
use WeBWorK3::Utils::Settings qw/isInteger isTimeString isTimeDuration isDecimal mergeCourseSettings
2525
isValidSetting/;
2626

27+
use DB::Utils qw/convertTimeDuration/;
2728
use TestUtils qw/removeIDs loadCSV/;
2829

2930
# Load the database
@@ -77,6 +78,26 @@ ok(isDecimal('00.33'), 'check type: decimal');
7778
ok(!isDecimal("0-.33"), 'check type: not a decimal');
7879
ok(!isDecimal('abc'), 'check type: not a decimal');
7980

81+
# Check that time duration conversion works as intended.
82+
is(convertTimeDuration('15 sec'), 15, 'convertTimeDuration: 15 sec');
83+
is(convertTimeDuration('15 secs'), 15, 'convertTimeDuration: 15 secs');
84+
85+
is(convertTimeDuration('15 min'), 900, 'convertTimeDuration: 15 min');
86+
is(convertTimeDuration('15 mins'), 900, 'convertTimeDuration: 15 mins');
87+
is(convertTimeDuration('15 minute'), 900, 'convertTimeDuration: 15 minute');
88+
is(convertTimeDuration('15 minutes'), 900, 'convertTimeDuration: 15 minutes');
89+
90+
is(convertTimeDuration('6 hour'), 21600, 'convertTimeDuration: 6 hour');
91+
is(convertTimeDuration('6 hours'), 21600, 'convertTimeDuration: 6 hours');
92+
is(convertTimeDuration('6 hr'), 21600, 'convertTimeDuration: 6 hr');
93+
is(convertTimeDuration('6 hrs'), 21600, 'convertTimeDuration: 6 hrs');
94+
95+
is(convertTimeDuration('3 day'), 259200, 'convertTimeDuration: 3 day');
96+
is(convertTimeDuration('3 days'), 259200, 'convertTimeDuration: 3 days');
97+
98+
is(convertTimeDuration('2 week'), 1209600, 'convertTimeDuration: 2 week');
99+
is(convertTimeDuration('2 weeks'), 1209600, 'convertTimeDuration: 2 weeks');
100+
80101
# Check that each of the given course_setting types are both valid and invalid.
81102
my $valid_setting = {
82103
setting_name => 'my_setting',

t/db/build_db.pl

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ BEGIN
2222
use Mojo::JSON qw/true false/;
2323

2424
use DB::Schema;
25-
use DB::Utils qw/updatePermissions/;
25+
use DB::Utils qw/updatePermissions convertTimeDuration/;
2626
use TestUtils qw/loadCSV/;
2727

2828
my $verbose = 1;
@@ -82,17 +82,24 @@ sub addCourses {
8282
}
8383
return;
8484
}
85+
use Data::Dumper;
86+
8587

8688
sub addSettings {
8789
say 'adding default settings' if $verbose;
8890

8991
my $settings_file = "$main::ww3_dir/conf/course_settings.yml";
9092
$settings_file = "$main::ww3_dir/conf/course_settings.dist.yml" unless -r $settings_file;
91-
say $settings_file;
9293
die "The default settings file: '$settings_file' does not exist or is not readable"
9394
unless -r $settings_file;
9495
my $course_settings = LoadFile($settings_file);
9596
for my $setting (@$course_settings) {
97+
98+
# If the setting is a time_duration, store it as a number of seconds in the db.
99+
if ($setting->{type} eq 'time_duration') {
100+
$setting->{default_value} = convertTimeDuration($setting->{default_value});
101+
}
102+
96103
# encode default_value as a JSON object.
97104
$setting->{default_value} = { value => $setting->{default_value} };
98105
$global_setting_rs->create($setting);
@@ -106,6 +113,12 @@ sub addSettings {
106113
my $global_setting = $global_setting_rs->find({ setting_name => $setting->{setting_name} });
107114
die "the setting: '$setting->{setting_name}' does not exist in the db" unless $global_setting;
108115

116+
# If the setting is a time_duration, store it as a number of seconds in the db.
117+
if ($global_setting->type eq 'time_duration') {
118+
$setting->{setting_value} = convertTimeDuration($setting->{setting_value});
119+
}
120+
121+
109122
$course->add_to_course_settings({
110123
course_id => $course->course_id,
111124
setting_id => $global_setting->setting_id,

t/db/sample_data/course_settings.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
course_name,setting_name,setting_value
22
Precalculus,institution,"Springfield CC"
3+
Precalculus,session_key_timeout,"20 mins"
34
"Abstract Algebra",institution,"Springfield University"
45
Topology,institution,"Springfield University"
56
Arithmetic,institution,"Springfield CC"

t/mojolicious/015_course_settings.t

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => '
4040
->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => false);
4141

4242
# Load the global settings from the file
43-
my $global_settings_from_file = LoadFile("$main::ww3_dir/conf/course_settings.yml");
43+
my $settings_file = "$main::ww3_dir/conf/course_settings.yml";
44+
$settings_file = "$main::ww3_dir/conf/course_settings.dist.yml" unless -r $settings_file;
45+
my $global_settings_from_file = LoadFile($settings_file);
4446

4547
# Get the global/default settings
4648

tests/unit-tests/settings.spec.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { CourseSetting, DBCourseSetting, GlobalSetting, SettingType
44
} from 'src/common/models/settings';
5+
import { convertTimeDuration, humanReadableTimeDuration } from 'src/common/models/parsers';
56

67
describe('Testing Course Settings', () => {
78
const global_setting = {
@@ -804,7 +805,7 @@ describe('Testing Course Settings', () => {
804805

805806
course_setting.set({
806807
setting_name: 'time_duration',
807-
default_value: '10 days',
808+
default_value: 1234,
808809
description: 'I am an time interval',
809810
doc: 'Extended help',
810811
type: 'time_duration',
@@ -816,6 +817,9 @@ describe('Testing Course Settings', () => {
816817
course_setting.set({ value: 3.14 });
817818
expect(course_setting.isValid()).toBe(false);
818819

820+
course_setting.set({ value: '3 days' });
821+
expect(course_setting.isValid()).toBe(false);
822+
819823
course_setting.set({ value: 'hi' });
820824
expect(course_setting.isValid()).toBe(false);
821825

@@ -824,4 +828,71 @@ describe('Testing Course Settings', () => {
824828
});
825829

826830
});
831+
832+
describe('Test converting of human readable time duration to number of seconds', () => {
833+
test('Test time duration of seconds', () => {
834+
expect(convertTimeDuration('1 sec')).toBe(1);
835+
expect(convertTimeDuration('15 secs')).toBe(15);
836+
});
837+
838+
test('Test time duration of mins', () => {
839+
expect(convertTimeDuration('1 min')).toBe(60);
840+
expect(convertTimeDuration('5 mins')).toBe(5 * 60);
841+
expect(convertTimeDuration('5 mins, 30 secs')).toBe(5*60+30);
842+
});
843+
844+
test('Test time duration of hours', () => {
845+
expect(convertTimeDuration('1 hour')).toBe(1 * 60 * 60);
846+
expect(convertTimeDuration('1 hr')).toBe(1 * 60 * 60);
847+
expect(convertTimeDuration('5 hours')).toBe(5 * 60 * 60);
848+
expect(convertTimeDuration('3 hrs')).toBe(3 * 60 * 60);
849+
expect(convertTimeDuration('3 hrs, 10 mins, 15 seconds')).toBe(3*60*60+10*60+15);
850+
});
851+
852+
test('Test time duration of days', () => {
853+
expect(convertTimeDuration('1 day')).toBe(1 * 24 * 60 * 60);
854+
expect(convertTimeDuration('3 days')).toBe(3 * 24 * 60 * 60);
855+
expect(convertTimeDuration('3 days, 12 hours')).toBe(3 * 24 * 60 * 60+ 12*60*60);
856+
});
857+
858+
test('Test time duration of weeks', () => {
859+
expect(convertTimeDuration('1 week')).toBe(1 * 7 * 24 * 60 * 60);
860+
expect(convertTimeDuration('2 weeks')).toBe(2 * 7 * 24 * 60 * 60);
861+
expect(convertTimeDuration('2 weeks, 5 days')).toBe(2 * 7 * 24 * 60 * 60 + 5 *24 *60 *60);
862+
});
863+
864+
});
865+
866+
describe('Test conversion of num. seconds to human readable time durations', () => {
867+
test('Test time duration of secs', () => {
868+
expect(humanReadableTimeDuration(0)).toBe('');
869+
expect(humanReadableTimeDuration(1)).toBe('1 sec');
870+
expect(humanReadableTimeDuration(15)).toBe('15 secs');
871+
});
872+
873+
test('Test time duration of mins', () => {
874+
expect(humanReadableTimeDuration(60)).toBe('1 min');
875+
expect(humanReadableTimeDuration(5 * 60)).toBe('5 mins');
876+
expect(humanReadableTimeDuration(5 * 60 + 30)).toBe('5 mins, 30 secs');
877+
});
878+
879+
test('Test time duration of hours', () => {
880+
expect(humanReadableTimeDuration(3600)).toBe('1 hour');
881+
expect(humanReadableTimeDuration(5 * 3600)).toBe('5 hours');
882+
expect(humanReadableTimeDuration(5 * 3600 + 30 * 60)).toBe('5 hours, 30 mins');
883+
});
884+
885+
test('Test time duration of days', () => {
886+
expect(humanReadableTimeDuration(3600 * 24)).toBe('1 day');
887+
expect(humanReadableTimeDuration(3 * 3600 * 24)).toBe('3 days');
888+
expect(humanReadableTimeDuration(3 * 3600 * 24 + 6 * 3600)).toBe('3 days, 6 hours');
889+
expect(humanReadableTimeDuration(3 * 3600 * 24 + 6 * 3600 + 30 * 60)).toBe('3 days, 6 hours, 30 mins');
890+
});
891+
892+
test('Test time duration of weeks', () => {
893+
expect(humanReadableTimeDuration(3600 * 24 * 7)).toBe('1 week');
894+
expect(humanReadableTimeDuration(3600 * 24 * 7 * 2)).toBe('2 weeks');
895+
expect(humanReadableTimeDuration(3600 * 24 * 7 * 2 + 3 * 3600 * 24)).toBe('2 weeks, 3 days');
896+
});
897+
});
827898
});

0 commit comments

Comments
 (0)