Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix pagination issues for facility user page #9422

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions kolibri/core/auth/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,8 @@ class FacilityUserViewSet(ValuesViewset):
DjangoFilterBackend,
filters.SearchFilter,
)
order_by_field = "username"

queryset = FacilityUser.objects.all()
serializer_class = FacilityUserSerializer
filter_class = FacilityUserFilter
Expand Down Expand Up @@ -338,6 +340,7 @@ def consolidate(self, items, queryset):
roles.append(role)
item["roles"] = roles
output.append(item)
output = sorted(output, key=lambda x: x[self.order_by_field])
return output

def set_password_if_needed(self, instance, serializer):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pickBy from 'lodash/pickBy';
import { FacilityUserResource } from 'kolibri.resources';
import samePageCheckGenerator from 'kolibri.utils.samePageCheckGenerator';
import { _userState } from '../mappers';
Expand All @@ -6,7 +7,13 @@ export function showUserPage(store, toRoute) {
store.dispatch('preparePage');
const facilityId = toRoute.params.facility_id || store.getters.activeFacilityId;
return FacilityUserResource.fetchCollection({
getParams: { member_of: facilityId, page_size: 30 },
getParams: pickBy({
member_of: facilityId,
page: toRoute.query.page || 1,
page_size: toRoute.query.page_size || 30,
search: toRoute.query.search && toRoute.query.search.trim(),
user_type: toRoute.query.user_type,
}),
force: true,
}).only(
samePageCheckGenerator(store),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,29 @@
:layout12="{ span: 5, alignment: 'right' }"
class="text-filter"
>
<FilterTextbox v-model="filterInput" :placeholder="filterPlaceholder" />
<slot name="filter"></slot>
</KGridItem>
</KGrid>

<div>
<slot
:items="userList"
:filterInput="filterInput"
>
<slot>
</slot>
</div>

<nav class="pagination-nav">
<span dir="auto" class="pagination-label">
{{ $tr('pagination', { visibleStartRange, visibleEndRange, numFilteredItems }) }}
{{ $translate('pagination', { visibleStartRange, visibleEndRange, numFilteredItems }) }}
</span>
<KButtonGroup>
<KIconButton
:ariaLabel="$tr('previousResults')"
:ariaLabel="$translate('previousResults')"
:disabled="previousButtonDisabled"
size="small"
icon="back"
@click="changePage(-1)"
/>
<KIconButton
:ariaLabel="$tr('nextResults')"
:ariaLabel="$translate('nextResults')"
:disabled="nextButtonDisabled"
size="small"
icon="forward"
Expand All @@ -50,172 +47,64 @@
<script>

import clamp from 'lodash/clamp';
import FilterTextbox from 'kolibri.coreVue.components.FilterTextbox';
import { FacilityUserResource } from 'kolibri.resources';
import store from 'kolibri.coreVue.vuex.store';
import { _userState } from '../modules/mappers';
import PaginatedListContainer from 'kolibri.coreVue.components.PaginatedListContainer';
import { crossComponentTranslator } from 'kolibri.utils.i18n';

export default {
name: 'PaginatedListContainerWithBackend',
components: {
FilterTextbox,
},
props: {
// The entire list of items
items: {
type: Array,
required: true,
},
filterPlaceholder: {
type: String,
required: true,
},
itemsPerPage: {
type: Number,
required: false,
default: 30,
required: true,
},
totalPageNumber: {
type: Number,
required: false,
default: 1,
required: true,
},
totalUsers: {
value: {
type: Number,
required: true,
},
roleFilter: {
type: Object,
required: false,
default: null,
},
excludeMemberOf: {
type: String,
required: false,
default: '',
},
userAssignmentType: {
type: String,
required: false,
default: '',
numFilteredItems: {
type: Number,
required: true,
},
},
data() {
return {
filterInput: '',
currentPage: 1,
userList: this.items,
totalPageNumbers: this.totalPageNumber,
usersCount: this.totalUsers,
};
},
computed: {
numFilteredItems() {
return this.usersCount;
},
startRange() {
return (this.currentPage - 1) * this.itemsPerPage;
return (this.value - 1) * this.itemsPerPage;
},
visibleStartRange() {
// return this.currentPage;
return Math.min(this.startRange + 1, this.numFilteredItems);
},
endRange() {
return this.currentPage * this.itemsPerPage;
return this.value * this.itemsPerPage;
},
visibleEndRange() {
return Math.min(this.endRange, this.numFilteredItems);
},
previousButtonDisabled() {
return this.currentPage === 1 || this.numFilteredItems === 0;
return this.value === 1 || this.numFilteredItems === 0;
},
nextButtonDisabled() {
return (
this.totalPageNumbers === 1 ||
this.currentPage === this.totalPageNumbers ||
this.totalPageNumber === 1 ||
this.value === this.totalPageNumber ||
this.numFilteredItems === 0
);
},
},
watch: {
filterInput: {
handler() {
this.currentPage = 1;
this.get_users();
},
},
roleFilter: {
handler() {
this.currentPage = 1;
this.get_users();
},
},
items: {
handler() {
this.get_users();
},
},
beforeCreate() {
this.$translator = crossComponentTranslator(PaginatedListContainer);
},
methods: {
get_users() {
const facilityId = store.getters.activeFacilityId;
FacilityUserResource.fetchCollection({
getParams: {
member_of: facilityId,
page_size: this.itemsPerPage,
page: this.currentPage,
search: this.filterInput,
exclude_member_of: !this.excludeMemberOf ? '' : this.excludeMemberOf,
user_type:
!this.roleFilter || this.roleFilter.value === 'all' ? '' : this.roleFilter.value,
exclude_user_type: this.userAssignmentType === 'coaches' ? 'learner' : '',
},
force: true,
}).then(
users => {
this.currentPage = users.page;
this.userList = users.results.map(_userState);
this.totalPageNumbers = users.total_pages;
this.usersCount = users.count;
},
error => {
// check if this error is raised by the api because of currentPage is more than
// the total pages
if (
error.response.status === 404 &&
error.response.data &&
error.response.data[0].id === 'NOT_FOUND'
) {
// set the currentPage to 1 and recall the api
this.currentPage = 1;
this.get_users();
} else {
store.dispatch('handleApiError', error);
}
}
);
},
changePage(change) {
// Clamp the newPage number between the bounds if browser doesn't correctly
// disable buttons (see #6454 issue with old versions of MS Edge)
this.currentPage = clamp(this.currentPage + change, 1, this.totalPageNumbers);
this.get_users();
},
},
$trs: {
previousResults: {
message: 'Previous results',
context:
'Text which indicates the previous page of results when a user makes a search query.\n',
},
nextResults: {
message: 'Next results',
context: 'Text which indicates the next page of results when a user makes a search query.',
this.$emit('input', clamp(this.value + change, 1, this.totalPageNumber));
},
pagination: {
message:
'{ visibleStartRange, number } - { visibleEndRange, number } of { numFilteredItems, number }',
context: "Refers to pagination. Only translate the word \"of''.",
$translate(msg, params) {
return this.$translator.$tr(msg, params);
},
},
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import mock from 'xhr-mock';
import { mount, RouterLinkStub } from '@vue/test-utils';
import VueRouter from 'vue-router';
import makeStore from '../../../../test/makeStore';
import UserPage from '../index';

jest.mock('kolibri.lib.logging');
jest.mock('kolibri.urls');
jest.mock('lockr');

const router = new VueRouter({
routes: [
{
path: '/userpage/',
name: 'UserPage',
},
],
});

UserPage.computed.newUserLink = () => ({});

function makeWrapper() {
const store = makeStore();
const wrapper = mount(UserPage, {
store,
router,
stubs: {
RouterLink: RouterLinkStub,
},
Expand Down
Loading