diff --git a/lib/DB/Schema/Result/ProblemSet/HWSet.pm b/lib/DB/Schema/Result/ProblemSet/HWSet.pm index 4b6f8dec..43b08645 100644 --- a/lib/DB/Schema/Result/ProblemSet/HWSet.pm +++ b/lib/DB/Schema/Result/ProblemSet/HWSet.pm @@ -27,6 +27,10 @@ sub valid_dates ($=) { return [ 'open', 'reduced_scoring', 'due', 'answer' ]; } +sub optional_fields_in_dates ($=) { + return { enable_reduced_scoring => 'bool' }; +} + =head2 C subroutine that returns the array for the required dates: C<['open', 'due' ,'answer']> @@ -79,11 +83,10 @@ This is a description of the homework set. sub valid_params ($=) { return { - enable_reduced_scoring => 'bool', - hide_hint => 'bool', - hardcopy_header => q{.*}, - set_header => q{.*}, - description => q{.*} + hide_hint => 'bool', + hardcopy_header => q{.*}, + set_header => q{.*}, + description => q{.*} }; } diff --git a/lib/DB/Schema/Result/ProblemSet/Quiz.pm b/lib/DB/Schema/Result/ProblemSet/Quiz.pm index dfee1779..dbf14196 100644 --- a/lib/DB/Schema/Result/ProblemSet/Quiz.pm +++ b/lib/DB/Schema/Result/ProblemSet/Quiz.pm @@ -27,6 +27,8 @@ sub valid_dates ($=) { return [ 'open', 'due', 'answer' ]; } +sub optional_fields_in_dates ($=) { return {}; } + =head2 C subroutine that returns the array for the required dates: C<['open', 'due' ,'answer']> diff --git a/lib/DB/Schema/Result/ProblemSet/ReviewSet.pm b/lib/DB/Schema/Result/ProblemSet/ReviewSet.pm index 164c9571..d7e94de0 100644 --- a/lib/DB/Schema/Result/ProblemSet/ReviewSet.pm +++ b/lib/DB/Schema/Result/ProblemSet/ReviewSet.pm @@ -26,6 +26,8 @@ sub valid_dates ($=) { return [ 'open', 'closed' ]; } +sub optional_fields_in_dates ($=) { return {}; } + =head2 C subroutine that returns the array for the required dates: C<['open', 'closed']> diff --git a/lib/DB/Schema/Result/UserSet.pm b/lib/DB/Schema/Result/UserSet.pm index c9d1ff95..49969aa6 100644 --- a/lib/DB/Schema/Result/UserSet.pm +++ b/lib/DB/Schema/Result/UserSet.pm @@ -155,13 +155,6 @@ __PACKAGE__->typecast_map( } ); -my $set_type = { - 1 => 'DB::Schema::Result::UserSet::HWSet', - 2 => 'DB::Schema::Result::UserSet::Quiz', - 3 => 'DB::Schema::Result::UserSet::JITAR', - 4 => 'DB::Schema::Result::UserSet::ReviewSet' -}; - sub set_type ($) { my %set_type_rev = reverse %{$DB::Schema::ResultSet::ProblemSet::SET_TYPES}; return $set_type_rev{ shift->type }; diff --git a/lib/DB/Schema/Result/UserSet/HWSet.pm b/lib/DB/Schema/Result/UserSet/HWSet.pm index 576efc35..4a473b6e 100644 --- a/lib/DB/Schema/Result/UserSet/HWSet.pm +++ b/lib/DB/Schema/Result/UserSet/HWSet.pm @@ -16,6 +16,10 @@ sub required_dates ($=) { return DB::Schema::Result::ProblemSet::HWSet::required_dates(); } +sub optional_fields_in_dates ($=) { + return DB::Schema::Result::ProblemSet::HWSet::optional_fields_in_dates(); +} + sub valid_params ($=) { return DB::Schema::Result::ProblemSet::HWSet::valid_params(); } diff --git a/lib/DB/Schema/Result/UserSet/Quiz.pm b/lib/DB/Schema/Result/UserSet/Quiz.pm index 3993ad9e..d0a0ac91 100644 --- a/lib/DB/Schema/Result/UserSet/Quiz.pm +++ b/lib/DB/Schema/Result/UserSet/Quiz.pm @@ -16,6 +16,10 @@ sub required_dates ($=) { return DB::Schema::Result::ProblemSet::Quiz::required_dates(); } +sub optional_fields_in_dates ($=) { + return DB::Schema::Result::ProblemSet::Quiz::optional_fields_in_dates(); +} + sub valid_params ($=) { return DB::Schema::Result::ProblemSet::Quiz::valid_params(); } diff --git a/lib/DB/Schema/Result/UserSet/ReviewSet.pm b/lib/DB/Schema/Result/UserSet/ReviewSet.pm index 9f56d6ce..7f97587f 100644 --- a/lib/DB/Schema/Result/UserSet/ReviewSet.pm +++ b/lib/DB/Schema/Result/UserSet/ReviewSet.pm @@ -16,6 +16,10 @@ sub required_dates ($=) { return DB::Schema::Result::ProblemSet::ReviewSet::required_dates(); } +sub optional_fields_in_dates ($=) { + return DB::Schema::Result::ProblemSet::ReviewSet::optional_fields_in_dates(); +} + sub valid_params ($=) { return DB::Schema::Result::ProblemSet::ReviewSet::valid_params(); } diff --git a/lib/DB/Schema/ResultSet/Course.pm b/lib/DB/Schema/ResultSet/Course.pm index f7921a3b..a4cdbb2a 100644 --- a/lib/DB/Schema/ResultSet/Course.pm +++ b/lib/DB/Schema/ResultSet/Course.pm @@ -12,7 +12,7 @@ use DB::Utils qw/getCourseInfo getUserInfo/; use DB::Exception; use Exception::Class ('DB::Exception::CourseNotFound', 'DB::Exception::CourseExists'); -#use DB::TestUtils qw/removeIDs/; +#use TestUtils qw/removeIDs/; use WeBWorK3::Utils::Settings qw/getDefaultCourseSettings mergeCourseSettings getDefaultCourseValues validateCourseSettings/; diff --git a/lib/DB/Schema/ResultSet/UserSet.pm b/lib/DB/Schema/ResultSet/UserSet.pm index 026e98ac..349337de 100644 --- a/lib/DB/Schema/ResultSet/UserSet.pm +++ b/lib/DB/Schema/ResultSet/UserSet.pm @@ -392,10 +392,26 @@ sub addUserSet ($self, %args) { $params->{set_version} = 1 unless defined($params->{set_version}); my $original_dates = $params->{set_dates} // {}; - # If any of the date are 0, then remove them. + # If any of the dates are 0, then remove them. + # Only check if the date_fields are 0. There is the option to store non-dates in the + # set_dates field. + my $date_fields; + + # Note: although this works okay, if another subtype of a UserSet is created, this needs to be updated. + if ($params->{type} == 1) { + $date_fields = \&DB::Schema::Result::UserSet::HWSet::valid_dates; + } elsif ($params->{type} == 2) { + $date_fields = \&DB::Schema::Result::UserSet::Quiz::valid_dates; + } elsif ($params->{type} == 4) { + $date_fields = \&DB::Schema::Result::UserSet::ReviewSet::valid_dates; + } else { + die "The type $params->{type} is not valid."; + } + if ($args{params}->{set_dates}) { - for my $key (keys %{ $args{params}->{set_dates} }) { - delete $args{params}->{set_dates}->{$key} if $args{params}->{set_dates}->{$key} == 0; + for my $key (@{ $date_fields->() }) { + delete $args{params}->{set_dates}->{$key} + if defined($args{params}->{set_dates}->{$key}) && $args{params}->{set_dates}->{$key} == 0; } } diff --git a/lib/DB/WithDates.pm b/lib/DB/WithDates.pm index b0b3a498..ca8a431f 100644 --- a/lib/DB/WithDates.pm +++ b/lib/DB/WithDates.pm @@ -10,25 +10,30 @@ use DB::Schema::Result::ProblemSet::HWSet; use DB::Exception; -our $valid_dates; # Arrayref of allowed/valid dates -our $required_dates; # Arrayref of required dates +my $valid_dates; # Arrayref of allowed/valid dates +my $required_dates; # Arrayref of required dates +my $optional_fields_in_dates; # hashref of other non-date fields in the hash and the type. sub validDates ($self, $field_name) { - $valid_dates = ref($self)->valid_dates; - $required_dates = ref($self)->required_dates; + $valid_dates = ref($self)->valid_dates; + $required_dates = ref($self)->required_dates; + $optional_fields_in_dates = ref($self)->optional_fields_in_dates; $self->validDateFields($field_name); $self->hasRequiredDateFields($field_name); $self->validDateFormat($field_name); $self->checkDates($field_name); + $self->validateOptionalFields($field_name); return 1; } sub validDateFields ($self, $field_name) { - my @fields = keys %{ $self->get_inflated_column($field_name) }; + my @fields = keys %{ $self->get_inflated_column($field_name) }; + my @all_fields = (@$valid_dates, keys %$optional_fields_in_dates); + # If this is not empty, there are illegal fields. - my @bad_fields = array_minus(@fields, @$valid_dates); - DB::Exception::InvalidDateField->throw(field_names => join(', ', @bad_fields)) + my @bad_fields = array_minus(@fields, @all_fields); + DB::Exception::InvalidDateField->throw(field_names => join(", ", @bad_fields)) if (scalar(@bad_fields) != 0); return 1; @@ -70,4 +75,21 @@ sub checkDates ($self, $field_name) { return 1; } +# This checks the options fields that aren't dates +sub validateOptionalFields ($self, $field_name) { + my $params_hash = $self->get_inflated_column($field_name); + # if it doesn't exist, it is valid + return 1 unless defined $params_hash; + + for my $key (keys %$optional_fields_in_dates) { + next unless defined $params_hash->{$key}; + my $re = $params_hash->{$key}; + my $valid = $re eq 'bool' ? JSON::PP::is_bool($params_hash->{$key}) : $params_hash->{$key} =~ qr/^$re$/x; + DB::Exception::InvalidParameter->throw( + message => "The parameter named $key is not valid. It has value $params_hash->{$key}") + unless $valid; + } + return 1; +} + 1; diff --git a/lib/WeBWorK3.pm b/lib/WeBWorK3.pm index fd997db6..50f8e57b 100644 --- a/lib/WeBWorK3.pm +++ b/lib/WeBWorK3.pm @@ -104,29 +104,40 @@ sub userRoutes ($self) { my $user_routes = $self->routes->any('/webwork3/api/users')->requires(authenticated => 1)->to(controller => 'User'); $user_routes->get('/')->to(action => 'getGlobalUsers'); $user_routes->post('/')->to(action => 'addGlobalUser'); - $user_routes->get('/:user_id')->to(action => 'getGlobalUser'); + $user_routes->get('/:user')->to(action => 'getGlobalUser'); $user_routes->put('/:user_id')->to(action => 'updateGlobalUser'); $user_routes->delete('/:user_id')->to(action => 'deleteGlobalUser'); $user_routes->get('/:user_id/courses')->to(action => 'getUserCourses'); # Get global users for a course. $self->routes->get('/webwork3/api/courses/:course_id/global-users')->to('User#getGlobalCourseUsers'); - # This is needed to get global users as instructor permission. Need to have - # the parameter course_id. - $self->routes->any('/webwork3/api/courses/:course_id/users/:user/exists')->requires(authenticated => 1) - ->to('User#getGlobalUser'); + return; } sub courseUserRoutes ($self) { - my $course_user_routes = $self->routes->any('/webwork3/api/courses/:course_id/users')->requires(authenticated => 1) - ->to(controller => 'User'); - $course_user_routes->get('/')->to(action => 'getCourseUsers'); - $course_user_routes->post('/')->to(action => 'addCourseUser'); - $course_user_routes->get('/:user_id')->to(action => 'getCourseUser'); - $course_user_routes->put('/:user_id')->to(action => 'updateCourseUser'); - $course_user_routes->delete('/:user_id')->to(action => 'deleteCourseUser'); - $self->routes->any('/webwork3/api/courses/:course_id/courseusers')->requires(authenticated => 1) - ->to('User#getMergedCourseUsers'); + my $course_routes = + $self->routes->any('/webwork3/api/courses/:course_id')->requires(authenticated => 1)->to(controller => 'User'); + $course_routes->get('/users')->to(action => 'getCourseUsers'); + $course_routes->get('/users')->to(action => 'getCourseUsers'); + $course_routes->post('/users')->to(action => 'addCourseUser'); + $course_routes->get('/users/:user_id')->to(action => 'getCourseUser'); + $course_routes->put('/users/:user_id')->to(action => 'updateCourseUser'); + $course_routes->delete('/users/:user_id')->to(action => 'deleteCourseUser'); + + # There are some routes needed for global user crud, but the permssions require that the + # user has permissions within a course. + + $course_routes->get('/global-courseusers')->to('User#getGlobalCourseUsers'); + $course_routes->post('/global-users')->to('User#addGlobalUserFromCourse'); + $course_routes->get('/global-users/:user')->to('User#getGlobalUserFromCourse'); + $course_routes->put('/global-users/:user_id')->to('User#updateGlobalUserFromCourse'); + $course_routes->delete('/global-users/:user_id')->to('User#deleteGlobalUserFromCourse'); + + $course_routes->get('/courseusers')->to('User#getMergedCourseUsers'); + + # This is used to check if a user with given username exists. + $course_routes->get('/users/:username/exists')->to('User#checkGlobalUser'); + return; } diff --git a/lib/WeBWorK3/Controller/User.pm b/lib/WeBWorK3/Controller/User.pm index 5612c8d1..61f939b6 100644 --- a/lib/WeBWorK3/Controller/User.pm +++ b/lib/WeBWorK3/Controller/User.pm @@ -1,17 +1,32 @@ package WeBWorK3::Controller::User; use Mojo::Base 'Mojolicious::Controller', -signatures; +use Try::Tiny; + sub getGlobalUsers ($self) { my @global_users = $self->schema->resultset('User')->getAllGlobalUsers; $self->render(json => \@global_users); return; } +# Passing the username into the getGlobalUser results in a problem with permssions. This route +# should be used to pass in the username. +sub checkGlobalUser ($self) { + try { + my $user = $self->schema->resultset('User')->getGlobalUser(info => { username => $self->param('username') }); + $self->render(json => $user); + return; + } catch { + $self->render(json => {}) if ref($_) eq 'DB::Exception::UserNotFound'; + }; + return; +} + sub getGlobalUser ($self) { my $user = - $self->param('user_id') =~ /^\d+$/ - ? $self->schema->resultset('User')->getGlobalUser(info => { user_id => int($self->param('user_id')) }) - : $self->schema->resultset('User')->getGlobalUser(info => { username => $self->param('user_id') }); + $self->param('user') =~ /^\d+$/ + ? $self->schema->resultset('User')->getGlobalUser(info => { user_id => int($self->param('user')) }) + : $self->schema->resultset('User')->getGlobalUser(info => { username => $self->param('user') }); $self->render(json => $user); return; } @@ -48,6 +63,14 @@ sub getGlobalCourseUsers ($self) { return; } +# The following are needed for handling global users from instructors in a course + +sub getGlobalUsersFromCourse ($self) { $self->getGlobalUsers; return } +sub getGlobalUserFromCourse ($self) { $self->getGlobalUser; return } +sub addGlobalUserFromCourse ($self) { $self->addGlobalUser; return } +sub updateGlobalUserFromCourse ($self) { $self->updateGlobalUser; return } +sub deleteGlobalUserFromCourse ($self) { $self->deleteGlobalUser; return } + # The following subs are related to users within a given course. sub getMergedCourseUsers ($self) { diff --git a/src/common/api-requests/errors.ts b/src/common/api-requests/errors.ts new file mode 100644 index 00000000..e6be5530 --- /dev/null +++ b/src/common/api-requests/errors.ts @@ -0,0 +1,15 @@ +// General Error coming from the API service + +import { logger } from 'boot/logger'; +import { Model } from '../models'; + +export interface ResponseError { + exception: string; + message: string; +} + +export const invalidError = (model: Model, msg: string) => { + logger.error(msg); + logger.error(JSON.stringify(model.toObject())); + return Promise.reject(new Error(msg)); +}; diff --git a/src/common/api-requests/interfaces.ts b/src/common/api-requests/interfaces.ts deleted file mode 100644 index 52db491a..00000000 --- a/src/common/api-requests/interfaces.ts +++ /dev/null @@ -1,6 +0,0 @@ -// General Error coming from the API service - -export interface ResponseError { - exception: string; - message: string; -} diff --git a/src/common/api-requests/user.ts b/src/common/api-requests/user.ts index 71e3dbd9..18fcefad 100644 --- a/src/common/api-requests/user.ts +++ b/src/common/api-requests/user.ts @@ -1,24 +1,32 @@ import { api } from 'boot/axios'; -import { ParseableCourseUser, ParseableUser } from 'src/common/models/users'; -import { ResponseError } from 'src/common/api-requests/interfaces'; +import { ParseableCourseUser, ParseableUser, User } from 'src/common/models/users'; +import { ResponseError } from 'src/common/api-requests/errors'; -export async function checkIfUserExists(course_id: number, username: string) { - const response = await api.get(`courses/${course_id}/users/${username}/exists`); - if (response.status === 250) { - throw response.data as ResponseError; +/** + * Checks if a global user exists. Both the course_id and username need to be passed in + * in order to check permissions. + * + * @returns either an existing user or an empty object. + */ + +export async function checkIfUserExists(course_id: number, username: string): Promise { + try { + const response = await api.get(`courses/${course_id}/users/${username}/exists`); + return response.data as ParseableCourseUser; + } catch (_err) { + return {}; } - return response.data as ParseableCourseUser; } /** * Gets the global user in the database given by username. This returns a user or throws a * ResponseError if the user is not found. */ -export async function getUser(username: string): Promise { +export async function getUser(username: string): Promise { const response = await api.get(`users/${username}`); if (response.status === 200) { - return response.data as ParseableUser; + return new User(response.data as ParseableUser); } else { throw response.data as ResponseError; } diff --git a/src/common/models/courses.ts b/src/common/models/courses.ts index c0e46578..37892d1a 100644 --- a/src/common/models/courses.ts +++ b/src/common/models/courses.ts @@ -1,16 +1,16 @@ import { RequiredFieldsException, Model, Dictionary, generic } from 'src/common/models'; -import { parseNonNegInt, parseBoolean, parseUsername, parseUserRole, UserRole } from './parsers'; +import { isNonNegInt, isValidUsername, UserRole, parseUserRole } from './parsers'; export interface ParseableCourse { - course_id?: number | string; + course_id?: number; course_name?: string; - visible?: string | number | boolean; + visible?: boolean; course_dates? : ParseableCourseDates; } export interface ParseableCourseDates { - start?: number | string; - end?: number | string; + start?: number; + end?: number; } class CourseDates extends Model { @@ -35,10 +35,14 @@ class CourseDates extends Model { } get start() { return this._start; } - set start(value: number | string) { this._start = parseNonNegInt(value); } + set start(value: number) { this._start = value; } get end() { return this._end; } - set end(value: number | string) { this._end = parseNonNegInt(value); } + set end(value: number) { this._end = value; } + + isValid(): boolean { + return this.start <= this.end; + } } export class Course extends Model { @@ -71,7 +75,6 @@ export class Course extends Model { if (params.course_id != undefined) this.course_id = params.course_id; if (params.course_name != undefined) this.course_name = params.course_name; if (params.visible != undefined) this.visible = params.visible; - super.checkParams(params as Dictionary); if (params.course_dates) this.setDates(params.course_dates); } @@ -80,23 +83,21 @@ export class Course extends Model { } get course_id(): number { return this._course_id; } - set course_id(value: string | number) { - this._course_id = parseNonNegInt(value); - } + set course_id(value: number) { this._course_id = value; } get course_name() { return this._course_name; } - set course_name(value: string) { - this._course_name = value; - } + set course_name(value: string) { this._course_name = value; } get visible() { return this._visible; } - set visible(value: string | number | boolean) { - this._visible = parseBoolean(value); - } + set visible(value: boolean) { this._visible = value; } clone() { return new Course(this.toObject() as ParseableCourse); } + + isValid(): boolean { + return this.course_name.length > 0 && isNonNegInt(this.course_id) && this.course_dates.isValid(); + } } /** @@ -104,11 +105,11 @@ export class Course extends Model { */ export interface ParseableUserCourse { - course_id?: number | string; - user_id?: number | string; + course_id?: number; + user_id?: number; course_name?: string; username?: string; - visible?: number | string | boolean; + visible?: boolean; role?: string; course_dates?: ParseableCourseDates; } @@ -134,12 +135,6 @@ export class UserCourse extends Model { constructor(params: ParseableUserCourse = {}) { super(); - if (params.course_name == undefined) { - throw new RequiredFieldsException('course_name'); - } - if (params.user_id == undefined) { - throw new RequiredFieldsException('user_id'); - } this.set(params); } @@ -148,9 +143,8 @@ export class UserCourse extends Model { if (params.course_name != undefined) this.course_name = params.course_name; if (params.visible != undefined) this.visible = params.visible; if (params.user_id != undefined) this.user_id = params.user_id; - if (params.username != undefined) this.username = params.username; - if (params.role != undefined) this.role = params.role; - super.checkParams(params as Dictionary); + if (params.username) this.username = params.username; + if (params.role) this.role = params.role; } setDates(date_params: ParseableCourseDates = {}) { @@ -158,31 +152,29 @@ export class UserCourse extends Model { } get course_id(): number { return this._course_id; } - set course_id(value: string | number) { - this._course_id = parseNonNegInt(value); - } + set course_id(value: number) { this._course_id = value; } get user_id() { return this._user_id; } - set user_id(value: string | number) { - this._user_id = parseNonNegInt(value); - } + set user_id(value: number) { this._user_id = value;} get username() { return this._username;} - set username(value: string) { - this._username = parseUsername(value); - } + set username(value: string) { this._username = value; } get course_name() { return this._course_name; } - set course_name(value: string) { - this._course_name = value; - } + set course_name(value: string) { this._course_name = value; } get visible() { return this._visible; } - set visible(value: string | number | boolean) { - this._visible = parseBoolean(value); - } + set visible(value: boolean) { this._visible = value; } get role(): UserRole { return this._role; } set role(value: string) { this._role = parseUserRole(value); } + clone(): UserCourse { + return new UserCourse(this.toObject()); + } + + isValid(): boolean { + return isNonNegInt(this.course_id) && isNonNegInt(this.user_id) && isValidUsername(this.username) + && this.role !== UserRole.unknown && this.course_name.length > 0 && this.course_dates.isValid(); + } } diff --git a/src/common/models/index.ts b/src/common/models/index.ts index ffedf6fa..75a993fc 100644 --- a/src/common/models/index.ts +++ b/src/common/models/index.ts @@ -41,7 +41,7 @@ export class Model { unknown as { toObject(): Dictionary}; obj[key] = param_obj.toObject(); } else { - obj[key] = (this as unknown as Dictionary)[key]; + if (this[key as keyof this] != undefined) obj[key] = (this as unknown as Dictionary)[key]; } } }); diff --git a/src/common/models/parsers.ts b/src/common/models/parsers.ts index 4c847bbc..701e30fb 100644 --- a/src/common/models/parsers.ts +++ b/src/common/models/parsers.ts @@ -100,41 +100,40 @@ export class UserRoleException extends ParseError { } } -// Parsing functions +// Parsing Regular Expressions + +export const non_neg_int_re = /^\s*(\d+)\s*$/; +export const non_neg_decimal_re = /(^\s*(\d+)(\.\d*)?\s*$)|(^\s*\.\d+\s*$)/; +export const mail_re = /^[\w.]+@([a-zA-Z_.]+)+\.[a-zA-Z]{2,9}$/; +export const username_re = /^[_a-zA-Z]([a-zA-Z._0-9])+$/; + +// Checking functions + +export const isNonNegInt = (v: number | string) => non_neg_int_re.test(`${v}`); +export const isNonNegDecimal = (v: number | string) => non_neg_decimal_re.test(`${v}`); +export const isValidUsername = (v: string) => username_re.test(v) || mail_re.test(v); +export const isValidEmail = (v: string) => mail_re.test(v); + +// Parsing functionis export function parseNonNegInt(val: string | number) { - if (/^\s*(\d+)\s*$/.test(`${val}`)) { - return parseInt(`${val}`); - } else { - throw new NonNegIntException(`The value ${val} is not a non-negative integer`); - } + if (isNonNegInt(val)) return parseInt(`${val}`); + throw new NonNegIntException(`The value ${val} is not a non-negative integer`); } export function parseNonNegDecimal(val: string | number) { - if (/(^\s*(\d+)(\.\d*)?\s*$)|(^\s*\.\d+\s*$)/.test(`${val}`)) { - return parseFloat(`${val}`); - } else { - throw new NonNegDecimalException(`The value ${val} is not a non-negative decimal`); - } + if (isNonNegDecimal(val)) return parseFloat(`${val}`); + throw new NonNegDecimalException(`The value ${val} is not a non-negative decimal`); } -export const mailRE = /^[\w.]+@([a-zA-Z_.]+)+\.[a-zA-Z]{2,9}$/; -export const usernameRE = /^[_a-zA-Z]([a-zA-Z._0-9])+$/; - -export function parseUsername(val: string | undefined) { - if (typeof val === 'string' && (val === '' || mailRE.test(`${val ?? ''}`) || usernameRE.test(`${val}`))) { - return val; - } else { - throw new UsernameParseException(`The value '${val?.toString() ?? ''}' is not a value username`); - } +export function parseUsername(val: string) { + if (isValidUsername(val)) return val; + throw new UsernameParseException(`The value '${val?.toString() ?? ''}' is not a value username`); } -export function parseEmail(val: string | undefined) { - if (typeof val === 'string' && mailRE.test(`${val}`)) { - return val; - } else { - throw new EmailParseException(`The value '${val?.toString() ?? ''}' is not a value email`); - } +export function parseEmail(val: string) { + if (isValidEmail(val)) return val; + throw new EmailParseException(`The value '${val?.toString() ?? ''}' is not a value email`); } const booleanRE = /^([01])|(true)|(false)$/; @@ -168,12 +167,15 @@ export enum UserRole { unknown = 'UNKNOWN' } +const user_roles = ['admin', 'instructor', 'ta', 'student', 'unknown']; +export const isValidUserRole = (v: string) => user_roles.includes(v.toLowerCase()); + export function parseUserRole(role: string): UserRole { if (role.toLocaleLowerCase() === 'admin') return UserRole.admin; if (role.toLocaleLowerCase() === 'instructor') return UserRole.instructor; if (role.toLocaleLowerCase() === 'ta') return UserRole.ta; if (role.toLocaleLowerCase() === 'student') return UserRole.student; - throw new UserRoleException(`The value '${role}' is not a valid role.`); + return UserRole.unknown; } export function parseString(_value: string | number | boolean) { diff --git a/src/common/models/problem_sets.ts b/src/common/models/problem_sets.ts index 1840a7a2..163d5dcc 100644 --- a/src/common/models/problem_sets.ts +++ b/src/common/models/problem_sets.ts @@ -1,8 +1,7 @@ /* These are Problem Set interfaces */ -import { logger } from 'boot/logger'; import { Model } from '.'; -import { parseBoolean, ParseError, parseNonNegInt } from './parsers'; +import { isNonNegInt, ParseError } from './parsers'; export enum ProblemSetType { HW = 'HW', @@ -11,6 +10,10 @@ export enum ProblemSetType { UNKNOWN = 'UNKNOWN' } +const set_types = ['hw', 'quiz', 'review', 'unknown']; + +export const isValidProblemSetType = (value: string) => set_types.includes(value); + /** * This takes in a general problem set and returns the specific subclassed ProblemSet. * The returned object is one of HomeworkSet, Quiz or ReviewSet or a ParseError is thrown. @@ -35,12 +38,15 @@ export type ProblemSetDates = HomeworkSetDates | QuizDates | ReviewSetDates; // Problem Set (HomeworkSet, Quiz, ReviewSet ) classes and interfaces for parsing. -export interface ParseableProblemSet { - set_id?: string | number; +export interface ParseableSetParams { + set_id?: number; set_name?: string; - course_id?: string | number; + course_id?: number; set_type?: string; - set_visible?: string | number | boolean; + set_visible?: boolean; + +} +export type ParseableProblemSet = ParseableSetParams & { set_params?: ParseableProblemSetParams; set_dates?: ParseableProblemSetDates; } @@ -53,7 +59,7 @@ export class ProblemSet extends Model { private _set_id = 0; private _set_visible = false; private _course_id = 0; - protected _set_type: ProblemSetType = ProblemSetType.UNKNOWN; + protected _set_type = 'UNKNOWN'; private _set_name = ''; constructor(params: ParseableProblemSet = {}) { @@ -72,52 +78,45 @@ export class ProblemSet extends Model { } set(params: ParseableProblemSet) { - if (params.set_id) this.set_id = params.set_id; + if (params.set_id !== undefined) this.set_id = params.set_id; if (params.set_visible !== undefined) this.set_visible = params.set_visible; if (params.course_id != undefined) this.course_id = params.course_id; - if (params.set_type) this.set_type = params.set_type; if (params.set_name) this.set_name = params.set_name; } public get set_id(): number { return this._set_id;} - public set set_id(value: number | string) { this._set_id = parseNonNegInt(value);} + public set set_id(value: number) { this._set_id = value; } public get course_id(): number { return this._course_id;} - public set course_id(value: number | string) { this._course_id = parseNonNegInt(value);} + public set course_id(value: number) { this._course_id = value;} public get set_visible() : boolean { return this._set_visible;} - public set set_visible(value: number | string | boolean) { this._set_visible = parseBoolean(value);} + public set set_visible(value: boolean) { this._set_visible = value;} - public get set_type(): ProblemSetType { return this._set_type; } - public set set_type(value: string) { - if (value === 'HW') { this._set_type = ProblemSetType.HW;} - else if (value === 'QUIZ') { this._set_type = ProblemSetType.QUIZ;} - else if (value === 'REVIEW') { this._set_type = ProblemSetType.REVIEW_SET;} - else { this._set_type = ProblemSetType.UNKNOWN; } - } + public get set_type(): string { return this._set_type; } public get set_name() : string { return this._set_name;} public set set_name(value: string) { this._set_name = value;} public get set_params(): ProblemSetParams { - throw 'The subclass must override set_params();'; + throw 'The subclass must override set_params()'; } public get set_dates(): ProblemSetDates { - throw 'The subclass must override set_dates();'; + throw 'The subclass must override set_dates()'; } - hasValidDates() { - throw 'The hasValidDates() method must be overridden.'; + isValid() { + return isNonNegInt(this.set_id) && isNonNegInt(this.course_id) && this.set_name.length > 0; } } // Quiz models and interfaces. export interface ParseableQuizDates { - open?: number | string; - due?: number | string; - answer?: number | string; + open?: number; + due?: number; + answer?: number; } export class QuizDates extends Model { @@ -136,24 +135,23 @@ export class QuizDates extends Model { if (params.answer != undefined) this.answer = params.answer; } - get all_field_names(): string[] { - return ['open', 'due', 'answer']; - } + static ALL_FIELDS = ['open', 'due', 'answer']; + get all_field_names(): string[] { return QuizDates.ALL_FIELDS; } get param_fields(): string[] { return [];} public get open(): number { return this._open;} - public set open(value: number | string) { - if (value != undefined) this._open = parseNonNegInt(value); + public set open(value: number) { + if (value != undefined) this._open = value; } public get due(): number { return this._due;} - public set due(value: number | string) { - if (value != undefined) this._due = parseNonNegInt(value); + public set due(value: number) { + if (value != undefined) this._due = value; } public get answer() : number { return this._answer;} - public set answer(value: number | string) { - if (value != undefined) this._answer = parseNonNegInt(value); + public set answer(value: number) { + if (value != undefined) this._answer = value; } public isValid() { @@ -168,8 +166,8 @@ export class QuizDates extends Model { } export interface ParseableQuizParams { - timed?: boolean | string | number; - quiz_duration?: number | string; + timed?: boolean; + quiz_duration?: number; } export class QuizParams extends Model { @@ -186,26 +184,26 @@ export class QuizParams extends Model { if (params.quiz_duration) this.quiz_duration = params.quiz_duration; } - get all_field_names(): string[] { - return ['timed', 'quiz_duration']; - } + static ALL_FIELDS = ['timed', 'quiz_duration']; + get all_field_names(): string[] { return QuizParams.ALL_FIELDS; } get param_fields(): string[] { return [];} public get timed() : boolean { return this._timed;} - public set timed(value: number | string | boolean) { this._timed = parseBoolean(value);} + public set timed(value: boolean) { this._timed = value;} public get quiz_duration(): number { return this._quiz_duration;} - public set quiz_duration(value: number | string) { this._quiz_duration = parseNonNegInt(value);} + public set quiz_duration(value: number) { this._quiz_duration = value;} + + clone(): QuizParams { + return new QuizParams(this.toObject()); + } + isValid(): boolean { + return isNonNegInt(this.quiz_duration); + } } -export interface ParseableQuiz { - set_id?: string | number; - set_name?: string; - course_id?: string | number; - set_type?: string; - set_visible?: string | number | boolean; - set_version?: string | number; +export type ParseableQuiz = ParseableSetParams & { set_params?: ParseableQuizParams; set_dates?: ParseableQuizDates; } @@ -223,30 +221,38 @@ export class Quiz extends ProblemSet { public get set_dates() { return this._set_dates; } public get set_params() { return this._set_params; } - public hasValidDates() { - return this.set_dates.isValid(); - } - clone() { return new Quiz(this.toObject()); } + isValid(): boolean { + return super.isValid() && this.set_dates.isValid() && this.set_params.isValid(); + } } // HomeworkSet model and interfaces. +export interface ParseableHomeworkSetDates { + open?: number; + due?: number; + reduced_scoring?: number; + answer?: number; + enable_reduced_scoring?: boolean; +} + export class HomeworkSetDates extends Model { - protected _open = 0; - // the reduced_scoring field will always have a value. The HomeworkSetParam field + private _open = 0; + // the reduced_scoring field will always have a value. The field // enable_reduced_scoring will dictate if it is used. - protected _reduced_scoring = 0; - protected _due = 0; - protected _answer = 0; + private _reduced_scoring = 0; + private _due = 0; + private _answer = 0; + private _enable_reduced_scoring = false; get all_field_names(): string[] { return HomeworkSetDates.ALL_FIELDS; } get param_fields(): string[] { return [];} - static ALL_FIELDS = ['open', 'reduced_scoring', 'due', 'answer']; + static ALL_FIELDS = ['open', 'reduced_scoring', 'due', 'answer', 'enable_reduced_scoring']; constructor(params: ParseableHomeworkSetDates = {}) { super(); @@ -258,40 +264,61 @@ export class HomeworkSetDates extends Model { if (params.reduced_scoring) this.reduced_scoring = params.reduced_scoring; if (params.due != undefined) this.due = params.due; if (params.answer != undefined) this.answer = params.answer; + if (params.enable_reduced_scoring != undefined) { + this._enable_reduced_scoring = params.enable_reduced_scoring; + } } public get open(): number { return this._open; } - public set open(value: number | string) { this._open = parseNonNegInt(value); } + public set open(value: number) { this._open = value; } public get reduced_scoring(): number { return this._reduced_scoring; } - public set reduced_scoring(value: number | string) { - this._reduced_scoring = parseNonNegInt(value); + public set reduced_scoring(value: number) { + this._reduced_scoring = value; } public get due(): number { return this._due; } - public set due(value: number | string) { this._due = parseNonNegInt(value); } + public set due(value: number) { this._due = value; } public get answer(): number { return this._answer; } - public set answer(value: number | string) { this._answer = parseNonNegInt(value); } + public set answer(value: number) { this._answer = value; } + + public get enable_reduced_scoring(): boolean { return this._enable_reduced_scoring; } + public set enable_reduced_scoring(value: boolean) { + this._enable_reduced_scoring = value; + } clone(): HomeworkSetDates { return new HomeworkSetDates(this.toObject()); } - public isValid(params: { enable_reduced_scoring: boolean; }) { - if (params.enable_reduced_scoring && this.open > this.reduced_scoring) return false; - if (params.enable_reduced_scoring && this.reduced_scoring > this.due) return false; + public isValid() { + if (this.enable_reduced_scoring && this.open > this.reduced_scoring) return false; + if (this.enable_reduced_scoring && this.reduced_scoring > this.due) return false; if (this.due && this.answer && this.due > this.answer) return false; return true; } } export interface ParseableHomeworkSetParams { - enable_reduced_scoring?: boolean | string | number; + hide_hint?: boolean; + hardcopy_header?: string; + set_header?: string; + description?: string; } export class HomeworkSetParams extends Model { - private _enable_reduced_scoring = false; + private _hide_hint = false; + private _hardcopy_header = ''; + private _set_header = ''; + private _description = ''; + + static ALL_FIELDS = ['hide_hint', 'hardcopy_header', 'set_header', 'description']; + + get all_field_names(): string[] { + return HomeworkSetParams.ALL_FIELDS; + } + get param_fields(): string[] { return [];} constructor(params: ParseableHomeworkSetParams = {}) { super(); @@ -299,35 +326,34 @@ export class HomeworkSetParams extends Model { } set(params: ParseableHomeworkSetParams) { - if (params.enable_reduced_scoring != undefined) this.enable_reduced_scoring = params.enable_reduced_scoring; + if (params.hide_hint != undefined) this.hide_hint = params.hide_hint; + if (params.hardcopy_header) this.hardcopy_header = params.hardcopy_header; + if (params.set_header) this.set_header = params.set_header; + if (params.description) this.description = params.description; } - get all_field_names(): string[] { - return ['enable_reduced_scoring']; - } - get param_fields(): string[] { return [];} + public get hide_hint() : boolean { return this._hide_hint;} + public set hide_hint(value: boolean) { this._hide_hint = value; } - public get enable_reduced_scoring() : boolean { return this._enable_reduced_scoring;} - public set enable_reduced_scoring(value: number | string | boolean) { - this._enable_reduced_scoring = parseBoolean(value); - } + public get hardcopy_header() : string { return this._hardcopy_header;} + public set hardcopy_header(value: string) { this._hardcopy_header = value; } -} + public get set_header() : string { return this._set_header;} + public set set_header(value: string) { this._set_header = value; } -export interface ParseableHomeworkSetDates { - open?: number | string; - due?: number | string; - reduced_scoring?: number | string; - answer?: number | string; + public get description() : string { return this._description;} + public set description(value: string) { this._description = value; } + + isValid(): boolean { + return true; + } + + clone(): HomeworkSetParams { + return new HomeworkSetParams(this.toObject()); + } } -export interface ParseableHomeworkSet { - set_id?: string | number; - set_name?: string; - course_id?: string | number; - set_type?: string; - set_visible?: string | number | boolean; - set_version?: string | number; +export type ParseableHomeworkSet = ParseableSetParams & { set_params?: ParseableHomeworkSetParams; set_dates?: ParseableHomeworkSetDates; } @@ -343,10 +369,6 @@ export class HomeworkSet extends ProblemSet { if (params.set_dates) this.set_dates.set(params.set_dates); } - hasValidDates(): boolean { - return this.set_dates.isValid({ enable_reduced_scoring: this.set_params.enable_reduced_scoring }); - } - public get set_dates() { return this._set_dates; } public get set_params() { return this._set_params; } @@ -354,50 +376,49 @@ export class HomeworkSet extends ProblemSet { return new HomeworkSet(this.toObject()); } + isValid(): boolean { + return super.isValid() && this.set_params.isValid() && this.set_dates.isValid(); + } } // Review Set model and interfaces. export interface ParseableReviewSetParams { - can_retake?: boolean | string | number; + can_retake?: boolean; } export class ReviewSetParams extends Model { - private _can_retake = false; + private _can_retake?: boolean; constructor(params: ParseableReviewSetParams = {}) { super(); this.set(params); } - set(params: ParseableReviewSetParams) { - if (params.can_retake != undefined) this.can_retake = params.can_retake; - } + static ALL_FIELDS = ['can_retake']; get all_field_names(): string[] { - return ['can_retake']; + return ReviewSetParams.ALL_FIELDS; } get param_fields(): string[] { return [];} - public get can_retake() : boolean { return this._can_retake;} - public set can_retake(value: number | string | boolean) { - this._can_retake = parseBoolean(value); + set(params: ParseableReviewSetParams) { + this.can_retake = params.can_retake; } + public get can_retake() : boolean | undefined { return this._can_retake;} + public set can_retake(value: boolean | undefined) { this._can_retake = value;} + + public isValid(): boolean { return true; } + public clone(): ReviewSetParams { return new ReviewSetParams(this.toObject()); } } export interface ParseableReviewSetDates { - open?: number | string; - closed?: number | string; + open?: number; + closed?: number; } -export interface ParseableReviewSet { - set_id?: string | number; - set_name?: string; - course_id?: string | number; - set_type?: string; - set_visible?: string | number | boolean; - set_version?: string | number; +export type ParseableReviewSet = ParseableSetParams & { set_params?: ParseableReviewSetParams; set_dates?: ParseableReviewSetDates; } @@ -418,19 +439,18 @@ export class ReviewSetDates extends Model { if (params.closed != undefined) this.closed = params.closed; } - get all_field_names(): string[] { - return ['open', 'closed']; - } + static ALL_FIELDS = [ 'open', 'closed']; + get all_field_names(): string[] { return ReviewSetDates.ALL_FIELDS; } get param_fields(): string[] { return [];} public get open(): number { return this._open;} - public set open(value: number | string) { - if (value != undefined) this._open = parseNonNegInt(value); + public set open(value: number) { + if (value != undefined) this._open = value; } public get closed(): number { return this._closed;} - public set closed(value: number | string) { - if (value != undefined) this._closed = parseNonNegInt(value); + public set closed(value: number) { + if (value != undefined) this._closed = value; } public isValid(): boolean { @@ -453,10 +473,6 @@ export class ReviewSet extends ProblemSet { if (params.set_dates) this.set_dates.set(params.set_dates); } - hasValidDates(): boolean { - return this.set_dates.isValid(); - } - public get set_dates() { return this._set_dates; } public get set_params() { return this._set_params; } @@ -516,7 +532,5 @@ export function convertSet(old_set: ProblemSet, new_set_type: ProblemSetType) { default: throw new ParseError('ProblemSetType', `convertSet does not support conversion to ${new_set_type || 'EMPTY'}`); } - - if (!new_set.hasValidDates()) logger.error('[problem_sets/convertSet] corrupt dates in conversion of set, TSNH?'); return new_set; } diff --git a/src/common/models/problems.ts b/src/common/models/problems.ts index 7f51a0ea..759182e8 100644 --- a/src/common/models/problems.ts +++ b/src/common/models/problems.ts @@ -1,4 +1,6 @@ -import { MergeError, parseNonNegDecimal, parseNonNegInt, parseUsername } from './parsers'; +// Definition of Problems (SetProblems, LibraryProblems and UserProblems) + +import { isNonNegDecimal, isNonNegInt, isValidUsername, MergeError } from './parsers'; import { Model, Dictionary, generic } from '.'; import { RenderParams, ParseableRenderParams } from './renderer'; import { UserSet } from './user_sets'; @@ -62,9 +64,9 @@ export class Problem extends Model { */ export interface ParseableLocationParams extends Partial> { - library_id?: string | number; + library_id?: number; file_path?: string; - problem_pool_id?: string | number; + problem_pool_id?: number; } export interface ParseableLibraryProblem { @@ -93,8 +95,8 @@ class ProblemLocationParams extends Model { } public get library_id() : number | undefined { return this._library_id; } - public set library_id(val: string | number | undefined) { - if (val != undefined) this._library_id = parseNonNegInt(val); + public set library_id(val: number | undefined) { + if (val != undefined) this._library_id = val; } public get file_path() : string | undefined { return this._file_path;} @@ -102,8 +104,20 @@ class ProblemLocationParams extends Model { if (value != undefined) this._file_path = value;} public get problem_pool_id() : number | undefined { return this._problem_pool_id; } - public set problem_pool_id(val: string | number | undefined) { - if (val != undefined) this._problem_pool_id = parseNonNegInt(val); + public set problem_pool_id(val: number | undefined) { + if (val != undefined) this._problem_pool_id = val; + } + + clone(): ProblemLocationParams { + return new ProblemLocationParams(this.toObject() as ParseableLocationParams); + } + + // Ensure that the _id fields are non-negative integers and that at least one + // of the three fields are defined. + isValid() { + if (this.library_id != undefined && !isNonNegInt(this.library_id)) return false; + if (this.problem_pool_id != undefined && !isNonNegInt(this.problem_pool_id)) return false; + return this.problem_pool_id != undefined || this.library_id != undefined || this.file_path != undefined; } } @@ -120,7 +134,7 @@ export class LibraryProblem extends Problem { super(params); this._problem_type = ProblemType.LIBRARY; if (params.location_params) { - this.setLocationParams(params.location_params); + this.location_params.set(params.location_params); } // For these problems, all buttons are shown by default. this.setRenderParams({ @@ -138,16 +152,10 @@ export class LibraryProblem extends Problem { } get problem_number(): number { return this._problem_number; } - set problem_number(value: string | number) { this._problem_number = parseNonNegInt(value); } + set problem_number(value: number) { this._problem_number = value; } get param_fields() { return [...super.param_fields, ...['location_params'] ]; } - // Maybe get rid of this and just assign location_params directly. - - setLocationParams(params: ParseableLocationParams) { - this._location_params.set(params); - } - clone(): LibraryProblem { return new LibraryProblem(this.toObject() as unknown as ParseableLibraryProblem); } @@ -156,6 +164,10 @@ export class LibraryProblem extends Problem { return this.location_params.file_path ?? ''; } + isValid() { + return isNonNegInt(this.problem_number) && this.location_params.isValid(); + } + requestParams(): ParseableRenderParams { const p = super.requestParams(); // Currently the file path must be sent for Render Params. @@ -168,10 +180,10 @@ export class LibraryProblem extends Problem { // This next set of interfaces and classes are related to SetProblems export interface ParseableSetProblemParams { - weight?: number | string; - library_id?: number | string; + weight?: number; + library_id?: number; file_path?: string; - problem_pool_id?: number | string; + problem_pool_id?: number; } /** @@ -203,31 +215,39 @@ export class SetProblemParams extends Model { } public get weight(): number { return this._weight; } - public set weight(value: number | string) { this._weight = parseNonNegDecimal(value);} + public set weight(value: number) { this._weight = value; } public get library_id() : number | undefined { return this._library_id; } - public set library_id(val: string | number | undefined) { - if (val != undefined) this._library_id = parseNonNegInt(val); + public set library_id(val: number | undefined) { + if (val != undefined) this._library_id = val; } public get file_path() : string | undefined { return this._file_path;} public set file_path(value: string | undefined) { - if (value != undefined) this._file_path = value; + if (value) this._file_path = value; } public get problem_pool_id() : number | undefined { return this._problem_pool_id; } - public set problem_pool_id(val: string | number | undefined) { - if (val != undefined) this._problem_pool_id = parseNonNegInt(val); + public set problem_pool_id(val: number | undefined) { + if (val != undefined) this._problem_pool_id = val; } + // Ensure that weight is not negative, the _id fields are non-negative integers and + // that at least one of the three location fields is defined. + isValid() { + if (!isNonNegDecimal(this.weight)) return false; + if (this.library_id != undefined && !isNonNegInt(this.library_id)) return false; + if (this.problem_pool_id != undefined && !isNonNegInt(this.problem_pool_id)) return false; + return Object.keys(this.toObject(['problem_pool_id', 'library_id', 'file_path'])).length > 0; + } } export interface ParseableSetProblem { render_params?: ParseableRenderParams; problem_params?: ParseableSetProblemParams; - problem_number?: number | string ; - set_problem_id?: number | string; - set_id?: number | string; + problem_number?: number; + set_problem_id?: number; + set_id?: number; } /** @@ -273,18 +293,16 @@ export class SetProblem extends Problem { get param_fields() { return [...super.param_fields, ...['problem_params'] ]; } public get set_problem_id() : number { return this._set_problem_id; } - public set set_problem_id(val: string | number) { this._set_problem_id = parseNonNegInt(val);} + public set set_problem_id(val: number) { this._set_problem_id = val;} public get set_id() : number { return this._set_id; } - public set set_id(val: string | number) { this._set_id = parseNonNegInt(val);} + public set set_id(val: number) { this._set_id = val;} get problem_number(): number { return this._problem_number; } - set problem_number(value: string | number) { - this._problem_number = parseNonNegInt(value); - } + set problem_number(val: number) { this._problem_number = val; } clone(): SetProblem { - return new SetProblem(this.toObject() as unknown as ParseableSetProblem); + return new SetProblem(this.toObject()); } path(): string { @@ -296,17 +314,22 @@ export class SetProblem extends Problem { p.sourceFilePath = this.problem_params.file_path ?? ''; return p; } + + isValid() { + return isNonNegInt(this.set_problem_id) && isNonNegInt(this.set_id) && + isNonNegInt(this.problem_number) && this.problem_params.isValid(); + } } export interface ParseableDBUserProblem { render_params?: RenderParams; - problem_params?: SetProblemParams; - set_problem_id?: number | string; - user_problem_id?: number | string; - user_set_id?: number | string; - seed?: number | string; - status?: number | string; - problem_version?: number | string; + problem_params?: ParseableSetProblemParams; + set_problem_id?: number; + user_problem_id?: number; + user_set_id?: number; + seed?: number; + status?: number; + problem_version?: number; } /** @@ -355,25 +378,25 @@ export class DBUserProblem extends Problem { get param_fields() { return ['problem_params', 'render_params']; } public get set_problem_id() : number { return this._set_problem_id; } - public set set_problem_id(val: string | number) { this._set_problem_id = parseNonNegInt(val);} + public set set_problem_id(val: number) { this._set_problem_id = val;} public get user_problem_id() : number { return this._user_problem_id; } - public set user_problem_id(val: string | number) { this._user_problem_id = parseNonNegInt(val);} + public set user_problem_id(val: number) { this._user_problem_id = val;} public get user_set_id() : number { return this._user_set_id; } - public set user_set_id(val: string | number) { this._user_set_id = parseNonNegInt(val);} + public set user_set_id(val: number) { this._user_set_id = val;} public get seed() : number { return this._seed; } - public set seed(val: string | number) { this._seed = parseNonNegInt(val);} + public set seed(val: number) { this._seed = val;} public get status() : number { return this._status; } - public set status(val: string | number) { this._status = parseNonNegDecimal(val);} + public set status(val: number) { this._status = val;} public get problem_version() : number { return this._problem_version; } - public set problem_version(val: string | number) { this._problem_version = parseNonNegInt(val);} + public set problem_version(val: number) { this._problem_version = val;} public clone() { - return new DBUserProblem(this.toObject() as unknown as ParseableDBUserProblem); + return new DBUserProblem(this.toObject()); } path(): string { @@ -386,6 +409,12 @@ export class DBUserProblem extends Problem { p.problemSeed = this.seed; return p; } + + isValid(): boolean { + return isNonNegInt(this.set_problem_id) && isNonNegInt(this._user_problem_id) && + isNonNegInt(this.user_set_id) && isNonNegInt(this.seed) && isNonNegDecimal(this.status) && + isNonNegInt(this.problem_version) && this.problem_params.isValid(); + } } // This next section is related to UserProblems which is a merge between @@ -394,15 +423,15 @@ export class DBUserProblem extends Problem { export interface ParseableUserProblem { render_params?: ParseableRenderParams; problem_params?: ParseableSetProblemParams; - set_problem_id?: number | string; - user_problem_id?: number | string; - user_id?: number | string; - user_set_id?: number | string; - set_id?: number | string; - seed?: number | string; - status?: number | string; - problem_version?: number | string; - problem_number?: number | string; + set_problem_id?: number; + user_problem_id?: number; + user_id?: number; + user_set_id?: number; + set_id?: number; + seed?: number; + status?: number; + problem_version?: number; + problem_number?: number; username?: string; set_name?: string; } @@ -467,34 +496,34 @@ export class UserProblem extends Problem { public get problem_params() { return this._problem_params; } public get set_problem_id() : number { return this._set_problem_id; } - public set set_problem_id(val: string | number) { this._set_problem_id = parseNonNegInt(val);} + public set set_problem_id(val: number) { this._set_problem_id = val;} public get user_problem_id() : number { return this._user_problem_id; } - public set user_problem_id(val: string | number) { this._user_problem_id = parseNonNegInt(val);} + public set user_problem_id(val: number) { this._user_problem_id = val;} public get user_id() : number { return this._user_id; } - public set user_id(val: string | number) { this._user_id = parseNonNegInt(val);} + public set user_id(val: number) { this._user_id = val;} public get user_set_id() : number { return this._user_set_id; } - public set user_set_id(val: string | number) { this._user_set_id = parseNonNegInt(val);} + public set user_set_id(val: number) { this._user_set_id = val;} public get set_id() : number { return this._set_id; } - public set set_id(val: string | number) { this._set_id = parseNonNegInt(val);} + public set set_id(val: number) { this._set_id = val;} public get seed() : number { return this._seed; } - public set seed(val: string | number) { this._seed = parseNonNegInt(val);} + public set seed(val: number) { this._seed = val;} public get status() : number { return this._status; } - public set status(val: string | number) { this._status = parseNonNegDecimal(val);} + public set status(val: number) { this._status = val;} public get problem_version() : number { return this._problem_version; } - public set problem_version(val: string | number) { this._problem_version = parseNonNegInt(val);} + public set problem_version(val: number) { this._problem_version = val;} public get problem_number() : number { return this._problem_number; } - public set problem_number(val: string | number) { this._problem_number = parseNonNegInt(val);} + public set problem_number(val: number) { this._problem_number = val;} public get username() : string { return this._username; } - public set username(val: string) { this._username = parseUsername(val);} + public set username(val: string) { this._username = val; } public get set_name() : string { return this._set_name; } public set set_name(val: string) { this._set_name = val;} @@ -512,6 +541,14 @@ export class UserProblem extends Problem { p.sourceFilePath = this.path(); return p; } + + isValid() { + return isNonNegInt(this.set_problem_id) && isNonNegInt(this._user_problem_id) && + isNonNegInt(this.user_id) && isNonNegInt(this.user_set_id) && isNonNegInt(this.set_id) && + isNonNegInt(this.seed) && isNonNegDecimal(this.status) && isNonNegInt(this.problem_number) && + isNonNegInt(this.problem_version) && this.problem_params.isValid() && + isValidUsername(this.username) && this.set_name.length > 0; + } } export type ParseableProblem = ParseableLibraryProblem | ParseableSetProblem | ParseableUserProblem; diff --git a/src/common/models/user_sets.ts b/src/common/models/user_sets.ts index c7a74259..ea95262a 100644 --- a/src/common/models/user_sets.ts +++ b/src/common/models/user_sets.ts @@ -4,7 +4,7 @@ */ import { Model } from '.'; -import { MergeError, parseBoolean, parseNonNegInt, parseUsername } from './parsers'; +import { isNonNegInt, isValidUsername, MergeError } from './parsers'; import { ProblemSetDates, ProblemSetParams, HomeworkSetParams, HomeworkSetDates, ParseableHomeworkSetParams, ParseableHomeworkSetDates, QuizParams, QuizDates, ParseableQuizDates, ParseableQuizParams, ParseableReviewSetDates, @@ -13,17 +13,18 @@ import { ProblemSetDates, ProblemSetParams, HomeworkSetParams, HomeworkSetDates, import { CourseUser } from './users'; type UserSetDates = UserHomeworkSetDates | UserQuizDates | UserReviewSetDates; +type UserSetParams = UserHomeworkSetParams | UserQuizParams | UserReviewSetParams; // The ParseableDBUserSet type is the same as the ParseableProblemSet type // with additional fields. This mimics the UserSet fields on the database. export type ParseableDBUserSet = { - user_set_id?: number | string; - set_version?: number | string; - course_user_id?: number | string; - set_id?: number | string; + user_set_id?: number; + set_version?: number; + course_user_id?: number; + set_id?: number; set_type?: string; - set_visible?: string | number | boolean; + set_visible?: boolean; set_params?: ParseableProblemSetParams; set_dates?: ParseableProblemSetDates; } @@ -39,10 +40,10 @@ export class DBUserSet extends Model { private _set_id = 0; protected _set_type = ProblemSetType.UNKNOWN; private _set_version = 1; - private _set_visible = false; + private _set_visible?: boolean; get set_dates(): UserSetDates { throw 'The subclass must override set_dates()'; } - get set_params(): ProblemSetParams { throw 'The subclass must override set_dates()'; } + get set_params(): UserSetParams { throw 'The subclass must override set_dates()'; } static ALL_FIELDS = ['user_set_id', 'set_id', 'set_type', 'course_user_id', 'set_version', 'set_visible']; @@ -62,31 +63,32 @@ export class DBUserSet extends Model { if (params.course_user_id != undefined) this.course_user_id = params.course_user_id; if (params.set_version != undefined) this.set_version = params.set_version; if (params.set_id != undefined) this.set_id = params.set_id; - if (params.set_visible != undefined) this.set_visible = params.set_visible; + this.set_visible = params.set_visible; } public get user_set_id(): number { return this._user_set_id;} - public set user_set_id(value: number | string) { this._user_set_id = parseNonNegInt(value);} + public set user_set_id(value: number) { this._user_set_id = value;} public get set_id(): number { return this._set_id;} - public set set_id(value: number | string) { this._set_id = parseNonNegInt(value);} + public set set_id(value: number) { this._set_id = value;} public get course_user_id(): number { return this._course_user_id;} - public set course_user_id(value: number | string) { this._course_user_id = parseNonNegInt(value);} + public set course_user_id(value: number) { this._course_user_id = value;} public get set_version(): number { return this._set_version;} - public set set_version(value: number | string) { this._set_version = parseNonNegInt(value);} - - public get set_visible(): boolean { return this._set_visible; } - public set set_visible(value: number | string | boolean) { this._set_visible = parseBoolean(value);} + public set set_version(value: number) { this._set_version = value;} - public hasValidDates(): boolean { - throw 'The subclass must override the hasValidDates() method.'; - } + public get set_visible(): boolean | undefined { return this._set_visible; } + public set set_visible(value: boolean | undefined) { this._set_visible = value;} public clone() { return new DBUserSet(this.toObject()); } + + public isValid(): boolean { + return isNonNegInt(this.user_set_id) && isNonNegInt(this.set_id) && + isNonNegInt(this.course_user_id) && isNonNegInt(this.set_version); + } } /** @@ -99,13 +101,19 @@ export type ParseableDBUserHomeworkSet = ParseableDBUserSet & set_dates?: ParseableHomeworkSetDates; } +/** + * This is nearly identical to the HomeworkSetDates model except that any of the + * date fields can be undefined. + */ export class UserHomeworkSetDates extends Model { - protected _open?: number; - protected _reduced_scoring?: number; - protected _due?: number; - protected _answer?: number; - - get all_field_names(): string[] { return ['open', 'reduced_scoring', 'due', 'answer']; } + private _open?: number; + private _reduced_scoring?: number; + private _due?: number; + private _answer?: number; + private _enable_reduced_scoring?: boolean; + + static ALL_FIELDS = ['open', 'reduced_scoring', 'due', 'answer', 'enable_reduced_scoring']; + get all_field_names(): string[] { return UserHomeworkSetDates.ALL_FIELDS; } get param_fields(): string[] { return [];} constructor(params: ParseableHomeworkSetDates = {}) { @@ -118,34 +126,35 @@ export class UserHomeworkSetDates extends Model { this.reduced_scoring = params.reduced_scoring; this.due = params.due; this.answer = params.answer; + this.enable_reduced_scoring = params.enable_reduced_scoring; } public get open(): number | undefined { return this._open;} - public set open(value: number | string | undefined) { - if (value != undefined) this._open = parseNonNegInt(value);} + public set open(value: number | undefined) { this._open = value;} public get reduced_scoring(): number | undefined { return this._reduced_scoring;} - public set reduced_scoring(value: number | string | undefined) { - if (value != undefined) this._reduced_scoring = parseNonNegInt(value); - } + public set reduced_scoring(value: number | undefined) { this._reduced_scoring = value; } public get due(): number | undefined { return this._due;} - public set due(value: number | string | undefined) { - if (value != undefined) this._due = parseNonNegInt(value); - } + public set due(value: number | undefined) { this._due = value; } public get answer() : number | undefined { return this._answer;} - public set answer(value: number | string | undefined) { - if (value != undefined) this._answer = parseNonNegInt(value); - } + public set answer(value: number | undefined) { this._answer = value; } - public isValid(params: { enable_reduced_scoring: boolean; }) { - if (params.enable_reduced_scoring && this.reduced_scoring == undefined) return false; - if (params.enable_reduced_scoring && this.reduced_scoring && this.open - && this.open > this.reduced_scoring) return false; - if (params.enable_reduced_scoring && this.reduced_scoring && this.due - && this.reduced_scoring > this.due) return false; - if (this.due && this.answer && this.due > this.answer) return false; + public get enable_reduced_scoring(): boolean | undefined { return this._enable_reduced_scoring; } + public set enable_reduced_scoring(value: boolean | undefined) { this._enable_reduced_scoring = value; } + + public isValid() { + if (this.enable_reduced_scoring && this.reduced_scoring == undefined) return false; + if (this.enable_reduced_scoring && this.reduced_scoring != undefined && + this.open != undefined && this.open > this.reduced_scoring) return false; + if (this.enable_reduced_scoring && this.reduced_scoring != undefined && + this.due != undefined && this.reduced_scoring > this.due) return false; + if (this.enable_reduced_scoring && this.reduced_scoring != undefined && + this.answer != undefined && this.reduced_scoring > this.answer) return false; + if (this.open != undefined && this.due != undefined && this.open > this.due) return false; + if (this.open != undefined && this.answer != undefined && this.open > this.answer) return false; + if (this.due != undefined && this.answer != undefined && this.due > this.answer) return false; return true; } @@ -154,8 +163,58 @@ export class UserHomeworkSetDates extends Model { } } +/** + * This is nearly identical to the HomeworkSetParams except that all fields are optional + */ + +export class UserHomeworkSetParams extends Model { + private _hide_hint?: boolean; + private _hardcopy_header?: string; + private _set_header?: string; + private _description?: string; + + static ALL_FIELDS = ['hide_hint', 'hardcopy_header', 'set_header', 'description']; + + get all_field_names(): string[] { + return HomeworkSetParams.ALL_FIELDS; + } + get param_fields(): string[] { return [];} + + constructor(params: ParseableHomeworkSetParams = {}) { + super(); + this.set(params); + } + + set(params: ParseableHomeworkSetParams) { + this.hide_hint = params.hide_hint; + this.hardcopy_header = params.hardcopy_header; + this.set_header = params.set_header; + this.description = params.description; + } + + public get hide_hint() : boolean | undefined { return this._hide_hint;} + public set hide_hint(value: boolean | undefined) { this._hide_hint = value; } + + public get hardcopy_header() : string | undefined { return this._hardcopy_header;} + public set hardcopy_header(value: string | undefined) { this._hardcopy_header = value; } + + public get set_header() : string | undefined { return this._set_header;} + public set set_header(value: string | undefined) { this._set_header = value; } + + public get description() : string | undefined { return this._description;} + public set description(value: string | undefined) { this._description = value; } + + isValid(): boolean { + return true; + } + + clone(): UserHomeworkSetParams { + return new UserHomeworkSetParams(this.toObject()); + } +} + export class DBUserHomeworkSet extends DBUserSet { - private _set_params = new HomeworkSetParams(); + private _set_params = new UserHomeworkSetParams(); private _set_dates = new UserHomeworkSetDates(); static ALL_FIELDS = [ ...DBUserSet.ALL_FIELDS, ...['set_params', 'set_dates']]; @@ -169,14 +228,9 @@ export class DBUserHomeworkSet extends DBUserSet { constructor(params: ParseableDBUserHomeworkSet = {}) { super(params as ParseableDBUserSet); this._set_type = ProblemSetType.HW; - if (params.set_params) this._set_params.set(params.set_params as ParseableHomeworkSetParams); + if (params.set_params) this._set_params.set(params.set_params); if (params.set_dates) this._set_dates.set(params.set_dates); } - - public hasValidDates() { - return this.set_dates.isValid({ enable_reduced_scoring: this.set_params.enable_reduced_scoring }); - } - public clone() { return new DBUserHomeworkSet(this.toObject()); } @@ -215,18 +269,18 @@ export class UserQuizDates extends Model { get param_fields(): string[] { return [];} public get open(): number | undefined { return this._open;} - public set open(value: number | string | undefined) { - if (value != undefined) this._open = parseNonNegInt(value); + public set open(value: number | undefined) { + if (value != undefined) this._open = value; } public get due(): number | undefined { return this._due;} - public set due(value: number | string | undefined) { - if (value != undefined) this._due = parseNonNegInt(value); + public set due(value: number | undefined) { + if (value != undefined) this._due = value; } public get answer() : number | undefined { return this._answer;} - public set answer(value: number | string | undefined) { - if (value != undefined) this._answer = parseNonNegInt(value); + public set answer(value: number | undefined) { + if (value != undefined) this._answer = value; } public isValid() { @@ -240,8 +294,46 @@ export class UserQuizDates extends Model { } } +/** + * This is nearly identical to the QuizParams model except that the fields can + * each be undefined. + */ + +export class UserQuizParams extends Model { + private _timed?: boolean; + private _quiz_duration?: number; + + constructor(params: ParseableQuizParams = {}) { + super(); + this.set(params); + } + + set(params: ParseableQuizParams) { + if (params.timed != undefined) this.timed = params.timed; + if (params.quiz_duration) this.quiz_duration = params.quiz_duration; + } + + static ALL_FIELDS = ['timed', 'quiz_duration']; + get all_field_names(): string[] { return QuizParams.ALL_FIELDS; } + get param_fields(): string[] { return [];} + + public get timed() : boolean | undefined { return this._timed;} + public set timed(value: boolean | undefined) { this._timed = value;} + + public get quiz_duration(): number | undefined { return this._quiz_duration;} + public set quiz_duration(value: number | undefined) { this._quiz_duration = value;} + + clone(): QuizParams { + return new QuizParams(this.toObject()); + } + + isValid(): boolean { + return this.quiz_duration != undefined && isNonNegInt(this.quiz_duration); + } +} + export class DBUserQuiz extends DBUserSet { - private _set_params = new QuizParams(); + private _set_params = new UserQuizParams(); private _set_dates = new UserQuizDates(); static ALL_FIELDS = [ ...DBUserSet.ALL_FIELDS, ...['set_params', 'set_dates']]; @@ -258,11 +350,6 @@ export class DBUserQuiz extends DBUserSet { if (params.set_params) this._set_params.set(params.set_params as ParseableQuizParams); if (params.set_dates) this._set_dates.set(params.set_dates); } - - public hasValidDates() { - return this.set_dates.isValid(); - } - public clone() { return new DBUserQuiz(this.toObject()); } @@ -299,13 +386,13 @@ export class UserReviewSetDates extends Model { get param_fields(): string[] { return [];} public get open(): number | undefined { return this._open;} - public set open(value: number | string | undefined) { - if (value != undefined) this._open = parseNonNegInt(value); + public set open(value: number | undefined) { + if (value != undefined) this._open = value; } public get closed(): number | undefined { return this._closed;} - public set closed(value: number | string | undefined) { - if (value != undefined) this._closed = parseNonNegInt(value); + public set closed(value: number | undefined) { + if (value != undefined) this._closed = value; } public isValid(): boolean { @@ -318,8 +405,36 @@ export class UserReviewSetDates extends Model { } } +/** + * This is nearly identical to ReviewSetParams except all fields can be undefined. + */ + +export class UserReviewSetParams extends Model { + private _can_retake?: boolean; + + constructor(params: ParseableReviewSetParams = {}) { + super(); + this.set(params); + } + + set(params: ParseableReviewSetParams) { + this.can_retake = params.can_retake; + } + + static ALL_FIELDS = ['can_retakes']; + get all_field_names(): string[] { return UserReviewSetParams.ALL_FIELDS; } + get param_fields(): string[] { return [];} + + public get can_retake() : boolean | undefined { return this._can_retake;} + public set can_retake(value: boolean | undefined) { this._can_retake = value; } + + isValid(): boolean { + return true; + } +} + export class DBUserReviewSet extends DBUserSet { - private _set_params = new ReviewSetParams(); + private _set_params = new UserReviewSetParams(); private _set_dates = new UserReviewSetDates(); static ALL_FIELDS = [ ...DBUserSet.ALL_FIELDS, ...['set_params', 'set_dates']]; @@ -337,10 +452,6 @@ export class DBUserReviewSet extends DBUserSet { if (params.set_dates) this._set_dates.set(params.set_dates); } - public hasValidDates() { - return this.set_dates.isValid(); - } - public clone() { return new DBUserReviewSet(this.toObject()); } @@ -363,12 +474,12 @@ export function parseDBUserSet(user_set: ParseableDBUserSet) { */ export interface ParseableUserSet { - user_set_id?: number | string; - set_id?: number | string; - course_user_id?: number | string; - user_id?: number | string; - set_version?: number | string; - set_visible?: number | string | boolean; + user_set_id?: number; + set_id?: number; + course_user_id?: number; + user_id?: number; + set_version?: number; + set_visible?: boolean; set_name?: string; username?: string; set_type?: string; @@ -420,39 +531,46 @@ export class UserSet extends Model { } public get user_set_id(): number { return this._user_set_id;} - public set user_set_id(value: number | string) { this._user_set_id = parseNonNegInt(value);} + public set user_set_id(value: number) { this._user_set_id = value;} public get set_id(): number { return this._set_id;} - public set set_id(value: number | string) { this._set_id = parseNonNegInt(value);} + public set set_id(value: number) { this._set_id = value;} public get course_user_id(): number { return this._course_user_id;} - public set course_user_id(value: number | string) { this._course_user_id = parseNonNegInt(value);} + public set course_user_id(value: number) { this._course_user_id = value;} public get user_id(): number { return this._user_id;} - public set user_id(value: number | string) { this._user_id = parseNonNegInt(value);} + public set user_id(value: number) { this._user_id = value;} public get set_version(): number { return this._set_version;} - public set set_version(value: number | string) { this._set_version = parseNonNegInt(value);} + public set set_version(value: number) { this._set_version = value;} public get set_visible(): boolean | undefined { return this._set_visible; } - public set set_visible(value: number | string | boolean | undefined) { - if (value != undefined) this._set_visible = parseBoolean(value); + public set set_visible(value: boolean | undefined) { + if (value != undefined) this._set_visible = value; } public get set_name(): string { return this._set_name; } public set set_name(value: string) { this._set_name = value; } public get username(): string { return this._username; } - public set username(value: string) { this._username = parseUsername(value);} + public set username(value: string) { this._username = value;} clone(): UserSet { - throw 'the clone() method should be overridden in a subclass of MergedUserSet'; + throw 'the clone() method should be overridden in a subclass of UserSet'; + } + + public isValid(): boolean { + return isNonNegInt(this.user_set_id) && isNonNegInt(this.set_id) && + isNonNegInt(this.course_user_id) && isNonNegInt(this.set_version) && + isNonNegInt(this.user_id) && isValidUsername(this.username) && + this.set_name.length > 0; } } /** - * MergedUserHomeworkSet is joined HomeworkSet and a UserSet - */ +* UserHomeworkSet is joined HomeworkSet and a DBUserSet +*/ export type ParseableUserHomeworkSet = ParseableUserSet & { @@ -474,18 +592,14 @@ export class UserHomeworkSet extends UserSet { if (params.set_dates) this.set_dates.set(params.set_dates); } - public hasValidDates() { - return this.set_dates.isValid({ enable_reduced_scoring: this.set_params.enable_reduced_scoring }); - } - public clone(): UserHomeworkSet { return new UserHomeworkSet(this.toObject()); } } /** - * MergedUserQuiz is a Quiz merged with a UserSet - */ +* UserQuiz is a Quiz merged with a DBUserSet +*/ export type ParseableUserQuiz = ParseableUserSet & { @@ -507,10 +621,6 @@ export class UserQuiz extends UserSet { if (params.set_dates) this._set_dates.set(params.set_dates); } - public hasValidDates() { - return this.set_dates.isValid(); - } - public clone() { return new UserQuiz(this.toObject()); } @@ -540,10 +650,6 @@ export class UserReviewSet extends UserSet { if (params.set_dates) this._set_dates.set(params.set_dates); } - public hasValidDates() { - return this.set_dates.isValid(); - } - public clone() { return new UserReviewSet(this.toObject()); } @@ -588,7 +694,7 @@ export function mergeUserSet(set: ProblemSet, db_user_set: DBUserSet, user: Cour user_set_id: db_user_set.user_set_id, set_name: set.set_name, set_type: set.set_type, - set_visible: db_user_set.set_visible, + set_visible: db_user_set.set_visible ?? set.set_visible, set_version: db_user_set.set_version }; diff --git a/src/common/models/users.ts b/src/common/models/users.ts index 13715e1c..104d2622 100644 --- a/src/common/models/users.ts +++ b/src/common/models/users.ts @@ -1,16 +1,18 @@ +/* This file contains the definitions of a User, DBCourseUser and Course User + in terms of a model. */ -import { parseNonNegInt, parseUsername, parseBoolean, parseEmail, parseUserRole, - UserRole } from 'src/common/models/parsers'; -import { RequiredFieldsException, Model } from 'src/common/models'; +import { isNonNegInt, isValidUsername, isValidEmail, parseUserRole, UserRole } + from 'src/common/models/parsers'; +import { Dictionary, Model } from 'src/common/models'; export interface ParseableUser { - user_id?: number | string; + user_id?: number; username?: string; email?: string; first_name?: string; last_name?: string; - is_admin?: string | number | boolean; - student_id?: string | number; + is_admin?: boolean; + student_id?: string; } /** @@ -20,10 +22,10 @@ export class User extends Model { private _user_id = 0; private _username = ''; private _is_admin = false; - private _email?: string; - private _first_name?: string; - private _last_name?: string; - private _student_id?: string; + private _email = ''; + private _first_name = ''; + private _last_name = ''; + private _student_id = ''; static ALL_FIELDS = ['user_id', 'username', 'is_admin', 'email', 'first_name', 'last_name', 'student_id']; @@ -33,16 +35,13 @@ export class User extends Model { constructor(params: ParseableUser = {}) { super(); - if (params.username == undefined) { - throw new RequiredFieldsException('username'); - } this.set(params); } set(params: ParseableUser) { if (params.username) this.username = params.username; - if (params.user_id) this.user_id = params.user_id; - if (params.is_admin) this.is_admin = params.is_admin; + if (params.user_id != undefined) this.user_id = params.user_id; + if (params.is_admin != undefined) this.is_admin = params.is_admin; if (params.email) this.email = params.email; if (params.first_name) this.first_name = params.first_name; if (params.last_name) this.last_name = params.last_name; @@ -51,52 +50,44 @@ export class User extends Model { // otherwise, typescript identifies this as number | string get user_id(): number { return this._user_id; } - set user_id(value: number | string) { - this._user_id = parseNonNegInt(value); - } + set user_id(value: number) { this._user_id = value; } get username() { return this._username; } - set username(value: string) { - this._username = parseUsername(value); - } + set username(value: string) { this._username = value; } get is_admin() { return this._is_admin; } - set is_admin(value: string | number | boolean) { - this._is_admin = parseBoolean(value); - } + set is_admin(value: boolean) { this._is_admin = value; } - get email(): string | undefined { return this._email; } - set email(value: string | undefined) { - if (value != undefined) this._email = parseEmail(value); - } + get email(): string { return this._email; } + set email(value: string) { this._email = value; } - get first_name(): string | undefined { return this._first_name; } - set first_name(value: string | undefined) { - if (value != undefined) this._first_name = value; - } + get first_name(): string { return this._first_name; } + set first_name(value: string) { this._first_name = value; } - get last_name(): string | undefined { return this._last_name; } - set last_name(value: string | undefined) { - if (value != undefined) this._last_name = value; - } + get last_name(): string { return this._last_name; } + set last_name(value: string) { this._last_name = value; } - get student_id(): string | undefined { return this._student_id; } - set student_id(value: string | number | undefined) { - if (value != undefined) this._student_id = `${value}`; - } + get student_id(): string { return this._student_id; } + set student_id(value: string) { this._student_id = value; } clone() { return new User(this.toObject() as ParseableUser); } + + // The email can be the empty string or valid email address. + isValid(): boolean { + return isNonNegInt(this.user_id) && isValidUsername(this.username) && + (this.email === '' || isValidEmail(this.email)); + } } export interface ParseableDBCourseUser { - course_user_id?: number | string; - user_id?: number | string; - course_id?: number | string; + course_user_id?: number; + user_id?: number; + course_id?: number; role?: string; - section?: string | number; - recitation?: string | number; + section?: string; + recitation?: string; } /** @@ -131,53 +122,53 @@ export class DBCourseUser extends Model { } get course_user_id(): number { return this._course_user_id; } - set course_user_id(value: string | number) { - this._course_user_id = parseNonNegInt(value); - } - - get course_id(): number | undefined { return this._course_id; } - set course_id(value: string | number | undefined) { - if (value != undefined) this._course_id = parseNonNegInt(value); - } + set course_user_id(value: number) { this._course_user_id = value; } - get user_id() { return this._user_id; } - set user_id(value: number | string | undefined) { - if (value != undefined) this._user_id = parseNonNegInt(value); - } + get course_id(): number { return this._course_id; } + set course_id(value: number) { this._course_id = value; } - get role(): string | undefined { return this._role; } - set role(value: string | undefined) { - if (value != undefined) this._role = parseUserRole(value); + get user_id(): number { return this._user_id; } + set user_id(value: number) { this._user_id = value; } + + get role(): UserRole { return this._role; } + set role(value: UserRole | string) { + if (typeof value === 'string') { + this._role = parseUserRole(value); + } else { + this._role = value; + } } get section(): string | undefined { return this._section; } - set section(value: string | number | undefined) { - if (value != undefined) this._section = `${value}`; - } + set section(value: string | undefined) { if (value != undefined) this._section = value; } get recitation(): string | undefined { return this._recitation; } - set recitation(value: string | number | undefined) { - if (value != undefined) this._recitation = `${value}`; - } + set recitation(value: string | undefined) { if (value != undefined) this._recitation = value; } clone() { return new DBCourseUser(this.toObject() as ParseableDBCourseUser); } + + isValid(): boolean { + return isNonNegInt(this.user_id) && isNonNegInt(this.course_user_id) && + isNonNegInt(this.course_id); + } + } export interface ParseableCourseUser { - course_user_id?: number | string; - user_id?: number | string; - course_id?: number | string; + course_user_id?: number; + user_id?: number; + course_id?: number; username?: string; email?: string; first_name?: string; last_name?: string; - is_admin?: boolean | number | string; + is_admin?: boolean; student_id?: string; - role?: string; - section?: string | number; - recitation?: string | number; + role?: string | UserRole; + section?: string; + recitation?: string; } /** @@ -191,12 +182,12 @@ export class CourseUser extends Model { private _course_id = 0; private _user_id = 0; private _is_admin = false; - private _username?: string; - private _email?: string; - private _first_name?: string; - private _last_name?: string; - private _student_id?: string; - private _role?: UserRole; + private _username = ''; + private _email = ''; + private _first_name = ''; + private _last_name = ''; + private _student_id = ''; + private _role = UserRole.unknown; private _section?: string; private _recitation?: string; @@ -211,10 +202,10 @@ export class CourseUser extends Model { this.set(params); } set(params: ParseableCourseUser) { - if (params.course_user_id) this.course_user_id = params.course_user_id; - if (params.course_id) this.course_id = params.course_id; - if (params.user_id) this.user_id = params.user_id; - if (params.is_admin) this.is_admin = params.is_admin; + if (params.course_user_id != undefined) this.course_user_id = params.course_user_id; + if (params.course_id != undefined) this.course_id = params.course_id; + if (params.user_id != undefined) this.user_id = params.user_id; + if (params.is_admin != undefined) this.is_admin = params.is_admin; if (params.username) this.username = params.username; if (params.email) this.email = params.email; if (params.first_name) this.first_name = params.first_name; @@ -226,66 +217,66 @@ export class CourseUser extends Model { } get course_user_id(): number { return this._course_user_id; } - set course_user_id(value: string | number) { - this._course_user_id = parseNonNegInt(value); - } + set course_user_id(value: number) { this._course_user_id = value; } get course_id(): number { return this._course_id; } - set course_id(value: string | number) { - this._course_id = parseNonNegInt(value); - } + set course_id(value: number) { this._course_id = value; } get user_id(): number { return this._user_id; } - set user_id(value: number | string) { - this._user_id = parseNonNegInt(value); - } + set user_id(value: number) { this._user_id = value; } - get username() { return this._username; } - set username(value: string | undefined) { - if (value != undefined) this._username = parseUsername(value); - } + get username(): string { return this._username; } + set username(value: string) { this._username = value; } - get is_admin() { return this._is_admin; } - set is_admin(value: string | number | boolean | undefined) { - if (value != undefined) this._is_admin = parseBoolean(value); - } + get is_admin(): boolean { return this._is_admin; } + set is_admin(value: boolean) { this._is_admin = value; } - get email(): string | undefined { return this._email; } - set email(value: string | undefined) { - if (value != undefined) this._email = parseEmail(value); - } + get email(): string { return this._email; } + set email(value: string) { this._email = value; } - get role(): string | undefined { return this._role; } - set role(value: string | undefined) { - if (value != undefined) this._role = parseUserRole(value); + get role(): UserRole { return this._role; } + set role(value: UserRole | string) { + if (typeof value === 'string') { + this._role = parseUserRole(value); + } else { + this._role = value; + } } get section(): string | undefined { return this._section; } - set section(value: string | number | undefined) { - if (value != undefined) this._section = `${value}`; - } + set section(value: string | undefined) { if (value != undefined) this._section = value; } get recitation(): string | undefined { return this._recitation; } - set recitation(value: string | number | undefined) { - if (value != undefined) this._recitation = `${value}`; - } + set recitation(value: string | undefined) { if (value != undefined) this._recitation = value; } - get first_name(): string | undefined { return this._first_name; } - set first_name(value: string | undefined) { - if (value != undefined) this._first_name = value; - } + get first_name(): string { return this._first_name; } + set first_name(value: string) { this._first_name = value; } - get last_name(): string | undefined { return this._last_name; } - set last_name(value: string | undefined) { - if (value != undefined) this._last_name = value; - } + get last_name(): string { return this._last_name; } + set last_name(value: string) { this._last_name = value; } - get student_id(): string | undefined { return this._student_id; } - set student_id(value: string | number | undefined) { - if (value != undefined) this._student_id = `${value}`; - } + get student_id(): string { return this._student_id; } + set student_id(value: string) { this._student_id = value; } clone() { return new CourseUser(this.toObject() as ParseableCourseUser); } + + // The email can be the empty string or valid email address. + isValid(): boolean { + return isNonNegInt(this.user_id) && isNonNegInt(this.course_user_id) && + isNonNegInt(this.course_id) && isValidUsername(this.username) && + this.role !== UserRole.unknown && (this.email === '' || isValidEmail(this.email)); + } + + validate(): Dictionary { + return { + course_id: isNonNegInt(this.course_id) || 'The course_id must be a non negative integer.', + course_user_id: isNonNegInt(this.course_user_id) || 'The course_user_id must be a non negative integer.', + user_id: isNonNegInt(this.user_id) || 'The user_id must be a non negative integer.', + username: isValidUsername(this.username) || 'The username must be valid.', + email: (this.email === '' || isValidEmail(this.email)) || 'The email must be valid', + role: this.role !== UserRole.unknown || 'The role is not valid.', + }; + } } diff --git a/src/components/admin/AddCourse.vue b/src/components/admin/AddCourse.vue index c67ff894..e786ae47 100644 --- a/src/components/admin/AddCourse.vue +++ b/src/components/admin/AddCourse.vue @@ -201,7 +201,7 @@ export default defineComponent({ await getUser(username.value) .then((_user) => { logger.debug(`[AddCourse/checkUser] Found user: ${username.value}`); - user.value = new User(_user); + user.value.set(_user.toObject()); instructor_exists.value = true; }) .catch((e) => { diff --git a/src/components/common/ProblemViewer.vue b/src/components/common/ProblemViewer.vue index 8ac653cd..7f235552 100644 --- a/src/components/common/ProblemViewer.vue +++ b/src/components/common/ProblemViewer.vue @@ -41,7 +41,7 @@ export default defineComponent({ file, problem, loadProblem: () => { - problem.value.setLocationParams({ file_path: srcFile.value }); + problem.value.location_params.set({ file_path: srcFile.value }); } }; } diff --git a/src/components/common/UserCourses.vue b/src/components/common/UserCourses.vue index edcb574b..e3990417 100644 --- a/src/components/common/UserCourses.vue +++ b/src/components/common/UserCourses.vue @@ -67,12 +67,11 @@ const session = useSessionStore(); if (session) await session.fetchUserCourses(parseNonNegInt(session.user.user_id)); const student_courses = computed(() => - // for some reason on load the user_course.role is undefined. - session.user_courses.filter(user_course => parseUserRole(user_course.role) === 'STUDENT')); + // for some reason on load the user_course.role is undefined. The ?? '' prevents an error. + session.user_courses.filter(user_course => parseUserRole(user_course.role ?? '') === 'STUDENT')); const instructor_courses = computed(() => - // For some reason on load the user_course.role is undefined. - session.user_courses.filter(user_course => parseUserRole(user_course.role) === 'INSTRUCTOR') + session.user_courses.filter(user_course => parseUserRole(user_course.role ?? '') === 'INSTRUCTOR') ); const user = computed(() => session.user); diff --git a/src/components/instructor/ClasslistManagerComponents/AddUsersFromFile.vue b/src/components/instructor/ClasslistManagerComponents/AddUsersFromFile.vue index 547c52ce..9c8a5ae1 100644 --- a/src/components/instructor/ClasslistManagerComponents/AddUsersFromFile.vue +++ b/src/components/instructor/ClasslistManagerComponents/AddUsersFromFile.vue @@ -10,21 +10,21 @@
- Load File + Load File
-
+
First row is Header
- Import All users as + Import All users as
-
- There are validation errors with the loaded data. Any data row with an - error will not be added. + There are validation errors with the loaded data. Any data row with an error + will not be added.
@@ -50,40 +50,42 @@
-
- + - -
@@ -91,7 +93,11 @@ - +
@@ -107,7 +113,7 @@ import { logger } from 'boot/logger'; import { useUserStore } from 'src/stores/users'; import { useSessionStore } from 'src/stores/session'; import type { Dictionary } from 'src/common/models'; -import type { ResponseError } from 'src/common/api-requests/interfaces'; +import type { ResponseError } from 'src/common/api-requests/errors'; import { CourseUser, User, ParseableCourseUser } from 'src/common/models/users'; import { invert } from 'src/common/utils'; import { getUser } from 'src/common/api-requests/user'; @@ -123,7 +129,7 @@ interface ParseError { type UserFromFile = { _row?: number; - _error?: ParseError + _error?: ParseError; } & Dictionary; const emit = defineEmits(['closeDialog']); @@ -134,9 +140,9 @@ const settings = useSettingsStore(); const $q = useQuasar(); const file = ref(new File([], '')); // Stores all users from the file as well as parsing errors. -const merged_users = ref>([]); +const course_users = ref>([]); // Stores the selected users. -const selected = ref>([]); +const selected_users = ref>([]); const column_headers = ref>({}); // Provides a map from column number to field name. It doesn't need to be reactive. const user_param_map: Dictionary = {}; @@ -146,7 +152,7 @@ const loading = ref(false); // used to indicate parsing is occurring const first_row_header = ref(false); const header_row = ref({}); -const merged_users_to_add = ref>([]); +const course_users_to_add = ref>([]); const selected_user_error = ref(false); const users_already_in_course = ref(false); @@ -158,18 +164,20 @@ const user_fields = [ { label: 'Email', field: 'email', regexp: /email/i }, { label: 'Section', field: 'section', regexp: /sect/i }, { label: 'Recitation', field: 'recitation', regexp: /recit/i }, - { label: 'Role', field: 'role', regexp: /role/i } ]; + { label: 'Role', field: 'role', regexp: /role/i }, +]; // Return an array of the roles in the course. const roles = computed(() => - (settings.getCourseSetting('roles').value as string[]).filter(v => v !== 'admin')); + (settings.getCourseSetting('roles').value as string[]).filter((v) => v !== 'admin') +); // use the first row to find the headers of the table (field names) // this is based on trying to match the needed field names to the know user // field names. const fillHeaders = () => { Object.keys(header_row.value).forEach((key) => { - user_fields.forEach((field: { regexp: RegExp; label: string; field: string}) => { + user_fields.forEach((field: { regexp: RegExp; label: string; field: string }) => { if (field.regexp.test(`${header_row.value[key]}`)) { column_headers.value[key] = field.label; user_param_map[key] = field.field; @@ -178,86 +186,107 @@ const fillHeaders = () => { }); }; -// This converts converts each selected row to a ParseableMerged User +// This converts converts each selected row to a ParseableCourseUser // based on the column headers. -const getMergedUser = (row: UserFromFile) => { +const getCourseUser = (row: UserFromFile) => { // The following pulls the keys out from the user_param_map and the the values out of row // to get the merged user. - const merged_user = Object.entries(user_param_map).reduce((acc, [k, v]) => - ({ ...acc, [v]: row[k as keyof UserFromFile] }), {}) as ParseableCourseUser; - // Set the role if a common role for all users is selected. - merged_user.role = use_single_role.value ? - common_role.value ?? 'UNKOWN' : - 'UNKNOWN'; - return merged_user; + const course_user = Object.entries(user_param_map).reduce( + (acc, [k, v]) => ({ ...acc, [v]: row[k as keyof UserFromFile] }), + {} + ) as ParseableCourseUser; + // Set the role if a common role for all users is selected_users. + course_user.role = use_single_role.value ? common_role.value ?? 'UNKOWN' : 'UNKNOWN'; + return course_user; }; // Parse the selected users from the file. const parseUsers = () => { // Clear Errors and reset reactive variables. loading.value = true; - merged_users_to_add.value = []; + course_users_to_add.value = []; selected_user_error.value = false; users_already_in_course.value = false; - merged_users.value.filter((u: UserFromFile) => u._error?.type !== 'none').forEach(u => { - // reset the error for each selected row - u._error = { - type: 'none', - message: '' - }; - }); + course_users.value + .filter((u: UserFromFile) => u._error?.type !== 'none') + .forEach((u) => { + // reset the error for each selected row + u._error = { + type: 'none', + message: '', + }; + }); // This is needed for parsing errors. const inverse_param_map = invert(user_param_map) as Dictionary; - selected.value.forEach((params: UserFromFile) => { + selected_users.value.forEach((params: UserFromFile) => { let parse_error: ParseError | null = null; const row = parseInt(`${params?._row || -1}`); - try { - const merged_user = getMergedUser(params); - // If the user is already in the course, show a warning - const u = users.course_users.find(_u => _u.username === merged_user.username); - if (u) { - users_already_in_course.value = true; - parse_error = { - type: 'warn', - message: `The user with username '${merged_user.username ?? ''}'` - + ' is already enrolled in the course.', - entire_row: true - }; - } else { - merged_users_to_add.value.push(new CourseUser(merged_user)); - } - } catch (error) { - const err = error as ParseError; - selected_user_error.value = true; - + const course_user_params = getCourseUser(params); + // If the user is already in the course, show a warning + const u = users.course_users.find((_u) => _u.username === course_user_params.username); + if (u) { + users_already_in_course.value = true; parse_error = { - type: 'error', - message: err.message + type: 'warn', + message: + `The user with username '${course_user_params.username ?? ''}'` + + ' is already enrolled in the course.', + entire_row: true, }; + } else { + const course_user = new CourseUser(course_user_params); + if (course_user.isValid()) { + course_users_to_add.value.push(course_user); + } else { + const validate = course_user.validate(); + // Find the field which didn't validate. + try { + Object.entries(validate).forEach(([k, v]) => { + if (typeof v === 'string') { + throw { + message: v, + field: k, + }; + } + }); + } catch (error) { + const err = error as ParseError; + selected_user_error.value = true; + + parse_error = { + type: 'error', + message: err.message, + }; - if (err.field === '_all') { - Object.assign(parse_error, { entire_row: true }); - } else if (err.field && - (User.ALL_FIELDS.indexOf(err.field) >= 0 || CourseUser.ALL_FIELDS.indexOf(err.field) >= 0)) { - if (inverse_param_map[err.field]) { - parse_error.col = inverse_param_map[err.field]; - } else { - parse_error.entire_row = true; + if (err.field === '_all') { + Object.assign(parse_error, { entire_row: true }); + } else if ( + err.field && + (User.ALL_FIELDS.indexOf(err.field) >= 0 || + CourseUser.ALL_FIELDS.indexOf(err.field) >= 0) + ) { + if (inverse_param_map[err.field]) { + parse_error.col = inverse_param_map[err.field]; + } else { + parse_error.entire_row = true; + } + } else if (err.field != undefined) { + // missing field + parse_error.entire_row = true; + } } - } else if (err.field != undefined) { // missing field - parse_error.entire_row = true; } } if (parse_error) { - const row_index = merged_users.value.findIndex((u: UserFromFile) => u._row === row); + const row_index = course_users.value.findIndex((u: UserFromFile) => u._row === row); if (row_index >= 0) { // Copy the user, update and splice in. This is needed to make the load file table reactive. - const user = { ...merged_users.value[row_index] }; + const user = { ...course_users.value[row_index] }; user._error = parse_error; - merged_users.value.splice(row_index, 1, user); + course_users.value.splice(row_index, 1, user); } } }); @@ -275,20 +304,23 @@ const loadFile = () => { reader.onload = (evt: ProgressEvent) => { if (evt && evt.target) { const reader = evt.target as FileReader; - const results = parse(reader.result as string, { header: false, skipEmptyLines: true }); + const results = parse(reader.result as string, { + header: false, + skipEmptyLines: true, + }); if (results.errors && results.errors.length > 0) { $q.notify({ message: results.errors[0].message, - color: 'red' + color: 'red', }); } else { const users: Array = []; const data = results.data as Array>; data.forEach((row: Array, index: number) => { const d: UserFromFile = {}; - d._error = { + d._error = { type: 'none', - message: '' + message: '', }; d._row = index; row.forEach((v: string, i: number) => { @@ -296,7 +328,7 @@ const loadFile = () => { }); users.push(d); }); - merged_users.value = users; + course_users.value = users; } } }; @@ -304,12 +336,12 @@ const loadFile = () => { // Add the Merged Users to the course. const addMergedUsers = async () => { - for await (const user of merged_users_to_add.value) { + for await (const user of course_users_to_add.value) { user.course_id = session.course.course_id; let global_user: User | undefined; try { // Skip if username is undefined? - global_user = await getUser(user.username ?? '') as User; + global_user = await getUser(user.username ?? ''); } catch (err) { const error = err as ResponseError; // this will occur is the user is not a global user @@ -319,9 +351,10 @@ const addMergedUsers = async () => { try { global_user = await users.addUser(new User(user)); $q.notify({ - message: `The global user with username '${global_user?.username ?? 'UNKNOWN'}'` + + message: + `The global user with username '${global_user?.username ?? 'UNKNOWN'}'` + ' was successfully added to the course.', - color: 'green' + color: 'green', }); } catch (e) { const error = e as ResponseError; @@ -333,69 +366,74 @@ const addMergedUsers = async () => { } try { await users.addCourseUser(new CourseUser(user)); - const full_name = `${user.first_name as string} ${user.last_name as string}`; + const full_name = `${user.first_name} ${user.last_name}`; $q.notify({ message: `The user ${full_name} was successfully added to the course.`, - color: 'green' + color: 'green', }); emit('closeDialog'); } catch (err) { const error = err as AxiosError; logger.error(error); - const data = error?.response?.data as ResponseError || { exception: '' }; + const data = (error?.response?.data as ResponseError) || { exception: '' }; $q.notify({ message: data.exception, - color: 'red' + color: 'red', }); } - } }; -watch([selected, common_role], parseUsers, { deep: true }); - -watch(() => column_headers, () => { - // Update the user_param_map if the column headers change. - Object.keys(column_headers.value).forEach(key => { - const user_field = user_fields.find(obj => obj.label === column_headers.value[key]); - if (user_field) { - user_param_map[key] = user_field.field; - } - }); - // And then reparse the file. - parseUsers(); -}, -{ deep: true }); +watch([selected_users, common_role], parseUsers, { deep: true }); + +watch( + () => column_headers, + () => { + // Update the user_param_map if the column headers change. + Object.keys(column_headers.value).forEach((key) => { + const user_field = user_fields.find( + (obj) => obj.label === column_headers.value[key] + ); + if (user_field) { + user_param_map[key] = user_field.field; + } + }); + // And then reparse the file. + parseUsers(); + }, + { deep: true } +); watch([first_row_header], () => { - selected.value = []; + selected_users.value = []; if (first_row_header.value) { - const first_row = merged_users.value.shift(); + const first_row = course_users.value.shift(); if (first_row) { header_row.value = first_row; fillHeaders(); } } else { - merged_users.value.unshift(header_row.value); + course_users.value.unshift(header_row.value); } }); const columns = computed(() => { - return merged_users.value.length === 0 ? [] : - Object.keys(merged_users.value[0]).filter((v: string) => (v !== '_row' && v !== '_error')) + return course_users.value.length === 0 + ? [] + : Object.keys(course_users.value[0]) + .filter((v: string) => v !== '_row' && v !== '_error') .map((v) => ({ name: v, label: v, field: v })); }); const getErrorClass = (col_name: string, err: ParseError) => { if (col_name === err.col || err.entire_row) { - return err.type === 'none' ? - '' : (err.type === 'error') ? 'cell-error' : 'cell-warn'; + return err.type === 'none' ? '' : err.type === 'error' ? 'cell-error' : 'cell-warn'; } }; -const hasError = (props: { col: {name: string}; row: { _error: ParseError }}) => +const hasError = (props: { col: { name: string }; row: { _error: ParseError } }) => props.row._error.type !== 'none' && - (props.col.name === props.row._error.col || props.row._error.entire_row); + (props.col.name === props.row._error.col || props.row._error.entire_row); diff --git a/src/components/instructor/ClasslistManagerComponents/AddUsersManually.vue b/src/components/instructor/ClasslistManagerComponents/AddUsersManually.vue index 1d82b032..15fcb18f 100644 --- a/src/components/instructor/ClasslistManagerComponents/AddUsersManually.vue +++ b/src/components/instructor/ClasslistManagerComponents/AddUsersManually.vue @@ -10,43 +10,50 @@
- +
- +
- +
- +
- +
- +
@@ -62,7 +69,6 @@ diff --git a/src/components/instructor/LibraryComponents/LibPanelOPL.vue b/src/components/instructor/LibraryComponents/LibPanelOPL.vue index 004b2c2d..ae779fff 100644 --- a/src/components/instructor/LibraryComponents/LibPanelOPL.vue +++ b/src/components/instructor/LibraryComponents/LibPanelOPL.vue @@ -56,7 +56,7 @@ import { useQuasar } from 'quasar'; import { useAppStateStore } from 'src/stores/app_state'; import { LibraryProblem } from 'src/common/models/problems'; -import type { ResponseError } from 'src/common/api-requests/interfaces'; +import type { ResponseError } from 'src/common/api-requests/errors'; import Problem from 'components/common/Problem.vue'; import { fetchDisciplines, fetchChapters, fetchSubjects, fetchSections, fetchLibraryProblems, LibraryCategory } from 'src/common/api-requests/library'; diff --git a/src/components/instructor/SetDetails/HomeworkDates.vue b/src/components/instructor/SetDetails/HomeworkDates.vue index e55c9b72..fb9414f8 100644 --- a/src/components/instructor/SetDetails/HomeworkDates.vue +++ b/src/components/instructor/SetDetails/HomeworkDates.vue @@ -8,7 +8,7 @@ :errorMessage="error_message" /> - + Reduced Scoring Date , required: true - }, - reduced_scoring: { - type: Boolean } }); @@ -69,13 +66,13 @@ watch(() => hw_dates.value, () => { logger.debug('[HomeworkDates] detected mutation in hw_dates...'); // avoid reactive loop - if (!props.reduced_scoring && hw_dates.value.reduced_scoring !== hw_dates.value.due) { + if (!hw_dates.value.reduced_scoring && hw_dates.value.reduced_scoring !== hw_dates.value.due) { hw_dates.value.reduced_scoring = hw_dates.value.due; } const dates = new HomeworkSetDates(hw_dates.value); - if (dates.isValid({ enable_reduced_scoring: props.reduced_scoring })) { + if (dates.isValid()) { logger.debug('[HomeworkDates] dates are valid -> telling parent & clearing error message.'); error_message.value = ''; emit('validDates', true); diff --git a/src/components/instructor/SetDetails/HomeworkSet.vue b/src/components/instructor/SetDetails/HomeworkSet.vue index 9e8f16cc..310b5488 100644 --- a/src/components/instructor/SetDetails/HomeworkSet.vue +++ b/src/components/instructor/SetDetails/HomeworkSet.vue @@ -21,12 +21,11 @@ Enable Reduced Scoring - + diff --git a/src/components/instructor/SetDetails/SetDetailProblems.vue b/src/components/instructor/SetDetails/SetDetailProblems.vue index 4d6615ae..5c865e18 100644 --- a/src/components/instructor/SetDetails/SetDetailProblems.vue +++ b/src/components/instructor/SetDetails/SetDetailProblems.vue @@ -33,7 +33,7 @@ import { useProblemSetStore } from 'src/stores/problem_sets'; import ProblemVue from 'components/common/Problem.vue'; import { ProblemSet } from 'src/common/models/problem_sets'; import { Problem, SetProblem } from 'src/common/models/problems'; -import { ResponseError } from 'src/common/api-requests/interfaces'; +import { ResponseError } from 'src/common/api-requests/errors'; import { logger } from 'boot/logger'; import { parseRouteSetID } from 'src/router/utils'; import { useSetProblemStore } from 'src/stores/set_problems'; @@ -43,7 +43,7 @@ const problem_sets = useProblemSetStore(); const set_problem_store = useSetProblemStore(); const route = useRoute(); // copy of the set_id prop and ensure it is a number -const problems = ref>([]); +const problems = ref([]); const problem_set = ref(new ProblemSet()); const set_id = computed(() => parseRouteSetID(route)); @@ -65,11 +65,8 @@ const reorderProblems = async () => { const promises: Promise[] = []; problems.value.forEach((prob, i) => { if (prob.problem_number !== i + 1) { - promises.push(set_problem_store.updateSetProblem({ - set_id: prob.set_id, - set_problem_id: prob.set_problem_id, - props: { problem_number: i + 1 } - })); + prob.problem_number = i + 1; + promises.push(set_problem_store.updateSetProblem(prob as SetProblem)); } }); await Promise.all(promises) diff --git a/src/components/instructor/SetDetails/SetUsers.vue b/src/components/instructor/SetDetails/SetUsers.vue index 0162bd9d..af4c937f 100644 --- a/src/components/instructor/SetDetails/SetUsers.vue +++ b/src/components/instructor/SetDetails/SetUsers.vue @@ -100,7 +100,7 @@ import { formatDate } from 'src/common/views'; import HomeworkDatesView from './HomeworkDates.vue'; import ReviewSetDatesView from './ReviewSetDates.vue'; import QuizDatesView from './QuizDates.vue'; -import type { ResponseError } from 'src/common/api-requests/interfaces'; +import type { ResponseError } from 'src/common/api-requests/errors'; type FieldType = ((row: UserHomeworkSet) => number) | @@ -258,7 +258,7 @@ const saveOverrides = async () => { } updated_user_set.set_dates.set(date_edit.value?.toObject() ?? {}); - if (updated_user_set.hasValidDates()) { + if (updated_user_set.set_dates.isValid()) { try { await problem_sets.updateUserSet(updated_user_set); const msg = `The problem set '${merged_user_set.value.set_name ?? ''}' ` + @@ -319,7 +319,7 @@ const updateProblemSet = () => { field: (row: UserHomeworkSet) => row.set_dates.answer ?? 0 }); } - if ((problem_set.value as unknown as UserHomeworkSet).set_params.enable_reduced_scoring) { + if ((problem_set.value as unknown as UserHomeworkSet).set_dates.enable_reduced_scoring) { columns.splice(columns.length - 1, 0, { name: 'reduced_scoring_date', @@ -382,7 +382,7 @@ watch(() => problem_sets.user_sets, updateProblemSet); const reduced_scoring = computed(() => { if (problem_set.value.set_type === 'HW') { const hw_set = problem_set.value as HomeworkSet; - return hw_set.set_params.enable_reduced_scoring; + return hw_set.set_dates.enable_reduced_scoring; } else { return false; } diff --git a/src/pages/instructor/ClasslistManager.vue b/src/pages/instructor/ClasslistManager.vue index 78e5b9ea..eedf15dc 100644 --- a/src/pages/instructor/ClasslistManager.vue +++ b/src/pages/instructor/ClasslistManager.vue @@ -63,7 +63,7 @@ import { logger } from 'src/boot/logger'; import { User, CourseUser } from 'src/common/models/users'; import { UserCourse } from 'src/common/models/courses'; -import type { ResponseError } from 'src/common/api-requests/interfaces'; +import type { ResponseError } from 'src/common/api-requests/errors'; import AddUsersManually from 'src/components/instructor/ClasslistManagerComponents/AddUsersManually.vue'; import AddUsersFromFile from 'src/components/instructor/ClasslistManagerComponents/AddUsersFromFile.vue'; import EditUsers from 'src/components/instructor/ClasslistManagerComponents/EditUsers.vue'; diff --git a/src/pages/instructor/ProblemSetDetails.vue b/src/pages/instructor/ProblemSetDetails.vue index c246dfa1..ecc29c3d 100644 --- a/src/pages/instructor/ProblemSetDetails.vue +++ b/src/pages/instructor/ProblemSetDetails.vue @@ -52,7 +52,7 @@ import { ProblemSet, ProblemSetType, convertSet, HomeworkSet, ReviewSet, Quiz import HomeworkSetView from 'src/components/instructor/SetDetails/HomeworkSet.vue'; import QuizView from 'src/components/instructor/SetDetails/Quiz.vue'; import ReviewSetView from 'src/components/instructor/SetDetails/ReviewSet.vue'; -import { ResponseError } from 'src/common/api-requests/interfaces'; +import { ResponseError } from 'src/common/api-requests/errors'; export default defineComponent({ name: 'ProblemSetDetails', diff --git a/src/stores/courses.ts b/src/stores/courses.ts index 1aa255eb..17dd09be 100644 --- a/src/stores/courses.ts +++ b/src/stores/courses.ts @@ -1,7 +1,7 @@ import { api } from 'boot/axios'; import { defineStore } from 'pinia'; import { logger } from 'src/boot/logger'; -import { ResponseError } from 'src/common/api-requests/interfaces'; +import { invalidError, ResponseError } from 'src/common/api-requests/errors'; import { Course, ParseableCourse } from 'src/common/models/courses'; @@ -44,6 +44,8 @@ export const useCourseStore = defineStore('courses', { * Adds a course to the database and the store. */ async addCourse(course: Course): Promise { + if (!course.isValid()) await invalidError(course, 'The added course is invalid'); + const response = await api.post('courses', course.toObject()); if (response.status === 200) { const new_course = new Course(response.data as ParseableCourse); @@ -59,6 +61,8 @@ export const useCourseStore = defineStore('courses', { * This updates the course in the database and the store. */ async updateCourse(course: Course): Promise { + if (!course.isValid()) await invalidError(course, 'The updated course is invalid'); + const response = await api.put(`courses/${course.course_id}`, course.toObject()); if (response.status === 200) { const updated_course = new Course(response.data as ParseableCourse); diff --git a/src/stores/problem_sets.ts b/src/stores/problem_sets.ts index 75145554..b1c45567 100644 --- a/src/stores/problem_sets.ts +++ b/src/stores/problem_sets.ts @@ -12,7 +12,7 @@ import { UserSet, mergeUserSet, DBUserSet, ParseableDBUserSet, parseDBUserSet } from 'src/common/models/user_sets'; import { CourseUser } from 'src/common/models/users'; -import { ResponseError } from 'src/common/api-requests/interfaces'; +import { invalidError, ResponseError } from 'src/common/api-requests/errors'; /** * This is an type to retrieve set info. @@ -137,6 +137,8 @@ export const useProblemSetStore = defineStore('problem_sets', { * Adds the given problem set to the store and the database. */ async addProblemSet(set: ProblemSet): Promise { + if (!set.isValid()) await invalidError(set, 'The added problem set is invalid'); + const response = await api.post(`courses/${set.course_id}/sets`, set.toObject()); const new_set = parseProblemSet(response.data as ParseableProblemSet); this.problem_sets.push(new_set); @@ -147,6 +149,8 @@ export const useProblemSetStore = defineStore('problem_sets', { * Updates the given set in the store and the database. */ async updateSet(set: ProblemSet): Promise { + if (!set.isValid()) await invalidError(set, 'The updated problem set is invalid'); + const response = await api.put(`courses/${set.course_id}/sets/${set.set_id}`, set.toObject()); if (response.status === 200) { const updated_set = parseProblemSet(response.data as ParseableProblemSet); @@ -228,6 +232,8 @@ export const useProblemSetStore = defineStore('problem_sets', { * Adds a User Set to the store and the database. */ async addUserSet(user_set: UserSet): Promise { + if (!user_set.isValid()) await invalidError(user_set, 'The added user set is invalid'); + const course_id = useSessionStore().course.course_id; const response = await api.post(`courses/${course_id}/sets/${user_set.set_id}/users`, user_set.toObject(DBUserSet.ALL_FIELDS)); @@ -244,6 +250,8 @@ export const useProblemSetStore = defineStore('problem_sets', { * Updates the given UserSet to the store and the database. */ async updateUserSet(set: UserSet): Promise { + if (!set.isValid()) await invalidError(set, 'The updated user set is invalid'); + const sessionStore = useSessionStore(); const course_id = sessionStore.course.course_id; const response = await api.put(`courses/${course_id}/sets/${set.set_id ?? 0}/users/${ diff --git a/src/stores/session.ts b/src/stores/session.ts index ef5cf915..31749171 100644 --- a/src/stores/session.ts +++ b/src/stores/session.ts @@ -6,7 +6,7 @@ import { User } from 'src/common/models/users'; import type { SessionInfo } from 'src/common/models/session'; import { ParseableUserCourse, UserCourse } from 'src/common/models/courses'; import { logger } from 'boot/logger'; -import { ResponseError } from 'src/common/api-requests/interfaces'; +import { ResponseError } from 'src/common/api-requests/errors'; import { useUserStore } from 'src/stores/users'; import { useSettingsStore } from 'src/stores/settings'; diff --git a/src/stores/set_problems.ts b/src/stores/set_problems.ts index fb2cc206..6413fc8c 100644 --- a/src/stores/set_problems.ts +++ b/src/stores/set_problems.ts @@ -3,6 +3,7 @@ import { api } from 'boot/axios'; import { defineStore } from 'pinia'; import { logger } from 'src/boot/logger'; +import { invalidError } from 'src/common/api-requests/errors'; import { ParseableProblem, parseProblem, SetProblem, ParseableSetProblem, UserProblem, mergeUserProblem, DBUserProblem, ParseableDBUserProblem, LibraryProblem } from 'src/common/models/problems'; @@ -115,6 +116,8 @@ export const useSetProblemStore = defineStore('set_problems', { * set_id to both the database and the store. */ async addSetProblem(problem: LibraryProblem, set_id: number): Promise { + if (!problem.isValid()) await invalidError(problem, 'The added problem is invalid'); + const course_id = useSessionStore().course.course_id; const prob = new SetProblem({ problem_params: problem.location_params, @@ -135,6 +138,8 @@ export const useSetProblemStore = defineStore('set_problems', { * Update the given SetProblem in both the database and the store. */ async updateSetProblem(problem: SetProblem): Promise { + if (!problem.isValid()) await invalidError(problem, 'The updated set problem is invalid'); + const course_id = useSessionStore().course.course_id; const prob = problem.toObject(); // delete the render params. Not in the database. diff --git a/src/stores/users.ts b/src/stores/users.ts index b38ca2a7..020dd994 100644 --- a/src/stores/users.ts +++ b/src/stores/users.ts @@ -4,7 +4,7 @@ import { defineStore } from 'pinia'; import { logger } from 'boot/logger'; import { DBCourseUser, ParseableCourseUser, ParseableDBCourseUser, ParseableUser } from 'src/common/models/users'; import { User, CourseUser } from 'src/common/models/users'; -import type { ResponseError } from 'src/common/api-requests/interfaces'; +import { invalidError, ResponseError } from 'src/common/api-requests/errors'; import { UserRole } from 'src/common/models/parsers'; import { useSessionStore } from './session'; @@ -99,13 +99,29 @@ export const useUserStore = defineStore('user', { } }, + /** + * Fetch a single global user and add to the store. + */ + async fetchUser(user_id: number): Promise { + const session_store = useSessionStore(); + const course_id = session_store.course.course_id; + const response = await api.get(`courses/${course_id}/global-users/${user_id}`); + if (response.status === 200) { + this.users.push(new User(response.data as ParseableUser)); + } else { + const error = response.data as ResponseError; + logger.error(`${error.exception}: ${error.message}`); + throw new Error(error.message); + } + }, + /** * Fetch the global users for a given course and store the results. */ // the users are stored in the same users field as the all global users // Perhaps this is a problem. async fetchGlobalCourseUsers(course_id: number): Promise { - const response = await api.get(`courses/${course_id}/global-users`); + const response = await api.get(`courses/${course_id}/global-courseusers`); if (response.status === 200) { const users = response.data as ParseableUser[]; this.users = users.map(user => new User(user)); @@ -119,7 +135,11 @@ export const useUserStore = defineStore('user', { * Updates the given user in the database and in the store. */ async updateUser(user: User): Promise { - const response = await api.put(`users/${user.user_id}`, user.toObject()); + if (!user.isValid()) return invalidError(user, 'The updated user is invalid'); + + const session_store = useSessionStore(); + const course_id = session_store.course.course_id; + const response = await api.put(`courses/${course_id}/global-users/${user.user_id}`, user.toObject()); if (response.status === 200) { const updated_user = new User(response.data as ParseableUser); const index = this.users.findIndex(user => user.user_id === updated_user.user_id); @@ -136,7 +156,9 @@ export const useUserStore = defineStore('user', { * Deletes the given User in the database and in the store. */ async deleteUser(user: User): Promise { - const response = await api.delete(`/users/${user.user_id ?? 0}`); + const session_store = useSessionStore(); + const course_id = session_store.course.course_id; + const response = await api.delete(`courses/${course_id}/global-users/${user.user_id}`); if (response.status === 200) { const index = this.users.findIndex((u) => u.user_id === user.user_id); // splice is used so vue3 reacts to changes. @@ -149,7 +171,11 @@ export const useUserStore = defineStore('user', { * Adds the given User to the database and to the store. */ async addUser(user: User): Promise { - const response = await api.post('users', user.toObject()); + if (!user.isValid()) return invalidError(user, 'The added user is invalid.'); + + const session_store = useSessionStore(); + const course_id = session_store.course.course_id; + const response = await api.post(`courses/${course_id}/global-users`, user.toObject()); if (response.status === 200) { const new_user = new User(response.data as ParseableUser); this.users.push(new_user); @@ -195,6 +221,10 @@ export const useUserStore = defineStore('user', { * Adds the given Course User to the store and the database. */ async addCourseUser(course_user: CourseUser): Promise { + if (!course_user.isValid()) { + return invalidError(course_user, 'The added course user is invalid'); + } + // When sending, only send the DBCourseUser fields. const response = await api.post(`courses/${course_user.course_id}/users`, course_user.toObject(DBCourseUser.ALL_FIELDS)); @@ -213,6 +243,10 @@ export const useUserStore = defineStore('user', { * Updates the given Course User to the store and the database. */ async updateCourseUser(course_user: CourseUser): Promise { + if (!course_user.isValid()) { + return invalidError(course_user, 'The updated course user is invalid'); + } + const url = `courses/${course_user.course_id || 0}/users/${course_user.user_id ?? 0}`; // When sending, only send the DBCourseUser fields. const response = await api.put(url, course_user.toObject(DBCourseUser.ALL_FIELDS)); @@ -235,8 +269,9 @@ export const useUserStore = defineStore('user', { const response = await api.delete(`courses/${course_user.course_id}/users/${course_user.user_id}`); if (response.status === 200) { const index = this.db_course_users.findIndex((u) => u.course_user_id === course_user.course_user_id); + // splice is used so vue3 reacts to changes. - this.course_users.splice(index, 1); + this.db_course_users.splice(index, 1); const deleted_course_user = new DBCourseUser(response.data as ParseableCourseUser); const user = this.users.find(u => u.user_id === deleted_course_user.user_id); return new CourseUser(Object.assign({}, user?.toObject(), deleted_course_user.toObject())); diff --git a/t/db/001_courses.t b/t/db/001_courses.t index 5878b64e..46994789 100644 --- a/t/db/001_courses.t +++ b/t/db/001_courses.t @@ -12,6 +12,7 @@ BEGIN { } use lib "$main::ww3_dir/lib"; +use lib "$main::ww3_dir/t/lib"; use List::MoreUtils qw(uniq); @@ -24,7 +25,7 @@ use DB::WithParams; use DB::WithDates; use DB::Schema; -use DB::TestUtils qw/loadCSV removeIDs loadSchema/; +use TestUtils qw/loadCSV removeIDs loadSchema/; # Load the database my $config_file = "$main::ww3_dir/conf/ww3-dev.yml"; diff --git a/t/db/002_course_settings.t b/t/db/002_course_settings.t index 14d25ccb..64af43d8 100644 --- a/t/db/002_course_settings.t +++ b/t/db/002_course_settings.t @@ -12,6 +12,7 @@ BEGIN { } use lib "$main::ww3_dir/lib"; +use lib "$main::ww3_dir/t/lib"; use Test::More; use Test::Exception; @@ -23,7 +24,7 @@ use WeBWorK3::Utils::Settings qw/getDefaultCourseSettings getDefaultCourseValues validateSettingsConfFile validateSingleCourseSetting validateSettingConfig isInteger isTimeString isTimeDuration isDecimal mergeCourseSettings/; -use DB::TestUtils qw/loadCSV removeIDs loadSchema/; +use TestUtils qw/removeIDs loadSchema/; # Load the database my $config_file = "$main::ww3_dir/conf/ww3-dev.yml"; diff --git a/t/db/003_users.t b/t/db/003_users.t index ce41df37..3704a353 100644 --- a/t/db/003_users.t +++ b/t/db/003_users.t @@ -12,6 +12,7 @@ BEGIN { } use lib "$main::ww3_dir/lib"; +use lib "$main::ww3_dir/t/lib"; use Test::More; use Test::Exception; @@ -22,7 +23,7 @@ use DateTime::Format::Strptime; use Mojo::JSON qw/true false/; use DB::Schema; -use DB::TestUtils qw/loadCSV removeIDs/; +use TestUtils qw/loadCSV removeIDs/; # Load the database my $config_file = "$main::ww3_dir/conf/ww3-dev.yml"; diff --git a/t/db/004_course_users.t b/t/db/004_course_users.t index 41cd11c9..d1d706a2 100644 --- a/t/db/004_course_users.t +++ b/t/db/004_course_users.t @@ -12,6 +12,7 @@ BEGIN { } use lib "$main::ww3_dir/lib"; +use lib "$main::ww3_dir/t/lib"; use Test::More; use Test::Exception; @@ -20,7 +21,7 @@ use Clone qw/clone/; use Mojo::JSON qw/true false/; use DB::Schema; -use DB::TestUtils qw/loadCSV removeIDs/; +use TestUtils qw/loadCSV removeIDs/; use DB::Utils qw/removeLoginParams/; # Load the database diff --git a/t/db/005_hwsets.t b/t/db/005_hwsets.t index 519efbef..6f240f61 100644 --- a/t/db/005_hwsets.t +++ b/t/db/005_hwsets.t @@ -12,6 +12,7 @@ BEGIN { } use lib "$main::ww3_dir/lib"; +use lib "$main::ww3_dir/t/lib"; use Test::More; use Test::Exception; @@ -21,7 +22,7 @@ use DateTime::Format::Strptime; use Mojo::JSON qw/true false/; use DB::Schema; -use DB::TestUtils qw/loadCSV removeIDs filterBySetType/; +use TestUtils qw/loadCSV removeIDs filterBySetType/; # Load the database my $config_file = "$main::ww3_dir/conf/ww3-dev.yml"; @@ -38,21 +39,39 @@ my $course_rs = $schema->resultset('Course'); my $user_rs = $schema->resultset('User'); # Load HW sets from CSV file -my @hw_sets = loadCSV("$main::ww3_dir/t/db/sample_data/hw_sets.csv"); +my @hw_sets = loadCSV( + "$main::ww3_dir/t/db/sample_data/hw_sets.csv", + { + boolean_fields => ['set_visible'], + param_boolean_fields => [ 'enable_reduced_scoring', 'hide_hint' ] + } +); for my $hw_set (@hw_sets) { $hw_set->{set_type} = 'HW'; $hw_set->{set_params} = {} unless defined $hw_set->{set_params}; } -my @quizzes = loadCSV("$main::ww3_dir/t/db/sample_data/quizzes.csv"); -for my $set (@quizzes) { - $set->{set_type} = 'QUIZ'; - $set->{set_params} = {} unless defined $set->{set_params}; - +my @quizzes = loadCSV( + "$main::ww3_dir/t/db/sample_data/quizzes.csv", + { + boolean_fields => ['set_visible'], + param_boolean_fields => ['timed'], + param_non_neg_int_fields => ['quiz_duration'] + } +); +for my $quiz (@quizzes) { + $quiz->{set_type} = "QUIZ"; + $quiz->{set_params} = {} unless defined($quiz->{set_params}); } -my @review_sets = loadCSV("$main::ww3_dir/t/db/sample_data/review_sets.csv"); +my @review_sets = loadCSV( + "$main::ww3_dir/t/db/sample_data/review_sets.csv", + { + boolean_fields => ['set_visible'], + param_boolean_fields => ['can_retake'] + } +); for my $set (@review_sets) { $set->{set_type} = 'REVIEW'; $set->{set_params} = {} unless defined $set->{set_params}; @@ -143,9 +162,15 @@ throws_ok { # Add a new problem set my $new_set_params = { - set_name => 'HW #9', - set_dates => { open => 100, reduced_scoring => 120, due => 140, answer => 200 }, - set_type => 'HW' + set_name => "HW #9", + set_dates => { + open => 100, + reduced_scoring => 120, + due => 140, + answer => 200, + enable_reduced_scoring => true + }, + set_type => "HW" }; my $new_set = $problem_set_rs->addProblemSet( @@ -245,9 +270,9 @@ throws_ok { # Check for undefined parameter fields my $new_set7 = { set_name => 'HW #11', - set_dates => { open => 100, due => 140, answer => 200 }, + set_dates => { open => 100, due => 140, answer => 200, enable_reduced_scoring => false }, set_type => 'HW', - set_params => { enable_reduced_scoring => false, not_a_valid_field => 5 } + set_params => { not_a_valid_field => 5 } }; throws_ok { $problem_set_rs->addProblemSet( @@ -265,9 +290,9 @@ throws_ok { params => { course_name => 'Precalculus', set_name => 'HW #11', - set_dates => { open => 100, due => 140, answer => 200 }, + set_dates => { open => 100, due => 140, answer => 200, enable_reduced_scoring => false }, set_type => 'HW', - set_params => { enable_reduced_scoring => false, hide_hint => 'yes' } + set_params => { hide_hint => 'yes' } } ); } @@ -279,17 +304,31 @@ throws_ok { params => { course_name => 'Precalculus', set_name => 'HW #11', - set_dates => { open => 100, due => 140, answer => 200 }, + set_dates => { open => 100, due => 140, answer => 200, enable_reduced_scoring => false }, set_type => 'HW', - set_params => { enable_reduced_scoring => 0, hide_hint => true } + set_params => { hide_hint => 0 } } ); } 'DB::Exception::InvalidParameter', 'addProblemSet: adding an non-valid boolean parameter'; +# Check to ensure true/false are passed into the enable_reduced_scoring in set_dates, not 0/1 +throws_ok { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_name => 'HW #11', + set_dates => { open => 100, due => 140, answer => 200, enable_reduced_scoring => 0 }, + set_type => 'HW', + set_params => { hide_hint => 0 } + } + ); +} +'DB::Exception::InvalidParameter', 'addProblemSet: adding an non-valid boolean parameter in set_dates'; + # Update a set -$new_set_params->{set_name} = 'HW #8'; -$new_set_params->{set_params} = { enable_reduced_scoring => true }; +$new_set_params->{set_name} = "HW #8"; +$new_set_params->{set_params} = { hide_hint => true }; $new_set_params->{type} = 1; my $updated_set = $problem_set_rs->updateProblemSet( @@ -300,7 +339,7 @@ my $updated_set = $problem_set_rs->updateProblemSet( params => { set_name => $new_set_params->{set_name}, set_params => { - enable_reduced_scoring => true + hide_hint => true } } ); diff --git a/t/db/006_quizzes.t b/t/db/006_quizzes.t index 18692319..7d157512 100644 --- a/t/db/006_quizzes.t +++ b/t/db/006_quizzes.t @@ -12,6 +12,7 @@ BEGIN { } use lib "$main::ww3_dir/lib"; +use lib "$main::ww3_dir/t/lib"; use Test::More; use Test::Exception; @@ -21,7 +22,7 @@ use DateTime::Format::Strptime; use Mojo::JSON qw/true false/; use DB::Schema; -use DB::TestUtils qw/loadCSV removeIDs filterBySetType/; +use TestUtils qw/loadCSV removeIDs filterBySetType/; # Load the database my $config_file = "$main::ww3_dir/conf/ww3-dev.yml"; diff --git a/t/db/007_user_set.t b/t/db/007_user_set.t index 07f285b1..6ee5c2d5 100644 --- a/t/db/007_user_set.t +++ b/t/db/007_user_set.t @@ -13,6 +13,7 @@ BEGIN { } use lib "$main::ww3_dir/lib"; +use lib "$main::ww3_dir/t/lib"; use DateTime::Format::Strptime; use Test::More; @@ -23,7 +24,7 @@ use YAML::XS qw/LoadFile/; use Mojo::JSON qw/true false/; use DB::Schema; -use DB::TestUtils qw/loadCSV removeIDs/; +use TestUtils qw/loadCSV removeIDs/; # Load the configuration files my $config_file = "$main::ww3_dir/conf/ww3-dev.yml"; @@ -41,27 +42,56 @@ my $problem_set_rs = $schema->resultset('ProblemSet'); my $course = $course_rs->find({ course_id => 1 }); # Load info from CSV files -my @hw_sets = loadCSV("$main::ww3_dir/t/db/sample_data/hw_sets.csv"); +my @hw_sets = loadCSV( + "$main::ww3_dir/t/db/sample_data/hw_sets.csv", + { + boolean_fields => ['set_visible'], + param_boolean_fields => [ 'enable_reduced_scoring', 'hide_hint' ] + } +); for my $hw_set (@hw_sets) { - $hw_set->{set_type} = 'HW'; - $hw_set->{set_version} = 1 unless defined($hw_set->{set_version}); + $hw_set->{set_type} = "HW"; + $hw_set->{set_version} = 1 unless defined($hw_set->{set_version}); + $hw_set->{set_params} = {} unless defined $hw_set->{set_params}; } -my @quizzes = loadCSV("$main::ww3_dir/t/db/sample_data/quizzes.csv"); +my @quizzes = loadCSV( + "$main::ww3_dir/t/db/sample_data/quizzes.csv", + { + boolean_fields => ['set_visible'], + param_boolean_fields => ['timed'], + param_non_neg_int_fields => ['quiz_duration'] + } +); for my $set (@quizzes) { - $set->{set_type} = 'QUIZ'; - $set->{set_version} = 1 unless defined($set->{set_version}); + $set->{set_type} = "QUIZ"; + $set->{set_version} = 1 unless defined($set->{set_version}); + $set->{set_params} = {} unless defined $set->{set_params}; } -my @review_sets = loadCSV("$main::ww3_dir/t/db/sample_data/review_sets.csv"); +my @review_sets = loadCSV( + "$main::ww3_dir/t/db/sample_data/review_sets.csv", + { + boolean_fields => ['set_visible'], + param_boolean_fields => ['can_retake'] + } +); + for my $set (@review_sets) { - $set->{set_type} = 'REVIEW'; - $set->{set_version} = 1 unless defined($set->{set_version}); + $set->{set_type} = "REVIEW"; + $set->{set_version} = 1 unless defined($set->{set_version}); + $set->{set_params} = {} unless defined $set->{set_params}; } my @all_problem_sets = (@hw_sets, @quizzes, @review_sets); -my @all_user_sets = loadCSV("$main::ww3_dir/t/db/sample_data/user_sets.csv"); +my @all_user_sets = loadCSV( + "$main::ww3_dir/t/db/sample_data/user_sets.csv", + { + boolean_fields => ['set_visible'], + param_boolean_fields => [ 'enable_reduced_scoring', 'hide_hint' ] + } +); for my $set (@all_user_sets) { $set->{set_version} = 1 unless defined($set->{set_version}); @@ -70,7 +100,8 @@ for my $set (@all_user_sets) { $_->{course_name} eq $set->{course_name} && $_->{set_name} eq $set->{set_name} } @all_problem_sets; - $set->{set_type} = $s->{set_type}; + $set->{set_params} = {} unless defined $set->{set_params}; + $set->{set_type} = $s->{set_type}; } my @merged_user_sets = @{ clone(\@all_user_sets) }; @@ -494,11 +525,12 @@ my $ralph_set_info = { course_name => 'Precalculus', set_name => 'HW #4', set_dates => { - open => $hw4->{set_dates}->{open} - 100, - answer => $hw4->{set_dates}->{answer} + 100, + open => $hw4->{set_dates}->{open} - 100, + answer => $hw4->{set_dates}->{answer} + 100, + enable_reduced_scoring => false, }, set_params => { - enable_reduced_scoring => false + hide_hint => false } }; diff --git a/t/db/008_problem_pools.t b/t/db/008_problem_pools.t index 114e35c5..292f2d10 100644 --- a/t/db/008_problem_pools.t +++ b/t/db/008_problem_pools.t @@ -12,13 +12,14 @@ BEGIN { } use lib "$main::ww3_dir/lib"; +use lib "$main::ww3_dir/t/lib"; use Test::More; use Test::Exception; use YAML::XS qw/LoadFile/; use DB::Schema; -use DB::TestUtils qw/loadCSV removeIDs/; +use TestUtils qw/loadCSV removeIDs/; # Load the database my $config_file = "$main::ww3_dir/conf/ww3-dev.yml"; diff --git a/t/db/009_problems.t b/t/db/009_problems.t index 3582949e..911f809c 100644 --- a/t/db/009_problems.t +++ b/t/db/009_problems.t @@ -12,6 +12,7 @@ BEGIN { } use lib "$main::ww3_dir/lib"; +use lib "$main::ww3_dir/t/lib"; use Test::More; use Test::Exception; @@ -19,7 +20,7 @@ use Clone qw/clone/; use YAML::XS qw/LoadFile/; use DB::Schema; -use DB::TestUtils qw/loadCSV removeIDs/; +use TestUtils qw/loadCSV removeIDs/; use DB::Utils qw/updateAllFields/; # Load the database diff --git a/t/db/010_user_problems.t b/t/db/010_user_problems.t index b47c0f61..3d533040 100644 --- a/t/db/010_user_problems.t +++ b/t/db/010_user_problems.t @@ -12,6 +12,7 @@ BEGIN { } use lib "$main::ww3_dir/lib"; +use lib "$main::ww3_dir/t/lib"; use Test::More; use Test::Exception; @@ -22,7 +23,7 @@ use List::MoreUtils qw/firstval/; use YAML::XS qw/LoadFile/; use DB::Schema; -use DB::TestUtils qw/loadCSV removeIDs loadSchema/; +use TestUtils qw/loadCSV removeIDs loadSchema/; use DB::Utils qw/updateAllFields/; # Set up the database. diff --git a/t/db/011_attempts.t b/t/db/011_attempts.t index 00bb2d84..7b6ce9a0 100644 --- a/t/db/011_attempts.t +++ b/t/db/011_attempts.t @@ -12,6 +12,7 @@ BEGIN { } use lib "$main::ww3_dir/lib"; +use lib "$main::ww3_dir/t/lib"; use Test::More; use Test::Exception; @@ -22,7 +23,7 @@ use Try::Tiny; use YAML::XS qw/LoadFile/; use DB::Schema; -use DB::TestUtils qw/loadCSV removeIDs loadSchema/; +use TestUtils qw/loadCSV removeIDs loadSchema/; use DB::Utils qw/updateAllFields/; # Set up the database. diff --git a/t/db/012_set_versions.t b/t/db/012_set_versions.t index 19a2c8a9..f480c3ee 100644 --- a/t/db/012_set_versions.t +++ b/t/db/012_set_versions.t @@ -12,6 +12,7 @@ BEGIN { } use lib "$main::ww3_dir/lib"; +use lib "$main::ww3_dir/t/lib"; use Test::More; use Test::Exception; @@ -20,7 +21,7 @@ use List::MoreUtils qw/firstval/; use Clone qw/clone/; use DB::Schema; -use DB::TestUtils qw/loadCSV removeIDs/; +use TestUtils qw/loadCSV removeIDs/; # Load the database my $config_file = "$main::ww3_dir/conf/ww3-dev.yml"; @@ -33,28 +34,55 @@ my $course_rs = $schema->resultset('Course'); my $user_rs = $schema->resultset('User'); my $user_set_rs = $schema->resultset('UserSet'); -# Load info from CSV files -my @hw_sets = loadCSV("$main::ww3_dir/t/db/sample_data/hw_sets.csv"); +# Load HW sets from CSV file +my @hw_sets = loadCSV( + "$main::ww3_dir/t/db/sample_data/hw_sets.csv", + { + boolean_fields => ['set_visible'], + param_boolean_fields => [ 'enable_reduced_scoring', 'hide_hint' ] + } +); for my $hw_set (@hw_sets) { - $hw_set->{set_type} = 'HW'; - $hw_set->{set_version} = 1 unless defined($hw_set->{set_version}); + $hw_set->{set_type} = 'HW'; + $hw_set->{set_params} = {} unless defined $hw_set->{set_params}; + } -my @quizzes = loadCSV("$main::ww3_dir/t/db/sample_data/quizzes.csv"); -for my $set (@quizzes) { - $set->{set_type} = 'QUIZ'; - $set->{set_version} = 1 unless defined($set->{set_version}); +my @quizzes = loadCSV( + "$main::ww3_dir/t/db/sample_data/quizzes.csv", + { + boolean_fields => ['set_visible'], + param_boolean_fields => ['timed'], + param_non_neg_int_fields => ['quiz_duration'] + } +); +for my $quiz (@quizzes) { + $quiz->{set_type} = "QUIZ"; + $quiz->{set_params} = {} unless defined($quiz->{set_params}); } -my @review_sets = loadCSV("$main::ww3_dir/t/db/sample_data/review_sets.csv"); +my @review_sets = loadCSV( + "$main::ww3_dir/t/db/sample_data/review_sets.csv", + { + boolean_fields => ['set_visible'], + param_boolean_fields => ['can_retake'] + } +); for my $set (@review_sets) { - $set->{set_type} = 'REVIEW'; - $set->{set_version} = 1 unless defined($set->{set_version}); + $set->{set_type} = 'REVIEW'; + $set->{set_params} = {} unless defined $set->{set_params}; + } my @all_problem_sets = (@hw_sets, @quizzes, @review_sets); -my @all_user_sets = loadCSV("$main::ww3_dir/t/db/sample_data/user_sets.csv"); +my @all_user_sets = loadCSV( + "$main::ww3_dir/t/db/sample_data/user_sets.csv", + { + boolean_fields => ['set_visible'], + param_boolean_fields => [ 'enable_reduced_scoring', 'hide_hint' ] + } +); for my $set (@all_user_sets) { $set->{set_version} = 1 unless defined($set->{set_version}); @@ -63,7 +91,8 @@ for my $set (@all_user_sets) { $_->{course_name} eq $set->{course_name} && $_->{set_name} eq $set->{set_name} } @all_problem_sets; - $set->{set_type} = $s->{set_type}; + $set->{set_type} = $s->{set_type}; + $set->{set_params} = {} unless defined $set->{set_params}; } my @merged_user_sets = @{ clone(\@all_user_sets) }; diff --git a/t/db/013_problem_versions.t b/t/db/013_problem_versions.t index 43b9bf01..5feeeab9 100644 --- a/t/db/013_problem_versions.t +++ b/t/db/013_problem_versions.t @@ -12,6 +12,7 @@ BEGIN { } use lib "$main::ww3_dir/lib"; +use lib "$main::ww3_dir/t/lib"; use Test::More; use Test::Exception; @@ -20,7 +21,7 @@ use List::MoreUtils qw/firstval/; use Clone qw/clone/; use DB::Schema; -use DB::TestUtils qw/loadCSV removeIDs/; +use TestUtils qw/loadCSV removeIDs/; # Load the database my $config_file = "$main::ww3_dir/conf/ww3-dev.yml"; diff --git a/t/db/build_db.pl b/t/db/build_db.pl index f976825b..816c66dd 100755 --- a/t/db/build_db.pl +++ b/t/db/build_db.pl @@ -12,6 +12,7 @@ BEGIN } use lib "$main::ww3_dir/lib"; +use lib "$main::ww3_dir/t/lib"; use Carp; use feature 'say'; @@ -22,7 +23,7 @@ BEGIN use Mojo::JSON qw/true false/; use DB::Schema; -use DB::TestUtils qw/loadCSV/; +use TestUtils qw/loadCSV/; my $verbose = 1; @@ -91,10 +92,10 @@ sub addUsers { my $admin = { username => 'admin', email => 'admin@google.com', - first_name => 'Andrea', - last_name => 'Administrator', + first_name => "Andrea", + last_name => "Administrator", is_admin => true, - login_params => { password => 'admin' } + login_params => { password => "admin" } }; $user_rs->create($admin); @@ -135,6 +136,7 @@ sub addSets { param_boolean_fields => [ 'enable_reduced_scoring', 'hide_hint' ] } ); + for my $set (@hw_sets) { my $course = $course_rs->find({ course_name => $set->{course_name} }); if (!defined($course)) { diff --git a/t/db/sample_data/hw_sets.csv b/t/db/sample_data/hw_sets.csv index fd629fed..de077fb5 100644 --- a/t/db/sample_data/hw_sets.csv +++ b/t/db/sample_data/hw_sets.csv @@ -1,4 +1,4 @@ -course_name,set_name,set_visible,SET_DATES:open,SET_DATES:reduced_scoring,SET_DATES:due,SET_DATES:answer,SET_PARAMS:enable_reduced_scoring +course_name,set_name,set_visible,SET_DATES:open,SET_DATES:reduced_scoring,SET_DATES:due,SET_DATES:answer,SET_DATES:enable_reduced_scoring Precalculus,"HW #1",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,2021-02-21T23:59:00Z,1 Precalculus,"HW #2",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,2021-02-21T23:59:00Z,1 Precalculus,"HW #3",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,2021-02-21T23:59:00Z,1 diff --git a/t/db/sample_data/user_sets.csv b/t/db/sample_data/user_sets.csv index d18d6cce..c1fcb19f 100644 --- a/t/db/sample_data/user_sets.csv +++ b/t/db/sample_data/user_sets.csv @@ -1,4 +1,4 @@ -course_name,username,set_name,SET_DATES:open,SET_DATES:reduced_scoring,SET_DATES:due,SET_DATES:answer,SET_PARAMS:enable_reduced_scoring +course_name,username,set_name,SET_DATES:open,SET_DATES:reduced_scoring,SET_DATES:due,SET_DATES:answer,SET_DATES:enable_reduced_scoring Precalculus,homer,"HW #1",2021-01-08T23:59:00Z,,2021-02-15T23:59:00Z,2021-02-22T23:59:00Z,1 Precalculus,bart,"HW #1",,,, Precalculus,ned,"HW #1",,,, diff --git a/t/db/test_course_user.pl b/t/db/test_course_user.pl index 9f1bb177..bd9c2fd6 100755 --- a/t/db/test_course_user.pl +++ b/t/db/test_course_user.pl @@ -21,7 +21,7 @@ BEGIN use DB::WithParams; use DB::WithDates; use DB::Schema; -use DB::TestUtils qw/loadCSV removeIDs/; +use TestUtils qw/loadCSV removeIDs/; # load the database my $db_file = "$main::test_dir/sample_db.sqlite"; diff --git a/lib/DB/TestUtils.pm b/t/lib/TestUtils.pm similarity index 89% rename from lib/DB/TestUtils.pm rename to t/lib/TestUtils.pm index 9fe530cc..56678b17 100644 --- a/lib/DB/TestUtils.pm +++ b/t/lib/TestUtils.pm @@ -1,4 +1,4 @@ -package DB::TestUtils; +package TestUtils; use warnings; use strict; @@ -42,6 +42,8 @@ sub buildHash ($input, $config) { } elsif (defined($input->{$key}) && $input->{$key} =~ /^\d{4}-\d{2}-\d{2}T\d\d:\d\d:\d\dZ$/) { my $dt = $strp_datetime->parse_datetime($input->{$key}); $output->{$field}->{$subfield} = $dt->epoch; + } elsif (grep {/^$subfield$/} @{ $config->{param_boolean_fields} }) { + $output->{$field}->{$subfield} = int($input->{$key}) ? true : false if defined($input->{$key}); } } elsif (grep { $_ eq $subfield } @{ $config->{param_boolean_fields} }) { $output->{$field}->{$subfield} = int($input->{$key}) ? true : false if defined($input->{$key}); @@ -52,11 +54,11 @@ sub buildHash ($input, $config) { } else { $output->{$field}->{$subfield} = $input->{$key} if defined($input->{$key}); } - } elsif (grep {/^$key$/} @{ $config->{boolean_fields} }) { + } elsif (grep { $_ eq $key } @{ $config->{boolean_fields} }) { $output->{$key} = defined($input->{$key}) && int($input->{$key}) ? true : false; - } elsif (grep {/^$key$/} @{ $config->{non_neg_int_fields} }) { + } elsif (grep { $_ eq $key } @{ $config->{non_neg_int_fields} }) { $output->{$key} = int($input->{$key}) if defined($input->{$key}); - } elsif (grep {/^$key$/} @{ $config->{non_neg_float_fields} }) { + } elsif (grep { $_ eq $key } @{ $config->{non_neg_float_fields} }) { $output->{$key} = 0 + $input->{$key} if defined($input->{$key}); } else { $output->{$key} = $input->{$key}; diff --git a/t/mojolicious/003_users.t b/t/mojolicious/003_users.t index fb987181..1413a10d 100644 --- a/t/mojolicious/003_users.t +++ b/t/mojolicious/003_users.t @@ -4,7 +4,7 @@ use Mojo::Base -strict; use Test::More; use Test::Mojo; -use Mojo::JSON; +use Mojo::JSON qw/true false/; BEGIN { use File::Basename qw/dirname/; @@ -158,18 +158,16 @@ $t->get_ok('/webwork3/api/users/1')->json_is('/username', 'admin'); my $admin_user = $t->tx->res->json; ok($admin_user->{is_admin}, 'testing that is_admin compares to 1.'); -is($admin_user->{is_admin}, Mojo::JSON::true, 'testing that is_admin compares to Mojo::JSON::true'); -ok(JSON::PP::is_bool($admin_user->{is_admin}), 'testing that is_admin is a Mojo::JSON::true or Mojo::JSON::false'); -ok(JSON::PP::is_bool($admin_user->{is_admin}) && $admin_user->{is_admin}, - 'testing that is_admin is a Mojo::JSON::true'); +is($admin_user->{is_admin}, true, 'testing that is_admin compares to true'); +ok(JSON::PP::is_bool($admin_user->{is_admin}), 'testing that is_admin is a true or false'); +ok(JSON::PP::is_bool($admin_user->{is_admin}) && $admin_user->{is_admin}, 'testing that is_admin is a true'); ok(not(JSON::PP::is_bool($admin_user->{user_id})), 'testing that $admin->{user_id} is not a JSON boolean'); ok(!$new_user_from_db->{is_admin}, 'testing new_user->{is_admin} is not truthy.'); -is($new_user_from_db->{is_admin}, Mojo::JSON::false, 'testing that new_user->{is_admin} compares to Mojo::JSON::false'); -ok(JSON::PP::is_bool($new_user_from_db->{is_admin}), - 'testing that new_user->{is_admin} is a Mojo::JSON::true or Mojo::JSON::false'); +is($new_user_from_db->{is_admin}, false, 'testing that new_user->{is_admin} compares to false'); +ok(JSON::PP::is_bool($new_user_from_db->{is_admin}), 'testing that new_user->{is_admin} is a true or false'); ok(JSON::PP::is_bool($new_user_from_db->{is_admin}) && !$new_user_from_db->{is_admin}, - 'testing that new_user->{is_admin} is a Mojo::JSON::false'); + 'testing that new_user->{is_admin} is a false'); done_testing; diff --git a/t/mojolicious/005_problem_sets.t b/t/mojolicious/005_problem_sets.t index 336ee1b2..92fe410e 100644 --- a/t/mojolicious/005_problem_sets.t +++ b/t/mojolicious/005_problem_sets.t @@ -4,6 +4,7 @@ use Mojo::Base -strict; use Test::More; use Test::Mojo; +use Mojo::JSON qw/true false/; use DateTime::Format::Strptime; @@ -14,11 +15,12 @@ BEGIN { } use lib "$main::ww3_dir/lib"; +use lib "$main::ww3_dir/t/lib"; use DB::Schema; use Clone qw/clone/; use YAML::XS qw/LoadFile/; -use DB::TestUtils qw/loadCSV/; +use TestUtils qw/loadCSV/; # Test the api with common 'courses/sets' routes. @@ -74,7 +76,7 @@ my $new_set_id = $t->tx->res->json('/set_id'); # Check that set_visible is a JSON boolean my $set_visible = $t->tx->res->json('/set_visible'); ok(!$set_visible, 'testing that set_visible is falsy'); -is($set_visible, Mojo::JSON::false, 'Test that set_visible compares to Mojo::JSON::false'); +is($set_visible, false, 'Test that set_visible compares to Mojo::JSON::false'); ok(JSON::PP::is_bool($set_visible), 'Test that set_visible is a JSON boolean'); ok(JSON::PP::is_bool($set_visible) && !$set_visible, 'Test that set_visible is a JSON::false'); @@ -82,13 +84,13 @@ ok(JSON::PP::is_bool($set_visible) && !$set_visible, 'Test that set_visible is a $t->put_ok( "/webwork3/api/courses/2/sets/$new_set_id" => json => { set_name => 'HW #11', - set_visible => Mojo::JSON::true + set_visible => true } )->content_type_is('application/json;charset=UTF-8')->json_is('/set_name' => 'HW #11'); $set_visible = $t->tx->res->json('/set_visible'); ok($set_visible, 'testing that set_visible is truthy'); -is($set_visible, Mojo::JSON::true, 'Test that set_visible compares to Mojo::JSON::true'); +is($set_visible, true, 'Test that set_visible compares to Mojo::JSON::true'); ok(JSON::PP::is_bool($set_visible), 'Test that set_visible is a JSON boolean'); ok(JSON::PP::is_bool($set_visible) && $set_visible, 'Test that set_visible is a JSON:: true'); @@ -112,23 +114,23 @@ $t->post_ok('/webwork3/api/courses/2/sets' => json => $another_new_set) $t->get_ok('/webwork3/api/courses/1/sets/1'); my $hw1 = $t->tx->res->json; -my $enabled = $hw1->{set_params}->{enable_reduced_scoring}; +my $enabled = $hw1->{set_dates}->{enable_reduced_scoring}; ok($enabled, 'testing that enabled_reduced_scoring compares to 1.'); -is($enabled, Mojo::JSON::true, 'testing that enabled_reduced_scoring compares to Mojo::JSON::true'); +is($enabled, true, 'testing that enabled_reduced_scoring compares to Mojo::JSON::true'); ok(JSON::PP::is_bool($enabled), 'testing that enabled_reduced_scoring is a Mojo::JSON::true or Mojo::JSON::false'); ok(JSON::PP::is_bool($enabled) && $enabled, 'testing that enabled_reduced_scoring is a Mojo::JSON::true'); # Check that updating a boolean parameter is working: $t->put_ok( "/webwork3/api/courses/2/sets/$new_set_id" => json => { - set_params => { hide_hint => Mojo::JSON::false } + set_params => { hide_hint => false } } )->content_type_is('application/json;charset=UTF-8'); my $hw2 = $t->tx->res->json; my $hide_hint = $hw2->{set_params}->{hide_hint}; ok(!$hide_hint, 'testing that hide_hint is falsy.'); -is($hide_hint, Mojo::JSON::false, 'testing that hide_hint compares to Mojo::JSON::false'); +is($hide_hint, false, 'testing that hide_hint compares to Mojo::JSON::false'); ok(JSON::PP::is_bool($hide_hint), 'testing that hide_hint is a Mojo::JSON::true or Mojo::JSON::false'); ok(JSON::PP::is_bool($hide_hint) && !$hide_hint, 'testing that hide_hint is a Mojo::JSON::false'); diff --git a/t/mojolicious/006_quizzes.t b/t/mojolicious/006_quizzes.t index 0da8af94..ac25401c 100644 --- a/t/mojolicious/006_quizzes.t +++ b/t/mojolicious/006_quizzes.t @@ -15,11 +15,12 @@ BEGIN { } use lib "$main::ww3_dir/lib"; +use lib "$main::ww3_dir/t/lib"; use DB::Schema; use Clone qw/clone/; use YAML::XS qw/LoadFile/; -use DB::TestUtils qw/loadCSV/; +use TestUtils qw/loadCSV/; use List::MoreUtils qw/firstval/; # Test the api with common 'courses/sets' routes for quizzes. @@ -67,9 +68,9 @@ $t->get_ok("/webwork3/api/courses/2/sets/$quiz1->{set_id}")->content_type_is('ap $quiz1 = $t->tx->res->json; my $timed = $quiz1->{set_params}->{timed}; ok($timed, 'testing that timed compares to 1.'); -is($timed, true, 'testing that timed compares to Mojo::JSON::true'); -ok(JSON::PP::is_bool($timed), 'testing that timed is a Mojo::JSON::true or Mojo::JSON::false'); -ok(JSON::PP::is_bool($timed) && $timed, 'testing that timed is a Mojo::JSON::true'); +is($timed, true, 'testing that timed compares to true'); +ok(JSON::PP::is_bool($timed), 'testing that timed is a true or false'); +ok(JSON::PP::is_bool($timed) && $timed, 'testing that timed is a true'); # Make a new quiz @@ -96,9 +97,9 @@ my $returned_quiz = $t->tx->res->json; my $new_quiz = $t->tx->res->json; my $problem_randorder = $new_quiz->{set_params}->{problem_randorder}; ok($problem_randorder, 'testing that problem_randorder compares to 1.'); -is($problem_randorder, true, 'testing that problem_randorder compares to Mojo::JSON::true'); -ok(JSON::PP::is_bool($problem_randorder), 'testing that problem_randorder is a Mojo::JSON::true or Mojo::JSON::false'); -ok(JSON::PP::is_bool($problem_randorder) && $problem_randorder, 'testing that problem_randorder is a Mojo::JSON::true'); +is($problem_randorder, true, 'testing that problem_randorder compares to true'); +ok(JSON::PP::is_bool($problem_randorder), 'testing that problem_randorder is a true or false'); +ok(JSON::PP::is_bool($problem_randorder) && $problem_randorder, 'testing that problem_randorder is a true'); # Check that updating a boolean parameter is working: @@ -114,12 +115,9 @@ my $updated_quiz = $t->tx->res->json; $problem_randorder = $updated_quiz->{set_params}->{problem_randorder}; ok(!$problem_randorder, 'testing that hide_hint is falsy.'); -is($problem_randorder, false, 'testing that problem_randorder compares to Mojo::JSON::false'); -ok(JSON::PP::is_bool($problem_randorder), 'testing that problem_randorder is a Mojo::JSON::true or Mojo::JSON::false'); -ok( - JSON::PP::is_bool($problem_randorder) && !$problem_randorder, - 'testing that problem_randorder is a Mojo::JSON::false' -); +is($problem_randorder, false, 'testing that problem_randorder compares to false'); +ok(JSON::PP::is_bool($problem_randorder), 'testing that problem_randorder is a true or false'); +ok(JSON::PP::is_bool($problem_randorder) && !$problem_randorder, 'testing that problem_randorder is a false'); # delete the added quiz $t->delete_ok("/webwork3/api/courses/2/sets/$returned_quiz->{set_id}") diff --git a/t/mojolicious/007_review_sets.t b/t/mojolicious/007_review_sets.t index 24b44a61..e890c06d 100644 --- a/t/mojolicious/007_review_sets.t +++ b/t/mojolicious/007_review_sets.t @@ -13,13 +13,14 @@ BEGIN { } use lib "$main::ww3_dir/lib"; +use lib "$main::ww3_dir/t/lib"; use DB::Schema; use Clone qw/clone/; use YAML::XS qw/LoadFile/; use DateTime::Format::Strptime; use List::MoreUtils qw/firstval/; -use DB::TestUtils qw/loadCSV/; +use TestUtils qw/loadCSV/; my $strp = DateTime::Format::Strptime->new(pattern => '%FT%T', on_error => 'croak'); # Test the api with common "users" routes. @@ -86,7 +87,6 @@ my $new_review_set_params = { $t->post_ok('/webwork3/api/courses/2/sets' => json => $new_review_set_params) ->content_type_is('application/json;charset=UTF-8')->json_is('/set_name' => 'Review #20') ->json_is('/set_type' => 'REVIEW'); -my $returned_quiz = $t->tx->res->json; $review_set1 = $t->tx->res->json; $can_retake = $review_set1->{set_params}->{can_retake}; @@ -98,7 +98,7 @@ ok(JSON::PP::is_bool($can_retake) && $can_retake, 'testing that can_retake is a # Check that updating a boolean parameter is working: $t->put_ok( - "/webwork3/api/courses/2/sets/$returned_quiz->{set_id}" => json => { + "/webwork3/api/courses/2/sets/$review_set1->{set_id}" => json => { set_params => { can_retake => false } @@ -113,8 +113,7 @@ ok(JSON::PP::is_bool($can_retake), 'testing that can_retake is a Mojo::JSON::tru ok(JSON::PP::is_bool($can_retake) && !$can_retake, 'testing that can_retake is a Mojo::JSON::false'); # delete the added review set -$t->delete_ok("/webwork3/api/courses/2/sets/$returned_quiz->{set_id}") - ->content_type_is('application/json;charset=UTF-8')->json_is('/set_type' => 'REVIEW') - ->json_is('/set_name' => 'Review #20'); +$t->delete_ok("/webwork3/api/courses/2/sets/$review_set1->{set_id}")->content_type_is('application/json;charset=UTF-8') + ->json_is('/set_type' => 'REVIEW')->json_is('/set_name' => 'Review #20'); done_testing(); diff --git a/tests/stores/courses.spec.ts b/tests/stores/courses.spec.ts index eb57bb35..13b38dd6 100644 --- a/tests/stores/courses.spec.ts +++ b/tests/stores/courses.spec.ts @@ -32,7 +32,7 @@ describe('Test the course store', () => { const parsed_courses = await loadCSV('t/db/sample_data/courses.csv', { boolean_fields: ['visible'], - non_neg_fields: ['course_id'], + non_neg_int_fields: ['course_id'], params: ['course_dates', 'course_params'] }); courses_from_csv = parsed_courses.map(course => { @@ -82,4 +82,32 @@ describe('Test the course store', () => { expect(cleanIDs(deleted_course)).toStrictEqual(cleanIDs(updated_course)); }); }); + + describe('Test that invalid courses are handled correctly', () => { + test('Try to add an invalid course', async () => { + const course_store = useCourseStore(); + + // This course is invalid because the name is the empty string. + const course = new Course({ + course_name: '' + }); + expect(course.isValid()).toBe(false); + await expect(async () => { await course_store.addCourse(course); }) + .rejects.toThrow('The added course is invalid'); + }); + + test('Try to update an invalid course', async () => { + const course_store = useCourseStore(); + const precalc = course_store.courses.find(c => c.course_name === 'Precalculus'); + if (precalc) { + precalc.course_dates.start = 200; + precalc.course_dates.end = 100; + expect(precalc.isValid()).toBe(false); + await expect(async () => { await course_store.updateCourse(precalc as Course); }) + .rejects.toThrow('The updated course is invalid'); + } else { + throw 'This should not have be thrown. Course precalc is not defined.'; + } + }); + }); }); diff --git a/tests/stores/problem_sets.spec.ts b/tests/stores/problem_sets.spec.ts index 20d7f310..d10fa311 100644 --- a/tests/stores/problem_sets.spec.ts +++ b/tests/stores/problem_sets.spec.ts @@ -21,7 +21,6 @@ import { HomeworkSet, ProblemSet, Quiz, ReviewSet } from 'src/common/models/prob import { Course } from 'src/common/models/courses'; import { cleanIDs, loadCSV } from '../utils'; -import { parseBoolean, parseNonNegInt } from 'src/common/models/parsers'; const app = createApp({}); @@ -39,33 +38,23 @@ describe('Problem Set store tests', () => { const problem_set_config = { params: ['set_params', 'set_dates' ], - boolean_fields: ['set_visible'] + boolean_fields: ['set_visible'], + param_boolean_fields: ['timed', 'enable_reduced_scoring', 'can_retake'], + param_non_neg_int_fields: ['quiz_duration'] }; const hw_sets_to_parse = await loadCSV('t/db/sample_data/hw_sets.csv', problem_set_config); - // Do some parsing cleanup. const hw_sets_from_csv = hw_sets_to_parse.filter(set => set.course_name === 'Precalculus') .map(set => new HomeworkSet(set)); - hw_sets_from_csv.forEach(set => { - set.set_params.enable_reduced_scoring = parseBoolean(set.set_params.enable_reduced_scoring); - }); const quizzes_to_parse = await loadCSV('t/db/sample_data/quizzes.csv', problem_set_config); const quizzes_from_csv = quizzes_to_parse.filter(set => set.course_name === 'Precalculus') .map(q => new Quiz(q)); - // Do some parsing cleanup. - quizzes_from_csv.forEach(q => { - q.set_params.timed = parseBoolean(q.set_params.timed); - q.set_params.quiz_duration = parseNonNegInt(q.set_params.quiz_duration); - }); const review_sets_to_parse = await loadCSV('t/db/sample_data/review_sets.csv', problem_set_config); const review_sets_from_csv = review_sets_to_parse.filter(set => set.course_name === 'Precalculus') .map(set => new ReviewSet(set)); - // Do some parsing cleanup. - review_sets_from_csv.forEach(set => { - set.set_params.can_retake = parseBoolean(set.set_params.can_retake); - }); + // combine quizzes, review sets and homework sets problem_sets_from_csv = [...hw_sets_from_csv, ...quizzes_from_csv, ...review_sets_from_csv]; @@ -99,14 +88,12 @@ describe('Problem Set store tests', () => { set_name: 'HW #9', course_id: precalc_course.course_id, set_visible: true, - set_params: { - enable_reduced_scoring: true - }, set_dates: { open: 1000, reduced_scoring: 1500, due: 2000, - answer: 3000 + answer: 3000, + enable_reduced_scoring: true } }; const new_set = new HomeworkSet(new_set_info); @@ -120,11 +107,11 @@ describe('Problem Set store tests', () => { test('Update a Homework Set', async () => { const problem_set_store = useProblemSetStore(); const problem_set = (problem_set_store.findProblemSet({ set_name: 'HW #9' }) as HomeworkSet).clone(); - problem_set.set_params.enable_reduced_scoring = false; + problem_set.set_dates.enable_reduced_scoring = false; await problem_set_store.updateSet(problem_set); const set_to_update = (problem_set_store.problem_sets .find(set => set.set_id === problem_set.set_id) as HomeworkSet).clone(); - expect(set_to_update?.set_params.enable_reduced_scoring).toBeFalsy(); + expect(set_to_update?.set_dates.enable_reduced_scoring).toBeFalsy(); // update the answer date set_to_update.set_dates.answer = (set_to_update.set_dates.answer ?? 0) + 100; @@ -143,6 +130,33 @@ describe('Problem Set store tests', () => { }); }); + describe('Test that invalid homework sets are handled correctly', () => { + test('Try to add an invalid homework set', async () => { + const problem_set_store = useProblemSetStore(); + + // A Homework set needs a not-empty set name. + const hw = new HomeworkSet({ + set_name: '' + }); + expect(hw.isValid()).toBe(false); + await expect(async () => { await problem_set_store.addProblemSet(hw); }) + .rejects.toThrow('The added problem set is invalid'); + }); + + test('Try to update an invalid global user', async () => { + const problem_set_store = useProblemSetStore(); + const hw1 = problem_set_store.findProblemSet({ set_name: 'HW #1' }); + if (hw1) { + hw1.set_id = 1.34; + expect(hw1.isValid()).toBe(false); + await expect(async () => { await problem_set_store.updateSet(hw1 as HomeworkSet); }) + .rejects.toThrow('The updated problem set is invalid'); + } else { + throw 'This should not have be thrown. Problem set hw1 is not defined.'; + } + }); + }); + describe('CRUD test for quizzes', () => { test('Add a new Quiz', async () => { const problem_set_store = useProblemSetStore(); diff --git a/tests/stores/session.spec.ts b/tests/stores/session.spec.ts index 9b06fe37..8c2c3ffc 100644 --- a/tests/stores/session.spec.ts +++ b/tests/stores/session.spec.ts @@ -11,8 +11,8 @@ import { createApp } from 'vue'; import { setActivePinia, createPinia } from 'pinia'; import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; +import { api } from 'boot/axios'; -import { api } from 'src/boot/axios'; import { getUser } from 'src/common/api-requests/user'; import { useSessionStore } from 'src/stores/session'; import { checkPassword } from 'src/common/api-requests/session'; @@ -60,7 +60,7 @@ describe('Session Store', () => { // Load the user course information for testing later. const parsed_courses = await loadCSV('t/db/sample_data/courses.csv', { boolean_fields: ['visible'], - non_neg_fields: ['course_id'], + non_neg_int_fields: ['course_id'], params: ['course_dates', 'course_params'] }); @@ -71,7 +71,7 @@ describe('Session Store', () => { const users_to_parse = await loadCSV('t/db/sample_data/students.csv', { boolean_fields: ['is_admin'], - non_neg_fields: ['user_id'] + non_neg_int_fields: ['user_id'] }); // Fetch the user lisa. This is used below. diff --git a/tests/stores/set_problems.spec.ts b/tests/stores/set_problems.spec.ts index 9b6892de..246d0311 100644 --- a/tests/stores/set_problems.spec.ts +++ b/tests/stores/set_problems.spec.ts @@ -26,7 +26,6 @@ import { UserProblem, ParseableSetProblem, parseProblem, SetProblem, SetProblemP import { DBUserHomeworkSet, mergeUserSet, UserSet } from 'src/common/models/user_sets'; import { Dictionary, generic } from 'src/common/models'; -import { parseBoolean, parseNonNegInt } from 'src/common/models/parsers'; import { loadCSV, cleanIDs } from '../utils'; const app = createApp({}); @@ -49,40 +48,32 @@ describe('Problem Set store tests', () => { const problem_set_config = { params: ['set_params', 'set_dates' ], - boolean_fields: ['set_visible'] + boolean_fields: ['set_visible'], + param_boolean_fields: ['timed', 'enable_reduced_scoring', 'can_retake'], + param_non_neg_int_fields: ['quiz_duration'] }; const hw_sets_to_parse = await loadCSV('t/db/sample_data/hw_sets.csv', problem_set_config); - // Do some parsing cleanup. const hw_sets_from_csv = hw_sets_to_parse.filter(set => set.course_name === 'Precalculus') .map(set => new HomeworkSet(set)); - hw_sets_from_csv.forEach(set => { - set.set_params.enable_reduced_scoring = parseBoolean(set.set_params.enable_reduced_scoring); - }); const quizzes_to_parse = await loadCSV('t/db/sample_data/quizzes.csv', problem_set_config); const quizzes_from_csv = quizzes_to_parse.filter(set => set.course_name === 'Precalculus') .map(q => new Quiz(q)); - // Do some parsing cleanup. - quizzes_from_csv.forEach(q => { - q.set_params.timed = parseBoolean(q.set_params.timed); - q.set_params.quiz_duration = parseNonNegInt(q.set_params.quiz_duration); - }); const review_sets_to_parse = await loadCSV('t/db/sample_data/review_sets.csv', problem_set_config); const review_sets_from_csv = review_sets_to_parse.filter(set => set.course_name === 'Precalculus') .map(set => new ReviewSet(set)); - // Do some parsing cleanup. - review_sets_from_csv.forEach(set => { - set.set_params.can_retake = parseBoolean(set.set_params.can_retake); - }); + // combine quizzes, review sets and homework sets const problem_sets_from_csv = [...hw_sets_from_csv, ...quizzes_from_csv, ...review_sets_from_csv]; // Load all problems from CSV files const problems_to_parse = await loadCSV('t/db/sample_data/problems.csv', { params: ['problem_params'], - non_neg_fields: ['problem_number'] + non_neg_int_fields: ['problem_number'], + param_non_neg_float_fields: ['weight'], + param_non_neg_int_fields: ['library_id', 'problem_pool_id'] }); // Filter only Precalc problems and remove any undefined library_ids @@ -92,14 +83,15 @@ describe('Problem Set store tests', () => { // Load the User Problem information from the csv file: const user_problems_from_csv = await loadCSV('t/db/sample_data/user_problems.csv', { params: [], - non_neg_fields: ['problem_number', 'seed'] + non_neg_int_fields: ['problem_number', 'seed'], + non_neg_float_fields: ['status'] }); // load all user problems in Precalc from CSV files and merge them with set problems. // Load the users from the csv file. const users_to_parse = await loadCSV('t/db/sample_data/students.csv', { boolean_fields: ['is_admin'], - non_neg_fields: ['user_id'] + non_neg_int_fields: ['user_id'] }); precalc_merged_problems = user_problems_from_csv @@ -168,6 +160,7 @@ describe('Problem Set store tests', () => { }) }); const library_problem = new LibraryProblem({ location_params: { file_path: 'path/to/the/problem.pg' } }); + expect(library_problem.isValid()).toBe(true); added_set_problem = await set_problem_store.addSetProblem(library_problem, hw1?.set_id ?? 0); expect(cleanIDs(added_set_problem)).toStrictEqual(cleanIDs(new_set_problem)); @@ -192,16 +185,50 @@ describe('Problem Set store tests', () => { }); }); + describe('Test that invalid set problems are handled correctly', () => { + test('Try to add an invalid set problem', async () => { + const problem_set_store = useProblemSetStore(); + const set_problem_store = useSetProblemStore(); + + const hw1 = problem_set_store.findProblemSet({ set_name: 'HW #1' }); + + // This user has an invalid username + const library_problem = new LibraryProblem({ + }); + expect(library_problem.isValid()).toBe(false); + await expect(async () => { await set_problem_store.addSetProblem(library_problem, hw1?.set_id ?? 0); }) + .rejects.toThrow('The added problem is invalid'); + }); + + test('Try to update an invalid set problem', async () => { + const set_problem_store = useSetProblemStore(); + + const prob1 = set_problem_store.findSetProblems({ set_name: 'HW #1' })[0].clone(); + + if (prob1) { + prob1.problem_number = -8; + expect(prob1.isValid()).toBe(false); + await expect(async () => { await set_problem_store.updateSetProblem(prob1); }) + .rejects.toThrow('The updated set problem is invalid'); + } else { + throw 'This should not have be thrown. The set problem is not defined.'; + } + }); + }); + describe('Fetch user problems', () => { test('Fetch all User Problems for a set in a course', async () => { const user_store = useUserStore(); await user_store.fetchGlobalCourseUsers(precalc_course.course_id); await user_store.fetchCourseUsers(precalc_course.course_id); + const problem_set_store = useProblemSetStore(); await problem_set_store.fetchProblemSets(precalc_course.course_id); const hw1 = problem_set_store.findProblemSet({ set_name: 'HW #1' }) ?? new ProblemSet(); + const set_problems_store = useSetProblemStore(); await set_problems_store.fetchUserProblems(hw1.set_id); + const hw1_user_problems = set_problems_store.findUserProblems({ set_name: 'HW #1' }); expect(cleanIDs(precalc_hw1_user_problems)).toStrictEqual(cleanIDs(hw1_user_problems)); }); diff --git a/tests/stores/user_sets.spec.ts b/tests/stores/user_sets.spec.ts index 0a7806c3..deb4d81e 100644 --- a/tests/stores/user_sets.spec.ts +++ b/tests/stores/user_sets.spec.ts @@ -27,7 +27,6 @@ import { UserSet, UserHomeworkSet, UserQuiz, UserReviewSet, mergeUserSet, parseDBUserSet, DBUserHomeworkSet } from 'src/common/models/user_sets'; -import { parseBoolean, parseNonNegInt } from 'src/common/models/parsers'; import { loadCSV, cleanIDs } from '../utils'; const app = createApp({}); @@ -48,33 +47,23 @@ describe('Tests user sets and merged user sets in the problem set store', () => const problem_set_config = { params: ['set_params', 'set_dates' ], - boolean_fields: ['set_visible'] + boolean_fields: ['set_visible'], + param_boolean_fields: ['timed', 'enable_reduced_scoring', 'can_retake'], + param_non_neg_int_fields: ['quiz_duration'] }; const hw_sets_to_parse = await loadCSV('t/db/sample_data/hw_sets.csv', problem_set_config); - // Do some parsing cleanup. const hw_sets_from_csv = hw_sets_to_parse.filter(set => set.course_name === 'Precalculus') .map(set => new HomeworkSet(set)); - hw_sets_from_csv.forEach(set => { - set.set_params.enable_reduced_scoring = parseBoolean(set.set_params.enable_reduced_scoring); - }); const quizzes_to_parse = await loadCSV('t/db/sample_data/quizzes.csv', problem_set_config); const quizzes_from_csv = quizzes_to_parse.filter(set => set.course_name === 'Precalculus') .map(q => new Quiz(q)); - // Do some parsing cleanup. - quizzes_from_csv.forEach(q => { - q.set_params.timed = parseBoolean(q.set_params.timed); - q.set_params.quiz_duration = parseNonNegInt(q.set_params.quiz_duration); - }); const review_sets_to_parse = await loadCSV('t/db/sample_data/review_sets.csv', problem_set_config); const review_sets_from_csv = review_sets_to_parse.filter(set => set.course_name === 'Precalculus') .map(set => new ReviewSet(set)); - // Do some parsing cleanup. - review_sets_from_csv.forEach(set => { - set.set_params.can_retake = parseBoolean(set.set_params.can_retake); - }); + // combine quizzes, review sets and homework sets problem_sets_from_csv = [...hw_sets_from_csv, ...quizzes_from_csv, ...review_sets_from_csv]; @@ -106,13 +95,16 @@ describe('Tests user sets and merged user sets in the problem set store', () => // Setup and load the user sets data from a csv file. const user_sets_to_parse = await loadCSV('t/db/sample_data/user_sets.csv', { - params: ['set_dates', 'set_params'] + params: ['set_dates', 'set_params'], + boolean_fields: ['set_visible'], + param_boolean_fields: ['timed', 'enable_reduced_scoring', 'can_retake'], + param_non_neg_int_fields: ['quiz_duration'] }); // Load the users from the CSV file and filter only the Precalc students. const users_to_parse = await loadCSV('t/db/sample_data/students.csv', { boolean_fields: ['is_admin'], - non_neg_fields: ['user_id'] + non_neg_int_fields: ['user_id'] }); const precalc_merged_users = users_to_parse.filter(user => user.course_name === 'Precalculus') @@ -151,17 +143,20 @@ describe('Tests user sets and merged user sets in the problem set store', () => const problem_set_store = useProblemSetStore(); new_hw_set = await problem_set_store.addProblemSet(new HomeworkSet({ set_name: 'HW #9', - course_id: precalc_course.course_id + course_id: precalc_course.course_id, })); const user_store = useUserStore(); const user = user_store.course_users[0]; const user_set = new UserHomeworkSet({ + username: 'homer', + set_name: 'HW #9', set_id: new_hw_set.set_id, course_user_id: user.course_user_id, set_visible: true }); + expect(user_set.isValid()).toBe(true); added_user_set = await problem_set_store.addUserSet(user_set) ?? new UserSet(); expect(cleanIDs(added_user_set)).toStrictEqual(cleanIDs(user_set)); }); @@ -188,6 +183,33 @@ describe('Tests user sets and merged user sets in the problem set store', () => }); }); + describe('Test that invalid user sets are handled correctly', () => { + test('Try to add an invalid user sets', async () => { + const problem_set_store = useProblemSetStore(); + + // A Homework set needs a not-empty set name. + const hw = new UserHomeworkSet({ + set_name: '' + }); + expect(hw.isValid()).toBe(false); + await expect(async () => { await problem_set_store.addUserSet(hw); }) + .rejects.toThrow('The added user set is invalid'); + }); + + test('Try to update an invalid user set', async () => { + const problem_set_store = useProblemSetStore(); + const hw1 = problem_set_store.findUserSet({ username: 'homer', set_name: 'HW #1' }); + if (hw1) { + hw1.set_id = 1.34; + expect(hw1.isValid()).toBe(false); + await expect(async () => { await problem_set_store.updateUserSet(hw1 as UserHomeworkSet); }) + .rejects.toThrow('The updated user set is invalid'); + } else { + throw 'This should not have be thrown. User set hw1 is not defined.'; + } + }); + }); + let new_quiz: ProblemSet; describe('CRUD functions on User Quiz', () => { let added_user_set: UserSet; @@ -206,7 +228,9 @@ describe('Tests user sets and merged user sets in the problem set store', () => const user_set = new UserQuiz({ set_id: new_quiz.set_id, course_user_id: user.course_user_id, - set_visible: true + set_visible: true, + set_name: new_quiz.set_name, + username: 'homer' }); added_user_set = await problem_set_store.addUserSet(user_set) ?? new UserSet(); expect(cleanIDs(added_user_set)).toStrictEqual(cleanIDs(user_set)); @@ -252,7 +276,9 @@ describe('Tests user sets and merged user sets in the problem set store', () => const user_set = new UserReviewSet({ set_id: new_review_set.set_id, course_user_id: user.course_user_id, - set_visible: true + set_visible: true, + set_name: new_review_set.set_name, + username: 'homer' }); added_user_review_set = await problem_set_store.addUserSet(user_set) ?? new UserSet(); expect(cleanIDs(added_user_review_set)).toStrictEqual(cleanIDs(user_set)); @@ -292,7 +318,7 @@ describe('Tests user sets and merged user sets in the problem set store', () => }); }); - let new_user_hw: UserSet; + let new_user_hw: UserHomeworkSet; describe('Test merged user sets', () => { test('Test all merged user sets', () => { const problem_set_store = useProblemSetStore(); @@ -312,51 +338,48 @@ describe('Tests user sets and merged user sets in the problem set store', () => open: 1000, reduced_scoring: 1100, due: 1500, - answer: 2000 - }, - set_params: { + answer: 2000, enable_reduced_scoring: true } })); // Add a User set new_user_hw = new UserHomeworkSet({ + username: 'homer', + set_name: new_hw_set.set_name, set_id: new_hw_set.set_id, course_user_id: user_to_assign.course_user_id, + set_visible: false, set_dates: { open: 1200, reduced_scoring: 1500, due: 2000, - answer: 2200 - }, - set_params: { + answer: 2200, enable_reduced_scoring: true } }); - const new_user_set = await problem_set_store.addUserSet(new_user_hw) ?? new UserSet(); const merged_hw = new UserHomeworkSet({ user_id: user_to_assign.user_id, - user_set_id: new_user_hw.user_set_id, - set_id: new_hw_set.set_id, + user_set_id: new_user_set.user_set_id, + set_id: new_user_set.set_id, course_user_id: user_to_assign.course_user_id, - set_version: new_user_hw.set_version, + set_version: new_user_set.set_version, set_visible: new_user_hw.set_visible, set_name: new_hw_set.set_name, username: user_to_assign.username, - set_type: new_hw_set.set_type, + set_type: new_user_set.set_type, set_params: new_user_hw.set_params.toObject(), set_dates: new_user_hw.set_dates.toObject() }); - expect(cleanIDs(new_user_hw)).toStrictEqual(cleanIDs(new_user_set)); + expect(new_user_hw).toStrictEqual(new_user_set); const merged_user_set = mergeUserSet(new_hw_set, new DBUserHomeworkSet(new_user_set.toObject()), user_to_assign); // Copy over the user_set_id from the database object to one in merged_hw. - // Not sure why directly setting the user_set_id results in a type error. - merged_hw.set({ user_set_id: merged_user_set?.user_set_id }); + merged_hw.user_set_id = merged_user_set?.user_set_id ?? 0; expect(merged_user_set).toStrictEqual(merged_hw); }); }); diff --git a/tests/stores/users.spec.ts b/tests/stores/users.spec.ts index a56a03d3..2105d561 100644 --- a/tests/stores/users.spec.ts +++ b/tests/stores/users.spec.ts @@ -13,11 +13,12 @@ import { createPinia, setActivePinia } from 'pinia'; import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; import { api } from 'boot/axios'; -import { cleanIDs, loadCSV } from '../utils'; import { useCourseStore } from 'src/stores/courses'; +import { useUserStore } from 'src/stores/users'; +import { cleanIDs, loadCSV } from '../utils'; + import { Course } from 'src/common/models/courses'; import { CourseUser, User } from 'src/common/models/users'; -import { useUserStore } from 'src/stores/users'; const app = createApp({}); @@ -37,7 +38,7 @@ describe('User store tests', () => { const users_to_parse = await loadCSV('t/db/sample_data/students.csv', { boolean_fields: ['is_admin'], - non_neg_fields: ['user_id'] + non_neg_int_fields: ['user_id'] }); // Do some parsing and cleanup. @@ -96,6 +97,35 @@ describe('User store tests', () => { }); }); + describe('Test that invalid global users are handled correctly', () => { + test('Try to add an invalid global user', async () => { + const user_store = useUserStore(); + + // This user has an invalid username + const user = new User({ + username: 'bad guy', + first_name: 'Bad', + last_name: 'Guy' + }); + expect(user.isValid()).toBe(false); + await expect(async () => { await user_store.addUser(user); }) + .rejects.toThrow('The added user is invalid'); + }); + + test('Try to update an invalid global user', async () => { + const user_store = useUserStore(); + const homer = user_store.getUserByName('homer'); + if (homer) { + homer.email = 'not@an@email.address'; + expect(homer.isValid()).toBe(false); + await expect(async () => { await user_store.updateUser(homer as User); }) + .rejects.toThrow('The updated user is invalid'); + } else { + throw 'This should not have be thrown. User homer is not defined.'; + } + }); + }); + describe('Fetch course users', () => { test('Fetch all users of a course', async () => { const user_store = useUserStore(); @@ -127,7 +157,7 @@ describe('User store tests', () => { const course_user_to_update = user_store.course_users .find(user => user.user_id === user_not_in_precalc.user_id)?.clone(); if (course_user_to_update) { - course_user_to_update.recitation = 3; + course_user_to_update.recitation = '3'; const updated_user = await user_store.updateCourseUser(course_user_to_update); expect(updated_user).toStrictEqual(course_user_to_update); } @@ -142,6 +172,35 @@ describe('User store tests', () => { }); }); + describe('Test that invalid course users are handled correctly', () => { + test('Try to add an invalid course user', async () => { + const user_store = useUserStore(); + + // This user has an invalid username + const user = new CourseUser({ + username: 'bad guy', + first_name: 'Bad', + last_name: 'Guy' + }); + expect(user.isValid()).toBe(false); + await expect(async () => { await user_store.addCourseUser(user); }) + .rejects.toThrow('The added course user is invalid'); + }); + + test('Try to update an invalid course user', async () => { + const user_store = useUserStore(); + const homer = user_store.course_users.find(user => user.username === 'homer'); + if (homer) { + homer.email = 'not@an@email.address'; + expect(homer.isValid()).toBe(false); + await expect(async () => { await user_store.updateCourseUser(homer); }) + .rejects.toThrow('The updated course user is invalid'); + } else { + throw 'This should not have be thrown. User homer is not defined.'; + } + }); + }); + describe('Check global and course users', () => { test('Fetch global users for a course', async () => { diff --git a/tests/unit-tests/course_users.spec.ts b/tests/unit-tests/course_users.spec.ts index 30b95d65..4c6a481f 100644 --- a/tests/unit-tests/course_users.spec.ts +++ b/tests/unit-tests/course_users.spec.ts @@ -6,119 +6,121 @@ // tests parsing and handling of merged users -import { EmailParseException, NonNegIntException, UsernameParseException, - UserRoleException } from 'src/common/models/parsers'; -import { CourseUser, DBCourseUser, ParseableDBCourseUser } from 'src/common/models/users'; +import { UserRole } from 'src/common/models/parsers'; +import { CourseUser, DBCourseUser } from 'src/common/models/users'; describe('Testing Database and client-side Course Users', () => { - - const default_db_course_user: ParseableDBCourseUser = { - course_user_id: 0, - user_id: 0, - course_id: 0, - role: 'UNKNOWN' - }; - - describe('Testing Database course users', () => { - - test('Create a new database course user', () => { + describe('Creating a DBCourseUser', () => { + const default_db_course_user = { + course_user_id: 0, + course_id: 0, + user_id: 0, + role: UserRole.unknown + }; + test('Checking the creation of a dBCourseUser', () => { const db_course_user = new DBCourseUser(); expect(db_course_user).toBeInstanceOf(DBCourseUser); expect(db_course_user.toObject()).toStrictEqual(default_db_course_user); + expect(db_course_user.isValid()).toBe(true); }); test('Check that calling all_fields() and params() is correct', () => { - const course_user_fields = ['course_user_id', 'course_id', 'user_id', 'role', + const db_course_user_fields = ['course_user_id', 'course_id', 'user_id', 'role', 'section', 'recitation']; - const course_user = new DBCourseUser(); - - expect(course_user.all_field_names.sort()).toStrictEqual(course_user_fields.sort()); - expect(course_user.param_fields.sort()).toStrictEqual([]); - - expect(DBCourseUser.ALL_FIELDS.sort()).toStrictEqual(course_user_fields.sort()); + const course = new DBCourseUser(); + expect(course.all_field_names.sort()).toStrictEqual(db_course_user_fields.sort()); + expect(course.param_fields.sort()).toStrictEqual([]); + expect(DBCourseUser.ALL_FIELDS.sort()).toStrictEqual(db_course_user_fields.sort()); }); - test('Check that cloning a DBCourseUser works', () => { - const course_user = new DBCourseUser({ role: 'student' }); - const course_user2: ParseableDBCourseUser = { ...default_db_course_user }; - course_user2.role = 'STUDENT'; - expect(course_user.clone().toObject()).toStrictEqual(course_user2); - expect(course_user.clone() instanceof DBCourseUser).toBe(true); + test('Check that cloning works', () => { + const db_course_user = new DBCourseUser(); + expect(db_course_user.clone().toObject()).toStrictEqual(default_db_course_user); + expect(db_course_user.clone()).toBeInstanceOf(DBCourseUser); }); + }); - test('create DBCourseUser with invalid role', () => { - expect(() => { - new DBCourseUser({ role: 'superhero' }); - }).toThrow(UserRoleException); - }); + describe('Updating a DBCourseUser', () => { + test('Update DBCourseUser directly', () => { + const db_course_user = new DBCourseUser(); + expect(db_course_user.isValid()).toBe(true); - test('set fields of DBCourseUser', () => { - const course_user = new DBCourseUser(); + db_course_user.course_user_id = 10; + expect(db_course_user.course_user_id).toBe(10); - course_user.role = 'student'; - expect(course_user.role).toBe('STUDENT'); + db_course_user.user_id = 20; + expect(db_course_user.user_id).toBe(20); - course_user.course_id = 34; - expect(course_user.course_id).toBe(34); + db_course_user.course_id = 5; + expect(db_course_user.course_id).toBe(5); - course_user.user_id = 3; - expect(course_user.user_id).toBe(3); + db_course_user.role = UserRole.admin; + expect(db_course_user.role).toBe(UserRole.admin); - course_user.user_id = '5'; - expect(course_user.user_id).toBe(5); + db_course_user.role = 'student'; + expect(db_course_user.role).toBe(UserRole.student); + }); - course_user.section = 2; - expect(course_user.section).toBe('2'); + test('Update DBCourseUser using the set method', () => { + const db_course_user = new DBCourseUser(); + expect(db_course_user.isValid()).toBe(true); - course_user.section = '12'; - expect(course_user.section).toBe('12'); + db_course_user.set({ course_user_id: 10 }); + expect(db_course_user.course_user_id).toBe(10); - }); + db_course_user.set({ user_id: 20 }); + expect(db_course_user.user_id).toBe(20); - test('set fields of DBCourseUser using set()', () => { - const course_user = new DBCourseUser(); - course_user.set({ role: 'student' }); - expect(course_user.role).toBe('STUDENT'); + db_course_user.set({ course_id: 5 }); + expect(db_course_user.course_id).toBe(5); - course_user.set({ course_id: 34 }); - expect(course_user.course_id).toBe(34); + db_course_user.set({ role: UserRole.admin }); + expect(db_course_user.role).toBe(UserRole.admin); - course_user.set({ user_id: 3 }); - expect(course_user.user_id).toBe(3); + db_course_user.set({ role: 'student' }); + expect(db_course_user.role).toBe(UserRole.student); + }); + }); - course_user.set({ user_id: '5' }); - expect(course_user.user_id).toBe(5); + describe('Checking for valid and invalid DBCourseUsers', () => { + test('Checking for invalid course_user_id', () => { + const db_course_user = new DBCourseUser(); + expect(db_course_user.isValid()).toBe(true); - course_user.set({ section: 2 }); - expect(course_user.section).toBe('2'); + db_course_user.course_user_id = -3; + expect(db_course_user.isValid()).toBe(false); - course_user.set({ section: '12' }); - expect(course_user.section).toBe('12'); + db_course_user.course_user_id = 3.14; + expect(db_course_user.isValid()).toBe(false); - }); + db_course_user.course_user_id = 7; + expect(db_course_user.isValid()).toBe(true); - test('set invalid role', () => { - const course_user = new DBCourseUser({ role: 'student' }); - expect(() => { course_user.role = 'superhero';}).toThrow(UserRoleException); }); - test('set invalid user_id', () => { - const course_user = new DBCourseUser(); - expect(() => { course_user.user_id = -1; }).toThrow(NonNegIntException); - expect(() => { course_user.user_id = '-1'; }).toThrow(NonNegIntException); - }); + test('Checking for invalid user_id', () => { + const db_course_user = new DBCourseUser(); + db_course_user.user_id = -5; + expect(db_course_user.isValid()).toBe(false); + + db_course_user.user_id = 1.34; + expect(db_course_user.isValid()).toBe(false); - test('set invalid course_id', () => { - const course_user = new DBCourseUser(); - expect(() => { course_user.course_id = -1;}).toThrow(NonNegIntException); - expect(() => { course_user.course_id = '-1'; }).toThrow(NonNegIntException); + db_course_user.user_id = 5; + expect(db_course_user.isValid()).toBe(true); }); - test('set invalid course_user_id', () => { - const course_user = new DBCourseUser(); - expect(() => { course_user.course_user_id = -1; }).toThrow(NonNegIntException); - expect(() => { course_user.course_user_id = '-1'; }).toThrow(NonNegIntException); + test('Checking for invalid course_id', () => { + const db_course_user = new DBCourseUser(); + db_course_user.course_id = -9; + expect(db_course_user.isValid()).toBe(false); + + db_course_user.course_id = 1.39; + expect(db_course_user.isValid()).toBe(false); + + db_course_user.course_id = 9; + expect(db_course_user.isValid()).toBe(true); }); }); @@ -128,15 +130,19 @@ describe('Testing Database and client-side Course Users', () => { course_user_id: 0, user_id: 0, course_id: 0, - is_admin: false + is_admin: false, + username: '', + email: '', + first_name: '', + last_name: '', + role: 'UNKNOWN', + student_id: '' }; test('Create a Valid CourseUser', () => { - const course_user1 = new CourseUser(); - - expect(course_user1).toBeInstanceOf(CourseUser); - expect(course_user1.toObject()).toStrictEqual(default_course_user); - + const course_user = new CourseUser(); + expect(course_user).toBeInstanceOf(CourseUser); + expect(course_user.toObject()).toStrictEqual(default_course_user); }); test('Check that calling all_fields() and params() is correct', () => { @@ -153,101 +159,178 @@ describe('Testing Database and client-side Course Users', () => { test('Check that cloning a merged user works', () => { const course_user = new CourseUser(); expect(course_user.clone().toObject()).toStrictEqual(default_course_user); - expect(course_user.clone() instanceof CourseUser).toBe(true); - }); - - test('Invalid user_id', () => { - expect(() => { - new CourseUser({ username: 'test', user_id: -1 }); - }).toThrow(NonNegIntException); - expect(() => { - new CourseUser({ username: 'test', user_id: '-1' }); - }).toThrow(NonNegIntException); - expect(() => { - new CourseUser({ username: 'test', user_id: 'one' }); - }).toThrow(NonNegIntException); - }); - - test('Invalid username', () => { - expect(() => { - new CourseUser({ username: '@test' }); - }).toThrow(UsernameParseException); - expect(() => { - new CourseUser({ username: '123test' }); - }).toThrow(UsernameParseException); - expect(() => { - new CourseUser({ username: 'user name' }); - }).toThrow(UsernameParseException); - }); - - test('Invalid email', () => { - expect(() => { - new CourseUser({ username: 'test', email: 'bad email' }); - }).toThrow(EmailParseException); - expect(() => { - new CourseUser({ username: 'test', email: 'user@info@site.com' }); - }).toThrow(EmailParseException); - }); - - test('set invalid role', () => { - const course_user = new CourseUser({ username: 'test', role: 'student' }); - expect(() => { - course_user.set({ role: 'superhero' }); - }).toThrow(UserRoleException); - }); - - test('set fields of CourseUser', () => { - const course_user = new CourseUser({ username: 'test' }); + expect(course_user.clone()).toBeInstanceOf(CourseUser); + + // The default user is not valid (The username is the empty string.) + expect(course_user.isValid()).toBe(false); + }); + }); + + describe('Setting fields of a CourseUser', () => { + test('Set CourseUser fields directly', () => { + const course_user = new CourseUser(); + + course_user.username = 'test2'; + expect(course_user.username).toBe('test2'); + + course_user.email = 'test@site.com'; + expect(course_user.email).toBe('test@site.com'); + + course_user.user_id = 15; + expect(course_user.user_id).toBe(15); + + course_user.first_name = 'Homer'; + expect(course_user.first_name).toBe('Homer'); + + course_user.last_name = 'Simpson'; + expect(course_user.last_name).toBe('Simpson'); + + course_user.is_admin = true; + expect(course_user.is_admin).toBe(true); + + course_user.student_id = '1234'; + expect(course_user.student_id).toBe('1234'); + + course_user.course_user_id = 10; + expect(course_user.course_user_id).toBe(10); + + course_user.course_id = 5; + expect(course_user.course_id).toBe(5); + + course_user.role = UserRole.admin; + expect(course_user.role).toBe(UserRole.admin); + + course_user.role = 'student'; + expect(course_user.role).toBe(UserRole.student); + }); + + test('Update CourseUser using the set method', () => { + const course_user = new CourseUser({ username: 'homer', role: UserRole.student }); + expect(course_user.isValid()).toBe(true); + + course_user.set({ course_user_id: 10 }); + expect(course_user.course_user_id).toBe(10); + + course_user.set({ user_id: 20 }); + expect(course_user.user_id).toBe(20); + + course_user.set({ course_id: 5 }); + expect(course_user.course_id).toBe(5); + + course_user.set({ role: UserRole.admin }); + expect(course_user.role).toBe(UserRole.admin); + course_user.set({ role: 'student' }); - expect(course_user.role).toBe('STUDENT'); + expect(course_user.role).toBe(UserRole.student); + + course_user.set({ username: 'test2' }); + expect(course_user.username).toBe('test2'); + + course_user.set({ email: 'test@site.com' }); + expect(course_user.email).toBe('test@site.com'); + + course_user.set({ first_name: 'Homer' }); + expect(course_user.first_name).toBe('Homer'); - course_user.set({ course_id: 34 }); - expect(course_user.course_id).toBe(34); + course_user.set({ last_name: 'Simpson' }); + expect(course_user.last_name).toBe('Simpson'); - course_user.set({ user_id: 3 }); - expect(course_user.user_id).toBe(3); + course_user.set({ is_admin: true }); + expect(course_user.is_admin).toBe(true); - course_user.set({ user_id: '5' }); - expect(course_user.user_id).toBe(5); + course_user.set({ student_id: '1234' }); + expect(course_user.student_id).toBe('1234'); + }); + }); + + describe('Testing for valid and invalid users.', () => { + test('setting invalid user_id', () => { + let user = new CourseUser({ username: 'homer', role: UserRole.student }); + expect(user.isValid()).toBe(true); + + user = new CourseUser({ username: 'homer', role: UserRole.student, user_id: -15 }); + expect(user.isValid()).toBe(false); + + user = new CourseUser({ username: 'homer', role: UserRole.student, user_id: 1.23 }); + expect(user.isValid()).toBe(false); + }); + + test('setting invalid course_user_id', () => { + let course_user = new CourseUser({ username: 'homer', role: UserRole.student }); + expect(course_user.isValid()).toBe(true); + + course_user = new CourseUser({ username: 'homer', role: UserRole.student }); + course_user.course_user_id = -3; + expect(course_user.isValid()).toBe(false); + + course_user = new CourseUser({ username: 'homer', role: UserRole.student }); + course_user.course_user_id = 3.14; + expect(course_user.isValid()).toBe(false); + + course_user = new CourseUser({ username: 'homer', role: UserRole.student }); + course_user.course_user_id = 7; + expect(course_user.isValid()).toBe(true); + }); + + test('setting invalid course_id', () => { + let course_user = new CourseUser({ username: 'homer', role: UserRole.student }); + course_user.course_id = -9; + expect(course_user.isValid()).toBe(false); + + course_user = new CourseUser({ username: 'homer', role: UserRole.student }); + course_user.course_id = 1.39; + expect(course_user.isValid()).toBe(false); + + course_user = new CourseUser({ username: 'homer', role: UserRole.student }); + course_user.course_id = 9; + expect(course_user.isValid()).toBe(true); + }); - course_user.set({ section: 2 }); - expect(course_user.section).toBe('2'); + test('setting invalid email', () => { + let course_user = new CourseUser({ username: 'homer', role: UserRole.student }); + expect(course_user.isValid()).toBe(true); - course_user.set({ section: '12' }); - expect(course_user.section).toBe('12'); + course_user = new CourseUser({ username: 'homer', role: UserRole.student }); + course_user.email = 'bad@email@address.com'; + expect(course_user.isValid()).toBe(false); + }); + test('setting invalid username', () => { + const user = new CourseUser({ username: 'my username' }); + expect(user.isValid()).toBe(false); }); + }); - test('set invalid user_id', () => { - const course_user = new CourseUser({ username: 'test' }); - expect(() => { - course_user.set({ user_id: -1 }); - }).toThrow(NonNegIntException); + describe('Test validation of CourseUsers', () => { + test('Test the validation of the user role', () => { + const user = new CourseUser({ username: 'homer' }); + const validate = user.validate(); + expect(validate.role).toBe('The role is not valid.'); + }); - expect(() => { - course_user.set({ user_id: '-1' }); - }).toThrow(NonNegIntException); + test('Test the validation of the course_id', () => { + const user = new CourseUser({ username: 'homer', course_id: -1 }); + const validate = user.validate(); + expect(validate.course_id).toBe('The course_id must be a non negative integer.'); + }); + test('Test the validation of the user_id', () => { + const user = new CourseUser({ username: 'homer', user_id: 23.5 }); + const validate = user.validate(); + expect(validate.user_id).toBe('The user_id must be a non negative integer.'); }); - test('set invalid course_id', () => { - const course_user = new CourseUser({ username: 'test' }); - expect(() => { - course_user.set({ course_id: -1 }); - }).toThrow(NonNegIntException); - expect(() => { - course_user.set({ course_id: '-1' }); - }).toThrow(NonNegIntException); + test('Test the validation of the course_user_id', () => { + const user = new CourseUser({ username: 'homer', course_user_id: -10 }); + const validate = user.validate(); + expect(validate.course_user_id).toBe('The course_user_id must be a non negative integer.'); }); - test('set invalid course_user_id', () => { - const course_user = new CourseUser({ username: 'test' }); - expect(() => { - course_user.set({ course_user_id: -1 }); - }).toThrow(NonNegIntException); - expect(() => { - course_user.set({ course_user_id: '-1' }); - }).toThrow(NonNegIntException); + test('Test the validation of the user role', () => { + const user = new CourseUser({ username: 'homer', role: 'programmer' }); + const validate = user.validate(); + expect(validate.role).toBe('The role is not valid.'); }); + }); }); diff --git a/tests/unit-tests/courses.spec.ts b/tests/unit-tests/courses.spec.ts index 87701154..0da6115d 100644 --- a/tests/unit-tests/courses.spec.ts +++ b/tests/unit-tests/courses.spec.ts @@ -1,18 +1,12 @@ -// tests parsing and handling of users - -import { Course, ParseableCourse } from 'src/common/models/courses'; -import { NonNegIntException } from 'src/common/models/parsers'; -import { InvalidFieldsException } from 'src/common/models'; +import { Course, ParseableCourse, UserCourse } from 'src/common/models/courses'; describe('Test Course Models', () => { - const default_course_dates = { start: 0, end: 0 }; - const default_course = { course_id: 0, course_name: 'Arithmetic', visible: true, - course_dates: { ...default_course_dates } + course_dates: { start: 0, end: 0 } }; describe('Creation of a Course', () => { @@ -20,9 +14,8 @@ describe('Test Course Models', () => { test('Create a Valid Course', () => { const course = new Course({ course_name: 'Arithmetic' }); expect(course).toBeInstanceOf(Course); - expect(course.toObject()).toStrictEqual(default_course); - + expect(course.isValid()).toBe(true); }); test('Check that calling all_fields() and params() is correct', () => { @@ -31,9 +24,7 @@ describe('Test Course Models', () => { expect(course.all_field_names.sort()).toStrictEqual(course_fields.sort()); expect(course.param_fields.sort()).toStrictEqual(['course_dates']); - expect(Course.ALL_FIELDS.sort()).toStrictEqual(course_fields.sort()); - }); test('Check that cloning works', () => { @@ -44,6 +35,46 @@ describe('Test Course Models', () => { }); + describe('Updating a course', () => { + test('set fields of a course', () => { + const course = new Course({ course_name: 'Arithmetic' }); + course.course_id = 5; + expect(course.course_id).toBe(5); + + course.course_name = 'Geometry'; + expect(course.course_name).toBe('Geometry'); + + course.visible = false; + expect(course.visible).toBe(false); + expect(course.isValid()).toBe(true); + + }); + + test('set fields of a course using the set method', () => { + const course = new Course({ course_name: 'Arithmetic' }); + course.set({ + course_id: 5, + course_name: 'Geometry', + visible: false + }); + expect(course.course_id).toBe(5); + expect(course.course_name).toBe('Geometry'); + expect(course.visible).toBe(false); + expect(course.isValid()).toBe(true); + }); + }); + + describe('Checking the course dates', () => { + test('checking for valid course dates', () => { + const course = new Course({ + course_name: 'Arithemetic', + course_dates: { start: 100, end: 100 } + }); + expect(course.course_dates.isValid()).toBe(true); + expect(course.isValid()).toBe(true); + }); + }); + describe('Checking valid and invalid creation parameters.', () => { test('Parsing of undefined and null values', () => { @@ -57,63 +88,143 @@ describe('Test Course Models', () => { expect(course1).toStrictEqual(course3); }); - test('Create a course with invalid params', () => { - // make a generic object and cast it as a Course - const p = { course_name: 'Arithmetic', CourseNumber: -1 } as unknown as ParseableCourse; - expect(() => { new Course(p);}) - .toThrow(InvalidFieldsException); + test('Create a course with invalid fields', () => { + const c1 = new Course({ course_name: 'Arithmetic', course_id: -1 }); + expect(c1.isValid()).toBe(false); + + const c2 = new Course({ course_name: '', course_id: 0 }); + expect(c2.isValid()).toBe(false); }); - test('Course with invalid course_id', () => { - expect(() => { - new Course({ course_name: 'Arithmetic', course_id: -1 }); - }).toThrow(NonNegIntException); + test('Create a course with invalid dates', () => { + const c1 = new Course({ + course_name: 'Arithmetic', + course_dates: { start: 100, end: 0 } + }); + expect(c1.isValid()).toBe(false); }); }); - describe('Updating a course', () => { - - test('set fields of a course', () => { - const course = new Course({ course_name: 'Arithmetic' }); - course.course_id = 5; - expect(course.course_id).toBe(5); + describe('Creating a UserCourse', () => { + const default_user_course = { + course_id: 0, + user_id: 0, + course_name: '', + username: '', + visible: true, + role: 'UNKNOWN', + course_dates : { start: 0, end: 0 } + }; + + describe('Creating a User Course', () => { + test('Create a Valid Course', () => { + const user_course = new UserCourse(); + expect(user_course).toBeInstanceOf(UserCourse); + expect(user_course.toObject()).toStrictEqual(default_user_course); + // The user course is not valid because the course name and username are empty strings + expect(user_course.isValid()).toBe(false); + }); + + test('Check that calling all_fields() and params() is correct', () => { + const user_course_fields = ['course_id', 'user_id', 'course_name', 'username', + 'visible', 'role', 'course_dates']; + const user_course = new UserCourse(); + expect(user_course.all_field_names.sort()).toStrictEqual(user_course_fields.sort()); + expect(UserCourse.ALL_FIELDS.sort()).toStrictEqual(user_course_fields.sort()); + expect(user_course.param_fields).toStrictEqual(['course_dates']); + }); + + test('Check that cloning works', () => { + const user_course = new UserCourse(); + expect(user_course.clone().toObject()).toStrictEqual(default_user_course); + expect(user_course.clone()).toBeInstanceOf(UserCourse); + }); + }); - course.course_name = 'Geometry'; - expect(course.course_name).toBe('Geometry'); + describe('Updating a UserCourse', () => { + test('set fields of a user course', () => { + const user_course = new UserCourse({ course_name: 'Arithmetic' }); + user_course.course_id = 5; + expect(user_course.course_id).toBe(5); - course.visible = true; - expect(course.visible).toBeTruthy(); + user_course.course_name = 'Geometry'; + expect(user_course.course_name).toBe('Geometry'); - course.visible = 0; - expect(course.visible).toBeFalsy(); + user_course.visible = false; + expect(user_course.visible).toBe(false); - course.visible = 'true'; - expect(course.visible).toBeTruthy(); + user_course.username = 'homer'; + expect(user_course.username).toBe('homer'); - course.visible = 'false'; - expect(course.visible).toBeFalsy(); + user_course.role = 'student'; + expect(user_course.role).toBe('STUDENT'); + expect(user_course.isValid()).toBe(true); + }); }); - test('set fields of a course using the set method', () => { - const course = new Course({ course_name: 'Arithmetic' }); - course.set({ course_id: 5 }); - expect(course.course_id).toBe(5); + describe('Checking valid and invalid creation parameters.', () => { + test('Parsing of undefined and null values', () => { + const course1 = new UserCourse({ course_name: 'Arithmetic' }); + const course2 = new UserCourse({ + course_name: 'Arithmetic', + user_id: undefined, + course_id: undefined + }); + expect(course1).toStrictEqual(course2); - course.set({ course_name: 'Geometry' }); - expect(course.course_name).toBe('Geometry'); + // the following allow to pass in non-valid parameters for testing + const params = { course_name: 'Arithmetic', course_id: null }; + const course3 = new UserCourse(params as unknown as ParseableCourse); + expect(course1).toStrictEqual(course3); + }); + + test('Create a course with invalid fields', () => { + const c1 = new UserCourse({ course_name: 'Arithmetic', username: 'homer', role: 'student' }); + expect(c1.isValid()).toBe(true); + + c1.course_name = ''; + expect(c1.isValid()).toBe(false); + + c1.set({ course_name: 'Arithmetic', user_id: -1 }); + expect(c1.isValid()).toBe(false); + + c1.set({ user_id: 10, course_id: -1 }); + expect(c1.isValid()).toBe(false); + + c1.course_id = 10; + expect(c1.isValid()).toBe(true); + + c1.role = 'wizard'; + expect(c1.isValid()).toBe(false); + + c1.role = 'ta'; + expect(c1.isValid()).toBe(true); + + c1.username = ''; + expect(c1.isValid()).toBe(false); + + c1.username = 'invalid user'; + expect(c1.isValid()).toBe(false); + + c1.username = 'homer@msn.com'; + expect(c1.isValid()).toBe(true); - course.set({ visible: true }); - expect(course.visible).toBeTruthy(); + }); - course.set({ visible: 'true' }); - expect(course.visible).toBeTruthy(); + test('Create a user course with invalid dates', () => { + const c1 = new UserCourse({ + course_name: 'Arithmetic', + username: 'homer', + role: 'student', + course_dates: { start: 100, end: 200 } + }); + expect(c1.isValid()).toBe(true); - course.set({ visible: 'false' }); - expect(course.visible).toBeFalsy(); + c1.setDates({ start: 100, end: 0 }); + expect(c1.isValid()).toBe(false); + }); - course.set({ visible: 0 }); - expect(course.visible).toBeFalsy(); }); }); }); diff --git a/tests/unit-tests/homework_sets.spec.ts b/tests/unit-tests/homework_sets.spec.ts index 17790754..e5e455c4 100644 --- a/tests/unit-tests/homework_sets.spec.ts +++ b/tests/unit-tests/homework_sets.spec.ts @@ -4,26 +4,24 @@ // The above is needed because the logger uses the window object, which is only present // when using the jsdom environment. -import { HomeworkSet, ParseableHomeworkSetDates, ParseableHomeworkSetParams, - ProblemSet } from 'src/common/models/problem_sets'; -import { BooleanParseException, NonNegIntException } from 'src/common/models/parsers'; +import { HomeworkSet, HomeworkSetDates, HomeworkSetParams, ProblemSet } from 'src/common/models/problem_sets'; describe('Tests for Homework Sets', () => { - const default_set_dates: ParseableHomeworkSetDates = { - answer: 0, - reduced_scoring: 0, - due: 0, - open: 0 - }; - - const default_set_params: ParseableHomeworkSetParams = { - enable_reduced_scoring: false - }; - const default_homework_set = { - set_dates: { ...default_set_dates }, - set_params: { ...default_set_params }, + set_dates: { + answer: 0, + reduced_scoring: 0, + due: 0, + open: 0, + enable_reduced_scoring: false + }, + set_params: { + hide_hint: false, + hardcopy_header: '', + set_header: '', + description: '' + }, set_id: 0, course_id: 0, set_name: '', @@ -32,7 +30,6 @@ describe('Tests for Homework Sets', () => { }; describe('Create new homework sets', () => { - test('Test default Homework Set', () => { const set = new HomeworkSet(); expect(set).toBeInstanceOf(HomeworkSet); @@ -41,11 +38,12 @@ describe('Tests for Homework Sets', () => { test('Build a HomeworkSet', () => { const set = new HomeworkSet(); + expect(set.set_type).toBe('HW'); expect(set).toBeInstanceOf(HomeworkSet); expect(set).toBeInstanceOf(ProblemSet); - const set1 = new HomeworkSet({ set_name: 'HW 1', set_visible: 0 }); + const set1 = new HomeworkSet({ set_name: 'HW 1', set_visible: true }); expect(set1).toBeInstanceOf(HomeworkSet); - expect(set1.set_visible).toBeFalsy(); + expect(set1.set_visible).toBe(true); const set2 = new HomeworkSet({ course_id:4, @@ -53,12 +51,13 @@ describe('Tests for Homework Sets', () => { answer: 1613951940, due: 1612137540, open: 1609545540, - reduced_scoring: 1610323140 + reduced_scoring: 1610323140, + enable_reduced_scoring: true }, set_id: 7, set_name: 'HW #1', set_params: { - enable_reduced_scoring: '1' + hide_hint: true }, set_visible: true }); @@ -68,12 +67,16 @@ describe('Tests for Homework Sets', () => { answer: 1613951940, due: 1612137540, open: 1609545540, - reduced_scoring: 1610323140 + reduced_scoring: 1610323140, + enable_reduced_scoring: true }, set_id: 7, set_name: 'HW #1', set_params: { - enable_reduced_scoring: true + hide_hint: true, + description: '', + hardcopy_header: '', + set_header: '' }, set_type: 'HW', set_visible: true @@ -89,75 +92,144 @@ describe('Tests for Homework Sets', () => { expect(hw.all_field_names.sort()).toStrictEqual(hw_fields.sort()); expect(hw.param_fields.sort()).toStrictEqual(['set_dates', 'set_params']); - expect(HomeworkSet.ALL_FIELDS.sort()).toStrictEqual(hw_fields.sort()); - }); - test('Check that cloning a Homework Sets works', () => { + test('Clone a HomeworkSet', () => { const hw = new HomeworkSet(); expect(hw.clone().toObject()).toStrictEqual(default_homework_set); expect(hw.clone()).toBeInstanceOf(HomeworkSet); }); - }); - describe('Check setting homework parameters', () => { - test('Check that the param defaults are working', () => { - const set1 = new HomeworkSet({ set_name: 'HW #1' }); - const set2 = new HomeworkSet({ set_name: 'HW #1', set_params: { enable_reduced_scoring: false } }); - expect(set1).toStrictEqual(set2); - const set3 = new HomeworkSet({ set_name: 'HW #1', set_dates: { - open: 0 - } }); - expect(set3.set_dates.open).toBe(set1.set_dates.open); + describe('Set homework fields', () => { + test('Check for setting fields directly', () => { + const hw = new HomeworkSet(); + expect(hw.set_type).toBe('HW'); + + hw.set_id = 5; + expect(hw.set_id).toBe(5); + + hw.set_name = 'HW #1'; + expect(hw.set_name).toBe('HW #1'); + + hw.course_id = 11; + expect(hw.course_id).toBe(11); + + hw.set_visible = false; + expect(hw.set_visible).toBe(false); }); - test('Test invalid Homework Set params', () => { - expect(() => { new HomeworkSet({ set_id: -1 });}).toThrow(NonNegIntException); - expect(() => { new HomeworkSet({ set_id: '-1' });}).toThrow(NonNegIntException); - expect(() => { new HomeworkSet({ course_id: -1 });}).toThrow(NonNegIntException); - expect(() => { new HomeworkSet({ course_id: '-1' });}).toThrow(NonNegIntException); - expect(() => { new HomeworkSet({ set_visible: 'T' });}).toThrow(BooleanParseException); + test('Check for setting fields using the set method', () => { + const hw = new HomeworkSet(); + + hw.set({ set_id: 5 }); + expect(hw.set_id).toBe(5); + + hw.set({ set_name: 'HW #1' }); + expect(hw.set_name).toBe('HW #1'); + + hw.set({ course_id: 11 }); + expect(hw.course_id).toBe(11); + + hw.set({ set_visible: false }); + expect(hw.set_visible).toBe(false); }); - test('Test valid Homework Set params', () => { - const set2 = new HomeworkSet(); - set2.set({ set_visible: 1 }); - expect(set2.set_visible).toBeTruthy(); + test('Test the validity of a HomeworkSet', () => { + let hw = new HomeworkSet(); + // A default homework set is missing a set_name. + expect(hw.isValid()).toBe(false); - set2.set({ set_visible: false }); - expect(set2.set_visible).toBeFalsy(); + hw = new HomeworkSet({ set_name: 'HW #1' }); + expect(hw.isValid()).toBe(true); - set2.set({ set_visible: 'true' }); - expect(set2.set_visible).toBeTruthy(); + hw = new HomeworkSet({ set_name: 'HW #1', set_id: 2.34 }); + expect(hw.isValid()).toBe(false); - set2.set({ set_name: 'HW #9' }); - expect(set2.set_name).toBe('HW #9'); + hw = new HomeworkSet({ set_name: 'HW #1', course_id: -32 }); + expect(hw.isValid()).toBe(false); }); }); describe('Test setting homework set dates.', () => { - test('Test the homework set dates', () => { - const set = new HomeworkSet(); - set.set_params.set({ enable_reduced_scoring: true }); - expect(set.set_params.enable_reduced_scoring).toBeTruthy(); + test('Test the default homework set dates', () => { + const set_dates = new HomeworkSetDates(); + expect(set_dates.toObject()).toStrictEqual(default_homework_set.set_dates); + expect(set_dates.isValid()).toBe(true); + }); + + test('Check that calling all_fields() and params() is correct', () => { + const hw_date_fields = ['open', 'reduced_scoring', 'due', 'answer', 'enable_reduced_scoring']; + const hw_dates = new HomeworkSetDates(); + + expect(hw_dates.all_field_names.sort()).toStrictEqual(hw_date_fields.sort()); + expect(hw_dates.param_fields).toStrictEqual([]); + expect(HomeworkSetDates.ALL_FIELDS.sort()).toStrictEqual(hw_date_fields.sort()); + }); + + test('Check that cloning homework set dates works', () => { + const hw_dates = new HomeworkSetDates(); + expect(hw_dates.clone().toObject()).toStrictEqual(default_homework_set.set_dates); + expect(hw_dates.clone()).toBeInstanceOf(HomeworkSetDates); + }); + }); + + describe('Check setting homework set dates', () => { + test('Set homework set dates directly', () => { + const hw_dates = new HomeworkSetDates(); + hw_dates.open = 100; + expect(hw_dates.open).toBe(100); + + hw_dates.reduced_scoring = 200; + expect(hw_dates.reduced_scoring).toBe(200); + + hw_dates.due = 300; + expect(hw_dates.due).toBe(300); + hw_dates.answer = 400; + expect(hw_dates.answer).toBe(400); + + hw_dates.enable_reduced_scoring = true; + expect(hw_dates.enable_reduced_scoring).toBe(true); + }); + + test('Set homework set dates using the set method', () => { + const hw_dates = new HomeworkSetDates(); + hw_dates.set({ open: 100 }); + expect(hw_dates.open).toBe(100); + + hw_dates.set({ reduced_scoring: 200 }); + expect(hw_dates.reduced_scoring).toBe(200); + + hw_dates.set({ due: 300 }); + expect(hw_dates.due).toBe(300); + + hw_dates.set({ answer: 400 }); + expect(hw_dates.answer).toBe(400); + + hw_dates.set({ enable_reduced_scoring: true }); + expect(hw_dates.enable_reduced_scoring).toBe(true); + }); + + test('Test for valid and invalid dates', () => { + const set = new HomeworkSet(); set.set_dates.set({ open: 0, reduced_scoring: 10, due: 10, answer: 20 }); - expect(set.hasValidDates()).toBeTruthy(); + expect(set.set_dates.isValid()).toBe(true); set.set_dates.set({ open: 0, reduced_scoring: 30, due: 10, - answer: 20 + answer: 20, + enable_reduced_scoring: true }); - expect(set.hasValidDates()).toBeFalsy(); + expect(set.set_dates.isValid()).toBe(false); set.set_dates.set({ open: 0, @@ -165,10 +237,10 @@ describe('Tests for Homework Sets', () => { due: 20, answer: 15 }); - expect(set.hasValidDates()).toBeFalsy(); + expect(set.set_dates.isValid()).toBe(false); - set.set_params.set({ enable_reduced_scoring: false }); - expect(set.set_params.enable_reduced_scoring).toBeFalsy(); + set.set_dates.enable_reduced_scoring = false; + expect(set.set_dates.enable_reduced_scoring).toBe(false); set.set_dates.set({ open: 30, @@ -176,7 +248,66 @@ describe('Tests for Homework Sets', () => { due: 40, answer: 50 }); - expect(set.hasValidDates()).toBeTruthy(); + expect(set.set_dates.isValid()).toBe(true); + }); + }); + + describe('Test setting homework set params.', () => { + test('Test the default homework set params', () => { + const set_params = new HomeworkSetParams(); + expect(set_params.toObject()).toStrictEqual(default_homework_set.set_params); + expect(set_params.isValid()).toBe(true); + }); + + test('Check that calling all_fields() and params() is correct', () => { + const hw_params_fields = ['hide_hint', 'hardcopy_header', 'set_header', 'description']; + const hw_params = new HomeworkSetParams(); + + expect(hw_params.all_field_names.sort()).toStrictEqual(hw_params_fields.sort()); + expect(hw_params.param_fields).toStrictEqual([]); + expect(HomeworkSetParams.ALL_FIELDS.sort()).toStrictEqual(hw_params_fields.sort()); + }); + + test('Check that cloning homework set dates works', () => { + const hw_params = new HomeworkSetParams(); + expect(hw_params.clone().toObject()).toStrictEqual(default_homework_set.set_params); + expect(hw_params.clone()).toBeInstanceOf(HomeworkSetParams); + }); + }); + + describe('Check setting homework set params', () => { + test('Set homework set params directly', () => { + const hw_params = new HomeworkSetParams(); + hw_params.hide_hint = true; + expect(hw_params.hide_hint).toBe(true); + + hw_params.hardcopy_header = 'my header'; + expect(hw_params.hardcopy_header).toBe('my header'); + + hw_params.set_header = 'my set header'; + expect(hw_params.set_header).toBe('my set header'); + + hw_params.description = 'the description'; + expect(hw_params.description).toBe('the description'); + }); + + test('Set homework set params using the set method', () => { + const hw_params = new HomeworkSetParams(); + hw_params.set({ hide_hint: true }); + expect(hw_params.hide_hint).toBe(true); + + hw_params.set({ hardcopy_header: 'my header' }); + expect(hw_params.hardcopy_header).toBe('my header'); + + hw_params.set({ set_header: 'my set header' }); + expect(hw_params.set_header).toBe('my set header'); + + hw_params.set({ description: 'the description' }); + expect(hw_params.description).toBe('the description'); }); + + // No tests for validity for this currently because there is nothing + // check. If the HomeworkSetParams are updated to include checking for validity + // then another test should be included. }); }); diff --git a/tests/unit-tests/library-problem.spec.ts b/tests/unit-tests/library-problem.spec.ts index 2f278a30..a91047cf 100644 --- a/tests/unit-tests/library-problem.spec.ts +++ b/tests/unit-tests/library-problem.spec.ts @@ -59,9 +59,9 @@ describe('Testing functionality of a Library Problem', () => { test('Check the changing problem location params works', () => { const prob = new LibraryProblem(); - prob.setLocationParams({ file_path: 'path' }); + prob.location_params.set({ file_path: 'path' }); expect(prob.location_params.file_path).toBe('path'); - prob.setLocationParams({ library_id: 1234 }); + prob.location_params.set({ library_id: 1234 }); expect(prob.location_params.library_id).toBe(1234); }); diff --git a/tests/unit-tests/parsing.spec.ts b/tests/unit-tests/parsing.spec.ts index a370c026..1245dc24 100644 --- a/tests/unit-tests/parsing.spec.ts +++ b/tests/unit-tests/parsing.spec.ts @@ -2,7 +2,7 @@ import { parseNonNegInt, parseBoolean, parseEmail, parseUsername, EmailParseException, NonNegIntException, BooleanParseException, UsernameParseException, - parseUserRole, UserRoleException, parseNonNegDecimal, NonNegDecimalException } from 'src/common/models/parsers'; + parseUserRole, parseNonNegDecimal, NonNegDecimalException } from 'src/common/models/parsers'; test('parsing nonnegative integers', () => { expect(parseNonNegInt(1)).toBe(1); @@ -69,5 +69,5 @@ test('parsing user roles', () => { expect(parseUserRole('instructor')).toBe('INSTRUCTOR'); expect(parseUserRole('TA')).toBe('TA'); expect(parseUserRole('student')).toBe('STUDENT'); - expect(() => {parseUserRole('not_existent'); }).toThrow(UserRoleException); + expect(parseUserRole('not_existent')).toBe('UNKNOWN'); }); diff --git a/tests/unit-tests/problem_sets.spec.ts b/tests/unit-tests/problem_sets.spec.ts new file mode 100644 index 00000000..9bd93f77 --- /dev/null +++ b/tests/unit-tests/problem_sets.spec.ts @@ -0,0 +1,61 @@ +import { ProblemSet } from 'src/common/models/problem_sets'; + +describe('Test generic ProblemSets', () => { + + describe('Creation of a ProblemSet', () => { + test('Test the class of a ProblemSet', () => { + const set = new ProblemSet(); + expect(set).toBeInstanceOf(ProblemSet); + }); + + test('Ensure that there are overrides', () => { + const set = new ProblemSet(); + expect(() => {set.set_params;}).toThrowError('The subclass must override set_params()'); + expect(() => {set.set_dates; }).toThrowError('The subclass must override set_dates()'); + expect(() => {set.clone(); }).toThrowError('The clone method must be overridden in a subclass.'); + }); + }); + + describe('Check setting generic fields', () => { + test('Check that all fields can be set directly', () => { + const set = new ProblemSet(); + set.set_id = 5; + expect(set.set_id).toBe(5); + + set.course_id = 10; + expect(set.course_id).toBe(10); + + set.set_visible = true; + expect(set.set_visible).toBe(true); + + set.set_name = 'Set #1'; + expect(set.set_name).toBe('Set #1'); + }); + + test('Check that calling all_fields() and params() is correct', () => { + const set_fields = ['set_id', 'set_visible', 'course_id', 'set_type', + 'set_name', 'set_params', 'set_dates']; + const problem_set = new ProblemSet(); + + expect(problem_set.all_field_names.sort()).toStrictEqual(set_fields.sort()); + expect(problem_set.param_fields.sort()).toStrictEqual(['set_dates', 'set_params']); + expect(ProblemSet.ALL_FIELDS.sort()).toStrictEqual(set_fields.sort()); + + }); + + test('Check that all fields can be set using the set() method', () => { + const set = new ProblemSet(); + set.set({ set_id: 5 }); + expect(set.set_id).toBe(5); + + set.set({ course_id: 10 }); + expect(set.course_id).toBe(10); + + set.set({ set_visible: true }); + expect(set.set_visible).toBe(true); + + set.set({ set_name: 'Set #1' }); + expect(set.set_name).toBe('Set #1'); + }); + }); +}); diff --git a/tests/unit-tests/quizzes.spec.ts b/tests/unit-tests/quizzes.spec.ts index 5bb12bd3..3bb06851 100644 --- a/tests/unit-tests/quizzes.spec.ts +++ b/tests/unit-tests/quizzes.spec.ts @@ -1,28 +1,18 @@ -/** - * @jest-environment jsdom - */ -// The above is needed because the logger uses the window object, which is only present -// when using the jsdom environment. +// Tests for Quizzes -import { Quiz, ProblemSet, ParseableQuizDates, ParseableQuizParams } from 'src/common/models/problem_sets'; -import { BooleanParseException, NonNegIntException } from 'src/common/models/parsers'; +import { Quiz, ProblemSet, QuizDates, QuizParams } from 'src/common/models/problem_sets'; describe('Testing for Quizzes', () => { - - const default_quiz_dates: ParseableQuizDates = { - answer: 0, - due: 0, - open: 0 - }; - - const default_quiz_params: ParseableQuizParams = { - timed: false, - quiz_duration: 0 - }; - const default_quiz = { - set_dates: { ...default_quiz_dates }, - set_params: { ...default_quiz_params }, + set_dates: { + answer: 0, + due: 0, + open: 0 + }, + set_params: { + timed: false, + quiz_duration: 0 + }, set_id: 0, course_id: 0, set_name: '', @@ -42,9 +32,9 @@ describe('Testing for Quizzes', () => { const quiz = new Quiz(); expect(quiz).toBeInstanceOf(Quiz); expect(quiz).toBeInstanceOf(ProblemSet); - const quiz1 = new Quiz({ set_name: 'HW #1', set_visible: 0 }); + const quiz1 = new Quiz({ set_name: 'HW #1', set_visible: false }); - expect(quiz.set_visible).toBeFalsy(); + expect(quiz.set_visible).toBe(false); expect(quiz1.set_name).toBe('HW #1'); const quiz2 = new Quiz({ @@ -91,9 +81,7 @@ describe('Testing for Quizzes', () => { expect(quiz.all_field_names.sort()).toStrictEqual(quiz_fields.sort()); expect(quiz.param_fields.sort()).toStrictEqual(['set_dates', 'set_params']); - expect(Quiz.ALL_FIELDS.sort()).toStrictEqual(quiz_fields.sort()); - }); test('Check that cloning a Quiz works', () => { @@ -103,80 +91,166 @@ describe('Testing for Quizzes', () => { }); }); - describe('Setting Quiz parameters', () => { - test('Check that the quiz param defaults are working', () => { - const quiz1 = new Quiz({ set_name: 'HW #1' }); - const quiz2 = new Quiz({ set_name: 'HW #1', set_params: { timed: false } }); - expect(quiz1).toStrictEqual(quiz2); - const quiz3 = new Quiz({ set_name: 'HW #1', set_dates: { - open: 0 - } }); - expect(quiz3.set_dates.open).toBe(quiz1.set_dates.open); + describe('Setting the quiz fields', () => { + test('Check for setting fields directly', () => { + const quiz = new Quiz(); + expect(quiz.set_type).toBe('QUIZ'); + + quiz.set_id = 5; + expect(quiz.set_id).toBe(5); + + quiz.set_name = 'Quiz #1'; + expect(quiz.set_name).toBe('Quiz #1'); + + quiz.course_id = 11; + expect(quiz.course_id).toBe(11); + + quiz.set_visible = false; + expect(quiz.set_visible).toBe(false); }); - test('Test invalid Quiz params', () => { - expect(() => { new Quiz({ set_id: -1 });}).toThrow(NonNegIntException); - expect(() => { new Quiz({ set_id: '-1' });}).toThrow(NonNegIntException); - expect(() => { new Quiz({ course_id: -1 });}).toThrow(NonNegIntException); - expect(() => { new Quiz({ course_id: '-1' });}).toThrow(NonNegIntException); + test('Check for setting fields using the set method', () => { + const quiz = new Quiz(); + + quiz.set({ set_id: 5 }); + expect(quiz.set_id).toBe(5); - expect(() => { new Quiz({ set_visible: 'T' });}).toThrow(BooleanParseException); + quiz.set({ set_name: 'Quiz #1' }); + expect(quiz.set_name).toBe('Quiz #1'); + quiz.set({ course_id: 11 }); + expect(quiz.course_id).toBe(11); + + quiz.set({ set_visible: false }); + expect(quiz.set_visible).toBe(false); }); + }); - test('Test valid Quiz params', () => { - const quiz2 = new Quiz(); - quiz2.set({ set_visible: 1 }); - expect(quiz2.set_visible).toBeTruthy(); + describe('Creating a Quiz Date', () => { + test('Test the default quiz dates', () => { + const quiz_dates = new QuizDates(); + expect(quiz_dates.toObject()).toStrictEqual(default_quiz.set_dates); + expect(quiz_dates.isValid()).toBe(true); + }); - quiz2.set({ set_visible: false }); - expect(quiz2.set_visible).toBeFalsy(); + test('Check that calling all_fields() and params() is correct', () => { + const quiz_date_fields = ['open', 'due', 'answer']; + const quiz_dates = new QuizDates(); - quiz2.set({ set_visible: 'true' }); - expect(quiz2.set_visible).toBeTruthy(); + expect(quiz_dates.all_field_names.sort()).toStrictEqual(quiz_date_fields.sort()); + expect(quiz_dates.param_fields).toStrictEqual([]); + expect(QuizDates.ALL_FIELDS.sort()).toStrictEqual(quiz_date_fields.sort()); + }); - quiz2.set({ set_name: 'HW #9' }); - expect(quiz2.set_name).toBe('HW #9'); + test('Check that cloning quiz dates works', () => { + const quiz_dates = new QuizDates(); + expect(quiz_dates.clone().toObject()).toStrictEqual(default_quiz.set_dates); + expect(quiz_dates.clone()).toBeInstanceOf(QuizDates); }); }); - describe('Setting Quiz Dates', () => { - test('Test the quiz dates', () => { - const quiz = new Quiz(); - quiz.set_params.set({ timed: true }); - expect(quiz.set_params.timed).toBeTruthy(); + describe('Check setting quiz dates', () => { + test('Set quiz dates directly', () => { + const quiz_dates = new QuizDates(); + quiz_dates.open = 100; + expect(quiz_dates.open).toBe(100); + + quiz_dates.due = 300; + expect(quiz_dates.due).toBe(300); - quiz.set_dates.set({ + quiz_dates.answer = 400; + expect(quiz_dates.answer).toBe(400); + + }); + + test('Set quiz dates using the set method', () => { + const quiz_dates = new QuizDates(); + quiz_dates.set({ open: 100 }); + expect(quiz_dates.open).toBe(100); + + quiz_dates.set({ due: 300 }); + expect(quiz_dates.due).toBe(300); + + quiz_dates.set({ answer: 400 }); + expect(quiz_dates.answer).toBe(400); + }); + + test('Test for valid and invalid dates', () => { + const quiz_dates = new QuizDates(); + quiz_dates.set({ open: 0, due: 10, answer: 20 }); - expect(quiz.hasValidDates()).toBeTruthy(); + expect(quiz_dates.isValid()).toBe(true); - quiz.set_dates.set({ + quiz_dates.set({ + open: 30, + due: 10, + answer: 20 + }); + expect(quiz_dates.isValid()).toBe(false); + + quiz_dates.set({ open: 0, due: 30, answer: 20 }); - expect(quiz.hasValidDates()).toBeFalsy(); + expect(quiz_dates.isValid()).toBe(false); + }); + }); - quiz.set_dates.set({ - open: 100, - due: 20, - answer: 150 - }); - expect(quiz.hasValidDates()).toBeFalsy(); + describe('Test setting quiz params.', () => { + test('Test the default quiz params', () => { + const quiz_params = new QuizParams(); + expect(quiz_params.toObject()).toStrictEqual(default_quiz.set_params); + expect(quiz_params.isValid()).toBe(true); + }); - quiz.set_params.set({ timed: false }); - expect(quiz.set_params.timed).toBeFalsy(); + test('Check that calling all_fields() and params() is correct', () => { + const quiz_params_fields = ['timed', 'quiz_duration']; + const quiz_params = new QuizParams(); - quiz.set_dates.set({ - open: 0, - due: 10, - answer: 15 - }); + expect(quiz_params.all_field_names.sort()).toStrictEqual(quiz_params_fields.sort()); + expect(quiz_params.param_fields).toStrictEqual([]); + expect(QuizParams.ALL_FIELDS.sort()).toStrictEqual(quiz_params_fields.sort()); + }); + + test('Check that cloning quiz dates works', () => { + const quiz_params = new QuizParams(); + expect(quiz_params.clone().toObject()).toStrictEqual(default_quiz.set_params); + expect(quiz_params.clone()).toBeInstanceOf(QuizParams); + }); + }); + + describe('Check setting quiz params', () => { + test('Set quiz params directly', () => { + const quiz_params = new QuizParams(); + quiz_params.timed = true; + expect(quiz_params.timed).toBe(true); + + quiz_params.quiz_duration = 30; + expect(quiz_params.quiz_duration).toBe(30); + }); + + test('Set quiz params using the set method', () => { + const quiz_params = new QuizParams(); + quiz_params.set({ timed: true }); + expect(quiz_params.timed).toBe(true); + + quiz_params.set({ quiz_duration: 30 }); + expect(quiz_params.quiz_duration).toBe(30); + }); + + }); + + describe('Check the Validity of the Quiz Params', () => { + test('Check valid and invalid params', () => { + const quiz_params = new QuizParams({ timed: true, quiz_duration: 30 }); + expect(quiz_params.isValid()).toBe(true); - expect(quiz.hasValidDates()).toBeTruthy(); + quiz_params.set({ quiz_duration: 30.5 }); + expect(quiz_params.isValid()).toBe(false); }); }); }); diff --git a/tests/unit-tests/review_sets.spec.ts b/tests/unit-tests/review_sets.spec.ts index 80b95d38..6c32d541 100644 --- a/tests/unit-tests/review_sets.spec.ts +++ b/tests/unit-tests/review_sets.spec.ts @@ -1,27 +1,15 @@ -/** - * @jest-environment jsdom - */ -// The above is needed because the logger uses the window object, which is only present -// when using the jsdom environment. +// Testing for ReviewSets. -import { BooleanParseException, NonNegIntException } from 'src/common/models/parsers'; -import { ParseableReviewSetDates, ParseableReviewSetParams, ProblemSet, - ReviewSet } from 'src/common/models/problem_sets'; +import { ProblemSet, ReviewSet, ReviewSetDates, ReviewSetParams } from 'src/common/models/problem_sets'; describe('Testing for Review Sets', () => { - const default_review_set_dates: ParseableReviewSetDates = { - open: 0, - closed: 0 - }; - - const default_review_set_params: ParseableReviewSetParams = { - can_retake: false - }; - const default_review_set = { - set_dates: { ...default_review_set_dates }, - set_params: { ...default_review_set_params }, + set_dates: { + open: 0, + closed: 0 + }, + set_params: {}, set_id: 0, course_id: 0, set_name: '', @@ -39,11 +27,12 @@ describe('Testing for Review Sets', () => { test('Build a Review_set', () => { const review_set = new ReviewSet(); + expect(review_set.set_type).toBe('REVIEW'); expect(review_set).toBeInstanceOf(ReviewSet); expect(review_set).toBeInstanceOf(ProblemSet); - const review_set1 = new ReviewSet({ set_name: 'Review Set #1', set_visible: 0 }); + const review_set1 = new ReviewSet({ set_name: 'Review Set #1', set_visible: false }); - expect(review_set.set_visible).toBeFalsy(); + expect(review_set.set_visible).toBe(false); expect(review_set1.set_name).toBe('Review Set #1'); const set_params = { @@ -55,7 +44,7 @@ describe('Testing for Review Sets', () => { set_id: 7, set_name: 'Review Set #1', set_params: { - can_retake: true, + can_retake: false }, set_visible: true, set_type: 'REVIEW' @@ -75,82 +64,161 @@ describe('Testing for Review Sets', () => { expect(review_set.all_field_names.sort()).toStrictEqual(review_set_fields.sort()); expect(review_set.param_fields.sort()).toStrictEqual(['set_dates', 'set_params']); - expect(ReviewSet.ALL_FIELDS.sort()).toStrictEqual(review_set_fields.sort()); - }); - test('Check that cloning a review_set works', () => { + test('Clone a ReviewSet', () => { const review_set = new ReviewSet(); expect(review_set.clone().toObject()).toStrictEqual(default_review_set); expect(review_set.clone()).toBeInstanceOf(ReviewSet); }); }); - describe('Setting review_set parameters', () => { - test('Check that the review_set param defaults are working', () => { - const review_set1 = new ReviewSet({ set_name: 'HW #1' }); - const review_set2 = new ReviewSet({ set_name: 'HW #1', set_params: { can_retake: false } }); - expect(review_set1).toStrictEqual(review_set2); - const review_set3 = new ReviewSet({ set_name: 'HW #1', set_dates: { - open: 0 - } }); - expect(review_set3.set_dates.open).toBe(review_set1.set_dates.open); + describe('Set homework fields', () => { + test('Check for setting fields directly', () => { + const set = new ReviewSet(); + expect(set.set_type).toBe('REVIEW'); + + set.set_id = 5; + expect(set.set_id).toBe(5); + + set.set_name = 'review set #1'; + expect(set.set_name).toBe('review set #1'); + + set.course_id = 11; + expect(set.course_id).toBe(11); + + set.set_visible = false; + expect(set.set_visible).toBe(false); }); - test('Test invalid review_set params', () => { - expect(() => { new ReviewSet({ set_id: -1 });}).toThrow(NonNegIntException); - expect(() => { new ReviewSet({ set_id: '-1' });}).toThrow(NonNegIntException); - expect(() => { new ReviewSet({ course_id: -1 });}).toThrow(NonNegIntException); - expect(() => { new ReviewSet({ course_id: '-1' });}).toThrow(NonNegIntException); + test('Check for setting fields using the set method', () => { + const set = new ReviewSet(); + + set.set({ set_id: 5 }); + expect(set.set_id).toBe(5); + + set.set({ set_name: 'set #1' }); + expect(set.set_name).toBe('set #1'); - expect(() => { new ReviewSet({ set_visible: 'T' });}).toThrow(BooleanParseException); + set.set({ course_id: 11 }); + expect(set.course_id).toBe(11); + set.set({ set_visible: false }); + expect(set.set_visible).toBe(false); }); - test('Test valid review_set params', () => { - const review_set2 = new ReviewSet(); - review_set2.set({ set_visible: 1 }); - expect(review_set2.set_visible).toBeTruthy(); + test('Test the validity of a ReviewSet', () => { + let hw = new ReviewSet(); + // A default review set is missing a set_name. + expect(hw.isValid()).toBe(false); - review_set2.set({ set_visible: false }); - expect(review_set2.set_visible).toBeFalsy(); + hw = new ReviewSet({ set_name: 'Review #1' }); + expect(hw.isValid()).toBe(true); - review_set2.set({ set_visible: 'true' }); - expect(review_set2.set_visible).toBeTruthy(); + hw = new ReviewSet({ set_name: 'Review #1', set_id: 2.34 }); + expect(hw.isValid()).toBe(false); - review_set2.set({ set_name: 'HW #9' }); - expect(review_set2.set_name).toBe('HW #9'); + hw = new ReviewSet({ set_name: 'Review #1', course_id: -32 }); + expect(hw.isValid()).toBe(false); }); }); - describe('Setting review_set Dates', () => { + describe('Test review_set Dates', () => { test('Test the review_set dates', () => { - const review_set = new ReviewSet(); - review_set.set_params.set({ can_retake: true }); - expect(review_set.set_params.can_retake).toBeTruthy(); + const review_set_dates = new ReviewSetDates(); + expect(review_set_dates.toObject()).toStrictEqual(default_review_set.set_dates); + expect(review_set_dates.isValid()).toBe(true); + }); - review_set.set_dates.set({ + test('Check that calling all_fields() and params() is correct', () => { + const review_date_fields = ['open', 'closed']; + const review_set_dates = new ReviewSetDates(); + + expect(review_set_dates.all_field_names.sort()).toStrictEqual(review_date_fields.sort()); + expect(review_set_dates.param_fields).toStrictEqual([]); + expect(ReviewSetDates.ALL_FIELDS.sort()).toStrictEqual(review_date_fields.sort()); + }); + + test('Check that cloning homework set dates works', () => { + const hw_dates = new ReviewSetDates(); + expect(hw_dates.clone().toObject()).toStrictEqual(default_review_set.set_dates); + expect(hw_dates.clone()).toBeInstanceOf(ReviewSetDates); + }); + }); + + describe('Check setting review set dates', () => { + test('Set review set dates directly', () => { + const hw_dates = new ReviewSetDates(); + hw_dates.open = 100; + expect(hw_dates.open).toBe(100); + + hw_dates.closed = 300; + expect(hw_dates.closed).toBe(300); + }); + + test('Set homework set dates using the set method', () => { + const hw_dates = new ReviewSetDates(); + hw_dates.set({ open: 100 }); + expect(hw_dates.open).toBe(100); + + hw_dates.set({ closed: 200 }); + expect(hw_dates.closed).toBe(200); + }); + + test('Test for valid and invalid dates', () => { + let set_dates = new ReviewSetDates({ open: 0, - closed: 20 + closed: 100 }); - expect(review_set.hasValidDates()).toBeTruthy(); + expect(set_dates.isValid()).toBe(true); - review_set.set_dates.set({ - open: 30, - closed: 20 + set_dates = new ReviewSetDates({ + open: 200, + closed: 100 }); - expect(review_set.hasValidDates()).toBeFalsy(); + expect(set_dates.isValid()).toBe(false); + }); + }); - review_set.set_params.set({ can_retake: false }); - expect(review_set.set_params.can_retake).toBeFalsy(); + describe('Test setting review set params.', () => { + test('Test the default review set params', () => { + const review_params = new ReviewSetParams(); + expect(review_params.toObject()).toStrictEqual(default_review_set.set_params); + expect(review_params.isValid()).toBe(true); + }); - review_set.set_dates.set({ - open: 0, - closed: 15 - }); + test('Check that calling all_fields() and params() is correct', () => { + const review_params_fields = ['can_retake']; + const review_params = new ReviewSetParams(); + + expect(review_params.all_field_names.sort()).toStrictEqual(review_params_fields.sort()); + expect(review_params.param_fields).toStrictEqual([]); + expect(ReviewSetParams.ALL_FIELDS.sort()).toStrictEqual(review_params_fields.sort()); + }); + + test('Check that cloning homework set dates works', () => { + const review_params = new ReviewSetParams(); + expect(review_params.clone().toObject()).toStrictEqual(default_review_set.set_params); + expect(review_params.clone()).toBeInstanceOf(ReviewSetParams); + }); + }); - expect(review_set.hasValidDates()).toBeTruthy(); + describe('Check setting review set params', () => { + test('Set review set params directly', () => { + const hw_params = new ReviewSetParams(); + hw_params.can_retake = true; + expect(hw_params.can_retake).toBe(true); }); + + test('Set homework set params using the set method', () => { + const hw_params = new ReviewSetParams(); + hw_params.set({ can_retake: true }); + expect(hw_params.can_retake).toBe(true); + }); + + // No tests for validity for this currently because there is nothing + // check. If the ReviewSetParams are updated to include checking for validity + // then another test should be included. }); }); diff --git a/tests/unit-tests/set_problems.spec.ts b/tests/unit-tests/set_problems.spec.ts index 71136e2f..080d1dcc 100644 --- a/tests/unit-tests/set_problems.spec.ts +++ b/tests/unit-tests/set_problems.spec.ts @@ -1,7 +1,6 @@ -import { NonNegDecimalException, NonNegIntException } from 'src/common/models/parsers'; import { ParseableSetProblem, ParseableSetProblemParams, ProblemType, SetProblem } from 'src/common/models/problems'; -describe('Test the SetProblem store', () => { +describe('Test the SetProblems', () => { const default_render_params = { problemSeed: 1234, @@ -28,175 +27,161 @@ describe('Test the SetProblem store', () => { problem_params: { ... default_problem_params } }; - test('Test creation of a Set Problem', () => { - const prob = new SetProblem(); - expect(prob instanceof SetProblem).toBe(true); + describe('Creating a Set Problem', () => { + test('Test creation of a Set Problem', () => { + const prob = new SetProblem(); + expect(prob instanceof SetProblem).toBe(true); - // remove the problem_type from the toObject, since we can't set it. - const p = prob.toObject(); - delete p.problem_type; + // remove the problem_type from the toObject, since we can't set it. + const p = prob.toObject(); + delete p.problem_type; - expect(p).toStrictEqual(default_problem_set); + expect(p).toStrictEqual(default_problem_set); + expect(prob.problem_type).toBe(ProblemType.SET); + }); - expect(prob.problem_type).toBe(ProblemType.SET); - }); - - test('Check that calling all_fields() and params() is correct', () => { - const set_problem_fields = ['render_params', 'problem_params', 'set_problem_id', - 'set_id', 'problem_number']; - const prob = new SetProblem(); - - expect(prob.all_field_names.sort()).toStrictEqual(set_problem_fields.sort()); - expect(prob.param_fields.sort()).toStrictEqual(['problem_params', 'render_params']); - - expect(SetProblem.ALL_FIELDS.sort()).toStrictEqual(set_problem_fields.sort()); - - }); + test('Check that calling all_fields() and params() is correct', () => { + const set_problem_fields = ['render_params', 'problem_params', 'set_problem_id', + 'set_id', 'problem_number']; + const prob = new SetProblem(); - test('Check that default rendering parameters are set.', () => { - const prob = new SetProblem(); - expect(prob.render_params.toObject()).toStrictEqual(default_render_params); - }); + expect(prob.all_field_names.sort()).toStrictEqual(set_problem_fields.sort()); + expect(prob.param_fields.sort()).toStrictEqual(['problem_params', 'render_params']); - test('Check the changing problem location params works', () => { - const prob = new SetProblem(); - prob.problem_params.set({ file_path: 'path' }); - expect(prob.problem_params.file_path).toBe('path'); - prob.problem_params.set({ library_id: 1234 }); - expect(prob.problem_params.library_id).toBe(1234); - }); - - test('Check that changing the render params works', () => { - const prob = new SetProblem(); - prob.setRenderParams({ problemSeed: 1234 }); - expect(prob.render_params.problemSeed).toBe(1234); - }); + expect(SetProblem.ALL_FIELDS.sort()).toStrictEqual(set_problem_fields.sort()); + }); - test('Check that cloning a library problem works', () => { - const prob = new SetProblem(); - expect(prob.clone().toObject()).toStrictEqual(default_problem_set); - expect(prob.clone() instanceof SetProblem).toBe(true); + test('Clone a Set problem', () => { + const prob = new SetProblem(); + expect(prob.clone().toObject()).toStrictEqual(default_problem_set); + expect(prob.clone() instanceof SetProblem).toBe(true); + }); }); - test('Check setting set problem fields directly', () => { - const prob = new SetProblem(); - prob.problem_number = 9; - expect(prob.problem_number).toBe(9); - prob.problem_number = '7'; - expect(prob.problem_number).toBe(7); - - prob.set_problem_id = 9; - expect(prob.set_problem_id).toBe(9); - prob.set_problem_id = '7'; - expect(prob.set_problem_id).toBe(7); - - prob.set_id = 5; - expect(prob.set_id).toBe(5); - prob.set_id = '11'; - expect(prob.set_id).toBe(11); - + describe('Checking rendering params', () => { + test('Check that default rendering parameters are set.', () => { + const prob = new SetProblem(); + expect(prob.render_params.toObject()).toStrictEqual(default_render_params); + }); + + test('Check that changing the render params works', () => { + const prob = new SetProblem(); + prob.setRenderParams({ problemSeed: 1234 }); + expect(prob.render_params.problemSeed).toBe(1234); + }); }); - test('Check setting set problem fields using set()', () => { - const prob = new SetProblem(); - prob.set({ problem_number: 9 }); - expect(prob.problem_number).toBe(9); - prob.set({ problem_number: '7' }); - expect(prob.problem_number).toBe(7); - - prob.set({ set_problem_id: 9 }); - expect(prob.set_problem_id).toBe(9); - prob.set({ set_problem_id: '7' }); - expect(prob.set_problem_id).toBe(7); - - prob.set({ set_id: 5 }); - expect(prob.set_id).toBe(5); - prob.set({ set_id: '11' }); - expect(prob.set_id).toBe(11); - + describe('Set Problem fields', () => { + test('Check setting set problem fields directly', () => { + const prob = new SetProblem(); + prob.problem_number = 9; + expect(prob.problem_number).toBe(9); + + prob.set_problem_id = 9; + expect(prob.set_problem_id).toBe(9); + + prob.set_id = 5; + expect(prob.set_id).toBe(5); + }); + + test('Check setting set problem fields using set()', () => { + const prob = new SetProblem(); + prob.set({ problem_number: 9 }); + expect(prob.problem_number).toBe(9); + + prob.set({ set_problem_id: 9 }); + expect(prob.set_problem_id).toBe(9); + + prob.set({ set_id: 5 }); + expect(prob.set_id).toBe(5); + }); + + test('Check for invalid SetProblems', () => { + const prob = new SetProblem({ problem_params: { file_path: '/this/is/the/path' } }); + expect(prob.isValid()).toBe(true); + + prob.problem_number = -1; + expect(prob.isValid()).toBe(false); + prob.problem_number = 1.234; + expect(prob.isValid()).toBe(false); + + prob.set_problem_id = -1; + expect(prob.isValid()).toBe(false); + prob.set_problem_id = 3.14; + expect(prob.isValid()).toBe(false); + + prob.set_id = -5; + expect(prob.isValid()).toBe(false); + prob.set_id = 2.34; + expect(prob.isValid()).toBe(false); + }); }); - test('Check that invalid fields setting throw exceptions', () => { - const prob = new SetProblem(); - expect(() => { prob.problem_number = -1; }).toThrow(NonNegIntException); - expect(() => { prob.problem_number = '-1'; }).toThrow(NonNegIntException); - expect(() => { prob.problem_number = 1.3; }).toThrow(NonNegIntException); - expect(() => { prob.problem_number = '1.5'; }).toThrow(NonNegIntException); + describe('Checking problem location params', () => { + test('Setting problem params directly', () => { + const prob = new SetProblem(); - expect(() => { prob.set_problem_id = -1; }).toThrow(NonNegIntException); - expect(() => { prob.set_problem_id = '-1'; }).toThrow(NonNegIntException); - expect(() => { prob.set_problem_id = 1.3; }).toThrow(NonNegIntException); - expect(() => { prob.set_problem_id = '1.5'; }).toThrow(NonNegIntException); + prob.problem_params.weight = 1.5; + expect(prob.problem_params.weight).toBe(1.5); - expect(() => { prob.set_id = -1; }).toThrow(NonNegIntException); - expect(() => { prob.set_id = '-1'; }).toThrow(NonNegIntException); - expect(() => { prob.set_id = 1.3; }).toThrow(NonNegIntException); - expect(() => { prob.set_id = '1.5'; }).toThrow(NonNegIntException); + prob.problem_params.library_id = 1234; + expect(prob.problem_params.library_id).toBe(1234); - }); + prob.problem_params.file_path = 'this_is_the_path'; + expect(prob.problem_params.file_path).toBe('this_is_the_path'); - test('Setting problem params directly', () => { - const prob = new SetProblem(); + prob.problem_params.problem_pool_id = 12; + expect(prob.problem_params.problem_pool_id).toBe(12); + }); - prob.problem_params.weight = 1.5; - expect(prob.problem_params.weight).toBe(1.5); - prob.problem_params.weight = '2.5'; - expect(prob.problem_params.weight).toBe(2.5); + test('Setting problem params using set()', () => { + const prob = new SetProblem(); - prob.problem_params.library_id = 1234; - expect(prob.problem_params.library_id).toBe(1234); - prob.problem_params.library_id = '4321'; - expect(prob.problem_params.library_id).toBe(4321); + prob.problem_params.set({ weight: 1.5 }); + expect(prob.problem_params.weight).toBe(1.5); - prob.problem_params.file_path = 'this_is_the_path'; - expect(prob.problem_params.file_path).toBe('this_is_the_path'); + prob.problem_params.set({ library_id: 1234 }); + expect(prob.problem_params.library_id).toBe(1234); - prob.problem_params.problem_pool_id = 12; - expect(prob.problem_params.problem_pool_id).toBe(12); - prob.problem_params.problem_pool_id = '43'; - expect(prob.problem_params.problem_pool_id).toBe(43); + prob.problem_params.set({ file_path: 'this_is_the_path' }); + expect(prob.problem_params.file_path).toBe('this_is_the_path'); - }); + prob.problem_params.set({ problem_pool_id: 12 }); + expect(prob.problem_params.problem_pool_id).toBe(12); + }); - test('Setting problem params using set()', () => { - const prob = new SetProblem(); + test('Test that problem params throw exception on invalid values.', () => { + const prob = new SetProblem(); + // need to defined either a library_id, problem_pool_id or file_path + expect(prob.problem_params.isValid()).toBe(false); - prob.problem_params.set({ weight: 1.5 }); - expect(prob.problem_params.weight).toBe(1.5); - prob.problem_params.set({ weight: '2.5' }); - expect(prob.problem_params.weight).toBe(2.5); + prob.problem_params.file_path = '/this/is/the/path'; + expect(prob.problem_params.isValid()).toBe(true); - prob.problem_params.set({ library_id: 1234 }); - expect(prob.problem_params.library_id).toBe(1234); - prob.problem_params.set({ library_id: '4321' }); - expect(prob.problem_params.library_id).toBe(4321); + prob.problem_params.weight = -1.5; + expect(prob.problem_params.isValid()).toBe(false); - prob.problem_params.set({ file_path: 'this_is_the_path' }); - expect(prob.problem_params.file_path).toBe('this_is_the_path'); + prob.problem_params.weight = 11.5; + expect(prob.problem_params.isValid()).toBe(true); - prob.problem_params.set({ problem_pool_id: 12 }); - expect(prob.problem_params.problem_pool_id).toBe(12); - prob.problem_params.set({ problem_pool_id: '43' }); - expect(prob.problem_params.problem_pool_id).toBe(43); - - }); + const prob2 = new SetProblem({ problem_params: { file_path: '/this/is/the/path' } }); + expect(prob2.problem_params.isValid()).toBe(true); - test('Test that problem params throw exception on invalid values.', () => { - const prob = new SetProblem(); + prob2.problem_params.library_id = 1.23; + expect(prob2.problem_params.isValid()).toBe(false); - expect(() => { prob.problem_params.weight = -1.5;}).toThrow(NonNegDecimalException); - expect(() => { prob.problem_params.weight = '-1.5';}).toThrow(NonNegDecimalException); + prob2.problem_params.library_id = -1; + expect(prob2.problem_params.isValid()).toBe(false); - expect(() => { prob.problem_params.library_id = -1; }).toThrow(NonNegIntException); - expect(() => { prob.problem_params.library_id = '-1'; }).toThrow(NonNegIntException); - expect(() => { prob.problem_params.library_id = 1.3; }).toThrow(NonNegIntException); - expect(() => { prob.problem_params.library_id = '1.5'; }).toThrow(NonNegIntException); + const prob3 = new SetProblem({ problem_params: { file_path: '/this/is/the/path' } }); + expect(prob3.problem_params.isValid()).toBe(true); - expect(() => { prob.problem_params.problem_pool_id = -1; }).toThrow(NonNegIntException); - expect(() => { prob.problem_params.problem_pool_id = '-1'; }).toThrow(NonNegIntException); - expect(() => { prob.problem_params.problem_pool_id = 1.3; }).toThrow(NonNegIntException); - expect(() => { prob.problem_params.problem_pool_id = '1.5'; }).toThrow(NonNegIntException); + prob3.problem_params.library_id = -1; + expect(prob3.problem_params.isValid()).toBe(false); + prob3.problem_params.library_id = 3.623; + expect(prob3.problem_params.isValid()).toBe(false); + }); }); }); diff --git a/tests/unit-tests/user_hw.spec.ts b/tests/unit-tests/user_hw.spec.ts index 2f741de2..f0ff9926 100644 --- a/tests/unit-tests/user_hw.spec.ts +++ b/tests/unit-tests/user_hw.spec.ts @@ -7,27 +7,26 @@ import { HomeworkSet } from 'src/common/models/problem_sets'; import { CourseUser } from 'src/common/models/users'; import { DBUserHomeworkSet, UserSet, mergeUserSet, ParseableDBUserHomeworkSet, - ParseableUserHomeworkSet, UserHomeworkSet, DBUserSet } from 'src/common/models/user_sets'; + ParseableUserHomeworkSet, UserHomeworkSet, DBUserSet, UserHomeworkSetParams, + UserHomeworkSetDates } from 'src/common/models/user_sets'; describe('Test user Homework sets', () => { + const default_db_user_homework_set: ParseableDBUserHomeworkSet = { + user_set_id: 0, + set_id: 0, + course_user_id: 0, + set_version: 1, + set_type: 'HW', + set_params: {}, + set_dates: {} + }; describe('Create a User Homework Set', () => { - const default_user_homework_set: ParseableDBUserHomeworkSet = { - user_set_id: 0, - set_id: 0, - course_user_id: 0, - set_version: 1, - set_visible: false, - set_type: 'HW', - set_params: { enable_reduced_scoring: false }, - set_dates: {} - }; - test('Create a DBUserHomeworkSet', () => { const user_hw = new DBUserHomeworkSet(); expect(user_hw).toBeInstanceOf(DBUserHomeworkSet); expect(user_hw).toBeInstanceOf(DBUserSet); - expect(user_hw.toObject()).toStrictEqual(default_user_homework_set); + expect(user_hw.toObject()).toStrictEqual(default_db_user_homework_set); }); test('Check that calling all_fields() and params() is correct', () => { @@ -42,48 +41,254 @@ describe('Test user Homework sets', () => { test('Check that cloning a DBUserHomeworkSet works', () => { const user_hw = new DBUserHomeworkSet(); - expect(user_hw.clone().toObject()).toStrictEqual(default_user_homework_set); + expect(user_hw.clone().toObject()).toStrictEqual(default_db_user_homework_set); expect(user_hw.clone()).toBeInstanceOf(DBUserHomeworkSet); }); }); - describe('Update a User Homework Set', () => { - test('Set params of a DBUserHomeworkSet', () => { - const user_hw = new DBUserHomeworkSet(); - user_hw.set_params.enable_reduced_scoring = true; - expect(user_hw.set_params.enable_reduced_scoring).toBeTruthy(); + describe('Update the fields of a User Homework Set', () => { + test('Set fields of user_set directly', () => { + const user_set = new DBUserHomeworkSet(); + user_set.user_set_id = 100; + expect(user_set.user_set_id).toBe(100); - user_hw.set_params.enable_reduced_scoring = '0'; - expect(user_hw.set_params.enable_reduced_scoring).toBeFalsy(); + user_set.set_id = 7; + expect(user_set.set_id).toBe(7); - user_hw.set_params.enable_reduced_scoring = 'true'; - expect(user_hw.set_params.enable_reduced_scoring).toBeTruthy(); + user_set.course_user_id = 25; + expect(user_set.course_user_id).toBe(25); + + user_set.set_version = 10; + expect(user_set.set_version).toBe(10); }); - test('Set dates of a DBUserHomeworkSet', () => { - const user_hw = new DBUserHomeworkSet(); - user_hw.set_dates.open = 100; - expect(user_hw.set_dates.open).toBe(100); + test('Set fields of user_set with set()', () => { + const user_set = new DBUserHomeworkSet(); + user_set.set({ user_set_id: 100 }); + expect(user_set.user_set_id).toBe(100); - user_hw.set_dates.set({ - open: 600, - due: 700, - answer: 800 - }); + user_set.set({ set_id: 7 }); + expect(user_set.set_id).toBe(7); - expect(user_hw.set_dates.toObject()).toStrictEqual({ - open: 600, - due: 700, - answer: 800 - }); + user_set.set({ course_user_id: 25 }); + expect(user_set.course_user_id).toBe(25); - expect(user_hw.hasValidDates()).toBeTruthy(); + user_set.set({ set_version: 10 }); + expect(user_set.set_version).toBe(10); + }); + }); - user_hw.set_params.enable_reduced_scoring = true; - expect(user_hw.hasValidDates()).toBeFalsy(); + describe('Test UserHomeworkParams', () => { + test('Creating user homework params', () => { + const params = new UserHomeworkSetParams(); + expect(params).toBeInstanceOf(UserHomeworkSetParams); + expect(params.toObject()).toStrictEqual(default_db_user_homework_set.set_params); + }); - user_hw.set_dates.reduced_scoring = 650; - expect(user_hw.hasValidDates()).toBeTruthy(); + test('Call all_fields() and params() of UserHomeworkSetParams', () => { + const user_hw_param_fields = ['hide_hint', 'hardcopy_header', 'set_header', 'description']; + const user_hw_params = new UserHomeworkSetParams(); + + expect(user_hw_params.all_field_names.sort()).toStrictEqual(user_hw_param_fields.sort()); + expect(user_hw_params.param_fields.sort()).toStrictEqual([]); + expect(UserHomeworkSetParams.ALL_FIELDS.sort()).toStrictEqual(user_hw_param_fields.sort()); + }); + + test('Clone a UserHomeworkSetParam', () => { + const user_hw_params = new UserHomeworkSetParams(); + expect(user_hw_params.clone().toObject()).toStrictEqual(default_db_user_homework_set.set_params); + expect(user_hw_params.clone()).toBeInstanceOf(UserHomeworkSetParams); + }); + }); + + describe('Update UserHomeworkParams', () => { + test('Update UserHomeworkParams directly', () => { + const params = new UserHomeworkSetParams(); + params.hide_hint = true; + expect(params.hide_hint).toBe(true); + + params.hide_hint = false; + expect(params.hide_hint).toBe(false); + + params.hide_hint = undefined; + expect(params.hide_hint).toBeUndefined(); + + params.hardcopy_header = 'this is the hardcopy header'; + expect(params.hardcopy_header).toBe('this is the hardcopy header'); + + params.hardcopy_header = undefined; + expect(params.hardcopy_header).toBeUndefined(); + + params.set_header = 'this is the set header'; + expect(params.set_header).toBe('this is the set header'); + + params.set_header = undefined; + expect(params.set_header).toBeUndefined(); + + params.description = 'this is the description'; + expect(params.description).toBe('this is the description'); + + params.description = undefined; + expect(params.description).toBeUndefined(); + }); + + test('Update the User Homework Sets Params using the set method.', () => { + const params = new UserHomeworkSetParams(); + params.set({ hide_hint: true }); + expect(params.hide_hint).toBe(true); + + params.set({ hide_hint: false }); + expect(params.hide_hint).toBe(false); + + params.set({ hide_hint: undefined }); + expect(params.hide_hint).toBeUndefined(); + + params.set({ hardcopy_header: 'this is the hardcopy header' }); + expect(params.hardcopy_header).toBe('this is the hardcopy header'); + + params.set({ hardcopy_header: undefined }); + expect(params.hardcopy_header).toBeUndefined(); + + params.set({ set_header: 'this is the set header' }); + expect(params.set_header).toBe('this is the set header'); + + params.set({ set_header: undefined }); + expect(params.set_header).toBeUndefined(); + + params.set({ description: 'this is the description' }); + expect(params.description).toBe('this is the description'); + + params.set({ description: undefined }); + expect(params.description).toBeUndefined(); + }); + + // No objects are invalid, so no testing is done. If the model is updated + // a section testing the validity of the UserHomeworkParams should occur here. + }); + + describe('Test UserHomeworkDates', () => { + test('Creating user homework dates', () => { + const dates = new UserHomeworkSetDates(); + expect(dates).toBeInstanceOf(UserHomeworkSetDates); + expect(dates.toObject()).toStrictEqual(default_db_user_homework_set.set_dates); + }); + + test('Call all_fields() and params() of UserHomeworkSetDates', () => { + const user_hw_dates_fields = ['open', 'reduced_scoring', 'due', 'answer', 'enable_reduced_scoring']; + const user_hw_dates = new UserHomeworkSetDates(); + + expect(user_hw_dates.all_field_names.sort()).toStrictEqual(user_hw_dates_fields.sort()); + expect(user_hw_dates.param_fields.sort()).toStrictEqual([]); + expect(UserHomeworkSetDates.ALL_FIELDS.sort()).toStrictEqual(user_hw_dates_fields.sort()); + }); + + test('Clone a UserHomeworkSetParam', () => { + const user_hw_dates = new UserHomeworkSetDates(); + expect(user_hw_dates.clone().toObject()).toStrictEqual(default_db_user_homework_set.set_dates); + expect(user_hw_dates.clone()).toBeInstanceOf(UserHomeworkSetDates); + }); + }); + + describe('Update UserHomeworkDates', () => { + test('Update UserHomeworkDates directly', () => { + const dates = new UserHomeworkSetDates(); + dates.open = 100; + expect(dates.open).toBe(100); + + dates.open = undefined; + expect(dates.open).toBeUndefined(); + + dates.reduced_scoring = 200; + expect(dates.reduced_scoring).toBe(200); + + dates.reduced_scoring = undefined; + expect(dates.reduced_scoring).toBeUndefined(); + + dates.due = 300; + expect(dates.due).toBe(300); + + dates.due = undefined; + expect(dates.due).toBeUndefined(); + + dates.answer = 400; + expect(dates.answer).toBe(400); + + dates.answer = undefined; + expect(dates.answer).toBeUndefined(); + + dates.enable_reduced_scoring = true; + expect(dates.enable_reduced_scoring).toBe(true); + + dates.enable_reduced_scoring = false; + expect(dates.enable_reduced_scoring).toBe(false); + + dates.enable_reduced_scoring = undefined; + expect(dates.enable_reduced_scoring).toBeUndefined(); + + }); + + test('Update the User Homework Sets dates using the set method.', () => { + const dates = new UserHomeworkSetDates(); + dates.set({ open: 100 }); + expect(dates.open).toBe(100); + + dates.set({ open: undefined }); + expect(dates.open).toBeUndefined(); + + dates.set({ reduced_scoring: 200 }); + expect(dates.reduced_scoring).toBe(200); + + dates.set({ reduced_scoring: undefined }); + expect(dates.reduced_scoring).toBeUndefined(); + + dates.set({ due: 300 }); + expect(dates.due).toBe(300); + + dates.set({ due: undefined }); + expect(dates.due).toBeUndefined(); + + dates.set({ answer: 400 }); + expect(dates.answer).toBe(400); + + dates.set({ answer: undefined }); + expect(dates.answer).toBeUndefined(); + }); + }); + + describe('Validity of UserHomeworkSetDates', () => { + test('Testing the validity of UserHomeworkSet dates', () => { + const dates = new UserHomeworkSetDates(); + expect(dates.isValid()).toBe(true); + + dates.set({ open: 100, due: 200, answer: 300 }); + expect(dates.isValid()).toBe(true); + + dates.set({ open: 100, reduced_scoring: 0, due: 200, answer: 300 }); + expect(dates.isValid()).toBe(true); + + dates.set({ open: 100, reduced_scoring: 150, due: 200, answer: 300, enable_reduced_scoring: true }); + expect(dates.isValid()).toBe(true); + + dates.set({ open: 100, reduced_scoring: 50, enable_reduced_scoring: true }); + expect(dates.isValid()).toBe(false); + + dates.set({ open: 100, reduced_scoring: 50, enable_reduced_scoring: false }); + expect(dates.isValid()).toBe(true); + + dates.set({ open: 100, due: 50 }); + expect(dates.isValid()).toBe(false); + + dates.set({ open: 100, answer: 50 }); + expect(dates.isValid()).toBe(false); + + dates.set({ reduced_scoring: 100, due: 50, enable_reduced_scoring: true }); + expect(dates.isValid()).toBe(false); + + dates.set({ reduced_scoring: 100, answer: 50, enable_reduced_scoring: true }); + expect(dates.isValid()).toBe(false); + + dates.set({ due: 300, answer: 200 }); + expect(dates.isValid()).toBe(false); }); }); @@ -99,8 +304,8 @@ describe('Test user Homework sets', () => { set_name: '', username: '', set_type: 'HW', - set_params: { enable_reduced_scoring: false }, - set_dates: { open: 0, reduced_scoring: 0, due: 0, answer: 0 } + set_params: { hide_hint: false, description: '', hardcopy_header: '', set_header: '' }, + set_dates: { open: 0, reduced_scoring: 0, due: 0, answer: 0, enable_reduced_scoring: false } }; test('Create a UserHomeworkSet', () => { @@ -110,7 +315,7 @@ describe('Test user Homework sets', () => { expect(user_hw.toObject()).toStrictEqual(default_user_homework_set); }); - test('Check that calling all_fields() and params() is correct', () => { + test('Call all_fields() and params() of UserHomeworkSet', () => { const user_hw_fields = ['user_set_id', 'set_id', 'course_user_id', 'set_version', 'set_type', 'user_id', 'set_visible', 'set_name', 'username', 'set_params', 'set_dates']; const hw = new UserHomeworkSet(); @@ -120,70 +325,99 @@ describe('Test user Homework sets', () => { expect(UserHomeworkSet.ALL_FIELDS.sort()).toStrictEqual(user_hw_fields.sort()); }); - test('Check that cloning a UserHomeworkSet works', () => { + test('Clone a UserHomeworkSet', () => { const hw = new UserHomeworkSet(); expect(hw.clone().toObject()).toStrictEqual(default_user_homework_set); - expect(hw.clone() instanceof UserHomeworkSet).toBeTruthy(); + expect(hw.clone()).toBeInstanceOf(UserHomeworkSet); }); - }); describe('Update user homework sets', () => { test('Set fields of Homework Set directly', () => { const user_set = new UserHomeworkSet(); + + expect(user_set.set_type).toBe('HW'); + user_set.user_set_id = 100; expect(user_set.user_set_id).toBe(100); - user_set.user_set_id = '20'; - expect(user_set.user_set_id).toBe(20); - user_set.set_id = 7; expect(user_set.set_id).toBe(7); - user_set.set_id = '9'; - expect(user_set.set_id).toBe(9); - user_set.course_user_id = 25; expect(user_set.course_user_id).toBe(25); - user_set.course_user_id = '18'; - expect(user_set.course_user_id).toBe(18); + user_set.user_id = 15; + expect(user_set.user_id).toBe(15); user_set.set_version = 10; expect(user_set.set_version).toBe(10); - user_set.set_version = '22'; - expect(user_set.set_version).toBe(22); - user_set.set_visible = true; - expect(user_set.set_visible).toBeTruthy(); - - user_set.set_visible = '0'; - expect(user_set.set_visible).toBeFalsy(); - - user_set.set_visible = 'true'; - expect(user_set.set_visible).toBeTruthy(); + expect(user_set.set_visible).toBe(true); user_set.set_name = 'HW #1'; expect(user_set.set_name).toBe('HW #1'); user_set.username = 'user'; expect(user_set.username).toBe('user'); + }); + test('Set fields of Homework Set use the set method', () => { + const user_set = new UserHomeworkSet(); + expect(user_set.set_type).toBe('HW'); + + user_set.set({ user_set_id: 100 }); + expect(user_set.user_set_id).toBe(100); + + user_set.set({ set_id: 7 }); + expect(user_set.set_id).toBe(7); + + user_set.set({ course_user_id: 25 }); + expect(user_set.course_user_id).toBe(25); + + user_set.set({ user_id: 15 }); + expect(user_set.user_id).toBe(15); + + user_set.set({ set_version: 10 }); + expect(user_set.set_version).toBe(10); + + user_set.set({ set_visible: true }); + expect(user_set.set_visible).toBe(true); + + user_set.set({ set_name: 'HW #1' }); + expect(user_set.set_name).toBe('HW #1'); + + user_set.set({ username: 'user' }); + expect(user_set.username).toBe('user'); }); - test('Set params of a UserHomeworkSet', () => { - const user_hw = new UserHomeworkSet(); - user_hw.set_params.enable_reduced_scoring = true; - expect(user_hw.set_params.enable_reduced_scoring).toBeTruthy(); + }); + + describe('Validity of a UserHomeworkSet', () => { + test('Test the validity of a UserHomeworkSet', () => { + let user_hw = new UserHomeworkSet(); + // This must have a valid username and set_name. + expect(user_hw.isValid()).toBe(false); - user_hw.set_params.enable_reduced_scoring = '0'; - expect(user_hw.set_params.enable_reduced_scoring).toBeFalsy(); + user_hw = new UserHomeworkSet({ username: 'homer', set_name: 'HW #1' }); + expect(user_hw.isValid()).toBe(true); - user_hw.set_params.enable_reduced_scoring = 'true'; - expect(user_hw.set_params.enable_reduced_scoring).toBeTruthy(); + user_hw = new UserHomeworkSet({ username: 'homer', set_name: 'HW #1', set_id: -1 }); + expect(user_hw.isValid()).toBe(false); + user_hw = new UserHomeworkSet({ username: 'homer', set_name: 'HW #1', user_id: 1.34 }); + expect(user_hw.isValid()).toBe(false); + + user_hw = new UserHomeworkSet({ username: 'homer', set_name: 'HW #1', user_set_id: -32 }); + expect(user_hw.isValid()).toBe(false); }); + }); + + // Note: the UserHomeworkSet params and dates are thoroughly tested in hw_sets.spec.ts + // since they are the same models as in a HomeworkSet. + + describe('Test UserHomeworkSet dates', () => { test('Set dates of a UserHomeworkSet', () => { const user_hw = new UserHomeworkSet(); @@ -206,17 +440,18 @@ describe('Test user Homework sets', () => { open: 600, reduced_scoring: 0, due: 700, - answer: 800 + answer: 800, + enable_reduced_scoring: false }); - expect(user_hw.hasValidDates()).toBeTruthy(); + expect(user_hw.set_dates.isValid()).toBe(true); - user_hw.set_params.enable_reduced_scoring = true; - expect(user_hw.hasValidDates()).toBeFalsy(); + user_hw.set_dates.enable_reduced_scoring = true; + expect(user_hw.set_dates.isValid()).toBe(false); user_hw.set_dates.reduced_scoring = 650; expect(user_hw.set_dates.reduced_scoring).toBe(650); - expect(user_hw.hasValidDates()).toBeTruthy(); + expect(user_hw.set_dates.isValid()).toBe(true); }); }); @@ -285,17 +520,17 @@ describe('Test user Homework sets', () => { // check that if the enable_reduced_scoring is flipped on, then the dates are // no longer valid - user_set.set_params.enable_reduced_scoring = true; - expect(user_set.hasValidDates()).toBeFalsy(); + user_set.set_dates.enable_reduced_scoring = true; + expect(user_set.set_dates.isValid()).toBe(false); // but then if the reduced_scoring date is set to the due date it will be user_set.set_dates.reduced_scoring = user_set.set_dates.due; - expect(user_set.hasValidDates()).toBeTruthy(); + expect(user_set.set_dates.isValid()).toBe(true); }); test('created a user homework set with reduced scoring dates', () => { - hw.set_params.enable_reduced_scoring = true; + hw.set_dates.enable_reduced_scoring = true; hw.set_dates.reduced_scoring = 175; const expected_user_hw = new UserHomeworkSet({ user_id: 99, @@ -308,7 +543,8 @@ describe('Test user Homework sets', () => { open: 150, reduced_scoring: 175, due: 200, - answer: 500 + answer: 500, + enable_reduced_scoring: true } }); const hw_set = mergeUserSet(hw, db_user_set, user); diff --git a/tests/unit-tests/user_problems.spec.ts b/tests/unit-tests/user_problems.spec.ts index cbfbf9ea..1d016735 100644 --- a/tests/unit-tests/user_problems.spec.ts +++ b/tests/unit-tests/user_problems.spec.ts @@ -1,6 +1,5 @@ // Testing UserProblems -import { NonNegDecimalException, NonNegIntException, UsernameParseException } from 'src/common/models/parsers'; import { DBUserProblem, ProblemType, UserProblem } from 'src/common/models/problems'; describe('Testing DB User Problems and User problems', () => { @@ -63,21 +62,15 @@ describe('Testing DB User Problems and User problems', () => { const prob = new DBUserProblem(); prob.problem_params.weight = 2; expect(prob.problem_params.weight).toBe(2); - prob.problem_params.weight = '3.5'; - expect(prob.problem_params.weight).toBe(3.5); prob.problem_params.library_id = 10; expect(prob.problem_params.library_id).toBe(10); - prob.problem_params.library_id = '12'; - expect(prob.problem_params.library_id).toBe(12); prob.problem_params.file_path = 'path/to/file'; expect(prob.problem_params.file_path).toBe('path/to/file'); prob.problem_params.problem_pool_id = 15; expect(prob.problem_params.problem_pool_id).toBe(15); - prob.problem_params.problem_pool_id = '25'; - expect(prob.problem_params.problem_pool_id).toBe(25); }); @@ -85,153 +78,105 @@ describe('Testing DB User Problems and User problems', () => { const prob = new DBUserProblem(); prob.problem_params.set({ weight: 2 }); expect(prob.problem_params.weight).toBe(2); - prob.problem_params.set({ weight: '3.5' }); - expect(prob.problem_params.weight).toBe(3.5); prob.problem_params.set({ library_id: 10 }); expect(prob.problem_params.library_id).toBe(10); - prob.problem_params.set({ library_id: '12' }); - expect(prob.problem_params.library_id).toBe(12); prob.problem_params.set({ file_path: 'path/to/file' }); expect(prob.problem_params.file_path).toBe('path/to/file'); prob.problem_params.set({ problem_pool_id: 15 }); expect(prob.problem_params.problem_pool_id).toBe(15); - prob.problem_params.set({ problem_pool_id: '25' }); - expect(prob.problem_params.problem_pool_id).toBe(25); - }); test('Check changes in fields set directly', () => { const prob = new DBUserProblem(); prob.set_problem_id = 5; expect(prob.set_problem_id).toBe(5); - prob.set_problem_id = '7'; - expect(prob.set_problem_id).toBe(7); prob.user_problem_id = 15; expect(prob.user_problem_id).toBe(15); - prob.user_problem_id = '27'; - expect(prob.user_problem_id).toBe(27); prob.user_set_id = 8; expect(prob.user_set_id).toBe(8); - prob.user_set_id = '34'; - expect(prob.user_set_id).toBe(34); prob.seed = 12345; expect(prob.seed).toBe(12345); - prob.seed = '7654'; - expect(prob.seed).toBe(7654); prob.status = 2; expect(prob.status).toBe(2); - prob.status = '2.5'; - expect(prob.status).toBe(2.5); prob.problem_version = 3; expect(prob.problem_version).toBe(3); - prob.problem_version = '4'; - expect(prob.problem_version).toBe(4); - }); test('Check changes in fields using set()', () => { const prob = new DBUserProblem(); prob.set({ set_problem_id: 5 }); expect(prob.set_problem_id).toBe(5); - prob.set({ set_problem_id: '7' }); - expect(prob.set_problem_id).toBe(7); prob.set({ user_problem_id: 15 }); expect(prob.user_problem_id).toBe(15); - prob.set({ user_problem_id: '27' }); - expect(prob.user_problem_id).toBe(27); prob.set({ user_set_id: 8 }); expect(prob.user_set_id).toBe(8); - prob.set({ user_set_id: '34' }); - expect(prob.user_set_id).toBe(34); prob.set({ seed: 12345 }); expect(prob.seed).toBe(12345); - prob.set({ seed: '7654' }); - expect(prob.seed).toBe(7654); prob.set({ status: 2 }); expect(prob.status).toBe(2); - prob.set({ status: '2.5' }); - expect(prob.status).toBe(2.5); prob.set({ problem_version: 3 }); expect(prob.problem_version).toBe(3); - prob.set({ problem_version: '4' }); - expect(prob.problem_version).toBe(4); - - }); - - test('Check that exceptions are thrown for invalid direct settings', () => { - const prob = new DBUserProblem(); - expect(() => {prob.set_problem_id = -1; }).toThrow(NonNegIntException); - expect(() => {prob.set_problem_id = '-5'; }).toThrow(NonNegIntException); - expect(() => {prob.set_problem_id = 1.5; }).toThrow(NonNegIntException); - expect(() => {prob.set_problem_id = '4.3'; }).toThrow(NonNegIntException); - - expect(() => {prob.user_set_id = -1; }).toThrow(NonNegIntException); - expect(() => {prob.user_set_id = '-5'; }).toThrow(NonNegIntException); - expect(() => {prob.user_set_id = 1.5; }).toThrow(NonNegIntException); - expect(() => {prob.user_set_id = '4.3'; }).toThrow(NonNegIntException); - - expect(() => {prob.user_problem_id = -10; }).toThrow(NonNegIntException); - expect(() => {prob.user_problem_id = '-15'; }).toThrow(NonNegIntException); - expect(() => {prob.user_problem_id = 2.5; }).toThrow(NonNegIntException); - expect(() => {prob.user_problem_id = '7.4'; }).toThrow(NonNegIntException); - - expect(() => {prob.seed = -5; }).toThrow(NonNegIntException); - expect(() => {prob.seed = '-3'; }).toThrow(NonNegIntException); - expect(() => {prob.seed = 1.25; }).toThrow(NonNegIntException); - expect(() => {prob.seed = '17.3'; }).toThrow(NonNegIntException); - - expect(() => {prob.status = -1; }).toThrow(NonNegDecimalException); - expect(() => {prob.status = '-5'; }).toThrow(NonNegDecimalException); - - expect(() => {prob.problem_version = -21; }).toThrow(NonNegIntException); - expect(() => {prob.problem_version = '-35'; }).toThrow(NonNegIntException); - expect(() => {prob.problem_version = 1.65; }).toThrow(NonNegIntException); - expect(() => {prob.problem_version = '4.33'; }).toThrow(NonNegIntException); }); - test('Check that exceptions are thrown for invalid field using set()', () => { - const prob = new DBUserProblem(); - expect(() => {prob.set({ set_problem_id: -1 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ set_problem_id: '-5' }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ set_problem_id: 1.5 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ set_problem_id: '4.3' }); }).toThrow(NonNegIntException); - - expect(() => {prob.set({ user_set_id: -1 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ user_set_id: '-5' }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ user_set_id: 1.5 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ user_set_id: '4.3' }); }).toThrow(NonNegIntException); - - expect(() => {prob.set({ user_problem_id: -10 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ user_problem_id: '-15' }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ user_problem_id: 2.5 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ user_problem_id: '7.4' }); }).toThrow(NonNegIntException); - - expect(() => {prob.set({ seed: -5 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ seed: '-3' }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ seed: 1.25 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ seed: '17.3' }); }).toThrow(NonNegIntException); - - expect(() => {prob.set({ status: -1 }); }).toThrow(NonNegDecimalException); - expect(() => {prob.set({ status: '-5' }); }).toThrow(NonNegDecimalException); - - expect(() => {prob.set({ problem_version: -21 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ problem_version: '-35' }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ problem_version: 1.65 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ problem_version: '4.33' }); }).toThrow(NonNegIntException); + test('Check for valid db user problem fields.', () => { + const prob = new DBUserProblem({ problem_params: { file_path: '/this/is/the/path' } }); + expect(prob.isValid()).toBe(true); + + prob.user_problem_id = -1; + expect(prob.isValid()).toBe(false); + prob.user_problem_id = 23.34; + expect(prob.isValid()).toBe(false); + prob.user_problem_id = 11; + expect(prob.isValid()).toBe(true); + + prob.set_problem_id = -1; + expect(prob.isValid()).toBe(false); + prob.set_problem_id = 8.32; + expect(prob.isValid()).toBe(false); + prob.set_problem_id = 11; + expect(prob.isValid()).toBe(true); + + prob.user_set_id = -1; + expect(prob.isValid()).toBe(false); + prob.user_set_id = 2.3; + expect(prob.isValid()).toBe(false); + prob.user_set_id = 11; + expect(prob.isValid()).toBe(true); + + prob.seed = -1; + expect(prob.isValid()).toBe(false); + prob.seed = 4.3; + expect(prob.isValid()).toBe(false); + prob.seed = 1324; + expect(prob.isValid()).toBe(true); + + prob.status = -1; + expect(prob.isValid()).toBe(false); + prob.status = 2; + expect(prob.isValid()).toBe(true); + prob.status = 2.5; + expect(prob.isValid()).toBe(true); + + prob.problem_version = -1; + expect(prob.isValid()).toBe(false); + prob.problem_version = 8.32; + expect(prob.isValid()).toBe(false); + prob.problem_version = 11; + expect(prob.isValid()).toBe(true); }); }); }); @@ -303,82 +248,54 @@ describe('Testing DB User Problems and User problems', () => { const prob = new UserProblem(); prob.problem_params.weight = 2; expect(prob.problem_params.weight).toBe(2); - prob.problem_params.weight = '3.5'; - expect(prob.problem_params.weight).toBe(3.5); prob.problem_params.library_id = 10; expect(prob.problem_params.library_id).toBe(10); - prob.problem_params.library_id = '12'; - expect(prob.problem_params.library_id).toBe(12); prob.problem_params.file_path = 'path/to/file'; expect(prob.problem_params.file_path).toBe('path/to/file'); prob.problem_params.problem_pool_id = 15; expect(prob.problem_params.problem_pool_id).toBe(15); - prob.problem_params.problem_pool_id = '25'; - expect(prob.problem_params.problem_pool_id).toBe(25); - }); test('Check the changing problem params using set() works', () => { const prob = new UserProblem(); prob.problem_params.set({ weight: 2 }); expect(prob.problem_params.weight).toBe(2); - prob.problem_params.set({ weight: '3.5' }); - expect(prob.problem_params.weight).toBe(3.5); prob.problem_params.set({ library_id: 10 }); expect(prob.problem_params.library_id).toBe(10); - prob.problem_params.set({ library_id: '12' }); - expect(prob.problem_params.library_id).toBe(12); prob.problem_params.set({ file_path: 'path/to/file' }); expect(prob.problem_params.file_path).toBe('path/to/file'); prob.problem_params.set({ problem_pool_id: 15 }); expect(prob.problem_params.problem_pool_id).toBe(15); - prob.problem_params.set({ problem_pool_id: '25' }); - expect(prob.problem_params.problem_pool_id).toBe(25); - }); test('Check changes in fields set directly', () => { const prob = new UserProblem(); prob.set_problem_id = 5; expect(prob.set_problem_id).toBe(5); - prob.set_problem_id = '7'; - expect(prob.set_problem_id).toBe(7); prob.user_problem_id = 15; expect(prob.user_problem_id).toBe(15); - prob.user_problem_id = '27'; - expect(prob.user_problem_id).toBe(27); prob.user_id = 8; expect(prob.user_id).toBe(8); - prob.user_id = '34'; - expect(prob.user_id).toBe(34); prob.seed = 12345; expect(prob.seed).toBe(12345); - prob.seed = '7654'; - expect(prob.seed).toBe(7654); prob.status = 2; expect(prob.status).toBe(2); - prob.status = '2.5'; - expect(prob.status).toBe(2.5); prob.problem_version = 3; expect(prob.problem_version).toBe(3); - prob.problem_version = '4'; - expect(prob.problem_version).toBe(4); prob.problem_number = 12; expect(prob.problem_number).toBe(12); - prob.problem_number = '7'; - expect(prob.problem_number).toBe(7); prob.username = 'user'; expect(prob.username).toBe('user'); @@ -387,45 +304,30 @@ describe('Testing DB User Problems and User problems', () => { prob.set_name = 'HW #1'; expect(prob.set_name).toBe('HW #1'); - }); test('Check changes in fields using set()', () => { const prob = new UserProblem(); prob.set({ set_problem_id: 5 }); expect(prob.set_problem_id).toBe(5); - prob.set({ set_problem_id: '7' }); - expect(prob.set_problem_id).toBe(7); prob.set({ user_problem_id: 15 }); expect(prob.user_problem_id).toBe(15); - prob.set({ user_problem_id: '27' }); - expect(prob.user_problem_id).toBe(27); prob.set({ user_id: 8 }); expect(prob.user_id).toBe(8); - prob.set({ user_id: '34' }); - expect(prob.user_id).toBe(34); prob.set({ seed: 12345 }); expect(prob.seed).toBe(12345); - prob.set({ seed: '7654' }); - expect(prob.seed).toBe(7654); prob.set({ status: 2 }); expect(prob.status).toBe(2); - prob.set({ status: '2.5' }); - expect(prob.status).toBe(2.5); prob.set({ problem_version: 3 }); expect(prob.problem_version).toBe(3); - prob.set({ problem_version: '4' }); - expect(prob.problem_version).toBe(4); prob.set({ problem_number: 12 }); expect(prob.problem_number).toBe(12); - prob.set({ problem_number: '7' }); - expect(prob.problem_number).toBe(7); prob.set({ username: 'user' }); expect(prob.username).toBe('user'); @@ -437,80 +339,79 @@ describe('Testing DB User Problems and User problems', () => { }); - test('Check that exceptions are thrown for invalid direct settings', () => { - const prob = new UserProblem(); - expect(() => {prob.set_problem_id = -1; }).toThrow(NonNegIntException); - expect(() => {prob.set_problem_id = '-5'; }).toThrow(NonNegIntException); - expect(() => {prob.set_problem_id = 1.5; }).toThrow(NonNegIntException); - expect(() => {prob.set_problem_id = '4.3'; }).toThrow(NonNegIntException); - - expect(() => {prob.user_id = -1; }).toThrow(NonNegIntException); - expect(() => {prob.user_id = '-5'; }).toThrow(NonNegIntException); - expect(() => {prob.user_id = 1.5; }).toThrow(NonNegIntException); - expect(() => {prob.user_id = '4.3'; }).toThrow(NonNegIntException); - - expect(() => {prob.user_problem_id = -10; }).toThrow(NonNegIntException); - expect(() => {prob.user_problem_id = '-15'; }).toThrow(NonNegIntException); - expect(() => {prob.user_problem_id = 2.5; }).toThrow(NonNegIntException); - expect(() => {prob.user_problem_id = '7.4'; }).toThrow(NonNegIntException); - - expect(() => {prob.seed = -5; }).toThrow(NonNegIntException); - expect(() => {prob.seed = '-3'; }).toThrow(NonNegIntException); - expect(() => {prob.seed = 1.25; }).toThrow(NonNegIntException); - expect(() => {prob.seed = '17.3'; }).toThrow(NonNegIntException); - - expect(() => {prob.status = -1; }).toThrow(NonNegDecimalException); - expect(() => {prob.status = '-5'; }).toThrow(NonNegDecimalException); - - expect(() => {prob.problem_version = -21; }).toThrow(NonNegIntException); - expect(() => {prob.problem_version = '-35'; }).toThrow(NonNegIntException); - expect(() => {prob.problem_version = 1.65; }).toThrow(NonNegIntException); - expect(() => {prob.problem_version = '4.33'; }).toThrow(NonNegIntException); - - expect(() => {prob.problem_number = -5; }).toThrow(NonNegIntException); - expect(() => {prob.problem_number = '-3'; }).toThrow(NonNegIntException); - expect(() => {prob.problem_number = 1.25; }).toThrow(NonNegIntException); - expect(() => {prob.problem_number = '17.3'; }).toThrow(NonNegIntException); - - expect(() => {prob.username = 'not a username';}).toThrow(UsernameParseException); - }); - - test('Check that exceptions are thrown for invalid field using set()', () => { - const prob = new UserProblem(); - expect(() => {prob.set({ set_problem_id: -1 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ set_problem_id: '-5' }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ set_problem_id: 1.5 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ set_problem_id: '4.3' }); }).toThrow(NonNegIntException); - - expect(() => {prob.set({ user_id: -1 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ user_id: '-5' }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ user_id: 1.5 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ user_id: '4.3' }); }).toThrow(NonNegIntException); - - expect(() => {prob.set({ user_problem_id: -10 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ user_problem_id: '-15' }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ user_problem_id: 2.5 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ user_problem_id: '7.4' }); }).toThrow(NonNegIntException); - - expect(() => {prob.set({ seed: -5 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ seed: '-3' }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ seed: 1.25 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ seed: '17.3' }); }).toThrow(NonNegIntException); - - expect(() => {prob.set({ status: -1 }); }).toThrow(NonNegDecimalException); - expect(() => {prob.set({ status: '-5' }); }).toThrow(NonNegDecimalException); - - expect(() => {prob.set({ problem_version: -21 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ problem_version: '-35' }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ problem_version: 1.65 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ problem_version: '4.33' }); }).toThrow(NonNegIntException); - - expect(() => {prob.set({ problem_number: -5 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ problem_number: '-3' }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ problem_number: 1.25 }); }).toThrow(NonNegIntException); - expect(() => {prob.set({ problem_number: '17.3' }); }).toThrow(NonNegIntException); - - expect(() => {prob.set({ username: 'not a username' });}).toThrow(UsernameParseException); + test('Check for valid user problem fields.', () => { + const prob = new UserProblem({ problem_params: { file_path: '/this/is/the/path' } }); + // The username must be valid and the set_name cannot be the empty string. + expect(prob.isValid()).toBe(false); + prob.set({ username: 'homer', set_name: 'HW #1' }); + + prob.user_problem_id = -1; + expect(prob.isValid()).toBe(false); + prob.user_problem_id = 23.34; + expect(prob.isValid()).toBe(false); + prob.user_problem_id = 11; + expect(prob.isValid()).toBe(true); + + prob.set_problem_id = -1; + expect(prob.isValid()).toBe(false); + prob.set_problem_id = 8.32; + expect(prob.isValid()).toBe(false); + prob.set_problem_id = 11; + expect(prob.isValid()).toBe(true); + + prob.user_id = -1; + expect(prob.isValid()).toBe(false); + prob.user_id = 8.32; + expect(prob.isValid()).toBe(false); + prob.user_id = 11; + expect(prob.isValid()).toBe(true); + + prob.user_set_id = -1; + expect(prob.isValid()).toBe(false); + prob.user_set_id = 2.3; + expect(prob.isValid()).toBe(false); + prob.user_set_id = 11; + expect(prob.isValid()).toBe(true); + + prob.set_id = -1; + expect(prob.isValid()).toBe(false); + prob.set_id = 2.3; + expect(prob.isValid()).toBe(false); + prob.set_id = 11; + expect(prob.isValid()).toBe(true); + + prob.seed = -1; + expect(prob.isValid()).toBe(false); + prob.seed = 4.3; + expect(prob.isValid()).toBe(false); + prob.seed = 1324; + expect(prob.isValid()).toBe(true); + + prob.status = -1; + expect(prob.isValid()).toBe(false); + prob.status = 2; + expect(prob.isValid()).toBe(true); + prob.status = 2.5; + expect(prob.isValid()).toBe(true); + + prob.problem_version = -1; + expect(prob.isValid()).toBe(false); + prob.problem_version = 8.32; + expect(prob.isValid()).toBe(false); + prob.problem_version = 11; + expect(prob.isValid()).toBe(true); + + prob.problem_number = -1; + expect(prob.isValid()).toBe(false); + prob.problem_number = 8.32; + expect(prob.isValid()).toBe(false); + prob.problem_number = 11; + expect(prob.isValid()).toBe(true); + + prob.username = 'homer the great!'; + expect(prob.isValid()).toBe(false); + prob.username = 'homer@msn.com'; + expect(prob.isValid()).toBe(true); }); }); }); diff --git a/tests/unit-tests/user_quiz.spec.ts b/tests/unit-tests/user_quiz.spec.ts index 1635df66..a7b5f5dc 100644 --- a/tests/unit-tests/user_quiz.spec.ts +++ b/tests/unit-tests/user_quiz.spec.ts @@ -1,6 +1,3 @@ -/** - * @jest-environment jsdom - */ // The above is needed because the logger uses the window object, which is only present // when using the jsdom environment. @@ -16,9 +13,8 @@ describe('Test User Quizzes', () => { set_id: 0, course_user_id: 0, set_version: 1, - set_visible: false, set_type: 'QUIZ', - set_params: { timed: false, quiz_duration: 0 }, + set_params: {}, set_dates: {} }; @@ -54,13 +50,7 @@ describe('Test User Quizzes', () => { const user_quiz = new DBUserQuiz(); user_quiz.set_params.timed = true; - expect(user_quiz.set_params.timed).toBeTruthy(); - - user_quiz.set_params.timed = '0'; - expect(user_quiz.set_params.timed).toBeFalsy(); - - user_quiz.set_params.timed = 'true'; - expect(user_quiz.set_params.timed).toBeTruthy(); + expect(user_quiz.set_params.timed).toBe(true); }); @@ -81,10 +71,10 @@ describe('Test User Quizzes', () => { answer: 800 }); - expect(user_quiz.hasValidDates()).toBeTruthy(); + expect(user_quiz.set_dates.isValid()).toBeTruthy(); user_quiz.set_dates.due = 1000; - expect(user_quiz.hasValidDates()).toBeFalsy(); + expect(user_quiz.set_dates.isValid()).toBeFalsy(); }); }); @@ -136,14 +126,7 @@ describe('Test User Quizzes', () => { const user_quiz = new UserQuiz(); user_quiz.set_params.timed = true; - expect(user_quiz.set_params.timed).toBeTruthy(); - - user_quiz.set_params.timed = '0'; - expect(user_quiz.set_params.timed).toBeFalsy(); - - user_quiz.set_params.timed = 'true'; - expect(user_quiz.set_params.timed).toBeTruthy(); - + expect(user_quiz.set_params.timed).toBe(true); }); test('Set dates of a UserQuiz', () => { @@ -163,10 +146,10 @@ describe('Test User Quizzes', () => { answer: 800 }); - expect(user_quiz.hasValidDates()).toBeTruthy(); + expect(user_quiz.set_dates.isValid()).toBeTruthy(); user_quiz.set_dates.due = 1000; - expect(user_quiz.hasValidDates()).toBeFalsy(); + expect(user_quiz.set_dates.isValid()).toBeFalsy(); }); }); diff --git a/tests/unit-tests/user_review_set.spec.ts b/tests/unit-tests/user_review_set.spec.ts index 7a61c9a3..0f143de2 100644 --- a/tests/unit-tests/user_review_set.spec.ts +++ b/tests/unit-tests/user_review_set.spec.ts @@ -17,9 +17,8 @@ describe('Testing db user Review Sets and User Review Sets', () => { set_id: 0, course_user_id: 0, set_version: 1, - set_visible: false, set_type: 'REVIEW', - set_params: { can_retake: false }, + set_params: {}, set_dates: {} }; @@ -66,10 +65,10 @@ describe('Testing db user Review Sets and User Review Sets', () => { closed: 800 }); - expect(user_quiz.hasValidDates()).toBeTruthy(); + expect(user_quiz.set_dates.isValid()).toBeTruthy(); user_quiz.set_dates.open = 1000; - expect(user_quiz.hasValidDates()).toBeFalsy(); + expect(user_quiz.set_dates.isValid()).toBeFalsy(); }); @@ -89,7 +88,7 @@ describe('Testing db user Review Sets and User Review Sets', () => { set_name: '', username: '', set_type: 'REVIEW', - set_params: { can_retake: false }, + set_params: {}, set_dates: { open: 0, closed: 0 } }; expect(user_review_set.toObject()).toStrictEqual(defaults); @@ -112,10 +111,10 @@ describe('Testing db user Review Sets and User Review Sets', () => { closed: 800 }); - expect(user_quiz.hasValidDates()).toBeTruthy(); + expect(user_quiz.set_dates.isValid()).toBeTruthy(); user_quiz.set_dates.open = 1000; - expect(user_quiz.hasValidDates()).toBeFalsy(); + expect(user_quiz.set_dates.isValid()).toBeFalsy(); }); }); diff --git a/tests/unit-tests/user_sets.spec.ts b/tests/unit-tests/user_sets.spec.ts index 933be36f..3ae638e3 100644 --- a/tests/unit-tests/user_sets.spec.ts +++ b/tests/unit-tests/user_sets.spec.ts @@ -1,10 +1,5 @@ -/** - * @jest-environment jsdom - */ -// The above is needed because the logger uses the window object, which is only present -// when using the jsdom environment. +// Tests for generic UserSets -import { BooleanParseException, NonNegIntException, UsernameParseException } from 'src/common/models/parsers'; import { DBUserSet, ParseableDBUserSet, UserSet } from 'src/common/models/user_sets'; describe('Test Generic User sets and Merged User sets', () => { @@ -13,7 +8,6 @@ describe('Test Generic User sets and Merged User sets', () => { user_set_id: 0, set_id: 0, course_user_id: 0, - set_visible: false, set_version: 1, set_type: 'UNKNOWN' }; @@ -50,27 +44,14 @@ describe('Test Generic User sets and Merged User sets', () => { user_set.user_set_id = 100; expect(user_set.user_set_id).toBe(100); - user_set.user_set_id = '20'; - expect(user_set.user_set_id).toBe(20); - user_set.set_id = 7; expect(user_set.set_id).toBe(7); - user_set.set_id = '9'; - expect(user_set.set_id).toBe(9); - user_set.course_user_id = 25; expect(user_set.course_user_id).toBe(25); - user_set.course_user_id = '18'; - expect(user_set.course_user_id).toBe(18); - user_set.set_version = 10; expect(user_set.set_version).toBe(10); - - user_set.set_version = '22'; - expect(user_set.set_version).toBe(22); - }); test('Set fields of user_set with set()', () => { @@ -78,67 +59,31 @@ describe('Test Generic User sets and Merged User sets', () => { user_set.set({ user_set_id: 100 }); expect(user_set.user_set_id).toBe(100); - user_set.set({ user_set_id: '33' }); - expect(user_set.user_set_id).toBe(33); - user_set.set({ set_id: 7 }); expect(user_set.set_id).toBe(7); - user_set.set({ set_id: '78' }); - expect(user_set.set_id).toBe(78); - user_set.set({ course_user_id: 25 }); expect(user_set.course_user_id).toBe(25); - user_set.set({ course_user_id: '34' }); - expect(user_set.course_user_id).toBe(34); - user_set.set({ set_version: 10 }); expect(user_set.set_version).toBe(10); - - user_set.set({ set_version: '54' }); - expect(user_set.set_version).toBe(54); - }); - test('Checking that setting invalid fields throws errors', () => { - const user_set = new DBUserSet(); - expect(() => { user_set.user_set_id = -1;}).toThrow(NonNegIntException); - expect(() => { user_set.user_set_id = '-1';}).toThrow(NonNegIntException); - expect(() => { user_set.user_set_id = 'one';}).toThrow(NonNegIntException); + test('Checking for valid and invalid user sets', () => { + let user_set = new DBUserSet(); + expect(user_set.isValid()).toBe(true); - expect(() => { user_set.set_id = -1;}).toThrow(NonNegIntException); - expect(() => { user_set.set_id = '-1';}).toThrow(NonNegIntException); - expect(() => { user_set.set_id = 'one';}).toThrow(NonNegIntException); + user_set = new DBUserSet({ user_set_id: -1 }); + expect(user_set.isValid()).toBe(false); - expect(() => { user_set.course_user_id = -10;}).toThrow(NonNegIntException); - expect(() => { user_set.course_user_id = '-10';}).toThrow(NonNegIntException); - expect(() => { user_set.course_user_id = 'ten';}).toThrow(NonNegIntException); + user_set = new DBUserSet({ set_id: -1 }); + expect(user_set.isValid()).toBe(false); - expect(() => { user_set.set_version = -1;}).toThrow(NonNegIntException); - expect(() => { user_set.set_version = '-1';}).toThrow(NonNegIntException); - expect(() => { user_set.set_version = 'one';}).toThrow(NonNegIntException); - - }); - - test('Checking that setting invalid fields with set() throws errors', () => { - const user_set = new DBUserSet(); - expect(() => { user_set.set({ user_set_id: -1 });}).toThrow(NonNegIntException); - expect(() => { user_set.set({ user_set_id: '-1' });}).toThrow(NonNegIntException); - expect(() => { user_set.set({ user_set_id: 'one' });}).toThrow(NonNegIntException); - - expect(() => { user_set.set({ set_id: -1 });}).toThrow(NonNegIntException); - expect(() => { user_set.set({ set_id: '-1' });}).toThrow(NonNegIntException); - expect(() => { user_set.set({ set_id: 'one' });}).toThrow(NonNegIntException); - - expect(() => { user_set.set({ course_user_id: -10 });}).toThrow(NonNegIntException); - expect(() => { user_set.set({ course_user_id: '-10' });}).toThrow(NonNegIntException); - expect(() => { user_set.set({ course_user_id: 'ten' });}).toThrow(NonNegIntException); - - expect(() => { user_set.set({ set_version: -1 });}).toThrow(NonNegIntException); - expect(() => { user_set.set({ set_version: '-1' });}).toThrow(NonNegIntException); - expect(() => { user_set.set({ set_version: 'one' });}).toThrow(NonNegIntException); + user_set = new DBUserSet({ course_user_id: -1 }); + expect(user_set.isValid()).toBe(false); + user_set = new DBUserSet({ set_version: 3.432 }); + expect(user_set.isValid()).toBe(false); }); }); @@ -165,40 +110,22 @@ describe('Test Generic User sets and Merged User sets', () => { }); describe('Update merged user sets', () => { - test('Set fields of Merged User Sets directly', () => { + test('Set fields of User Sets directly', () => { const user_set = new UserSet(); user_set.user_set_id = 100; expect(user_set.user_set_id).toBe(100); - user_set.user_set_id = '20'; - expect(user_set.user_set_id).toBe(20); - user_set.set_id = 7; expect(user_set.set_id).toBe(7); - user_set.set_id = '9'; - expect(user_set.set_id).toBe(9); - user_set.course_user_id = 25; expect(user_set.course_user_id).toBe(25); - user_set.course_user_id = '18'; - expect(user_set.course_user_id).toBe(18); - user_set.set_version = 10; expect(user_set.set_version).toBe(10); - user_set.set_version = '22'; - expect(user_set.set_version).toBe(22); - user_set.set_visible = true; - expect(user_set.set_visible).toBeTruthy(); - - user_set.set_visible = '0'; - expect(user_set.set_visible).toBeFalsy(); - - user_set.set_visible = 'true'; - expect(user_set.set_visible).toBeTruthy(); + expect(user_set.set_visible).toBe(true); user_set.set_name = 'HW #1'; expect(user_set.set_name).toBe('HW #1'); @@ -213,36 +140,18 @@ describe('Test Generic User sets and Merged User sets', () => { user_set.set({ user_set_id: 100 }); expect(user_set.user_set_id).toBe(100); - user_set.set({ user_set_id: '33' }); - expect(user_set.user_set_id).toBe(33); - user_set.set({ set_id: 7 }); expect(user_set.set_id).toBe(7); - user_set.set({ set_id: '78' }); - expect(user_set.set_id).toBe(78); - user_set.set({ course_user_id: 25 }); expect(user_set.course_user_id).toBe(25); - user_set.set({ course_user_id: '34' }); - expect(user_set.course_user_id).toBe(34); - user_set.set({ set_version: 10 }); expect(user_set.set_version).toBe(10); - user_set.set({ set_version: '54' }); - expect(user_set.set_version).toBe(54); - user_set.set({ set_visible: true }); expect(user_set.set_visible).toBeTruthy(); - user_set.set({ set_visible: '0' }); - expect(user_set.set_visible).toBeFalsy(); - - user_set.set({ set_visible: 'true' }); - expect(user_set.set_visible).toBeTruthy(); - user_set.set({ set_name: 'HW #1' }); expect(user_set.set_name).toBe('HW #1'); @@ -251,56 +160,34 @@ describe('Test Generic User sets and Merged User sets', () => { }); - test('Checking that setting invalid fields throws errors', () => { - const user_set = new UserSet(); - expect(() => { user_set.user_set_id = -1;}).toThrow(NonNegIntException); - expect(() => { user_set.user_set_id = '-1';}).toThrow(NonNegIntException); - expect(() => { user_set.user_set_id = 'one';}).toThrow(NonNegIntException); - - expect(() => { user_set.set_id = -1;}).toThrow(NonNegIntException); - expect(() => { user_set.set_id = '-1';}).toThrow(NonNegIntException); - expect(() => { user_set.set_id = 'one';}).toThrow(NonNegIntException); - - expect(() => { user_set.course_user_id = -10;}).toThrow(NonNegIntException); - expect(() => { user_set.course_user_id = '-10';}).toThrow(NonNegIntException); - expect(() => { user_set.course_user_id = 'ten';}).toThrow(NonNegIntException); + test('Checking for valid and invalid User Sets', () => { + let user_set = new UserSet(); + // The default user_set is not valid because it needs a username and set_name. + expect(user_set.isValid()).toBe(false); - expect(() => { user_set.set_version = -1;}).toThrow(NonNegIntException); - expect(() => { user_set.set_version = '-1';}).toThrow(NonNegIntException); - expect(() => { user_set.set_version = 'one';}).toThrow(NonNegIntException); + user_set = new UserSet({ username: 'homer', set_name: 'HW #1' }); + expect(user_set.isValid()).toBe(true); - expect(() => { user_set.set_visible = 2; }).toThrow(BooleanParseException); - expect(() => { user_set.set_visible = 'FalSe'; }).toThrow(BooleanParseException); - - expect(() => { user_set.username = '1234user'; }).toThrow(UsernameParseException); - expect(() => { user_set.username = 'bad username'; }).toThrow(UsernameParseException); - - }); - - test('Checking that setting invalid fields with set() throws errors', () => { - const user_set = new UserSet(); - expect(() => { user_set.set({ user_set_id: -1 });}).toThrow(NonNegIntException); - expect(() => { user_set.set({ user_set_id: '-1' });}).toThrow(NonNegIntException); - expect(() => { user_set.set({ user_set_id: 'one' });}).toThrow(NonNegIntException); + user_set = new UserSet({ username: 'homer@msn.com', set_name: 'HW #1' }); + expect(user_set.isValid()).toBe(true); - expect(() => { user_set.set({ set_id: -1 });}).toThrow(NonNegIntException); - expect(() => { user_set.set({ set_id: '-1' });}).toThrow(NonNegIntException); - expect(() => { user_set.set({ set_id: 'one' });}).toThrow(NonNegIntException); + user_set = new UserSet({ username: 'homer rules', set_name: 'HW #1' }); + expect(user_set.isValid()).toBe(false); - expect(() => { user_set.set({ course_user_id: -10 });}).toThrow(NonNegIntException); - expect(() => { user_set.set({ course_user_id: '-10' });}).toThrow(NonNegIntException); - expect(() => { user_set.set({ course_user_id: 'ten' });}).toThrow(NonNegIntException); + user_set = new UserSet({ username: 'homer' }); + expect(user_set.isValid()).toBe(false); - expect(() => { user_set.set({ set_version: -1 });}).toThrow(NonNegIntException); - expect(() => { user_set.set({ set_version: '-1' });}).toThrow(NonNegIntException); - expect(() => { user_set.set({ set_version: 'one' });}).toThrow(NonNegIntException); + user_set = new UserSet({ username: 'homer', set_name: 'HW #1', user_set_id: -1 }); + expect(user_set.isValid()).toBe(false); - expect(() => { user_set.set({ set_visible: 2 }); }).toThrow(BooleanParseException); - expect(() => { user_set.set({ set_visible: 'FalSe' }); }).toThrow(BooleanParseException); + user_set = new UserSet({ username: 'homer', set_name: 'HW #1', set_id: -1 }); + expect(user_set.isValid()).toBe(false); - expect(() => { user_set.set({ username: '1234user' }); }).toThrow(UsernameParseException); - expect(() => { user_set.set({ username: 'bad username' }); }).toThrow(UsernameParseException); + user_set = new UserSet({ username: 'homer', set_name: 'HW #1', user_id: -1 }); + expect(user_set.isValid()).toBe(false); + user_set = new UserSet({ username: 'homer', set_name: 'HW #1', set_version: 3.14 }); + expect(user_set.isValid()).toBe(false); }); }); }); diff --git a/tests/unit-tests/users.spec.ts b/tests/unit-tests/users.spec.ts index e8877831..31cdb58c 100644 --- a/tests/unit-tests/users.spec.ts +++ b/tests/unit-tests/users.spec.ts @@ -1,152 +1,119 @@ // tests parsing and handling of users -import { BooleanParseException, EmailParseException, NonNegIntException, - UsernameParseException } from 'src/common/models/parsers'; -import { RequiredFieldsException } from 'src/common/models'; import { User } from 'src/common/models/users'; -const default_user = { - user_id: 0, - username: 'test', - is_admin: false, -}; +describe('Testing User and CourseUsers', () => { + const default_user = { + user_id: 0, + username: '', + is_admin: false, + email: '', + first_name: '', + last_name: '', + student_id: '' + }; -test('Create a default User', () => { - const user = new User({ username: 'test' }); - expect(user instanceof User).toBe(true); - expect(user.toObject()).toStrictEqual(default_user); + describe('Create a new User', () => { + test('Create a default User', () => { + const user = new User(); + expect(user instanceof User).toBe(true); + expect(user.toObject()).toStrictEqual(default_user); + }); -}); + test('Check that calling all_fields() and params() is correct', () => { + const user_fields = ['user_id', 'username', 'is_admin', 'email', 'first_name', + 'last_name', 'student_id']; + const user = new User(); -test('Missing Username', () => { - // missing username - expect(() => { new User();}).toThrow(RequiredFieldsException); -}); + expect(user.all_field_names.sort()).toStrictEqual(user_fields.sort()); + expect(user.param_fields.sort()).toStrictEqual([]); + expect(User.ALL_FIELDS.sort()).toStrictEqual(user_fields.sort()); + }); -test('Check that calling all_fields() and params() is correct', () => { - const user_fields = ['user_id', 'username', 'is_admin', 'email', 'first_name', - 'last_name', 'student_id']; - const user = new User({ username: 'test' }); + test('Check that cloning a User works', () => { + const user = new User(); + expect(user.clone().toObject()).toStrictEqual(default_user); + expect(user.clone()).toBeInstanceOf(User); - expect(user.all_field_names.sort()).toStrictEqual(user_fields.sort()); - expect(user.param_fields.sort()).toStrictEqual([]); + // The default user is not valid. The username cannot be the empty string. + expect(user.isValid()).toBe(false); + }); - expect(User.ALL_FIELDS.sort()).toStrictEqual(user_fields.sort()); + }); -}); + describe('Setting fields of a User', () => { + test('Set User field directly', () => { + const user = new User(); -test('Check that cloning a User works', () => { - const user = new User({ username: 'test' }); - expect(user.clone().toObject()).toStrictEqual(default_user); - expect(user.clone() instanceof User).toBe(true); -}); + user.username = 'test2'; + expect(user.username).toBe('test2'); -test('Invalid user_id', () => { - expect(() => { - new User({ username: 'test', user_id: -1 }); - }).toThrow(NonNegIntException); - expect(() => { - new User({ username: 'test', user_id: '-1' }); - }).toThrow(NonNegIntException); - expect(() => { - new User({ username: 'test', user_id: 'one' }); - }).toThrow(NonNegIntException); - expect(() => { - new User({ username: 'test', user_id: 'false' }); - }).toThrow(NonNegIntException); -}); + user.email = 'test@site.com'; + expect(user.email).toBe('test@site.com'); -test('Invalid username', () => { - expect(() => { - new User({ username: '@test' }); - }).toThrow(UsernameParseException); - expect(() => { - new User({ username: '123test' }); - }).toThrow(UsernameParseException); - expect(() => { - new User({ username: 'user name' }); - }).toThrow(UsernameParseException); -}); + user.user_id = 15; + expect(user.user_id).toBe(15); -test('Invalid email', () => { - expect(() => { - new User({ username: 'test', email: 'bad email' }); - }).toThrow(EmailParseException); - expect(() => { - new User({ username: 'test', email: 'user@info@site.com' }); - }).toThrow(EmailParseException); -}); + user.first_name = 'Homer'; + expect(user.first_name).toBe('Homer'); -test('setting user fields', () => { - const user = new User({ username: 'test' }); + user.last_name = 'Simpson'; + expect(user.last_name).toBe('Simpson'); - user.username = 'test2'; - expect(user.username).toBe('test2'); + user.is_admin = true; + expect(user.is_admin).toBe(true); - user.email = 'test@site.com'; - expect(user.email).toBe('test@site.com'); + user.student_id = '1234'; + expect(user.student_id).toBe('1234'); - user.user_id = 15; - expect(user.user_id).toBe(15); + }); - user.first_name = 'Homer'; - expect(user.first_name).toBe('Homer'); + test('set fields using set() method', () => { + const user = new User({ username: 'test' }); - user.last_name = 'Simpson'; - expect(user.last_name).toBe('Simpson'); + user.set({ username: 'test2' }); + expect(user.username).toBe('test2'); + user.set({ email: 'test@site.com' }); + expect(user.email).toBe('test@site.com'); - user.is_admin = true; - expect(user.is_admin).toBe(true); + user.set({ user_id: 15 }); + expect(user.user_id).toBe(15); - user.is_admin = 1; - expect(user.is_admin).toBe(true); + user.set({ first_name: 'Homer' }); + expect(user.first_name).toBe('Homer'); - user.is_admin = '0'; - expect(user.is_admin).toBe(false); + user.set({ last_name: 'Simpson' }); + expect(user.last_name).toBe('Simpson'); -}); + user.set({ is_admin: true }); + expect(user.is_admin).toBe(true); -test('set fields using set() method', () => { - const user = new User({ username: 'test' }); + user.set({ student_id: '1234' }); + expect(user.student_id).toBe('1234'); + }); + }); - user.set({ username: 'test2' }); - expect(user.username).toBe('test2'); - user.set({ email: 'test@site.com' }); - expect(user.email).toBe('test@site.com'); + describe('Testing for valid and invalid users.', () => { - user.set({ user_id: 15 }); - expect(user.user_id).toBe(15); + test('setting invalid email', () => { + const user = new User({ username: 'test' }); + expect(user.isValid()).toBe(true); - user.set({ first_name: 'Homer' }); - expect(user.first_name).toBe('Homer'); + user.email = 'bad@email@address.com'; + expect(user.isValid()).toBe(false); + }); - user.set({ last_name: 'Simpson' }); - expect(user.last_name).toBe('Simpson'); + test('setting invalid user_id', () => { + const user = new User({ username: 'test' }); + expect(user.isValid()).toBe(true); - user.set({ is_admin: true }); - expect(user.is_admin).toBe(true); - - user.set({ is_admin: 1 }); - expect(user.is_admin).toBe(true); - - user.set({ is_admin: '0' }); - expect(user.is_admin).toBe(false); -}); - -test('setting invalid email', () => { - const user = new User({ username: 'test' }); - expect(() => { user.email = 'bad@email@address.com'; }) - .toThrow(EmailParseException); -}); - -test('setting invalid user_id', () => { - const user = new User({ username: 'test' }); - expect(() => { user.user_id = -15; }) - .toThrow(NonNegIntException); -}); + user.user_id = -15; + expect(user.isValid()).toBe(false); + }); -test('setting invalid admin', () => { - const user = new User({ username: 'test' }); - expect(() => { user.is_admin = 'FALSE'; }) - .toThrow(BooleanParseException); + test('setting invalid username', () => { + const user = new User({ username: 'my username' }); + expect(user.isValid()).toBe(false); + }); + }); }); diff --git a/tests/utils.ts b/tests/utils.ts index 90646fce..867f0f1b 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -4,7 +4,7 @@ import papa from 'papaparse'; import fs from 'fs'; import { Dictionary, generic, Model } from 'src/common/models'; -import { parseBoolean, parseNonNegInt } from 'src/common/models/parsers'; +import { parseBoolean, parseNonNegDecimal, parseNonNegInt } from 'src/common/models/parsers'; /** * Used for parsing a csv file. The params field is an array of strings that are in stored as @@ -16,11 +16,15 @@ import { parseBoolean, parseNonNegInt } from 'src/common/models/parsers'; interface CSVConfig { params?: string[]; boolean_fields?: string[]; - non_neg_fields?: string[]; + non_neg_int_fields?: string[]; + non_neg_float_fields?: string[]; + param_boolean_fields?: string[]; + param_non_neg_int_fields?: string[]; + param_non_neg_float_fields?: string[]; } /** - * Convert the data in the form of an array of objects of strings or numbersand converts to the proper + * Convert the data in the form of an array of objects of strings or numbers and converts to the proper * form to pass to a desired model. These typically come from a CSV file where the dates and params * are located in separate columns in the CSV file and are converted to a nested object. In addition, * booleans and integers are parsed. @@ -29,6 +33,9 @@ interface CSVConfig { function convert(data: Dictionary[], config: CSVConfig): Dictionary>[] { const keys = Object.keys(data[0]); const param_fields = config.params ?? []; + const param_boolean_fields = config.param_boolean_fields ?? []; + const param_non_neg_int_fields = config.param_non_neg_int_fields ?? []; + const param_non_neg_float_fields = config.param_non_neg_float_fields ?? []; // Store the param fields and the matching regular expressions const p_fields: Dictionary = {}; @@ -40,7 +47,7 @@ function convert(data: Dictionary[], config: CSVConfig): Dictionary prev = [...prev, ...value], [] as string[]); const known_fields = [...all_param_fields, ...(config.boolean_fields ?? []), - ...(config.non_neg_fields ?? [])]; + ...(config.non_neg_int_fields ?? []), ...(config.non_neg_float_fields ?? [])]; const other_fields = keys.filter(k => known_fields.indexOf(k) < 0); return data.map(row => { @@ -52,18 +59,28 @@ function convert(data: Dictionary[], config: CSVConfig): Dictionary { + (config.non_neg_int_fields ?? []).forEach(key => { if (row[key] != undefined) d[key] = parseNonNegInt(row[key]); }); + // Parse float fields + (config.non_neg_float_fields ?? []).forEach(key => { + if (row[key] != undefined) d[key] = parseNonNegDecimal(row[key]); + }); // Parse parameter fields Object.entries(p_fields).forEach(([key, ]) => { - d[key] = p_fields[key].reduce((prev: Dictionary, val) => { + d[key] = p_fields[key].reduce((prev: Dictionary, val) => { const field = val.split(':')[1]; // Parse any date field as date. + if (row[val]) { - prev[field] = /DATES:/.test(val) ? - Date.parse(row[val]) / 1000 : - row[val]; + // parse booleans, floats and integers + prev[field] = + param_boolean_fields.includes(field) ? (parseInt(row[val]) === 1 ? true : false) : + param_non_neg_int_fields.includes(field) ? parseInt(row[val]) : + param_non_neg_float_fields.includes(field) ? parseFloat(row[val]) : + /DATES:/.test(val) ? + Date.parse(row[val]) / 1000 : + row[val]; } return prev; }, {});