diff --git a/contentcuration/contentcuration/frontend/channelEdit/views/trash/TrashModal.vue b/contentcuration/contentcuration/frontend/channelEdit/views/trash/TrashModal.vue index dde813b877..00822ee76b 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/views/trash/TrashModal.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/views/trash/TrashModal.vue @@ -253,7 +253,7 @@ ]; }, items() { - return sortBy(this.getContentNodeChildren(this.trashId), 'modified'); + return sortBy(this.getContentNodeChildren(this.trashId), 'modified').reverse(); }, backLink() { return { @@ -277,9 +277,9 @@ }, }, created() { - (this.loadContentNodes({ parent__in: [this.rootId] }), - this.loadAncestors({ id: this.nodeId }), - this.loadNodes()); + this.loadContentNodes({ parent__in: [this.rootId] }); + this.loadAncestors({ id: this.nodeId }); + this.loadNodes(); }, mounted() { this.updateTabTitle(this.$store.getters.appendChannelName(this.$tr('trashModalTitle'))); @@ -298,10 +298,12 @@ this.loading = false; return; } - this.loadChildren({ parent: this.trashId }).then(childrenResponse => { - this.loading = false; - this.more = childrenResponse.more || null; - }); + this.loadChildren({ parent: this.trashId, ordering: '-modified' }).then( + childrenResponse => { + this.loading = false; + this.more = childrenResponse.more || null; + }, + ); }, moveNodes(target) { return this.moveContentNodes({ diff --git a/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/actions.js b/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/actions.js index eb7bdb7c97..66d368a51d 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/actions.js +++ b/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/actions.js @@ -71,8 +71,11 @@ export function loadContentNodeByNodeId(context, nodeId) { }); } -export function loadChildren(context, { parent, published = null, complete = null }) { - const params = { parent, max_results: 25 }; +export function loadChildren( + context, + { parent, published = null, complete = null, ordering = null }, +) { + const params = { parent, max_results: 25, ordering }; if (published !== null) { params.published = published; } diff --git a/contentcuration/contentcuration/frontend/shared/data/resources.js b/contentcuration/contentcuration/frontend/shared/data/resources.js index 09ee1490e7..37c307d850 100644 --- a/contentcuration/contentcuration/frontend/shared/data/resources.js +++ b/contentcuration/contentcuration/frontend/shared/data/resources.js @@ -557,6 +557,14 @@ class IndexedDBResource { collection = collection.filter(filterFn); } if (paginationActive) { + // Default pagination field is 'lft' + let paginationField = 'lft'; + if (params.ordering) { + paginationField = params.ordering.replace(/^-/, ''); + } + // Determine the operator based on the ordering direction. + const operator = params.ordering && params.ordering.startsWith('-') ? 'lt' : 'gt'; + let results; if (sortBy) { // If we still have a sortBy value here, then we have not sorted using orderBy @@ -574,7 +582,8 @@ class IndexedDBResource { more: hasMore ? { ...params, - lft__gt: results[maxResults - 1].lft, + // Dynamically set the pagination cursor based on the pagination field and operator. + [`${paginationField}__${operator}`]: results[maxResults - 1][paginationField], } : null, }; diff --git a/contentcuration/contentcuration/viewsets/contentnode.py b/contentcuration/contentcuration/viewsets/contentnode.py index 02aea2ffd3..6a0e1fa829 100644 --- a/contentcuration/contentcuration/viewsets/contentnode.py +++ b/contentcuration/contentcuration/viewsets/contentnode.py @@ -518,7 +518,7 @@ def update(self, instance, validated_data): def retrieve_thumbail_src(item): - """ Get either the encoding or the url to use as the src attribute """ + """Get either the encoding or the url to use as the src attribute""" try: if item.get("thumbnail_encoding"): encoding = json.loads(item.get("thumbnail_encoding")) @@ -705,54 +705,64 @@ def dict_if_none(obj, field_name=None): class ContentNodePagination(ValuesViewsetCursorPagination): """ - A simplified cursor pagination class for ContentNodeViewSet. - Instead of using an opaque cursor, it uses the lft value for filtering. - As such, if this pagination scheme is used without applying a filter - that will guarantee membership to a specific MPTT tree, such as parent - or tree_id, the pagination scheme will not be predictable. + A simplified cursor pagination class + Instead of using a fixed 'lft' cursor, it dynamically sets the pagination field and operator + based on the incoming `ordering` query parameter. """ - cursor_query_param = "lft__gt" - ordering = "lft" page_size_query_param = "max_results" max_page_size = 100 + def get_pagination_params(self): + # Default ordering is "lft" if not provided. + ordering_param = self.request.query_params.get("ordering", "lft") + # Remove the leading '-' if present to get the field name. + pagination_field = ordering_param.lstrip("-") + # Determine operator: if ordering starts with '-', use __lt; otherwise __gt. + operator = "__lt" if ordering_param.startswith("-") else "__gt" + return pagination_field, operator + def decode_cursor(self, request): """ - Given a request with a cursor, return a `Cursor` instance. + Given a request with a cursor parameter, return a `Cursor` instance. + The cursor parameter name is dynamically built from the pagination field and operator. """ - # Determine if we have a cursor, and if so then decode it. - value = request.query_params.get(self.cursor_query_param) + pagination_field, operator = self.get_pagination_params() + cursor_param = f"{pagination_field}{operator}" + value = request.query_params.get(cursor_param) if value is None: return None - try: - value = int(value) - except ValueError: - raise ValidationError( - "lft must be an integer but an invalid value was given." - ) + if pagination_field == "lft": + try: + value = int(value) + except ValueError: + raise ValidationError( + "lft must be an integer but an invalid value was given." + ) return Cursor(offset=0, reverse=False, position=value) def encode_cursor(self, cursor): """ - Given a Cursor instance, return an url with query parameter. + Given a Cursor instance, return a URL with the dynamic pagination cursor query parameter. """ - return replace_query_param( - self.base_url, self.cursor_query_param, str(cursor.position) - ) + pagination_field, operator = self.get_pagination_params() + cursor_param = f"{pagination_field}{operator}" + return replace_query_param(self.base_url, cursor_param, str(cursor.position)) def get_more(self): + """ + Construct a "more" URL (or query parameters) that includes the pagination cursor + built from the dynamic field and operator. + """ + pagination_field, operator = self.get_pagination_params() + cursor_param = f"{pagination_field}{operator}" position, offset = self._get_more_position_offset() if position is None and offset is None: return None params = self.request.query_params.copy() - params.update( - { - self.cursor_query_param: position, - } - ) + params.update({cursor_param: position}) return params @@ -1068,7 +1078,7 @@ def copy( position=None, mods=None, excluded_descendants=None, - **kwargs + **kwargs, ): target, position = self.validate_targeting_args(target, position) @@ -1133,7 +1143,7 @@ def perform_create(self, serializer, change=None): ) def update_descendants(self, pk, mods): - """ Update a node and all of its descendants with the given mods """ + """Update a node and all of its descendants with the given mods""" root = ContentNode.objects.get(id=pk) if root.kind_id != content_kinds.TOPIC: diff --git a/deploy/includes/content/_proxy.conf b/deploy/includes/content/_proxy.conf index 5490c72742..0c60f64828 100644 --- a/deploy/includes/content/_proxy.conf +++ b/deploy/includes/content/_proxy.conf @@ -7,7 +7,6 @@ limit_except GET HEAD OPTIONS { proxy_http_version 1.1; proxy_set_header Host $proxy_host; -proxy_set_header Accept-Encoding Identity; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_redirect off; proxy_buffering off;