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;