We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Pagination of projects is incorrect
On the home page, the pagination is off for large sets of projects. 

#### Example
Select the EU trust fund for Africa in the organisations filter. The pagination widget shows that there are 22 pages of projects, but the last [two are empty](https://rsr.akvo.org/en/projects/?organisation=3394&page=22)!

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `akvo/rest/views/project.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 """Akvo RSR is covered by the GNU Affero General Public License.
3 
4 See more details in the license.txt file located at the root folder of the Akvo RSR module.
5 For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.
6 """
7 
8 from django.db.models import Q
9 from rest_framework.decorators import api_view
10 from rest_framework.response import Response
11 
12 from akvo.codelists.store.codelists_v202 import SECTOR_CATEGORY
13 from akvo.rest.serializers import (ProjectSerializer, ProjectExtraSerializer,
14                                    ProjectExtraDeepSerializer,
15                                    ProjectIatiExportSerializer,
16                                    ProjectUpSerializer,
17                                    ProjectDirectorySerializer,
18                                    TypeaheadOrganisationSerializer,
19                                    TypeaheadSectorSerializer,)
20 from akvo.rest.views.utils import (
21     int_or_none, get_cached_data, get_qs_elements_for_page, set_cached_data
22 )
23 from akvo.rsr.models import Project
24 from akvo.rsr.filters import location_choices, get_m49_filter
25 from akvo.utils import codelist_choices
26 from ..viewsets import PublicProjectViewSet
27 
28 
29 class ProjectViewSet(PublicProjectViewSet):
30 
31     """
32     Viewset providing Project data.
33     """
34     queryset = Project.objects.select_related(
35         'categories',
36         'keywords',
37         'partners',
38     ).prefetch_related(
39         'publishingstatus',
40     )
41     serializer_class = ProjectSerializer
42     project_relation = ''
43 
44     def get_queryset(self):
45         """
46         Allow custom filter for sync_owner, since this field has been replaced by the
47         reporting org partnership.
48         """
49 
50         sync_owner = self.request.query_params.get('sync_owner', None)
51         reporting_org = self.request.query_params.get('reporting_org', None)
52 
53         reporting_org = reporting_org or sync_owner
54         if reporting_org:
55             self.queryset = self.queryset.filter(
56                 partnerships__iati_organisation_role=101,
57                 partnerships__organisation__pk=reporting_org
58             ).distinct()
59         return super(ProjectViewSet, self).get_queryset()
60 
61 
62 class ProjectIatiExportViewSet(PublicProjectViewSet):
63     """Lean viewset for project data, as used in the My IATI section of RSR."""
64     queryset = Project.objects.only(
65         'id',
66         'title',
67         'is_public',
68         'status',
69     ).select_related(
70         'partners',
71     ).prefetch_related(
72         'iati_checks',
73         'publishingstatus',
74         'partnerships',
75     )
76     serializer_class = ProjectIatiExportSerializer
77     project_relation = ''
78     paginate_by_param = 'limit'
79     max_paginate_by = 50
80 
81     def get_queryset(self):
82         """
83         Allow custom filter for sync_owner, since this field has been replaced by the
84         reporting org partnership.
85         """
86         reporting_org = self.request.query_params.get('reporting_org', None)
87         if reporting_org:
88             self.queryset = self.queryset.filter(
89                 partnerships__iati_organisation_role=101,
90                 partnerships__organisation__pk=reporting_org
91             ).distinct()
92         return super(ProjectIatiExportViewSet, self).get_queryset()
93 
94 
95 class ProjectExtraViewSet(ProjectViewSet):
96 
97     """
98     Viewset providing extra Project data.
99 
100     Allowed parameters are:
101     __limit__ (default 10, max 30),
102     __partnerships\__organisation__ (filter on organisation ID), and
103     __publishingstatus\__status__ (filter on publishing status)
104     """
105 
106     queryset = Project.objects.prefetch_related(
107         'publishingstatus',
108         'sectors',
109         'partnerships',
110         'budget_items',
111         'legacy_data',
112         'links',
113         'locations',
114         'locations__country',
115         'planned_disbursements',
116         'policy_markers',
117         'documents',
118         'comments',
119         'conditions',
120         'contacts',
121         'project_updates',
122         'recipient_countries',
123         'recipient_regions',
124         'related_projects',
125         'results',
126         'sectors',
127         'transactions',
128     )
129     serializer_class = ProjectExtraSerializer
130     paginate_by_param = 'limit'
131     paginate_by = 10
132     max_paginate_by = 30
133 
134 
135 class ProjectExtraDeepViewSet(ProjectViewSet):
136 
137     """
138     Viewset providing extra deep (depth=2 or bigger) Project data.
139 
140     Allowed parameters are:
141     __limit__ (default 5, max 10),
142     __partnerships\__organisation__ (filter on organisation ID), and
143     __publishingstatus\__status__ (filter on publishing status)
144     """
145 
146     queryset = Project.objects.prefetch_related(
147         'publishingstatus',
148         'sectors',
149         'partnerships',
150         'budget_items',
151         'legacy_data',
152         'links',
153         'locations',
154         'locations__country',
155         'planned_disbursements',
156         'policy_markers',
157         'documents',
158         'comments',
159         'conditions',
160         'contacts',
161         'project_updates',
162         'recipient_countries',
163         'recipient_regions',
164         'related_projects',
165         'results',
166         'sectors',
167         'transactions',
168     )
169     serializer_class = ProjectExtraDeepSerializer
170     paginate_by_param = 'limit'
171     paginate_by = 5
172     max_paginate_by = 10
173 
174 
175 class ProjectUpViewSet(ProjectViewSet):
176 
177     """
178     Viewset providing extra data and limited filtering for Up in one go.
179 
180     Allowed parameters are:
181     __limit__ (default 30, max 100),
182     __partnerships\__organisation__ (filter on organisation ID), and
183     __publishingstatus\__status__ (filter on publishing status)
184     """
185 
186     queryset = Project.objects.select_related(
187         'primary_location',
188         'categories',
189         'keywords',
190         'partners',
191     ).prefetch_related(
192         'publishingstatus',
193         'project_updates',
194     )
195     serializer_class = ProjectUpSerializer
196     paginate_by_param = 'limit'
197     max_paginate_by = 100
198 
199 
200 ###############################################################################
201 # Project directory
202 ###############################################################################
203 
204 @api_view(['GET'])
205 def project_directory(request):
206     """Return the values for various project filters.
207 
208     Based on the current filters, it returns new options for all the (other)
209     filters. This is used to generate dynamic filters.
210 
211     """
212 
213     # Fetch projects based on whether we are an Akvo site or RSR main site
214     page = request.rsr_page
215     projects = page.projects() if page else Project.objects.all().public().published()
216 
217     # Exclude projects which don't have an image or a title
218     # FIXME: This happens silently and may be confusing?
219     projects = projects.exclude(Q(title='') | Q(current_image=''))
220 
221     # Filter projects based on query parameters
222     filter_, text_filter = _create_filters_query(request)
223     projects = projects.filter(filter_).distinct() if filter_ is not None else projects
224     # NOTE: The text filter is handled differently/separately from the other filters.
225     # The text filter allows users to enter free form text, which could result in no
226     # projects being found for the given text. Other fields only allow selecting from
227     # a list of options, and for every combination that is shown to users and
228     # selectable by them, at least one project exists.
229     # When no projects are returned for a given search string, if the text search is
230     # not handled separately, the options for all the other filters are empty, and
231     # this causes the filters to get cleared automatically. This is very weird UX.
232     projects_text_filtered = (
233         projects.filter(text_filter) if text_filter is not None else projects
234     )
235     if projects_text_filtered.exists():
236         projects = projects_text_filtered
237 
238     # Pre-fetch related fields to make things faster
239     projects = projects.select_related(
240         'primary_location',
241         'primary_organisation',
242     ).prefetch_related(
243         'locations',
244         'locations__country',
245         'recipient_countries',
246         'recipient_countries__country',
247     )
248 
249     # Get the relevant data for typeaheads based on filtered projects (minus
250     # text filtering, if no projects were found)
251     cached_locations, _ = get_cached_data(request, 'locations', None, None)
252     if cached_locations is None:
253         cached_locations = [
254             {'id': choice[0], 'name': choice[1]}
255             for choice in location_choices(projects)
256         ]
257         set_cached_data(request, 'locations', cached_locations)
258 
259     organisations = projects.all_partners().values('id', 'name', 'long_name')
260 
261     # FIXME: Currently only vocabulary 2 is supported (as was the case with
262     # static filters). This could be extended to other vocabularies, in future.
263     valid_sectors = dict(codelist_choices(SECTOR_CATEGORY))
264     sectors = projects.sectors().filter(
265         vocabulary='2', sector_code__in=valid_sectors
266     ).values('sector_code').distinct()
267 
268     # NOTE: We use projects_text_filtered for displaying projects
269     count = projects_text_filtered.count()
270     display_projects = get_qs_elements_for_page(projects_text_filtered, request).select_related(
271         'primary_organisation'
272     )
273 
274     # NOTE: We use the _get_cached_data function to individually cache small
275     # bits of data to avoid the response from never getting saved in the cache,
276     # because the response is larger than the max size of data that can be
277     # saved in the cache.
278     cached_projects, showing_cached_projects = get_cached_data(
279         request, 'projects', display_projects, ProjectDirectorySerializer
280     )
281     cached_organisations, _ = get_cached_data(
282         request, 'organisations', organisations, TypeaheadOrganisationSerializer
283     )
284 
285     response = {
286         'project_count': count,
287         'projects': cached_projects,
288         'showing_cached_projects': showing_cached_projects,
289         'organisation': cached_organisations,
290         'sector': TypeaheadSectorSerializer(sectors, many=True).data,
291         'location': cached_locations,
292     }
293 
294     return Response(response)
295 
296 
297 def _create_filters_query(request):
298     """Returns a Q object expression based on query parameters."""
299     keyword_param = int_or_none(request.GET.get('keyword'))
300     location_param = int_or_none(request.GET.get('location'))
301     status_param = int_or_none(request.GET.get('status'))
302     organisation_param = int_or_none(request.GET.get('organisation'))
303     sector_param = int_or_none(request.GET.get('sector'))
304     title_or_subtitle_param = request.GET.get('title_or_subtitle')
305 
306     keyword_filter = Q(keywords__id=keyword_param) if keyword_param else None
307     location_filter = get_m49_filter(location_param) if location_param else None
308     status_filter = Q(iati_status=status_param) if status_param else None
309     organisation_filter = Q(partners__id=organisation_param) if organisation_param else None
310     sector_filter = (
311         Q(sectors__sector_code=sector_param, sectors__vocabulary='2')
312         if sector_param else None
313     )
314     title_or_subtitle_filter = (
315         Q(title__icontains=title_or_subtitle_param) | Q(subtitle__icontains=title_or_subtitle_param)
316     ) if title_or_subtitle_param else None
317     all_filters = [
318         keyword_filter,
319         location_filter,
320         status_filter,
321         organisation_filter,
322         sector_filter,
323     ]
324     filters = filter(None, all_filters)
325     return reduce(lambda x, y: x & y, filters) if filters else None, title_or_subtitle_filter
326
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/akvo/rest/views/project.py b/akvo/rest/views/project.py
--- a/akvo/rest/views/project.py
+++ b/akvo/rest/views/project.py
@@ -5,6 +5,7 @@
 For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.
 """
 
+from django.conf import settings
 from django.db.models import Q
 from rest_framework.decorators import api_view
 from rest_framework.response import Response
@@ -289,6 +290,7 @@
         'organisation': cached_organisations,
         'sector': TypeaheadSectorSerializer(sectors, many=True).data,
         'location': cached_locations,
+        'page_size_default': settings.PROJECT_DIRECTORY_PAGE_SIZES[0],
     }
 
     return Response(response)

{"golden_diff": "diff --git a/akvo/rest/views/project.py b/akvo/rest/views/project.py\n--- a/akvo/rest/views/project.py\n+++ b/akvo/rest/views/project.py\n@@ -5,6 +5,7 @@\n For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.\n \"\"\"\n \n+from django.conf import settings\n from django.db.models import Q\n from rest_framework.decorators import api_view\n from rest_framework.response import Response\n@@ -289,6 +290,7 @@\n         'organisation': cached_organisations,\n         'sector': TypeaheadSectorSerializer(sectors, many=True).data,\n         'location': cached_locations,\n+        'page_size_default': settings.PROJECT_DIRECTORY_PAGE_SIZES[0],\n     }\n \n     return Response(response)\n", "issue": "Pagination of projects is incorrect\nOn the home page, the pagination is off for large sets of projects. \r\n\r\n#### Example\r\nSelect the EU trust fund for Africa in the organisations filter. The pagination widget shows that there are 22 pages of projects, but the last [two are empty](https://rsr.akvo.org/en/projects/?organisation=3394&page=22)!\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n\"\"\"Akvo RSR is covered by the GNU Affero General Public License.\n\nSee more details in the license.txt file located at the root folder of the Akvo RSR module.\nFor additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.\n\"\"\"\n\nfrom django.db.models import Q\nfrom rest_framework.decorators import api_view\nfrom rest_framework.response import Response\n\nfrom akvo.codelists.store.codelists_v202 import SECTOR_CATEGORY\nfrom akvo.rest.serializers import (ProjectSerializer, ProjectExtraSerializer,\n                                   ProjectExtraDeepSerializer,\n                                   ProjectIatiExportSerializer,\n                                   ProjectUpSerializer,\n                                   ProjectDirectorySerializer,\n                                   TypeaheadOrganisationSerializer,\n                                   TypeaheadSectorSerializer,)\nfrom akvo.rest.views.utils import (\n    int_or_none, get_cached_data, get_qs_elements_for_page, set_cached_data\n)\nfrom akvo.rsr.models import Project\nfrom akvo.rsr.filters import location_choices, get_m49_filter\nfrom akvo.utils import codelist_choices\nfrom ..viewsets import PublicProjectViewSet\n\n\nclass ProjectViewSet(PublicProjectViewSet):\n\n    \"\"\"\n    Viewset providing Project data.\n    \"\"\"\n    queryset = Project.objects.select_related(\n        'categories',\n        'keywords',\n        'partners',\n    ).prefetch_related(\n        'publishingstatus',\n    )\n    serializer_class = ProjectSerializer\n    project_relation = ''\n\n    def get_queryset(self):\n        \"\"\"\n        Allow custom filter for sync_owner, since this field has been replaced by the\n        reporting org partnership.\n        \"\"\"\n\n        sync_owner = self.request.query_params.get('sync_owner', None)\n        reporting_org = self.request.query_params.get('reporting_org', None)\n\n        reporting_org = reporting_org or sync_owner\n        if reporting_org:\n            self.queryset = self.queryset.filter(\n                partnerships__iati_organisation_role=101,\n                partnerships__organisation__pk=reporting_org\n            ).distinct()\n        return super(ProjectViewSet, self).get_queryset()\n\n\nclass ProjectIatiExportViewSet(PublicProjectViewSet):\n    \"\"\"Lean viewset for project data, as used in the My IATI section of RSR.\"\"\"\n    queryset = Project.objects.only(\n        'id',\n        'title',\n        'is_public',\n        'status',\n    ).select_related(\n        'partners',\n    ).prefetch_related(\n        'iati_checks',\n        'publishingstatus',\n        'partnerships',\n    )\n    serializer_class = ProjectIatiExportSerializer\n    project_relation = ''\n    paginate_by_param = 'limit'\n    max_paginate_by = 50\n\n    def get_queryset(self):\n        \"\"\"\n        Allow custom filter for sync_owner, since this field has been replaced by the\n        reporting org partnership.\n        \"\"\"\n        reporting_org = self.request.query_params.get('reporting_org', None)\n        if reporting_org:\n            self.queryset = self.queryset.filter(\n                partnerships__iati_organisation_role=101,\n                partnerships__organisation__pk=reporting_org\n            ).distinct()\n        return super(ProjectIatiExportViewSet, self).get_queryset()\n\n\nclass ProjectExtraViewSet(ProjectViewSet):\n\n    \"\"\"\n    Viewset providing extra Project data.\n\n    Allowed parameters are:\n    __limit__ (default 10, max 30),\n    __partnerships\\__organisation__ (filter on organisation ID), and\n    __publishingstatus\\__status__ (filter on publishing status)\n    \"\"\"\n\n    queryset = Project.objects.prefetch_related(\n        'publishingstatus',\n        'sectors',\n        'partnerships',\n        'budget_items',\n        'legacy_data',\n        'links',\n        'locations',\n        'locations__country',\n        'planned_disbursements',\n        'policy_markers',\n        'documents',\n        'comments',\n        'conditions',\n        'contacts',\n        'project_updates',\n        'recipient_countries',\n        'recipient_regions',\n        'related_projects',\n        'results',\n        'sectors',\n        'transactions',\n    )\n    serializer_class = ProjectExtraSerializer\n    paginate_by_param = 'limit'\n    paginate_by = 10\n    max_paginate_by = 30\n\n\nclass ProjectExtraDeepViewSet(ProjectViewSet):\n\n    \"\"\"\n    Viewset providing extra deep (depth=2 or bigger) Project data.\n\n    Allowed parameters are:\n    __limit__ (default 5, max 10),\n    __partnerships\\__organisation__ (filter on organisation ID), and\n    __publishingstatus\\__status__ (filter on publishing status)\n    \"\"\"\n\n    queryset = Project.objects.prefetch_related(\n        'publishingstatus',\n        'sectors',\n        'partnerships',\n        'budget_items',\n        'legacy_data',\n        'links',\n        'locations',\n        'locations__country',\n        'planned_disbursements',\n        'policy_markers',\n        'documents',\n        'comments',\n        'conditions',\n        'contacts',\n        'project_updates',\n        'recipient_countries',\n        'recipient_regions',\n        'related_projects',\n        'results',\n        'sectors',\n        'transactions',\n    )\n    serializer_class = ProjectExtraDeepSerializer\n    paginate_by_param = 'limit'\n    paginate_by = 5\n    max_paginate_by = 10\n\n\nclass ProjectUpViewSet(ProjectViewSet):\n\n    \"\"\"\n    Viewset providing extra data and limited filtering for Up in one go.\n\n    Allowed parameters are:\n    __limit__ (default 30, max 100),\n    __partnerships\\__organisation__ (filter on organisation ID), and\n    __publishingstatus\\__status__ (filter on publishing status)\n    \"\"\"\n\n    queryset = Project.objects.select_related(\n        'primary_location',\n        'categories',\n        'keywords',\n        'partners',\n    ).prefetch_related(\n        'publishingstatus',\n        'project_updates',\n    )\n    serializer_class = ProjectUpSerializer\n    paginate_by_param = 'limit'\n    max_paginate_by = 100\n\n\n###############################################################################\n# Project directory\n###############################################################################\n\n@api_view(['GET'])\ndef project_directory(request):\n    \"\"\"Return the values for various project filters.\n\n    Based on the current filters, it returns new options for all the (other)\n    filters. This is used to generate dynamic filters.\n\n    \"\"\"\n\n    # Fetch projects based on whether we are an Akvo site or RSR main site\n    page = request.rsr_page\n    projects = page.projects() if page else Project.objects.all().public().published()\n\n    # Exclude projects which don't have an image or a title\n    # FIXME: This happens silently and may be confusing?\n    projects = projects.exclude(Q(title='') | Q(current_image=''))\n\n    # Filter projects based on query parameters\n    filter_, text_filter = _create_filters_query(request)\n    projects = projects.filter(filter_).distinct() if filter_ is not None else projects\n    # NOTE: The text filter is handled differently/separately from the other filters.\n    # The text filter allows users to enter free form text, which could result in no\n    # projects being found for the given text. Other fields only allow selecting from\n    # a list of options, and for every combination that is shown to users and\n    # selectable by them, at least one project exists.\n    # When no projects are returned for a given search string, if the text search is\n    # not handled separately, the options for all the other filters are empty, and\n    # this causes the filters to get cleared automatically. This is very weird UX.\n    projects_text_filtered = (\n        projects.filter(text_filter) if text_filter is not None else projects\n    )\n    if projects_text_filtered.exists():\n        projects = projects_text_filtered\n\n    # Pre-fetch related fields to make things faster\n    projects = projects.select_related(\n        'primary_location',\n        'primary_organisation',\n    ).prefetch_related(\n        'locations',\n        'locations__country',\n        'recipient_countries',\n        'recipient_countries__country',\n    )\n\n    # Get the relevant data for typeaheads based on filtered projects (minus\n    # text filtering, if no projects were found)\n    cached_locations, _ = get_cached_data(request, 'locations', None, None)\n    if cached_locations is None:\n        cached_locations = [\n            {'id': choice[0], 'name': choice[1]}\n            for choice in location_choices(projects)\n        ]\n        set_cached_data(request, 'locations', cached_locations)\n\n    organisations = projects.all_partners().values('id', 'name', 'long_name')\n\n    # FIXME: Currently only vocabulary 2 is supported (as was the case with\n    # static filters). This could be extended to other vocabularies, in future.\n    valid_sectors = dict(codelist_choices(SECTOR_CATEGORY))\n    sectors = projects.sectors().filter(\n        vocabulary='2', sector_code__in=valid_sectors\n    ).values('sector_code').distinct()\n\n    # NOTE: We use projects_text_filtered for displaying projects\n    count = projects_text_filtered.count()\n    display_projects = get_qs_elements_for_page(projects_text_filtered, request).select_related(\n        'primary_organisation'\n    )\n\n    # NOTE: We use the _get_cached_data function to individually cache small\n    # bits of data to avoid the response from never getting saved in the cache,\n    # because the response is larger than the max size of data that can be\n    # saved in the cache.\n    cached_projects, showing_cached_projects = get_cached_data(\n        request, 'projects', display_projects, ProjectDirectorySerializer\n    )\n    cached_organisations, _ = get_cached_data(\n        request, 'organisations', organisations, TypeaheadOrganisationSerializer\n    )\n\n    response = {\n        'project_count': count,\n        'projects': cached_projects,\n        'showing_cached_projects': showing_cached_projects,\n        'organisation': cached_organisations,\n        'sector': TypeaheadSectorSerializer(sectors, many=True).data,\n        'location': cached_locations,\n    }\n\n    return Response(response)\n\n\ndef _create_filters_query(request):\n    \"\"\"Returns a Q object expression based on query parameters.\"\"\"\n    keyword_param = int_or_none(request.GET.get('keyword'))\n    location_param = int_or_none(request.GET.get('location'))\n    status_param = int_or_none(request.GET.get('status'))\n    organisation_param = int_or_none(request.GET.get('organisation'))\n    sector_param = int_or_none(request.GET.get('sector'))\n    title_or_subtitle_param = request.GET.get('title_or_subtitle')\n\n    keyword_filter = Q(keywords__id=keyword_param) if keyword_param else None\n    location_filter = get_m49_filter(location_param) if location_param else None\n    status_filter = Q(iati_status=status_param) if status_param else None\n    organisation_filter = Q(partners__id=organisation_param) if organisation_param else None\n    sector_filter = (\n        Q(sectors__sector_code=sector_param, sectors__vocabulary='2')\n        if sector_param else None\n    )\n    title_or_subtitle_filter = (\n        Q(title__icontains=title_or_subtitle_param) | Q(subtitle__icontains=title_or_subtitle_param)\n    ) if title_or_subtitle_param else None\n    all_filters = [\n        keyword_filter,\n        location_filter,\n        status_filter,\n        organisation_filter,\n        sector_filter,\n    ]\n    filters = filter(None, all_filters)\n    return reduce(lambda x, y: x & y, filters) if filters else None, title_or_subtitle_filter\n", "path": "akvo/rest/views/project.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n\"\"\"Akvo RSR is covered by the GNU Affero General Public License.\n\nSee more details in the license.txt file located at the root folder of the Akvo RSR module.\nFor additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.\n\"\"\"\n\nfrom django.conf import settings\nfrom django.db.models import Q\nfrom rest_framework.decorators import api_view\nfrom rest_framework.response import Response\n\nfrom akvo.codelists.store.codelists_v202 import SECTOR_CATEGORY\nfrom akvo.rest.serializers import (ProjectSerializer, ProjectExtraSerializer,\n                                   ProjectExtraDeepSerializer,\n                                   ProjectIatiExportSerializer,\n                                   ProjectUpSerializer,\n                                   ProjectDirectorySerializer,\n                                   TypeaheadOrganisationSerializer,\n                                   TypeaheadSectorSerializer,)\nfrom akvo.rest.views.utils import (\n    int_or_none, get_cached_data, get_qs_elements_for_page, set_cached_data\n)\nfrom akvo.rsr.models import Project\nfrom akvo.rsr.filters import location_choices, get_m49_filter\nfrom akvo.utils import codelist_choices\nfrom ..viewsets import PublicProjectViewSet\n\n\nclass ProjectViewSet(PublicProjectViewSet):\n\n    \"\"\"\n    Viewset providing Project data.\n    \"\"\"\n    queryset = Project.objects.select_related(\n        'categories',\n        'keywords',\n        'partners',\n    ).prefetch_related(\n        'publishingstatus',\n    )\n    serializer_class = ProjectSerializer\n    project_relation = ''\n\n    def get_queryset(self):\n        \"\"\"\n        Allow custom filter for sync_owner, since this field has been replaced by the\n        reporting org partnership.\n        \"\"\"\n\n        sync_owner = self.request.query_params.get('sync_owner', None)\n        reporting_org = self.request.query_params.get('reporting_org', None)\n\n        reporting_org = reporting_org or sync_owner\n        if reporting_org:\n            self.queryset = self.queryset.filter(\n                partnerships__iati_organisation_role=101,\n                partnerships__organisation__pk=reporting_org\n            ).distinct()\n        return super(ProjectViewSet, self).get_queryset()\n\n\nclass ProjectIatiExportViewSet(PublicProjectViewSet):\n    \"\"\"Lean viewset for project data, as used in the My IATI section of RSR.\"\"\"\n    queryset = Project.objects.only(\n        'id',\n        'title',\n        'is_public',\n        'status',\n    ).select_related(\n        'partners',\n    ).prefetch_related(\n        'iati_checks',\n        'publishingstatus',\n        'partnerships',\n    )\n    serializer_class = ProjectIatiExportSerializer\n    project_relation = ''\n    paginate_by_param = 'limit'\n    max_paginate_by = 50\n\n    def get_queryset(self):\n        \"\"\"\n        Allow custom filter for sync_owner, since this field has been replaced by the\n        reporting org partnership.\n        \"\"\"\n        reporting_org = self.request.query_params.get('reporting_org', None)\n        if reporting_org:\n            self.queryset = self.queryset.filter(\n                partnerships__iati_organisation_role=101,\n                partnerships__organisation__pk=reporting_org\n            ).distinct()\n        return super(ProjectIatiExportViewSet, self).get_queryset()\n\n\nclass ProjectExtraViewSet(ProjectViewSet):\n\n    \"\"\"\n    Viewset providing extra Project data.\n\n    Allowed parameters are:\n    __limit__ (default 10, max 30),\n    __partnerships\\__organisation__ (filter on organisation ID), and\n    __publishingstatus\\__status__ (filter on publishing status)\n    \"\"\"\n\n    queryset = Project.objects.prefetch_related(\n        'publishingstatus',\n        'sectors',\n        'partnerships',\n        'budget_items',\n        'legacy_data',\n        'links',\n        'locations',\n        'locations__country',\n        'planned_disbursements',\n        'policy_markers',\n        'documents',\n        'comments',\n        'conditions',\n        'contacts',\n        'project_updates',\n        'recipient_countries',\n        'recipient_regions',\n        'related_projects',\n        'results',\n        'sectors',\n        'transactions',\n    )\n    serializer_class = ProjectExtraSerializer\n    paginate_by_param = 'limit'\n    paginate_by = 10\n    max_paginate_by = 30\n\n\nclass ProjectExtraDeepViewSet(ProjectViewSet):\n\n    \"\"\"\n    Viewset providing extra deep (depth=2 or bigger) Project data.\n\n    Allowed parameters are:\n    __limit__ (default 5, max 10),\n    __partnerships\\__organisation__ (filter on organisation ID), and\n    __publishingstatus\\__status__ (filter on publishing status)\n    \"\"\"\n\n    queryset = Project.objects.prefetch_related(\n        'publishingstatus',\n        'sectors',\n        'partnerships',\n        'budget_items',\n        'legacy_data',\n        'links',\n        'locations',\n        'locations__country',\n        'planned_disbursements',\n        'policy_markers',\n        'documents',\n        'comments',\n        'conditions',\n        'contacts',\n        'project_updates',\n        'recipient_countries',\n        'recipient_regions',\n        'related_projects',\n        'results',\n        'sectors',\n        'transactions',\n    )\n    serializer_class = ProjectExtraDeepSerializer\n    paginate_by_param = 'limit'\n    paginate_by = 5\n    max_paginate_by = 10\n\n\nclass ProjectUpViewSet(ProjectViewSet):\n\n    \"\"\"\n    Viewset providing extra data and limited filtering for Up in one go.\n\n    Allowed parameters are:\n    __limit__ (default 30, max 100),\n    __partnerships\\__organisation__ (filter on organisation ID), and\n    __publishingstatus\\__status__ (filter on publishing status)\n    \"\"\"\n\n    queryset = Project.objects.select_related(\n        'primary_location',\n        'categories',\n        'keywords',\n        'partners',\n    ).prefetch_related(\n        'publishingstatus',\n        'project_updates',\n    )\n    serializer_class = ProjectUpSerializer\n    paginate_by_param = 'limit'\n    max_paginate_by = 100\n\n\n###############################################################################\n# Project directory\n###############################################################################\n\n@api_view(['GET'])\ndef project_directory(request):\n    \"\"\"Return the values for various project filters.\n\n    Based on the current filters, it returns new options for all the (other)\n    filters. This is used to generate dynamic filters.\n\n    \"\"\"\n\n    # Fetch projects based on whether we are an Akvo site or RSR main site\n    page = request.rsr_page\n    projects = page.projects() if page else Project.objects.all().public().published()\n\n    # Exclude projects which don't have an image or a title\n    # FIXME: This happens silently and may be confusing?\n    projects = projects.exclude(Q(title='') | Q(current_image=''))\n\n    # Filter projects based on query parameters\n    filter_, text_filter = _create_filters_query(request)\n    projects = projects.filter(filter_).distinct() if filter_ is not None else projects\n    # NOTE: The text filter is handled differently/separately from the other filters.\n    # The text filter allows users to enter free form text, which could result in no\n    # projects being found for the given text. Other fields only allow selecting from\n    # a list of options, and for every combination that is shown to users and\n    # selectable by them, at least one project exists.\n    # When no projects are returned for a given search string, if the text search is\n    # not handled separately, the options for all the other filters are empty, and\n    # this causes the filters to get cleared automatically. This is very weird UX.\n    projects_text_filtered = (\n        projects.filter(text_filter) if text_filter is not None else projects\n    )\n    if projects_text_filtered.exists():\n        projects = projects_text_filtered\n\n    # Pre-fetch related fields to make things faster\n    projects = projects.select_related(\n        'primary_location',\n        'primary_organisation',\n    ).prefetch_related(\n        'locations',\n        'locations__country',\n        'recipient_countries',\n        'recipient_countries__country',\n    )\n\n    # Get the relevant data for typeaheads based on filtered projects (minus\n    # text filtering, if no projects were found)\n    cached_locations, _ = get_cached_data(request, 'locations', None, None)\n    if cached_locations is None:\n        cached_locations = [\n            {'id': choice[0], 'name': choice[1]}\n            for choice in location_choices(projects)\n        ]\n        set_cached_data(request, 'locations', cached_locations)\n\n    organisations = projects.all_partners().values('id', 'name', 'long_name')\n\n    # FIXME: Currently only vocabulary 2 is supported (as was the case with\n    # static filters). This could be extended to other vocabularies, in future.\n    valid_sectors = dict(codelist_choices(SECTOR_CATEGORY))\n    sectors = projects.sectors().filter(\n        vocabulary='2', sector_code__in=valid_sectors\n    ).values('sector_code').distinct()\n\n    # NOTE: We use projects_text_filtered for displaying projects\n    count = projects_text_filtered.count()\n    display_projects = get_qs_elements_for_page(projects_text_filtered, request).select_related(\n        'primary_organisation'\n    )\n\n    # NOTE: We use the _get_cached_data function to individually cache small\n    # bits of data to avoid the response from never getting saved in the cache,\n    # because the response is larger than the max size of data that can be\n    # saved in the cache.\n    cached_projects, showing_cached_projects = get_cached_data(\n        request, 'projects', display_projects, ProjectDirectorySerializer\n    )\n    cached_organisations, _ = get_cached_data(\n        request, 'organisations', organisations, TypeaheadOrganisationSerializer\n    )\n\n    response = {\n        'project_count': count,\n        'projects': cached_projects,\n        'showing_cached_projects': showing_cached_projects,\n        'organisation': cached_organisations,\n        'sector': TypeaheadSectorSerializer(sectors, many=True).data,\n        'location': cached_locations,\n        'page_size_default': settings.PROJECT_DIRECTORY_PAGE_SIZES[0],\n    }\n\n    return Response(response)\n\n\ndef _create_filters_query(request):\n    \"\"\"Returns a Q object expression based on query parameters.\"\"\"\n    keyword_param = int_or_none(request.GET.get('keyword'))\n    location_param = int_or_none(request.GET.get('location'))\n    status_param = int_or_none(request.GET.get('status'))\n    organisation_param = int_or_none(request.GET.get('organisation'))\n    sector_param = int_or_none(request.GET.get('sector'))\n    title_or_subtitle_param = request.GET.get('title_or_subtitle')\n\n    keyword_filter = Q(keywords__id=keyword_param) if keyword_param else None\n    location_filter = get_m49_filter(location_param) if location_param else None\n    status_filter = Q(iati_status=status_param) if status_param else None\n    organisation_filter = Q(partners__id=organisation_param) if organisation_param else None\n    sector_filter = (\n        Q(sectors__sector_code=sector_param, sectors__vocabulary='2')\n        if sector_param else None\n    )\n    title_or_subtitle_filter = (\n        Q(title__icontains=title_or_subtitle_param) | Q(subtitle__icontains=title_or_subtitle_param)\n    ) if title_or_subtitle_param else None\n    all_filters = [\n        keyword_filter,\n        location_filter,\n        status_filter,\n        organisation_filter,\n        sector_filter,\n    ]\n    filters = filter(None, all_filters)\n    return reduce(lambda x, y: x & y, filters) if filters else None, title_or_subtitle_filter\n", "path": "akvo/rest/views/project.py"}]}