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

Add cursor-based pagination #1197

Merged
merged 12 commits into from
Feb 5, 2019
10 changes: 10 additions & 0 deletions dev
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ intro() {
echo " ${BOLD}psql${NORMAL} runs psql connected to development database."
echo " ${BOLD}pyfmt${NORMAL} runs isort + black on python code."
echo " ${BOLD}fakedata${NORMAL} populates database with testing data."
echo " ${BOLD}fakebigdata${NORMAL} populates database with LARGE amount of testing data."
echo
}

Expand Down Expand Up @@ -271,6 +272,13 @@ create_fake_data() {
docker-compose run --rm misago python manage.py createfakehistory 600
}

# Shortcut for creating big dev forum
create_fake_bigdata() {
docker-compose run --rm misago python manage.py createfakecategories 48
docker-compose run --rm misago python manage.py createfakecategories 24 1
docker-compose run --rm misago python manage.py createfakehistory 2190 120
}

# Command dispatcher
if [[ $1 ]]; then
if [[ $1 = "init" ]]; then
Expand Down Expand Up @@ -319,6 +327,8 @@ if [[ $1 ]]; then
black devproject misago
elif [[ $1 = "fakedata" ]]; then
create_fake_data
elif [[ $1 = "fakebigdata" ]]; then
create_fake_bigdata
else
invalid_argument $1
fi
Expand Down
16 changes: 8 additions & 8 deletions frontend/src/components/profile/feed/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,28 @@ export function Threads(props) {
const message = ngettext(
"You have started %(threads)s thread.",
"You have started %(threads)s threads.",
props.posts.count
props.profile.threads
)

header = interpolate(
message,
{
threads: props.posts.count
threads: props.profile.threads
},
true
)
} else {
const message = ngettext(
"%(username)s has started %(threads)s thread.",
"%(username)s has started %(threads)s threads.",
props.posts.count
props.profile.threads
)

header = interpolate(
message,
{
username: props.profile.username,
threads: props.posts.count
threads: props.profile.threads
},
true
)
Expand Down Expand Up @@ -81,28 +81,28 @@ export function Posts(props) {
const message = ngettext(
"You have posted %(posts)s message.",
"You have posted %(posts)s messages.",
props.posts.count
props.profile.posts
)

header = interpolate(
message,
{
posts: props.posts.count
posts: props.profile.posts
},
true
)
} else {
const message = ngettext(
"%(username)s has posted %(posts)s message.",
"%(username)s has posted %(posts)s messages.",
props.posts.count
props.profile.posts
)

header = interpolate(
message,
{
username: props.profile.username,
posts: props.posts.count
posts: props.profile.posts
},
true
)
Expand Down
22 changes: 8 additions & 14 deletions frontend/src/components/profile/feed/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ export default class extends React.Component {
}
}

loadItems(page = 1) {
loadItems(start = 0) {
ajax
.get(this.props.api, {
page: page || 1
start: start || 0
})
.then(
data => {
if (page === 1) {
if (start === 0) {
store.dispatch(posts.load(data))
} else {
store.dispatch(posts.append(data))
Expand All @@ -48,7 +48,7 @@ export default class extends React.Component {
isLoading: true
})

this.loadItems(this.props.posts.page + 1)
this.loadItems(this.props.posts.next)
}

componentDidMount() {
Expand Down Expand Up @@ -77,7 +77,7 @@ export default class extends React.Component {
}

export function Feed(props) {
if (!props.posts.count) {
if (!props.posts.results.length) {
return <p className="lead">{props.emptyMessage}</p>
}

Expand All @@ -91,14 +91,14 @@ export function Feed(props) {
<LoadMoreButton
isLoading={props.isLoading}
loadMore={props.loadMore}
more={props.posts.more}
next={props.posts.next}
/>
</div>
)
}

export function LoadMoreButton(props) {
if (!props.more) return null
if (!props.next) return null

return (
<div className="pager-more">
Expand All @@ -107,13 +107,7 @@ export function LoadMoreButton(props) {
loading={props.isLoading}
onClick={props.loadMore}
>
{interpolate(
gettext("Show more (%(more)s)"),
{
more: props.more
},
true
)}
{gettext("Show older activity")}
</Button>
</div>
)
Expand Down
58 changes: 21 additions & 37 deletions frontend/src/components/threads/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,8 @@ export default class extends WithDropdown {

dropdown: false,
subcategories: [],

count: 0,
more: 0,

page: 1,
pages: 1

next: 0,
}

let category = this.getCategory()
Expand All @@ -72,14 +68,8 @@ export default class extends WithDropdown {
initWithPreloadedData(category, data) {
this.state = Object.assign(this.state, {
moderation: getModerationActions(data.results),

subcategories: data.subcategories,

count: data.count,
more: data.more,

page: data.page,
pages: data.pages
next: data.next
})

this.startPolling(category)
Expand All @@ -89,14 +79,14 @@ export default class extends WithDropdown {
this.loadThreads(category)
}

loadThreads(category, page = 1) {
loadThreads(category, next = 0) {
ajax
.get(
this.props.options.api,
{
category: category,
list: this.props.route.list.type,
page: page || 1
start: next || 0
},
"threads"
)
Expand All @@ -107,7 +97,7 @@ export default class extends WithDropdown {
return
}

if (page === 1) {
if (next === 0) {
store.dispatch(hydrate(data.results))
} else {
store.dispatch(append(data.results, this.getSorting()))
Expand All @@ -121,11 +111,7 @@ export default class extends WithDropdown {

subcategories: data.subcategories,

count: data.count,
more: data.more,

page: data.page,
pages: data.pages
next: data.next,
})

this.startPolling(category)
Expand Down Expand Up @@ -207,7 +193,7 @@ export default class extends WithDropdown {
isBusy: true
})

this.loadThreads(this.getCategory(), this.state.page + 1)
this.loadThreads(this.getCategory(), this.state.next)
}

pollResponse = data => {
Expand Down Expand Up @@ -255,21 +241,19 @@ export default class extends WithDropdown {
}

getMoreButton() {
if (this.state.more) {
return (
<div className="pager-more">
<Button
className="btn btn-default btn-outline"
loading={this.state.isBusy || this.state.busyThreads.length}
onClick={this.loadMore}
>
{gettext("Show more")}
</Button>
</div>
)
} else {
return null
}
if (!this.state.next) return null

return (
<div className="pager-more">
<Button
className="btn btn-default btn-outline"
loading={this.state.isBusy || this.state.busyThreads.length}
onClick={this.loadMore}
>
{gettext("Show more")}
</Button>
</div>
)
}

getClassName() {
Expand Down
1 change: 0 additions & 1 deletion misago/conf/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,6 @@
# Threads lists pagination settings

MISAGO_THREADS_PER_PAGE = 25
MISAGO_THREADS_TAIL = 15


# Posts lists pagination settings
Expand Down
43 changes: 43 additions & 0 deletions misago/core/cursorpagination.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from django.core.paginator import EmptyPage, InvalidPage


def get_page(queryset, order_by, per_page, start=0):
if start < 0:
raise InvalidPage()

object_list = list(_slice_queryset(queryset, order_by, per_page, start))
if start and not object_list:
raise EmptyPage()

next_cursor = None
if len(object_list) > per_page:
next_slice_first_item = object_list.pop(-1)
attr_name = order_by.lstrip("-")
next_cursor = getattr(next_slice_first_item, attr_name)

return CursorPage(start, object_list, next_cursor)


def _slice_queryset(queryset, order_by, per_page, start):
page_len = int(per_page) + 1
if start:
if order_by.startswith("-"):
filter_name = "%s__lte" % order_by[1:]
else:
filter_name = "%s__gte" % order_by
return queryset.filter(**{filter_name: start})[:page_len]
return queryset[:page_len]


class CursorPage:
def __init__(self, start, object_list, next_=None):
self.start = start or 0
self.first = self.start == 0
self.object_list = object_list
self.next = next_

def __len__(self):
return len(self.object_list)

def has_next(self):
return bool(self.next)