zcash-grant-system/backend/grant/utils/pagination.py

381 lines
13 KiB
Python

import abc
from sqlalchemy import or_
from grant.ccr.models import CCR
from grant.comment.models import Comment, comments_schema
from grant.milestone.models import Milestone
from grant.proposal.models import db, ma, Proposal, ProposalContribution, ProposalArbiter, proposal_contributions_schema
from grant.user.models import User, UserSettings, users_schema
from .enums import CCRStatus, ProposalStatus, ProposalStage, Category, ContributionStatus, ProposalArbiterStatus, \
MilestoneStage
def extract_filters(sw, strings):
filters = [f[len(sw):] for f in strings if f.startswith(sw)]
filters = [f for f in filters if not f.startswith('NOT_')]
return filters
class PaginationException(Exception):
pass
class Pagination(abc.ABC):
def validate_filters(self, filters: list):
if self.FILTERS:
for f in filters:
if f not in self.FILTERS:
self._raise(f'unsupported filter: {f}')
def validate_sort(self, sort: str):
if self.SORT_MAP:
if sort not in self.SORT_MAP:
self._raise(f'unsupported sort: {sort}')
def _raise(self, desc: str):
name = self.__class__.__name__
raise PaginationException(f'{name} {desc}')
# if we ever want to do more interacting from outside
# consider moving these args into __init__ and attaching to self
@abc.abstractmethod
def paginate(
self,
schema: ma.Schema,
query: db.Query,
page: int,
filters: list,
search: str,
sort: str,
):
pass
class ProposalPagination(Pagination):
def __init__(self):
self.FILTERS = [f'STATUS_{s}' for s in ProposalStatus.list()]
self.FILTERS.extend([f'STAGE_{s}' for s in ProposalStage.list()])
self.FILTERS.extend([f'STAGE_NOT_{s}' for s in ProposalStage.list()])
self.FILTERS.extend([f'CAT_{c}' for c in Category.list()])
self.FILTERS.extend([f'ARBITER_{c}' for c in ProposalArbiterStatus.list()])
self.FILTERS.extend([f'MILESTONE_{c}' for c in MilestoneStage.list()])
self.FILTERS.extend(['ACCEPTED_WITH_FUNDING', 'ACCEPTED_WITHOUT_FUNDING'])
self.PAGE_SIZE = 9
self.SORT_MAP = {
'CREATED:DESC': Proposal.date_created.desc(),
'CREATED:ASC': Proposal.date_created,
'PUBLISHED:DESC': Proposal.date_published.desc(), # NEWEST
'PUBLISHED:ASC': Proposal.date_published, # OLDEST
}
def paginate(
self,
schema: ma.Schema,
query: db.Query = None,
page: int = 1,
filters: list = None,
search: str = None,
sort: str = 'PUBLISHED:DESC',
):
query = query or Proposal.query
sort = sort or 'PUBLISHED:DESC'
# FILTER
if filters:
self.validate_filters(filters)
status_filters = extract_filters('STATUS_', filters)
stage_filters = extract_filters('STAGE_', filters)
stage_not_filters = extract_filters('STAGE_NOT_', filters, )
cat_filters = extract_filters('CAT_', filters)
arbiter_filters = extract_filters('ARBITER_', filters)
milestone_filters = extract_filters('MILESTONE_', filters)
if status_filters:
query = query.filter(Proposal.status.in_(status_filters))
if stage_filters:
query = query.filter(Proposal.stage.in_(stage_filters))
if stage_not_filters:
query = query.filter(Proposal.stage.notin_(stage_not_filters))
if cat_filters:
query = query.filter(Proposal.category.in_(cat_filters))
if arbiter_filters:
query = query.join(Proposal.arbiter) \
.filter(ProposalArbiter.status.in_(arbiter_filters))
if milestone_filters:
query = query.join(Proposal.milestones) \
.filter(Milestone.stage.in_(milestone_filters))
if 'ACCEPTED_WITH_FUNDING' in filters:
query = query.filter(Proposal.accepted_with_funding == True)
if 'ACCEPTED_WITHOUT_FUNDING' in filters:
query = query.filter(Proposal.accepted_with_funding == False)
# SORT (see self.SORT_MAP)
if sort:
self.validate_sort(sort)
query = query.order_by(self.SORT_MAP[sort])
# SEARCH
if search:
query = query.filter(Proposal.title.ilike(f'%{search}%'))
res = query.paginate(page, self.PAGE_SIZE, False)
return {
'page': res.page,
'total': res.total,
'page_size': self.PAGE_SIZE,
'items': schema.dump(res.items),
'filters': filters,
'search': search,
'sort': sort
}
class ContributionPagination(Pagination):
def __init__(self):
self.FILTERS = [f'STATUS_{s}' for s in ContributionStatus.list()]
self.FILTERS.extend(['REFUNDABLE', 'DONATION'])
self.PAGE_SIZE = 9
self.SORT_MAP = {
'CREATED:DESC': ProposalContribution.date_created.desc(),
'CREATED:ASC': ProposalContribution.date_created,
'AMOUNT:DESC': ProposalContribution.amount.desc(),
'AMOUNT:ASC': ProposalContribution.amount,
}
def paginate(
self,
schema: ma.Schema = proposal_contributions_schema,
query: db.Query = None,
page: int = 1,
filters: list = None,
search: str = None,
sort: str = 'PUBLISHED:DESC',
):
query = query or ProposalContribution.query
sort = sort or 'CREATED:DESC'
# FILTER
if filters:
self.validate_filters(filters)
status_filters = extract_filters('STATUS_', filters)
if status_filters:
query = query.filter(ProposalContribution.status.in_(status_filters))
if 'REFUNDABLE' in filters:
query = query.filter(ProposalContribution.refund_tx_id == None) \
.filter(ProposalContribution.staking == False) \
.filter(ProposalContribution.status == ContributionStatus.CONFIRMED) \
.join(Proposal) \
.filter(or_(
Proposal.stage == ProposalStage.FAILED,
Proposal.stage == ProposalStage.CANCELED,
)) \
.join(ProposalContribution.user) \
.join(UserSettings) \
.filter(UserSettings.refund_address != None)
if 'DONATION' in filters:
query = query.filter(ProposalContribution.refund_tx_id == None) \
.filter(ProposalContribution.status == ContributionStatus.CONFIRMED) \
.join(Proposal) \
.filter(or_(
Proposal.stage == ProposalStage.FAILED,
Proposal.stage == ProposalStage.CANCELED,
)) \
.join(ProposalContribution.user, isouter=True) \
.join(UserSettings, isouter=True) \
.filter(UserSettings.refund_address == None)
# SORT (see self.SORT_MAP)
if sort:
self.validate_sort(sort)
query = query.order_by(self.SORT_MAP[sort])
# SEARCH can match txids or amounts
if search:
query = query.filter(or_(
ProposalContribution.amount.ilike(f'%{search}%'),
ProposalContribution.tx_id.ilike(f'%{search}%'),
))
res = query.paginate(page, self.PAGE_SIZE, False)
return {
'page': res.page,
'total': res.total,
'page_size': self.PAGE_SIZE,
'items': schema.dump(res.items),
'filters': filters,
'search': search,
'sort': sort
}
class UserPagination(Pagination):
def __init__(self):
self.FILTERS = ['BANNED', 'SILENCED', 'ARBITER']
self.PAGE_SIZE = 9
self.SORT_MAP = {
'EMAIL:DESC': User.email_address.desc(),
'EMAIL:ASC': User.email_address,
'NAME:DESC': User.display_name.desc(),
'NAME:ASC': User.display_name,
}
def paginate(
self,
schema: ma.Schema = users_schema,
query: db.Query = None,
page: int = 1,
filters: list = None,
search: str = None,
sort: str = 'EMAIL:DESC',
):
query = query or Proposal.query
sort = sort or 'EMAIL:DESC'
# FILTER
if filters:
self.validate_filters(filters)
if 'BANNED' in filters:
query = query.filter(User.banned == True)
if 'SILENCED' in filters:
query = query.filter(User.silenced == True)
if 'ARBITER' in filters:
query = query.join(User.arbiter_proposals) \
.filter(ProposalArbiter.status == ProposalArbiterStatus.ACCEPTED)
# SORT (see self.SORT_MAP)
if sort:
self.validate_sort(sort)
query = query.order_by(self.SORT_MAP[sort])
# SEARCH
if search:
query = query.filter(
User.email_address.ilike(f'%{search}%') |
User.display_name.ilike(f'%{search}%')
)
res = query.paginate(page, self.PAGE_SIZE, False)
return {
'page': res.page,
'total': res.total,
'page_size': self.PAGE_SIZE,
'items': schema.dump(res.items),
'filters': filters,
'search': search,
'sort': sort
}
class CommentPagination(Pagination):
def __init__(self):
self.FILTERS = ['REPORTED', 'HIDDEN']
self.PAGE_SIZE = 10
self.SORT_MAP = {
'CREATED:DESC': Comment.date_created.desc(),
'CREATED:ASC': Comment.date_created,
}
def paginate(
self,
schema: ma.Schema = comments_schema,
query: db.Query = None,
page: int = 1,
filters: list = None,
search: str = None,
sort: str = 'CREATED:DESC',
):
query = query or Comment.query
sort = sort or 'CREATED:DESC'
# FILTER
if filters:
self.validate_filters(filters)
if 'REPORTED' in filters:
query = query.filter(Comment.reported == True)
if 'HIDDEN' in filters:
query = query.filter(Comment.hidden == True)
# SORT (see self.SORT_MAP)
if sort:
self.validate_sort(sort)
query = query.order_by(self.SORT_MAP[sort])
# SEARCH
if search:
query = query.filter(
Comment.content.ilike(f'%{search}%')
)
res = query.paginate(page, self.PAGE_SIZE, False)
return {
'page': res.page,
'total': res.total,
'page_size': self.PAGE_SIZE,
'items': schema.dump(res.items),
'filters': filters,
'search': search,
'sort': sort
}
class CCRPagination(Pagination):
def __init__(self):
self.FILTERS = [f'STATUS_{s}' for s in CCRStatus.list()]
self.PAGE_SIZE = 9
self.SORT_MAP = {
'CREATED:DESC': CCR.date_created.desc(),
'CREATED:ASC': CCR.date_created
}
def paginate(
self,
schema: ma.Schema,
query: db.Query = None,
page: int = 1,
filters: list = None,
search: str = None,
sort: str = 'CREATED:DESC',
):
query = query or CCR.query
sort = sort or 'CREATED:DESC'
# FILTER
if filters:
self.validate_filters(filters)
status_filters = extract_filters('STATUS_', filters)
if status_filters:
query = query.filter(CCR.status.in_(status_filters))
# SORT (see self.SORT_MAP)
if sort:
self.validate_sort(sort)
query = query.order_by(self.SORT_MAP[sort])
# SEARCH
if search:
query = query.filter(CCR.title.ilike(f'%{search}%'))
res = query.paginate(page, self.PAGE_SIZE, False)
return {
'page': res.page,
'total': res.total,
'page_size': self.PAGE_SIZE,
'items': schema.dump(res.items),
'filters': filters,
'search': search,
'sort': sort
}
# expose pagination methods here
ccr = CCRPagination().paginate
proposal = ProposalPagination().paginate
contribution = ContributionPagination().paginate
comment = CommentPagination().paginate
user = UserPagination().paginate