Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
b25c5a6
WIP: change Course model to handle fields
pstaabp May 16, 2022
e0de746
WIP: updating courses and generic problem sets on improvements.
pstaabp May 19, 2022
b7ff6fb
WIP: more work on switching models.
pstaabp May 20, 2022
fc7dd5d
WIP: work on getting the UI simpler with newer models.
pstaabp May 20, 2022
bc83691
WIP: fixed loading of users by file.
pstaabp May 20, 2022
bd8538b
WIP: cleanup
pstaabp May 21, 2022
2b60eaf
Merge branch 'db-send-json-boolean' into model-improvements
pstaabp May 21, 2022
887f84f
FIX: set_visible for problem_sets was not a JSON boolean.
pstaabp May 21, 2022
6ab7ec4
WIP: Move the enable_reduced_scoring from set_params to set_dates in …
pstaabp May 23, 2022
9280563
WIP: contined work on the models and tests.
pstaabp May 23, 2022
9265b45
Merge branch 'db-send-json-boolean' into model-improvements
pstaabp May 28, 2022
e138f0a
FIX: for dates in csv files, remove time-zone and also hack to get te…
pstaabp Jun 1, 2022
ed81b0e
FIX: store tests
pstaabp Jun 2, 2022
133098c
FIX: refactor the way csv files are loaded in the perl test files.
pstaabp Jun 2, 2022
d3fb3f6
FIX: linting and perlcritic cleanup.
pstaabp Jun 2, 2022
cbfab52
FIX: updated UI to reflect that enable_reduced_scoring is moved from …
pstaabp Jun 8, 2022
0692d06
Merge branch 'main' into model-improvements
pstaabp Jun 9, 2022
5c924d4
FIX: errors with tests after merge.
pstaabp Jun 9, 2022
9af6bc2
Merge branch 'db-send-json-boolean' into model-improvements
pstaabp Jun 13, 2022
75080a6
TEST: adding tests to ensure that only valid models are added to the …
pstaabp Jun 15, 2022
55a31fb
FIX: linting and typos
pstaabp Jun 15, 2022
d879ccd
FIX: tests now return the database to it's original state.
pstaabp Jun 16, 2022
1edc7ab
FIX: linting
pstaabp Jun 17, 2022
e532ccf
Merge branch 'db-send-json-boolean' into model-improvements
pstaabp Jun 17, 2022
e328665
FIX: testing errors
pstaabp Jun 17, 2022
f6a0977
FIX: cleanup and using true/false in tests.
pstaabp Jun 17, 2022
753e0be
Merge branch 'db-send-json-boolean' into model-improvements
pstaabp Jun 29, 2022
c02696c
WIP: check that enable_reduced_scoring is a boolean.
pstaabp Jun 29, 2022
5165dbe
FIX: perltidy/perlcritic errors
pstaabp Jun 29, 2022
5196256
Merge branch 'db-send-json-boolean' into model-improvements
pstaabp Jun 29, 2022
04e8fe5
Merge branch 'db-send-json-boolean' into model-improvements
pstaabp Jul 2, 2022
f16d56d
FIX: issues after merge with main.
pstaabp Jul 2, 2022
53839a5
Merge branch 'db-send-json-boolean' into model-improvements
pstaabp Jul 2, 2022
e4a5dc9
FIX: cleanup after merge
pstaabp Jul 5, 2022
282c1ea
FIX: unit tests for review sets.
pstaabp Jul 6, 2022
597aa7f
Merge branch 'db-send-json-boolean' into model-improvements
pstaabp Jul 8, 2022
7f6baed
FIX: linting after merge.
pstaabp Jul 8, 2022
f688760
FIX: perltidy
pstaabp Jul 11, 2022
dbe2f02
FIX: add users manually wasn't working. There was related model/stor…
pstaabp Jul 15, 2022
9750fa0
FIX: linting error.
pstaabp Jul 15, 2022
89c5842
FIX: make a server call only if the username is valid
pstaabp Jul 15, 2022
14919ce
Merge branch 'main' into model-improvements
pstaabp Jul 22, 2022
5ce8918
FIX: duplicated rule
pstaabp Jul 22, 2022
7731de8
FIX: move TestUtils.pm to t/lib and remove unnecessary $set_type vari…
pstaabp Jul 22, 2022
a365b0a
FIX: updated lib in build_db.pl script.
pstaabp Jul 22, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions lib/DB/Schema/Result/ProblemSet/HWSet.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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<required_dates>

subroutine that returns the array for the required dates: C<['open', 'due' ,'answer']>
Expand Down Expand Up @@ -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{.*}
};
}

Expand Down
2 changes: 2 additions & 0 deletions lib/DB/Schema/Result/ProblemSet/Quiz.pm
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ sub valid_dates ($=) {
return [ 'open', 'due', 'answer' ];
}

sub optional_fields_in_dates ($=) { return {}; }

=head2 C<required_dates>

subroutine that returns the array for the required dates: C<['open', 'due' ,'answer']>
Expand Down
2 changes: 2 additions & 0 deletions lib/DB/Schema/Result/ProblemSet/ReviewSet.pm
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ sub valid_dates ($=) {
return [ 'open', 'closed' ];
}

sub optional_fields_in_dates ($=) { return {}; }

=head2 C<required_dates>

subroutine that returns the array for the required dates: C<['open', 'closed']>
Expand Down
7 changes: 0 additions & 7 deletions lib/DB/Schema/Result/UserSet.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
4 changes: 4 additions & 0 deletions lib/DB/Schema/Result/UserSet/HWSet.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't make any sense, and needs to be rethought. It doesn't make sense to have optional non-date fields in the dates.

The JSON column structure is going to need to be thought out better. Right now there are several bad design ideas going on with that. The original idea was for these to be for non-essential uncommon things. Not dates that are pretty well mapped out. Furthermore, this is making the implementation rather convoluted.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the enable_reduced_scoring into the date structure so checking date validity is self-contained for cases of reduced scoring. This solved the issue of needing to pass in enable_reduced_scoring from the set_params field to check the validity. In order to not hard-code this field into the dates, I added this other method.

Let me know if you have some better thoughts on this or we can revert back to the way I handled it before.

Although I'm crazy about having a non-date field inside this, it does make a lot of other things easier to handle now.


sub valid_params ($=) {
return DB::Schema::Result::ProblemSet::HWSet::valid_params();
}
Expand Down
4 changes: 4 additions & 0 deletions lib/DB/Schema/Result/UserSet/Quiz.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
4 changes: 4 additions & 0 deletions lib/DB/Schema/Result/UserSet/ReviewSet.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
2 changes: 1 addition & 1 deletion lib/DB/Schema/ResultSet/Course.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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/;

Expand Down
22 changes: 19 additions & 3 deletions lib/DB/Schema/ResultSet/UserSet.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
36 changes: 29 additions & 7 deletions lib/DB/WithDates.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
39 changes: 25 additions & 14 deletions lib/WeBWorK3.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
29 changes: 26 additions & 3 deletions lib/WeBWorK3/Controller/User.pm
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down Expand Up @@ -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) {
Expand Down
15 changes: 15 additions & 0 deletions src/common/api-requests/errors.ts
Original file line number Diff line number Diff line change
@@ -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));
};
6 changes: 0 additions & 6 deletions src/common/api-requests/interfaces.ts

This file was deleted.

26 changes: 17 additions & 9 deletions src/common/api-requests/user.ts
Original file line number Diff line number Diff line change
@@ -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<ParseableCourseUser> {
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<ParseableUser> {
export async function getUser(username: string): Promise<User> {
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;
}
Expand Down
Loading