diff --git a/fuel/app/classes/controller/api/admin.php b/fuel/app/classes/controller/api/admin.php index df6e1bb2b..9f3b080f8 100644 --- a/fuel/app/classes/controller/api/admin.php +++ b/fuel/app/classes/controller/api/admin.php @@ -69,13 +69,20 @@ public function post_user($user_id) return \Service_User::update_user($user_id, $user); } - public function get_widget_search(string $input) + public function get_instance_search(string $input, string $page_number) { $input = trim($input); $input = urldecode($input); + $page_number = (int) $page_number; //no need to search if for some reason an empty string is passed - if ($input == '') return []; - return \Materia\Widget_Instance_Manager::get_search($input); + if ($input == '') + { + return [ + 'pagination' => [], + 'next_page' => $page_number + ]; + } + return \Materia\Widget_Instance_Manager::get_paginated_instance_search($input, $page_number); } public function get_extra_attempts(string $inst_id) diff --git a/fuel/app/classes/materia/api/v1.php b/fuel/app/classes/materia/api/v1.php index 509e66ab8..1083462ab 100644 --- a/fuel/app/classes/materia/api/v1.php +++ b/fuel/app/classes/materia/api/v1.php @@ -64,10 +64,10 @@ static public function widget_instances_get($inst_ids = null, bool $deleted = fa * * @return array of objects containing total_num_pages and widget instances that are visible to the user. */ - static public function widget_paginate_instances_get($page_number = 0) + static public function widget_paginate_user_instances_get($page_number = 0) { if (\Service_User::verify_session() !== true) return Msg::no_login(); - $data = Widget_Instance_Manager::get_paginated_for_user(\Model_User::find_current_id(), $page_number); + $data = Widget_Instance_Manager::get_paginated_instances_for_user(\Model_User::find_current_id(), $page_number); return $data; } @@ -218,7 +218,7 @@ static public function widget_instance_new($widget_id=null, $name=null, $qset=nu * @param int $close_at * @param int $attempts * @param bool $guest_access - * @param bool $is_student_made + * @param bool $is_student_made // NOT USED * * @return array An associative array with details about the save */ @@ -237,6 +237,10 @@ static public function widget_instance_update($inst_id=null, $name=null, $qset=n // student made widgets are locked forever if ($inst->is_student_made) { + if ($guest_access === false) + { + return new Msg('Student-made widgets must stay in guest access mode.', 'Student Made', 'error', false); + } $attempts = -1; $guest_access = true; } @@ -328,28 +332,33 @@ static public function widget_instance_update($inst_id=null, $name=null, $qset=n } if ($guest_access !== null) { - if ($inst->guest_access != $guest_access) - { - $activity = new Session_Activity([ - 'user_id' => \Model_User::find_current_id(), - 'type' => Session_Activity::TYPE_EDIT_WIDGET_SETTINGS, - 'item_id' => $inst_id, - 'value_1' => 'Guest Access', - 'value_2' => $guest_access - ]); - $activity->db_store(); - } - $inst->guest_access = $guest_access; - // when disabling guest mode on a widget, make sure no students have access to that widget - if ( ! $guest_access) + // if the user is a student and they're not the owner, they can't do anything + // if the user is a student and they're the owner, they're allowed to set it to guest access + if (($inst->user_id == \Model_User::find_current_id() && $guest_access) || ! Perm_Manager::is_student(\Model_User::find_current_id())) { - $access = Perm_Manager::get_all_users_explicit_perms($inst_id, Perm::INSTANCE)['widget_user_perms']; - foreach ($access as $user_id => $user_perms) + if ($inst->guest_access != $guest_access) { - if (Perm_Manager::is_student($user_id)) + $activity = new Session_Activity([ + 'user_id' => \Model_User::find_current_id(), + 'type' => Session_Activity::TYPE_EDIT_WIDGET_SETTINGS, + 'item_id' => $inst_id, + 'value_1' => 'Guest Access', + 'value_2' => $guest_access + ]); + $activity->db_store(); + } + $inst->guest_access = $guest_access; + // when disabling guest mode on a widget, make sure no students have access to that widget + if ( ! $guest_access) + { + $access = Perm_Manager::get_all_users_explicit_perms($inst_id, Perm::INSTANCE)['widget_user_perms']; + foreach ($access as $user_id => $user_perms) { - \Model_Notification::send_item_notification(\Model_user::find_current_id(), $user_id, Perm::INSTANCE, $inst_id, 'disabled', null); - Perm_Manager::clear_user_object_perms($inst_id, Perm::INSTANCE, $user_id); + if (Perm_Manager::is_student($user_id) && $user_id != $inst->user_id) + { + \Model_Notification::send_item_notification(\Model_user::find_current_id(), $user_id, Perm::INSTANCE, $inst_id, 'disabled', null); + Perm_Manager::clear_user_object_perms($inst_id, Perm::INSTANCE, $user_id); + } } } } @@ -357,7 +366,8 @@ static public function widget_instance_update($inst_id=null, $name=null, $qset=n if ($embedded_only !== null) { - if ($inst->embedded_only != $embedded_only) + // if current user is student, they cannot change embedded_only + if ($inst->embedded_only != $embedded_only && ! Perm_Manager::is_student(\Model_User::find_current_id())) { $activity = new Session_Activity([ 'user_id' => \Model_User::find_current_id(), @@ -367,8 +377,9 @@ static public function widget_instance_update($inst_id=null, $name=null, $qset=n 'value_2' => $embedded_only ]); $activity->db_store(); + + $inst->embedded_only = $embedded_only; } - $inst->embedded_only = $embedded_only; } try @@ -662,11 +673,12 @@ static public function guest_widget_instance_scores_get($inst_id, $play_id) */ static public function play_logs_get($inst_id, $semester = 'all', $year = 'all', $page_number=1) { - if ( ! Util_Validator::is_valid_hash($inst_id)) return Msg::invalid_input($inst_id); + if ( ! Util_Validator::is_valid_hash($inst_id)) return Msg::invalid_input($inst_id); if (\Service_User::verify_session() !== true) return Msg::no_login(); if ( ! static::has_perms_to_inst($inst_id, [Perm::VISIBLE, Perm::FULL])) return Msg::no_perm(); + $is_student = ! \Service_User::verify_session(['basic_author', 'super_user']); - $data = Session_Play::get_by_inst_id_paginated($inst_id, $semester, $year, $page_number); + $data = Session_Play::get_by_inst_id_paginated($inst_id, $semester, $year, $page_number, $is_student); return $data; } @@ -881,23 +893,39 @@ static public function semester_date_ranges_get() return Utils::get_date_ranges(); } - static public function users_search($search) + /** + * Paginated search for users that match input + * + * @param string Search query + * @param string Page number + * @return array List of users + */ + static public function users_search($input, $page_number = 0) { if (\Service_User::verify_session() !== true) return Msg::no_login(); - $user_objects = \Model_User::find_by_name_search($search); - $user_arrays = []; + $items_per_page = 50; + $offset = $items_per_page * $page_number; + + // query DB for only a single page + 1 item + $displayable_items = \Model_User::find_by_name_search($input, $offset, $items_per_page + 1); + + $has_next_page = sizeof($displayable_items) > $items_per_page ? true : false; + + if ($has_next_page) array_pop($displayable_items); - // scrub the user models with to_array - if (count($user_objects)) + foreach ($displayable_items as $key => $person) { - foreach ($user_objects as $key => $person) - { - $user_arrays[$key] = $person->to_array(); - } + $displayable_items[$key] = $person->to_array(); } - return $user_arrays; + $data = [ + 'pagination' => $displayable_items, + ]; + + if ($has_next_page) $data['next_page'] = $page_number + 1; + + return $data; } /** * Gets information about the current user diff --git a/fuel/app/classes/materia/perm/manager.php b/fuel/app/classes/materia/perm/manager.php index bf3e703da..043f9aae1 100644 --- a/fuel/app/classes/materia/perm/manager.php +++ b/fuel/app/classes/materia/perm/manager.php @@ -38,7 +38,7 @@ static public function is_super_user() // The session caching has been removed due to issues related to the cache when the role is added or revoked // Ideally we can still find a way to cache this and make it more performant!! return (\Fuel::$is_cli === true && ! \Fuel::$is_test) || self::does_user_have_role([\Materia\Perm_Role::SU]); - + } /** @@ -351,10 +351,10 @@ static public function remove_users_from_roles_system_only(Array $user_ids = [], ->execute(); } } + return $success; } - /* ********************** User to Object Rights *************************************** */ diff --git a/fuel/app/classes/materia/session/play.php b/fuel/app/classes/materia/session/play.php index 679da49ab..2c4a1627c 100644 --- a/fuel/app/classes/materia/session/play.php +++ b/fuel/app/classes/materia/session/play.php @@ -213,7 +213,7 @@ public function resume($play_id) * Must be fast because it can be asked to retrieve large data sets * */ - public static function get_by_inst_id($inst_id, $semester='all', $year='all') + public static function get_by_inst_id($inst_id, $semester='all', $year='all', $is_student=false) { if ($semester != 'all' && $year != 'all') { @@ -230,7 +230,23 @@ public static function get_by_inst_id($inst_id, $semester='all', $year='all') if (is_null($plays)) { - $query = \DB::select( + // if user is student, do not query user information + if ($is_student) + { + $query = \DB::select( + 's.id', + ['s.created_at', 'time'], + ['s.is_complete', 'done'], + ['s.percent', 'perc'], + ['s.elapsed', 'elapsed'], + ['s.qset_id', 'qset_id'] + ) + ->from(['log_play', 's']) + ->where('s.inst_id', $inst_id); + } + else + { + $query = \DB::select( 's.id', ['s.created_at', 'time'], ['s.is_complete', 'done'], @@ -246,6 +262,7 @@ public static function get_by_inst_id($inst_id, $semester='all', $year='all') ->join(['users', 'u'], 'LEFT OUTER') ->on('u.id', '=', 's.user_id') ->where('s.inst_id', $inst_id); + } if (isset($date)) { @@ -256,14 +273,28 @@ public static function get_by_inst_id($inst_id, $semester='all', $year='all') \Cache::set('play-logs.'.$inst_id.'.'.$cache_id, $plays); } + else + { + // if user is student, do not show user information + if ($is_student) + { + foreach ($plays as &$play) + { + $play['user_id'] = 0; + unset($play['first']); + unset($play['last']); + unset($play['username']); + } + } + } return $plays; } - public static function get_by_inst_id_paginated($inst_id, $semester='all', $year='all', $page_number=1) + public static function get_by_inst_id_paginated($inst_id, $semester='all', $year='all', $page_number=1, $is_student=false) { $items_per_page = 100; - $data = self::get_by_inst_id($inst_id, $semester, $year); + $data = self::get_by_inst_id($inst_id, $semester, $year, $is_student); $total_num_pages = ceil(sizeof($data) / $items_per_page); $offset = $items_per_page * ($page_number - 1); $page = array_slice($data, $offset, $items_per_page); @@ -327,6 +358,8 @@ public function get_by_id($play_id=0) $this->elapsed = $r['elapsed']; $this->context_id = $r['context_id']; $this->semester = $r['semester']; + $this->auth = $r['auth']; + $this->environment_data = $r['environment_data']; return true; } } diff --git a/fuel/app/classes/materia/widget/instance.php b/fuel/app/classes/materia/widget/instance.php index 1291b2a8b..526847108 100644 --- a/fuel/app/classes/materia/widget/instance.php +++ b/fuel/app/classes/materia/widget/instance.php @@ -467,6 +467,10 @@ public function duplicate(int $owner_id, string $new_name = null, bool $copy_exi // update name if ( ! empty($new_name)) $duplicate->name = $new_name; + // is_embedded and embedded_only should default to false for new instances (since the new instance won't have the play history requisite for is_embedded) + $duplicate->is_embedded = false; + $duplicate->embedded_only = false; + // these values aren't saved to the db - but the frontend will make use of them $duplicate->clean_name = \Inflector::friendly_title($duplicate->name, '-', true); $base_url = "{$duplicate->id}/{$duplicate->clean_name}"; diff --git a/fuel/app/classes/materia/widget/instance/manager.php b/fuel/app/classes/materia/widget/instance/manager.php index d6ef5f140..65f7251c1 100644 --- a/fuel/app/classes/materia/widget/instance/manager.php +++ b/fuel/app/classes/materia/widget/instance/manager.php @@ -14,7 +14,7 @@ static public function get($inst_id, $load_qset=false, $timestamp=false, $delete return count($instances) > 0 ? $instances[0] : false; } - static public function get_all(Array $inst_ids, $load_qset=false, $timestamp=false, bool $deleted=false): array + static public function get_all(Array $inst_ids, $load_qset=false, $timestamp=false, bool $deleted=false, $offset=0, $limit=80): array { if ( ! is_array($inst_ids) || count($inst_ids) < 1) return []; @@ -27,6 +27,9 @@ static public function get_all(Array $inst_ids, $load_qset=false, $timestamp=fal ->where('id', 'IN', $inst_ids) ->and_where('is_deleted', '=', $deleted ? '1' : '0') ->order_by('created_at', 'desc') + ->order_by('id', 'desc') + ->offset("$offset") + ->limit("$limit") ->execute() ->as_array(); @@ -63,7 +66,7 @@ public static function get_all_for_user($user_id, $load_qset=false) { $inst_ids = Perm_Manager::get_all_objects_for_user($user_id, Perm::INSTANCE, [Perm::FULL, Perm::VISIBLE]); - if ( ! empty($inst_ids)) return Widget_Instance_Manager::get_all($inst_ids, $load_qset); + if ( ! empty($inst_ids)) return self::get_all($inst_ids, $load_qset); else return []; } @@ -76,22 +79,25 @@ public static function get_all_for_user($user_id, $load_qset=false) * * @return array of widget instances that are visible to the user. */ - public static function get_paginated_for_user($user_id, $page_number = 0) + public static function get_paginated_instances_for_user($user_id, $page_number = 0) { $inst_ids = Perm_Manager::get_all_objects_for_user($user_id, Perm::INSTANCE, [Perm::FULL, Perm::VISIBLE]); - $displayable_inst = self::get_all($inst_ids); - $widgets_per_page = 80; - $total_num_pages = ceil(sizeof($displayable_inst) / $widgets_per_page); - $offset = $widgets_per_page * $page_number; - $has_next_page = $offset + $widgets_per_page < sizeof($displayable_inst) ? true : false; - // inst_ids corresponds to a single page's worth of instances - $displayable_inst = array_slice($displayable_inst, $offset, $widgets_per_page); + $items_per_page = 80; + $offset = $items_per_page * $page_number; + + // query DB for only a single page of instances + 1 + $displayable_items = self::get_all($inst_ids, false, false, false, $offset, $items_per_page + 1); + + // if the returned number of instances is greater than a page, there's more pages + $has_next_page = sizeof($displayable_items) > $items_per_page ? true : false; + + if ($has_next_page) array_pop($displayable_items); $data = [ - 'pagination' => $displayable_inst, + 'pagination' => $displayable_items ]; - + if ($has_next_page) $data['next_page'] = $page_number + 1; return $data; @@ -135,20 +141,55 @@ public static function lock($inst_id) return $locked_by == $me; } + /** + * Widget instance paginated search results + * + * @param input search input + * @param page_number page number + * + * @return array of items related to the given input + */ + public static function get_paginated_instance_search(string $input, $page_number = 0) + { + $items_per_page = 80; + $offset = $items_per_page * $page_number; + + // query DB for only a single page of instances + 1 + $displayable_items = self::get_widget_instance_search($input, $offset, $items_per_page + 1); + + // if the returned number of instances is greater than a page, there's more pages + $has_next_page = sizeof($displayable_items) > $items_per_page ? true : false; + + if ($has_next_page) array_pop($displayable_items); + + $data = [ + 'pagination' => $displayable_items, + ]; + + if ($has_next_page) $data['next_page'] = $page_number + 1; + + return $data; + } + /** * Gets all widget instances related to a given input, including id or name. * * @param input search input + * @param offset start search at this row in results + * @param limit number of rows to include * * @return array of widget instances related to the given input */ - public static function get_search(string $input): array + public static function get_widget_instance_search(string $input, int $offset = 0, int $limit = 80): array { $results = \DB::select() ->from('widget_instance') ->where('id', 'LIKE', "%$input%") ->or_where('name', 'LIKE', "%$input%") ->order_by('created_at', 'desc') + ->order_by('id', 'desc') + ->offset($offset) + ->limit($limit) ->execute() ->as_array(); diff --git a/fuel/app/classes/model/user.php b/fuel/app/classes/model/user.php index c29874777..122456326 100644 --- a/fuel/app/classes/model/user.php +++ b/fuel/app/classes/model/user.php @@ -87,7 +87,7 @@ public static function find_by_username($username) ->get_one(); } - static public function find_by_name_search($name) + static public function find_by_name_search($name, $offset = 0, $limit=80) { $name = preg_replace('/\s+/', '', $name); // remove spaces @@ -108,11 +108,19 @@ static public function find_by_name_search($name) ->or_where(\DB::expr('REPLACE(CONCAT(first, last), " ", "")'), 'LIKE', "%$name%") ->or_where('email', 'LIKE', "$name%") ->and_where_close() - ->limit(50) + ->offset($offset) + ->limit($limit) ->as_object('Model_User') ->execute(); - return $matches; + // convert object to array + $list = []; + foreach ($matches as $match) + { + $list[] = $match; + } + + return $list; } public static function validate($factory) diff --git a/fuel/app/config/css.php b/fuel/app/config/css.php index 60a1c9000..a00308cf2 100644 --- a/fuel/app/config/css.php +++ b/fuel/app/config/css.php @@ -14,42 +14,43 @@ ], 'groups' => [ - 'homepage' => [$webpack.'css/homepage.css'], - 'user-admin' => [$webpack.'css/user-admin.css'], - 'support' => [$webpack.'css/support.css'], - 'catalog' => [$webpack.'css/catalog.css'], - 'detail' => [$webpack.'css/detail.css'], - 'playpage' => [$webpack.'css/player-page.css'], - 'lti' => [ - $webpack.'css/lti.css', - $webpack.'css/lti-select-item.css', - $webpack.'css/lti-error.css', + 'main' => [$webpack.'css/commons.css'], + 'homepage' => [$webpack.'css/homepage.css'], + 'user-admin' => [$webpack.'css/user-admin.css'], + 'support' => [$webpack.'css/support.css'], + 'catalog' => [$webpack.'css/catalog.css'], + 'detail' => [$webpack.'css/detail.css'], + 'playpage' => [$webpack.'css/player-page.css'], + 'lti' => [ + $webpack.'css/lti.css', + $webpack.'css/lti-select-item.css', + $webpack.'css/lti-error.css', ], - 'my_widgets' => [$webpack.'css/my-widgets.css'], - 'widget_create' => [$webpack.'css/creator-page.css'], - 'profile' => [$webpack.'css/profile.css'], - 'login' => [$webpack.'css/login.css'], - 'scores' => [$webpack.'css/scores.css'], + 'my_widgets' => [$webpack.'css/my-widgets.css'], + 'widget_create' => [$webpack.'css/creator-page.css'], + 'profile' => [$webpack.'css/profile.css'], + 'login' => [$webpack.'css/login.css'], + 'scores' => [$webpack.'css/scores.css'], 'pre_embed_placeholder' => [$webpack.'css/pre-embed-common-styles.css'], - 'embed_scores' => [$webpack.'css/scores.css'], - 'question_import' => [ - $vendor.'jquery.dataTables.min.css', - $webpack.'css/util-question-import.css', - $webpack.'css/question-importer.css', + 'embed_scores' => [$webpack.'css/scores.css'], + 'question_import' => [ + $vendor.'jquery.dataTables.min.css', + $webpack.'css/util-question-import.css', + $webpack.'css/question-importer.css', ], - 'questionimport' => [$webpack.'css/question-importer.css'], - 'qset_history' => [$webpack.'css/qset-history.css'], - 'rollback_dialog' => [$webpack.'css/util-rollback-confirm.css'], - 'media_import' => [$webpack.'css/media.css'], - 'help' => [$webpack.'css/help.css'], - 'fonts' => [ - $g_fonts.'css2?family=Kameron:wght@700&display=block', - $g_fonts.'css2?family=Lato:ital,wght@0,300;0,400;0,700;0,900;1,700&display=block', + 'questionimport' => [$webpack.'css/question-importer.css'], + 'qset_history' => [$webpack.'css/qset-history.css'], + 'rollback_dialog' => [$webpack.'css/util-rollback-confirm.css'], + 'media_import' => [$webpack.'css/media.css'], + 'help' => [$webpack.'css/help.css'], + 'fonts' => [ + $g_fonts.'css2?family=Kameron:wght@700&display=block', + $g_fonts.'css2?family=Lato:ital,wght@0,300;0,400;0,700;0,900;1,700&display=block', ], - 'guide' => [$webpack.'css/guides.css'], + 'guide' => [$webpack.'css/guides.css'], 'draft-not-playable' => [$webpack.'css/draft-not-playable.css'], - '404' => [$webpack.'css/404.css'], - '500' => [$webpack.'css/500.css'], - 'no_permission' => [$webpack.'css/no-permission.css'] + '404' => [$webpack.'css/404.css'], + '500' => [$webpack.'css/500.css'], + 'no_permission' => [$webpack.'css/no-permission.css'] ], ]; \ No newline at end of file diff --git a/fuel/app/config/js.php b/fuel/app/config/js.php index 582187e15..f23e1cfb8 100644 --- a/fuel/app/config/js.php +++ b/fuel/app/config/js.php @@ -37,9 +37,8 @@ 'open_preview' => [$webpack.'js/lti-open-preview.js'], 'error_general' => [$webpack.'js/lti-error.js'], 'react' => [ - '//unpkg.com/react@16.13.1/umd/react.development.js', - '//unpkg.com/react-dom@16.13.1/umd/react-dom.development.js', - $webpack.'js/include.js' + $webpack.'js/include.js', + $webpack.'js/commons.js' ], 'question-importer' => [$webpack.'js/question-importer.js'] ] diff --git a/fuel/app/modules/lti/classes/ltievents.php b/fuel/app/modules/lti/classes/ltievents.php index 217c2c63a..a7e6d455c 100644 --- a/fuel/app/modules/lti/classes/ltievents.php +++ b/fuel/app/modules/lti/classes/ltievents.php @@ -252,13 +252,13 @@ protected static function get_lti_play_state($play_id = false) // Do we have session vars stored by the given play_id? // We only store variables by the first play ID, so this is the first attempt - $launch = \Session::get("lti-{$play_id}", false); + $launch = static::get_lti_launch($play_id); if ($launch) return self::PLAY_STATE_FIRST_LAUNCH; // Do we have variables that are *linked* to the given play_id? // We only do this for replays, so this is a replay - $token = \Session::get("lti-link-{$play_id}", false); - $launch = \Session::get("lti-{$token}", false); + $token = static::get_lti_token($play_id); + $launch = static::get_lti_launch($token); if ($launch) return self::PLAY_STATE_REPLAY; // Nothing in the request, nothing in the session, assume not an LTI launch @@ -267,11 +267,11 @@ protected static function get_lti_play_state($play_id = false) protected static function session_get_launch($play_id) { - $launch = \Session::get("lti-{$play_id}", false); + $launch = static::get_lti_launch($play_id); if ($launch) return $launch; - $token = \Session::get("lti-link-{$play_id}", false); - return \Session::get("lti-{$token}", false); + $token = static::get_lti_token($play_id); + return static::get_lti_launch($token); } protected static function store_lti_request_into_session($token, $inst_id, $is_embedded) @@ -345,4 +345,33 @@ protected static function log($play_id) Log::profile($log_array, 'lti'); } + + // Attempts to retrieve LTI token from session. + // If not present, instantiate a play with the provided ID and check its environment data for the token instead. + protected static function get_lti_token($play_id) + { + $token = \Session::get("lti-link-{$play_id}", false); + + if ( ! $token) + { + $play = new \Materia\Session_Play(); + $play->get_by_id($play_id); + + if ($play->auth == 'lti') + { + $ev_data = json_decode(base64_decode($play->environment_data), true); + if (isset($ev_data['input']['token'])) $token = $ev_data['input']['token']; + } + } + + return $token; + } + + // Attempts to retrieve LTI launch data from session. + // TODO: future versions of Materia should consider storing and pulling launch data from the associated play's environment data instead + protected static function get_lti_launch($token) + { + $launch = \Session::get("lti-{$token}", false); + return $launch; + } } diff --git a/fuel/app/tests/api/v1.php b/fuel/app/tests/api/v1.php index 42b524c6d..2300c9529 100644 --- a/fuel/app/tests/api/v1.php +++ b/fuel/app/tests/api/v1.php @@ -180,9 +180,32 @@ public function test_widget_instances_get() } - public function test_widget_paginate_instances_get() + public function test_widget_paginate_user_instances_get() { + // Create widget instance + $this->_as_author(); + $title = "My Test Widget"; + $question = 'What rhymes with harvest fests but are half as exciting (or tasty)'; + $answer = 'Tests'; + $qset = $this->create_new_qset($question, $answer); + $widget = $this->make_disposable_widget(); + + $instance = Api_V1::widget_instance_new($widget->id, $title, $qset, true); + + // ----- loads author's instances -------- + $output = Api_V1::widget_paginate_user_instances_get(); + $this->assertIsArray($output); + $this->assertArrayHasKey('pagination', $output); + foreach ($output['pagination'] as $key => $value) + { + $this->assert_is_widget_instance($value, true); + } + // ======= AS NO ONE ======== + \Auth::logout(); + // ----- returns no login -------- + $output = Api_V1::widget_paginate_user_instances_get(); + $this->assert_invalid_login_message($output); } public function test_widget_instance_new() @@ -1024,10 +1047,6 @@ public function test_play_logs_get() } - public function test_paginated_play_logs_get() - { - } - public function test_score_summary_get() { // ======= AS NO ONE ======== @@ -1522,10 +1541,10 @@ public function test_users_search_as_student() $output = Api_V1::users_search('droptables'); $this->assertIsArray($output); - $this->assertCount(2, $output); - $this->assert_is_user_array($output[0]); - $this->assertFalse(array_key_exists('password', $output)); - $this->assertFalse(array_key_exists('login_hash', $output)); + $this->assertIsArray($output['pagination']); + $this->assert_is_user_array($output['pagination'][0]); + $this->assertFalse(array_key_exists('password', $output['pagination'])); + $this->assertFalse(array_key_exists('login_hash', $output['pagination'])); } public function test_users_search_as_author() @@ -1538,10 +1557,10 @@ public function test_users_search_as_author() $output = Api_V1::users_search('droptables'); $this->assertIsArray($output); - $this->assertCount(2, $output); - $this->assert_is_user_array($output[0]); - $this->assertFalse(array_key_exists('password', $output)); - $this->assertFalse(array_key_exists('login_hash', $output)); + $this->assertIsArray($output['pagination']); + $this->assert_is_user_array($output['pagination'][0]); + $this->assertFalse(array_key_exists('password', $output['pagination'])); + $this->assertFalse(array_key_exists('login_hash', $output['pagination'])); } public function test_users_search_as_super_user() @@ -1554,10 +1573,10 @@ public function test_users_search_as_super_user() $output = Api_V1::users_search('droptables'); $this->assertIsArray($output); - $this->assertCount(2, $output); - $this->assert_is_user_array($output[0]); - $this->assertFalse(array_key_exists('password', $output[0])); - $this->assertFalse(array_key_exists('login_hash', $output[0])); + $this->assertIsArray($output['pagination']); + $this->assert_is_user_array($output['pagination'][0]); + $this->assertFalse(array_key_exists('password', $output['pagination'])); + $this->assertFalse(array_key_exists('login_hash', $output['pagination'])); } protected function assert_is_semester_rage($semester) diff --git a/fuel/app/tests/model/user.php b/fuel/app/tests/model/user.php index 4cd42d0c4..7d7c13c2e 100644 --- a/fuel/app/tests/model/user.php +++ b/fuel/app/tests/model/user.php @@ -15,12 +15,12 @@ public function test_find_by_name_search_doesnt_find_super_users() { // su should't be found $su = $this->make_random_super_user(); - $x = Model_User::find_by_name_search($su->email)->as_array(); + $x = Model_User::find_by_name_search($su->email); self::assertEmpty($x); // add a student with the same name, should only find the one student $this->make_random_student(); - $x = Model_User::find_by_name_search('drop')->as_array(); + $x = Model_User::find_by_name_search('drop'); self::assertCount(1, $x); self::assertInstanceOf('Model_User', $x[0]); self::assertNotEquals($su->id, $x[0]->id); @@ -30,7 +30,7 @@ public function test_find_by_name_search_finds_students_by_email() { $user = $this->make_random_student(); - $x = Model_User::find_by_name_search($user->email)->as_array(); + $x = Model_User::find_by_name_search($user->email); self::assertCount(1, $x); self::assertInstanceOf('Model_User', $x[0]); self::assertEquals($user->id, $x[0]->id); @@ -40,7 +40,7 @@ public function test_find_by_name_search_finds_students_by_first_name() { $user = $this->make_random_student(); - $x = Model_User::find_by_name_search($user->first)->as_array(); + $x = Model_User::find_by_name_search($user->first); self::assertCount(1, $x); self::assertInstanceOf('Model_User', $x[0]); self::assertEquals($user->id, $x[0]->id); @@ -50,7 +50,7 @@ public function test_find_by_name_search_finds_students_by_last_name() { $user = $this->make_random_student(); - $x = Model_User::find_by_name_search($user->last)->as_array(); + $x = Model_User::find_by_name_search($user->last); self::assertCount(1, $x); self::assertInstanceOf('Model_User', $x[0]); self::assertEquals($user->id, $x[0]->id); @@ -60,7 +60,7 @@ public function test_find_by_name_search_finds_students_by_username() { $user = $this->make_random_student(); - $x = Model_User::find_by_name_search($user->username)->as_array(); + $x = Model_User::find_by_name_search($user->username); self::assertCount(1, $x); self::assertInstanceOf('Model_User', $x[0]); self::assertEquals($user->id, $x[0]->id); @@ -70,7 +70,7 @@ public function test_find_by_name_search_finds_students() { $user = $this->make_random_student(); - $x = Model_User::find_by_name_search($user->email)->as_array(); + $x = Model_User::find_by_name_search($user->email); self::assertCount(1, $x); self::assertInstanceOf('Model_User', $x[0]); self::assertEquals($user->id, $x[0]->id); @@ -79,7 +79,7 @@ public function test_find_by_name_search_finds_students() public function test_find_by_name_search_finds_authors() { $user = $this->make_random_author(); - $x = Model_User::find_by_name_search($user->email)->as_array(); + $x = Model_User::find_by_name_search($user->email); self::assertCount(1, $x); self::assertInstanceOf('Model_User', $x[0]); self::assertEquals($user->id, $x[0]->id); @@ -90,7 +90,7 @@ public function test_find_by_name_search_finds_multiple_matches() $user1 = $this->make_random_author(); $user2 = $this->make_random_student(); - $x = Model_User::find_by_name_search('drop')->as_array(); + $x = Model_User::find_by_name_search('drop'); self::assertCount(2, $x); self::assertInstanceOf('Model_User', $x[0]); self::assertInstanceOf('Model_User', $x[1]); diff --git a/materia-app.Dockerfile b/materia-app.Dockerfile index 776f6c291..0efa9e484 100644 --- a/materia-app.Dockerfile +++ b/materia-app.Dockerfile @@ -40,6 +40,11 @@ RUN php composer-setup.php --install-dir=/usr/local/bin --filename=composer --ve # Use the default production configuration RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" +# By default the php-fpm image is including access.log in the docker stream +# These logs aren't particularly useful and add considerable bloat to the prod logs that have to be filtered out +# Modify php-fpm.d/docker.conf to point access.log to /dev/null/, which effectively prevents it from being picked up by the log driver +RUN sed -i 's/access.log = .*/access.log = \/dev\/null/' /usr/local/etc/php-fpm.d/docker.conf + WORKDIR /var/www/html # ===================================================================================================== diff --git a/package.json b/package.json index 1e57d1913..d6a4a7170 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "d3": "^7.2.0", "fs-extra": "^8.0.1", "js-base64": "^3.7.2", - "react-datepicker": "^4.8.0", + "react-datepicker": "^5.0.0", "react-overlays": "^5.2.1", "react-query": "^3.39.2", "uuid": "^9.0.1" @@ -55,11 +55,11 @@ "jquery-ui": "1.12.1", "lint-staged": "^10.2.11", "mini-css-extract-plugin": "^2.7.2", - "node-sass": "^8.0.0", "nodemon": "^2.0.20", "react": "^16.13.1", "react-dom": "^16.13.1", "react-test-renderer": "^17.0.2", + "sass": "^1.69.5", "sass-loader": "^13.2.0", "webpack": "^5.76.0", "webpack-cli": "^5.0.1", diff --git a/public/dist/package.json b/public/dist/package.json index 4dad46c01..6ae44d552 100644 --- a/public/dist/package.json +++ b/public/dist/package.json @@ -5,6 +5,7 @@ "license": "AGPL-3.0", "files": [ "path.js", + "js/commons.js", "js/materia.js", "js/materia.enginecore.js", "js/materia.creatorcore.js", @@ -18,11 +19,11 @@ "js/question-importer.js", "css/question-importer.css", "js/guides.js", - "css/widget-guide.css", + "css/guides.css", "js/scores.js", "css/scores.css" ], - "version": "0.2.0", + "version": "0.4.0", "repository": { "type": "git", "url": "https://github.com/ucfopen/Materia" diff --git a/src/components/attempts-slider.jsx b/src/components/attempts-slider.jsx index 1fb7a0e37..b3977b7c2 100644 --- a/src/components/attempts-slider.jsx +++ b/src/components/attempts-slider.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react' import './my-widgets-settings-dialog.scss' -const AttemptsSlider = ({inst, parentState, setParentState}) => { +const AttemptsSlider = ({inst, is_student, parentState, setParentState, currentAttemptsVal}) => { const [rawSliderVal, setRawSliderVal] = useState(parseInt(parentState.sliderVal)) const [sliderStopped, setSliderStopped] = useState(false) @@ -16,12 +16,19 @@ const AttemptsSlider = ({inst, parentState, setParentState}) => { const sliderStop = e => { setSliderStopped(true) } - + // now that the slider value isn't actively changing, round the raw value to the nearest stop // pass that rounded value up to the parent component useEffect(() => { if (sliderStopped && parentState.formData.changes.access != 'guest') { const sliderInfo = getSliderInfo(rawSliderVal) + // students cannot change attempts to anything other than + // the original number of attempts or unlimited + if (is_student && sliderInfo.val != currentAttemptsVal && sliderInfo.val != '100') { + setSliderStopped(false) + setRawSliderVal(parseInt(parentState.sliderVal)) + return + } setParentState({...parentState, sliderVal: sliderInfo.val, lastActive: sliderInfo.last}) setSliderStopped(false) } @@ -66,21 +73,26 @@ const AttemptsSlider = ({inst, parentState, setParentState}) => { const updateSliderNum = (val, index) => { // Attempts always unlimited when guest access is true if (parentState.formData.changes.access === 'guest') return + if (is_student && val != currentAttemptsVal && val != '100') return setParentState({...parentState, sliderVal: val.toString(), lastActive: index}) } const generateStopSpan = (stopId, sliderPosition, display) => { - const spanClass = parentState.lastActive === stopId ? 'active' : '' const stopClickHandler = () => updateSliderNum(sliderPosition, stopId) return ( {display} ) } + const selectChange = e => { + if (parentState.formData.changes.access === 'guest') return + let sliderInfo = getSliderInfo(parseInt(e.target.value)) + setParentState({...parentState, sliderVal: sliderInfo.val, lastActive: sliderInfo.last}) + } let guestModeRender = null if (parentState.formData.changes.access === 'guest') { @@ -93,6 +105,19 @@ const AttemptsSlider = ({inst, parentState, setParentState}) => { return (
Your session with Materia is considered invalid and you have been logged out. You'll have to log back in to see this content.
+ + + Okay + + +