diff --git a/resources/views/table/wp-users/books-column.blade.php b/resources/views/table/wp-users/books-column.blade.php new file mode 100644 index 00000000..c8567e79 --- /dev/null +++ b/resources/views/table/wp-users/books-column.blade.php @@ -0,0 +1,16 @@ +@foreach ($books as $book) + + + {{ $book->domain . $book->path }} + + + + {{ __('Dashboard', 'pressbooks-multi-institution') }} | + + + {{ __('View', 'pressbooks-multi-institution') }} + + + +
+@endforeach diff --git a/src/Actions/AssignUserToInstitution.php b/src/Actions/AssignUserToInstitution.php index a44e55bc..1f28d8b7 100644 --- a/src/Actions/AssignUserToInstitution.php +++ b/src/Actions/AssignUserToInstitution.php @@ -4,6 +4,9 @@ use Illuminate\Support\Str; use PressbooksMultiInstitution\Models\Institution; +use WP_User; + +use function PressbooksMultiInstitution\Support\get_institution_by_manager; class AssignUserToInstitution { @@ -15,6 +18,14 @@ public function handle(int $userId): bool return false; } + $institution = get_institution_by_manager(); + + return $institution === 0 ? + $this->assignByUserDomain($user) : $this->assignUserByInstitution($user, $institution); + } + + private function assignByUserDomain(WP_User $user): bool + { $email = Str::of($user->user_email); $domain = (string) $email->after('@')->trim(); @@ -27,7 +38,18 @@ public function handle(int $userId): bool } $institution->users()->create([ - 'user_id' => $userId, + 'user_id' => $user->ID, + ]); + + return true; + } + + private function assignUserByInstitution(WP_User $user, int $institution): bool + { + $institution = Institution::find($institution); + + $institution->users()->create([ + 'user_id' => $user->ID, ]); return true; diff --git a/src/Bootstrap.php b/src/Bootstrap.php index ab659da5..41d00c54 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.php @@ -11,6 +11,7 @@ use PressbooksMultiInstitution\Services\MenuManager; use PressbooksMultiInstitution\Services\PermissionsManager; use PressbooksMultiInstitution\Views\BookList; +use PressbooksMultiInstitution\Views\WpUserList; use PressbooksMultiInstitution\Views\UserList; /** @@ -40,6 +41,7 @@ public function setUp(): void Container::getInstance()->singleton(BookList::class, fn () => new BookList(app('db'))); Container::getInstance()->singleton(UserList::class, fn () => new UserList(app('db'))); + Container::getInstance()->singleton(WpUserList::class, fn () => new WpUserList); } private function registerActions(): void diff --git a/src/Services/MenuManager.php b/src/Services/MenuManager.php index 56b94b88..1118a880 100644 --- a/src/Services/MenuManager.php +++ b/src/Services/MenuManager.php @@ -86,21 +86,35 @@ public function handleMenus(): void if (!is_main_site() || !is_super_admin()) { unset($submenu['index.php'][0]); } + if (get_institution_by_manager() !== 0) { - remove_menu_page($this->getContextSlug('customize.php', true)); - remove_menu_page($this->getContextSlug('edit.php?post_type=page', true)); - // Remove the default dashboard page and point to the institutional dashboard - foreach ($menu as &$item) { - if ($item[2] == network_admin_url('index.php')) { - $item[2] = network_site_url('wp-admin/index.php?page=pb_institutional_manager'); - break; - } - } - remove_menu_page($this->getContextSlug('admin.php?page=pb_network_integrations', false)); - remove_menu_page('settings.php'); - remove_menu_page('pb_network_integrations'); - remove_menu_page($this->slug); + $this->removeMenuItems($menu); + add_action('admin_bar_menu', [$this, 'modifyAdminBarMenus'], 1000); + + if (count($submenu['users.php']) > 1) { + // remove submenu items for users menu + $submenu['users.php'] = array_slice($submenu['users.php'], 0, 1); + } + } + } + + private function removeMenuItems(array &$menu): void + { + remove_menu_page($this->getContextSlug('customize.php', true)); + remove_menu_page($this->getContextSlug('edit.php?post_type=page', true)); + remove_menu_page($this->getContextSlug('admin.php?page=pb_network_integrations', false)); + remove_menu_page('settings.php'); + remove_menu_page('pb_network_integrations'); + remove_menu_page($this->slug); + remove_menu_page('h5p'); + + // Remove the default dashboard page and point to the institutional dashboard + foreach ($menu as &$item) { + if ($item[2] == network_admin_url('index.php')) { + $item[2] = network_site_url('wp-admin/index.php?page=pb_institutional_manager'); + break; + } } } diff --git a/src/Services/PermissionsManager.php b/src/Services/PermissionsManager.php index 2b429e1a..083ed5d4 100644 --- a/src/Services/PermissionsManager.php +++ b/src/Services/PermissionsManager.php @@ -9,8 +9,10 @@ use PressbooksMultiInstitution\Views\BookList; use PressbooksMultiInstitution\Views\UserList; +use PressbooksMultiInstitution\Views\WpUserList; + use function Pressbooks\Admin\NetworkManagers\_restricted_users; -use function PressbooksMultiInstitution\Support\get_allowed_book_pages; +use function PressbooksMultiInstitution\Support\get_restricted_book_pages; use function PressbooksMultiInstitution\Support\get_allowed_pages; use function PressbooksMultiInstitution\Support\get_institution_books; use function PressbooksMultiInstitution\Support\get_institution_by_manager; @@ -39,6 +41,7 @@ public function setupFilters(): void Container::get(TableViews::class)->init(); Container::get(BookList::class)->init(); Container::get(UserList::class)->init(); + Container::get(WpUserList::class)->init(); do_action('pb_institutional_filters_created', $institution, $institutionalManagers, $institutionalUsers); } @@ -65,7 +68,7 @@ public function handlePagesPermissions($institution, $institutionalManagers, $in */ add_filter('can_edit_network', function ($canEdit) use ($allowedBooks) { - if (is_network_admin() && !in_array($_REQUEST['id'], $allowedBooks)) { + if (is_network_admin() && isset($_REQUEST['id']) && !in_array($_REQUEST['id'], $allowedBooks)) { $canEdit = false; } return $canEdit; @@ -157,7 +160,7 @@ private function currentUserHasAccess(string $currentPageParam, array $allowedBo global $pagenow; $allowedPages = get_allowed_pages(); - $bookPages = get_allowed_book_pages(); + $restrictedBookPages = get_restricted_book_pages(); $isAccessAllowed = false; @@ -181,18 +184,21 @@ private function currentUserHasAccess(string $currentPageParam, array $allowedBo // Check if the current page is a book page and if the user has access to it $userBooks = array_slice(array_keys(get_blogs_of_user(get_current_user_id())), 1); // remove the main site - if ((in_array($currentBlogId, $userBooks) || in_array($currentBlogId, $allowedBooks)) && !in_array($pagenow, $bookPages)) { + if ( + (in_array($currentBlogId, $userBooks) || in_array($currentBlogId, $allowedBooks)) && + !in_array($pagenow, $restrictedBookPages) + ) { $isAccessAllowed = true; } $institutionalUsers = apply_filters('pb_institutional_users', []); if ($currentPageParam === 'pb_network_analytics_userlist' || $pagenow === 'users.php' || $pagenow === 'user-edit.php') { - if (isset($_REQUEST['user_id']) && in_array($_REQUEST['user_id'], $institutionalUsers)) { - $isAccessAllowed = true; - } + $isAccessAllowed = true; + + $userId = $_REQUEST['id'] ?? $_REQUEST['user_id'] ?? null; - if (isset($_REQUEST['id']) && !in_array($_REQUEST['id'], $institutionalUsers)) { + if ($userId && ! in_array($userId, $institutionalUsers)) { $isAccessAllowed = false; } } diff --git a/src/Support/helpers.php b/src/Support/helpers.php index ab3a334a..b5093f90 100644 --- a/src/Support/helpers.php +++ b/src/Support/helpers.php @@ -67,13 +67,14 @@ function get_allowed_pages(): array 'plugins.php', 'media-new.php', 'users.php', + 'user-new.php', 'export-personal-data.php', 'erase-personal-data.php', 'options-privacy.php' ]; } -function get_allowed_book_pages(): array +function get_restricted_book_pages(): array { return [ 'site-info.php', diff --git a/src/Views/WpUserList.php b/src/Views/WpUserList.php new file mode 100644 index 00000000..bfb1dd64 --- /dev/null +++ b/src/Views/WpUserList.php @@ -0,0 +1,103 @@ + InstitutionUser::query() + ->where('user_id', $userId) + ->first() + ?->institution + ->name ?? __('Unassigned', 'pressbooks-multi-institution'), + 'books' => $this->getBooksColumnValue($userId), + default => $value, + }; + } + + private function getBooksColumnValue(int $userId): string + { + $blogs = get_blogs_of_user($userId); + + unset($blogs[get_main_site_id()]); + + return app('Blade')->render('PressbooksMultiInstitution::table.wp-users.books-column', [ + 'books' => $blogs, + ]); + } + + public function manageTableColumns(array $columns): array + { + unset($columns['blogs']); + + return array_slice($columns, 0, 4, true) + + ['institution' => __('Institution', 'pressbooks-multi-institution')] + + array_slice($columns, 4, null, true) + + ['books' => __('Books', 'pressbooks-multi-institution')]; + } + + public function makeInstitutionColumnSortable(array $columns): array + { + $columns['institution'] = 'institution'; + return $columns; + } + + public function modifyUserQuery(WP_User_Query $query): void + { + global $pagenow; + if (! is_super_admin() || ! is_main_site() || $pagenow !== 'users.php') { + return; + } + + global $wpdb; + $query->query_from .= " LEFT JOIN {$wpdb->base_prefix}institutions_users AS iu ON {$wpdb->users}.ID = iu.user_id"; + $query->query_from .= " LEFT JOIN {$wpdb->base_prefix}institutions AS i ON iu.institution_id = i.id"; + + $institution = get_institution_by_manager(); + if ($institution !== 0) { + $query->query_where .= $wpdb->prepare(" AND iu.institution_id = %d", $institution); + } + + $order = (isset($_GET['order']) && $_GET['order'] === 'asc') ? 'ASC' : 'DESC'; + + $query->query_orderby = "ORDER BY i.name " . $order; + } + + public function removeSuperAdminFilter(array $views): array + { + $institution = get_institution_by_manager(); + + if($institution === 0) { + return $views; + } + + unset($views['super']); + + $totalUsers = Institution::find($institution)->users()->count(); + $views['all'] = " " . + __('All', 'pressbooks-multi-institution') . " ({$totalUsers})"; + + return $views; + } +} diff --git a/tests/Feature/Views/WpUserListTest.php b/tests/Feature/Views/WpUserListTest.php new file mode 100644 index 00000000..3d1ed361 --- /dev/null +++ b/tests/Feature/Views/WpUserListTest.php @@ -0,0 +1,175 @@ +assertFalse(has_filter('wpmu_users_columns')); + $this->assertFalse(has_filter('manage_users_custom_column')); + $this->assertFalse(has_filter('manage_users-network_sortable_columns')); + $this->assertFalse(has_action('pre_user_query')); + $this->assertFalse(has_filter('views_users-network')); + + Container::get(WpUserList::class)->init(); + + $this->assertTrue(has_filter('wpmu_users_columns')); + $this->assertTrue(has_filter('manage_users_custom_column')); + $this->assertTrue(has_filter('manage_users-network_sortable_columns')); + $this->assertTrue(has_action('pre_user_query')); + $this->assertTrue(has_filter('views_users-network')); + } + + /** + * @test + */ + public function it_displays_custom_columns_value(): void + { + $this->createInstitutionsUsers(1, 1); + + $institution = Institution::query()->first(); + $userId = $institution->users()->first()->user_id; + + $bookId = $this->newBook(); + + // assign $userId to $bookId + add_user_to_blog($bookId, $userId, 'administrator'); + + $wpUserList = Container::get(WpUserList::class); + + $this->assertEquals( + $institution->name, + $wpUserList->displayCustomColumns('', 'institution', $userId) + ); + + $this->assertEquals( + 'Unassigned', + $wpUserList->displayCustomColumns('', 'institution', 99) + ); + + $this->assertStringContainsString( + '', + $wpUserList->displayCustomColumns('', 'books', $userId) + ); + + $this->assertStringContainsString( + 'Dashboard', + $wpUserList->displayCustomColumns('', 'books', $userId) + ); + + $this->assertStringContainsString( + 'View', + $wpUserList->displayCustomColumns('', 'books', $userId) + ); + } + + /** + * @test + */ + public function it_adds_institution_column(): void + { + $columns = ['name' => 'Name', 'email' => 'Email', 'registered' => 'Registered']; + $expected = ['name' => 'Name', 'email' => 'Email', 'institution' => 'Institution', 'registered' => 'Registered', 'books' => 'Books']; + + $this->assertEquals( + $expected, + Container::get(WpUserList::class)->manageTableColumns($columns) + ); + } + + /** + * @test + */ + public function it_makes_institution_column_sortable(): void + { + $columns = ['name' => 'name', 'email' => 'email', 'registered' => 'registered']; + $expected = ['name' => 'name', 'email' => 'email', 'registered' => 'registered', 'institution' => 'institution']; + + $this->assertEquals( + $expected, + Container::get(WpUserList::class)->makeInstitutionColumnSortable($columns) + ); + } + + /** + * @test + */ + public function it_modifies_user_query(): void + { + $query = new \WP_User_Query(['blog_id' => 1]); + + global $pagenow; + $pagenow = 'users.php'; + $userId = $this->newSuperAdmin(); + wp_set_current_user($userId); + $_GET['order'] = 'desc'; + + Container::get(WpUserList::class)->modifyUserQuery($query); + + $this->assertStringContainsString('LEFT JOIN', $query->query_from); + $this->assertStringContainsString('institutions_users', $query->query_from); + $this->assertStringContainsString('institutions', $query->query_from); + $this->assertStringContainsString('ORDER BY i.name DESC', $query->query_orderby); + } + + /** + * @test + */ + public function it_add_where_conditions_to_query_for_ims(): void + { + $query = new \WP_User_Query(['blog_id' => 1]); + + $this->createInstitutionsUsers(1, 3); + + $institution = Institution::query()->first(); + $userId = $this->newInstitutionalManager($institution); + + grant_super_admin($userId); + wp_set_current_user($userId); + + global $pagenow; + $pagenow = 'users.php'; + + Container::get(WpUserList::class)->modifyUserQuery($query); + + $this->assertStringContainsString('LEFT JOIN', $query->query_from); + $this->assertStringContainsString('institutions_users', $query->query_from); + $this->assertStringContainsString('institutions', $query->query_from); + $this->assertStringContainsString('AND iu.institution_id = ' . $institution->id, $query->query_where); + } + + /** + * @test + */ + public function it_removes_super_admin_filter(): void + { + $this->createInstitutionsUsers(1, 3); + + $institution = Institution::query()->first(); + $userId = $this->newInstitutionalManager($institution); + wp_set_current_user($userId); + + $views = ['all' => 'All', 'super' => 'Super']; + $totalUsers = $institution->users()->count(); + + $this->assertEquals( + ['all' => " All ({$totalUsers})"], + Container::get(WpUserList::class)->removeSuperAdminFilter($views) + ); + } +}