diff --git a/rodan-client/code/src/js/Behaviors/BehaviorTable.js b/rodan-client/code/src/js/Behaviors/BehaviorTable.js index e0924f77c..fc5489762 100644 --- a/rodan-client/code/src/js/Behaviors/BehaviorTable.js +++ b/rodan-client/code/src/js/Behaviors/BehaviorTable.js @@ -53,14 +53,6 @@ export default class BehaviorTable extends Marionette.Behavior { this._handleCollectionEventSync(view.collection); } - - if (view.collection._route === "projects") - { - Radio.channel('rodan').on(RODAN_EVENTS.REQUEST__NAVIGATION_PAGINATION_FIRST, () => this._handlePaginationFirst()); - Radio.channel('rodan').on(RODAN_EVENTS.REQUEST__NAVIGATION_PAGINATION_PREVIOUS, () => this._handlePaginationPrevious()); - Radio.channel('rodan').on(RODAN_EVENTS.REQUEST__NAVIGATION_PAGINATION_NEXT, () => this._handlePaginationNext()); - Radio.channel('rodan').on(RODAN_EVENTS.REQUEST__NAVIGATION_PAGINATION_LAST, () => this._handlePaginationLast()); - } } /** @@ -581,7 +573,6 @@ export default class BehaviorTable extends Marionette.Behavior $(this.el).find('.table-control #pagination-select').hide(); $(this.el).find('.table-control #pagination-select').empty(); $(this.el).find('.table-control #pagination-select-text').hide(); - Radio.channel('radio').request(RODAN_EVENTS.REQUEST__UPDATE_NAVIGATION_PAGINATION); // If collection, setup pagination. if (collection) diff --git a/rodan-client/code/src/js/Collections/Global/GlobalCollection.js b/rodan-client/code/src/js/Collections/Global/GlobalCollection.js index dbbd0164c..8d11ee205 100644 --- a/rodan-client/code/src/js/Collections/Global/GlobalCollection.js +++ b/rodan-client/code/src/js/Collections/Global/GlobalCollection.js @@ -50,7 +50,6 @@ export default class GlobalCollection extends BaseCollection _retrieveCollection(options) { options = options ? options : {}; - this.reset(); var data = options.hasOwnProperty('data') ? options.data : {}; if (!this._allowPagination) { @@ -59,6 +58,6 @@ export default class GlobalCollection extends BaseCollection options.data = data; /** @ignore */ this.url = Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__SERVER_GET_ROUTE, this._route); - this.fetch(options); + this.fetch({ ...options, reset: true }); } } diff --git a/rodan-client/code/src/js/Collections/ProjectCollection.js b/rodan-client/code/src/js/Collections/ProjectCollection.js new file mode 100644 index 000000000..310afe26c --- /dev/null +++ b/rodan-client/code/src/js/Collections/ProjectCollection.js @@ -0,0 +1,21 @@ +import BaseCollection from './BaseCollection'; +import Project from 'js/Models/Project'; + +/** + * Collection of Project models. + */ +export default class ProjectCollection extends BaseCollection +{ +/////////////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS +/////////////////////////////////////////////////////////////////////////////////////// + /** + * Initializes the instance. + */ + initialize() + { + /** @ignore */ + this.model = Project; + this._route = 'projects'; + } +} \ No newline at end of file diff --git a/rodan-client/code/src/js/Controllers/ControllerProject.js b/rodan-client/code/src/js/Controllers/ControllerProject.js index e1779e820..d493247b7 100644 --- a/rodan-client/code/src/js/Controllers/ControllerProject.js +++ b/rodan-client/code/src/js/Controllers/ControllerProject.js @@ -14,6 +14,7 @@ import ViewProjectCollection from 'js/Views/Master/Main/Project/Collection/ViewP import ViewUserCollectionItem from 'js/Views/Master/Main/User/Collection/ViewUserCollectionItem'; import ViewWorkflowRunCollection from 'js/Views/Master/Main/WorkflowRun/Collection/ViewWorkflowRunCollection'; import WorkflowRunCollection from 'js/Collections/WorkflowRunCollection'; +import ProjectCollection from '../Collections/ProjectCollection'; /** * Controller for Projects. @@ -29,6 +30,7 @@ export default class ControllerProject extends BaseController initialize() { this._activeProject = null; + this._projectCollection = new ProjectCollection(); } /////////////////////////////////////////////////////////////////////////////////////// @@ -130,8 +132,16 @@ export default class ControllerProject extends BaseController */ _handleEventProjectGenericResponse() { - Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__MODAL_HIDE); Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__GLOBAL_PROJECTS_LOAD, {}); + const fetchProjects = new Promise((resolve, reject) => { + this._projectCollection.fetch({ success: () => resolve(), reject: () => reject()}); + }); + + return fetchProjects.then(() => { + Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__MODAL_HIDE); + }).catch(() => { + Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__MODAL_ERROR, { title: 'Error', content: 'An error occurred while updating the project.' }) + }); } /** @@ -139,9 +149,7 @@ export default class ControllerProject extends BaseController */ _handleEventProjectDeleteResponse() { - Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__MODAL_HIDE); - Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__GLOBAL_PROJECTS_LOAD, {}); - Radio.channel('rodan').trigger(RODAN_EVENTS.EVENT__PROJECT_SELECTED_COLLECTION); + this._handleEventProjectGenericResponse().then(() => Radio.channel('rodan').trigger(RODAN_EVENTS.EVENT__PROJECT_SELECTED_COLLECTION)); } /** @@ -202,9 +210,10 @@ export default class ControllerProject extends BaseController */ _handleEventCollectionSelected() { - var collection = Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__GLOBAL_PROJECT_COLLECTION); - Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__UPDATER_SET_COLLECTIONS, {collections: [collection]}); - var view = new ViewProjectCollection({collection: collection}); + this._projectCollection.fetch(); + var globalProjectCollection = Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__GLOBAL_PROJECT_COLLECTION); + Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__UPDATER_SET_COLLECTIONS, {collections: [globalProjectCollection, this._projectCollection]}); + var view = new ViewProjectCollection({collection: this._projectCollection}); Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__MAINREGION_SHOW_VIEW, {view: view}); } diff --git a/rodan-client/code/src/js/Shared/RODAN_EVENTS.js b/rodan-client/code/src/js/Shared/RODAN_EVENTS.js index 0381edb26..685ac8181 100644 --- a/rodan-client/code/src/js/Shared/RODAN_EVENTS.js +++ b/rodan-client/code/src/js/Shared/RODAN_EVENTS.js @@ -98,14 +98,6 @@ class RODAN_EVENTS this.REQUEST__SHOW_NAVIGATION_PAGINATION = 'REQUEST__SHOW_NAVIGATION_PAGINATION'; /** Request update navigation pagination */ this.REQUEST__UPDATE_NAVIGATION_PAGINATION = 'REQUEST__UPDATE_NAVIGATION_PAGINATION'; - /** Request pagination from navigation bar for first */ - this.REQUEST__NAVIGATION_PAGINATION_FIRST = 'REQUEST__NAVIGATION_PAGINATION_FIRST'; - /** Request pagination from navigation bar for previous */ - this.REQUEST__NAVIGATION_PAGINATION_PREVIOUS = 'REQUEST__NAVIGATION_PAGINATION_PREVIOUS'; - /** Request pagination from navigation bar for next */ - this.REQUEST__NAVIGATION_PAGINATION_NEXT = 'REQUEST__NAVIGATION_PAGINATION_NEXT'; - /** Request pagination from navigation bar for last */ - this.REQUEST__NAVIGATION_PAGINATION_LAST = 'REQUEST__NAVIGATION_PAGINATION_LAST'; /////////////////////////////////////////////////////////////////////////////////////// // Global Collections diff --git a/rodan-client/code/src/js/Views/Master/Navigation/LayoutViewNavigation.js b/rodan-client/code/src/js/Views/Master/Navigation/LayoutViewNavigation.js index e2cb14aea..4bfd2b4f6 100644 --- a/rodan-client/code/src/js/Views/Master/Navigation/LayoutViewNavigation.js +++ b/rodan-client/code/src/js/Views/Master/Navigation/LayoutViewNavigation.js @@ -27,6 +27,8 @@ export default class LayoutViewNavigation extends Marionette.View this.addRegions({ regionNavigationTree: '#region-navigation_tree' }); + this._projectCollection = Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__GLOBAL_PROJECT_COLLECTION); + this._projectCollection.on('all', () => this._handleProjectPaginationAppearance()); } /////////////////////////////////////////////////////////////////////////////////////// @@ -51,9 +53,8 @@ export default class LayoutViewNavigation extends Marionette.View */ _handleAuthenticationSuccess() { - var model = new Backbone.Model({name: 'Projects'}); - var object = {model: model, collection: Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__GLOBAL_PROJECT_COLLECTION)}; - this.showChildView('regionNavigationTree', new ViewNavigationNodeRoot(object)); + const model = new Backbone.Model({name: 'Projects'}); + this.showChildView('regionNavigationTree', new ViewNavigationNodeRoot({ model, collection: this._projectCollection })); this.$el.find('#button-navigation_logout').prop('disabled', false); this.$el.find('#button-navigation_preferences').prop('disabled', false); } @@ -66,6 +67,7 @@ export default class LayoutViewNavigation extends Marionette.View this.getRegion('regionNavigationTree').empty(); this.$el.find('#button-navigation_logout').prop('disabled', true); this.$el.find('#button-navigation_preferences').prop('disabled', true); + this._handleRequestHidePaginationButtons(); } /** @@ -160,7 +162,7 @@ export default class LayoutViewNavigation extends Marionette.View */ _handleRequestButtonFirst() { - Radio.channel('rodan').trigger(RODAN_EVENTS.REQUEST__NAVIGATION_PAGINATION_FIRST); + this._projectCollection.fetchPage({ page: 1 }); } /** @@ -168,7 +170,7 @@ export default class LayoutViewNavigation extends Marionette.View */ _handleRequestNavigationPaginationPrevious() { - Radio.channel('rodan').trigger(RODAN_EVENTS.REQUEST__NAVIGATION_PAGINATION_PREVIOUS); + this._projectCollection.fetchPage({ page: this._projectCollection.getPagination().get("current") - 1 }); } /** @@ -176,7 +178,7 @@ export default class LayoutViewNavigation extends Marionette.View */ _handleRequestNavigationPaginationNext() { - Radio.channel('rodan').trigger(RODAN_EVENTS.REQUEST__NAVIGATION_PAGINATION_NEXT); + this._projectCollection.fetchPage({ page: this._projectCollection.getPagination().get("current") + 1 }); } /** @@ -184,7 +186,7 @@ export default class LayoutViewNavigation extends Marionette.View */ _handleRequestNavigationPaginationLast() { - Radio.channel('rodan').trigger(RODAN_EVENTS.REQUEST__NAVIGATION_PAGINATION_LAST); + this._projectCollection.fetchPage({ page: this._projectCollection.getPagination().get("total") }); } /** @@ -192,7 +194,7 @@ export default class LayoutViewNavigation extends Marionette.View */ _handleProjectPaginationAppearance() { - var attrs = Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__GLOBAL_PROJECT_COLLECTION)._pagination.attributes; + var attrs = this._projectCollection._pagination.attributes; if (attrs.total > 1) { // Show and enable all paginations in nav @@ -213,10 +215,7 @@ export default class LayoutViewNavigation extends Marionette.View else { // Hide all pagination controls in nav - this.$el.find('#button-navigation_first').hide(); - this.$el.find('#button-navigation_previous').hide(); - this.$el.find('#button-navigation_next').hide(); - this.$el.find('#button-navigation_last').hide(); + this._handleRequestHidePaginationButtons(); } } @@ -236,6 +235,17 @@ export default class LayoutViewNavigation extends Marionette.View this.$el.find('#button-navigation_next').prop('disabled', false); this.$el.find('#button-navigation_last').prop('disabled', false); } + + /** + * Handle request to hide pagination buttons when they are not visible. + */ + _handleRequestHidePaginationButtons() + { + this.$el.find('#button-navigation_first').hide(); + this.$el.find('#button-navigation_previous').hide(); + this.$el.find('#button-navigation_next').hide(); + this.$el.find('#button-navigation_last').hide(); + } } LayoutViewNavigation.prototype.template = _.template($('#template-navigation').text()); LayoutViewNavigation.prototype.ui = { diff --git a/rodan-main/code/rodan/paginators/pagination.py b/rodan-main/code/rodan/paginators/pagination.py index bee3ec666..197a2f871 100644 --- a/rodan-main/code/rodan/paginators/pagination.py +++ b/rodan-main/code/rodan/paginators/pagination.py @@ -2,6 +2,26 @@ from rest_framework.response import Response from collections import OrderedDict from django.conf import settings +from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage + +class SafePaginator(Paginator): + """ + A paginator that returns a valid page even if the page argument isn't a number or isn't + in range instead of throwing an exception. + """ + def page(self, number): + """ + Return a valid page, even if the page argument isn't a number or isn't + in range. + """ + try: + number = self.validate_number(number) + except PageNotAnInteger: + number = 1 + except EmptyPage: + number = self.num_pages + + return super(SafePaginator, self).page(number) class CustomPagination(PageNumberPagination): @@ -12,6 +32,7 @@ class CustomPagination(PageNumberPagination): page_size_query_param = "page_size" max_page_size = settings.REST_FRAMEWORK["MAX_PAGE_SIZE"] + django_paginator_class = SafePaginator def get_paginated_response(self, data): return Response(