diff --git a/.travis.yml b/.travis.yml index a1fb5adcbf..d43767e4ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ cache: pip services: - postgresql + - memcached python: - "2.7" diff --git a/akvo/rest/views/partnership.py b/akvo/rest/views/partnership.py index 78d7c21632..153d6b5873 100644 --- a/akvo/rest/views/partnership.py +++ b/akvo/rest/views/partnership.py @@ -32,5 +32,5 @@ class PartnershipMoreLinkViewSet(PublicProjectViewSet): Specific endpoint for the '+X partners' links in RSR. Contains the name, long name and logo of an organisation and the partnership role. """ - queryset = Partnership.objects.all() + queryset = Partnership.objects.select_related('organisation', 'project').all() serializer_class = PartnershipBasicSerializer diff --git a/akvo/rsr/middleware.py b/akvo/rsr/middleware.py index 27e5edcb4a..942bdf04b2 100644 --- a/akvo/rsr/middleware.py +++ b/akvo/rsr/middleware.py @@ -8,16 +8,14 @@ """ import logging -import json + from django.conf import settings from django.core.exceptions import DisallowedHost from django.db.models import Q -from django.core.urlresolvers import (get_resolver, LocaleRegexURLResolver) from django.shortcuts import redirect + from akvo.rsr.context_processors import extra_context from akvo.rsr.models import PartnerSite -from django.utils import translation -from django.http import HttpResponseRedirect, HttpResponse def _is_rsr_host(hostname): @@ -52,64 +50,9 @@ def _build_api_link(request, resource, object_id): ) -class DefaultLanguageMiddleware(object): - - """A non working (BROKEN) default language middleware. - - A try in supporting default languages, but since this will redirect all url_patterns and - not only i18n ones it's broken. - """ - - def __init__(self): - """.""" - self._is_language_prefix_patterns_used = False - for url_pattern in get_resolver(None).url_patterns: - if isinstance(url_pattern, LocaleRegexURLResolver): - self._is_language_prefix_patterns_used = True - break - - def is_language_prefix_patterns_used(self): - """.""" - return self._is_language_prefix_patterns_used - - def is_i18n_path(self, path): - """.""" - from akvo.urls import localised_patterns - print "=> {}".format(localised_patterns) - from django.core.urlresolvers import resolve, Resolver404 - try: - resolve(path, localised_patterns) - return True - except Resolver404: - return False - except TypeError: - return False - return False - - def process_request(self, request): - """Redirect to selected language.""" - if not request.rsr_page: - return None - - if self.is_i18n_path(request.path): - print "{} was i18n path".format(request.path) - else: - print "{} was not 18n path".format(request.path) - - language_from_path = translation.get_language_from_path(request.path_info) - if not language_from_path: - if request.rsr_page.default_language: - lang = request.rsr_page.default_language - return HttpResponseRedirect('/{}{}'.format(lang, request.path)) - return HttpResponseRedirect('/en{}'.format(request.path)) - return None - - class HostDispatchMiddleware(object): + """RSR page dispatch middleware.""" - """RSR page dispath middleware.""" - - # def process_request(self, request, cname_domain=False, rsr_page=None): def process_request(self, request): """Route on request.""" request.rsr_page = None @@ -191,32 +134,3 @@ def process_response(request, response): if depth > '1': return redirect(_build_api_link(request, 'project_extra_deep', object_id)) return response - - -class NonHtmlDebugToolbarMiddleware(object): - """ - The Django Debug Toolbar usually only works for views that return HTML. - This middleware wraps any non-HTML response in HTML if the request - has a 'debug' query parameter (e.g. http://localhost/foo?debug) - Special handling for json (pretty printing) and - binary data (only show data length) - """ - - @staticmethod - def process_response(request, response): - if request.GET.get('debug') == '': - if response['Content-Type'] == 'application/octet-stream': - new_content = 'Binary Data, ' \ - 'Length: {}'.format(len(response.content)) - response = HttpResponse(new_content) - elif response['Content-Type'] != 'text/html': - content = response.content - try: - json_ = json.loads(content) - content = json.dumps(json_, sort_keys=True, indent=2) - except ValueError: - pass - response = HttpResponse('
{}'
-                                        '
'.format(content)) - - return response diff --git a/akvo/rsr/models/project.py b/akvo/rsr/models/project.py index ad2b9d5481..0adf7193aa 100644 --- a/akvo/rsr/models/project.py +++ b/akvo/rsr/models/project.py @@ -979,13 +979,13 @@ def budget_total(self): return Project.objects.budget_total().get(pk=self.pk).budget_total def has_multiple_budget_currencies(self): - budget_items = BudgetItem.objects.filter(project__id=self.pk) - num_currencies = len(set([self.currency] + [c.currency if c.currency else self.currency for c in budget_items])) - - if num_currencies > 1: - return True - else: - return False + # Using a python loop for iteration, because it's faster when + # budget_items have been pre-fetched + budget_items = self.budget_items.all() + num_currencies = len( + set([self.currency] + [c.currency for c in budget_items if c.currency]) + ) + return num_currencies > 1 def budget_currency_totals(self): budget_items = BudgetItem.objects.filter(project__id=self.pk) diff --git a/akvo/rsr/tests/views/test_project.py b/akvo/rsr/tests/views/test_project.py new file mode 100644 index 0000000000..13c8aedf16 --- /dev/null +++ b/akvo/rsr/tests/views/test_project.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +""" +Akvo RSR is covered by the GNU Affero General Public License. + +See more details in the license.txt file located at the root folder of the Akvo RSR module. +For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >. +""" + +from __future__ import print_function + +from unittest import skip + +import django_perf_rec +from django.test import TestCase + + +@skip('Needs Django >= 1.8') +class ProjectPerfomanceTestCase(TestCase): + """Test performance of project views.""" + + def setUp(self): + return + + def test_project_directory_listing(self): + with django_perf_rec.record(): + self.client.get('/en/projects/', follow=True) diff --git a/akvo/rsr/views/project.py b/akvo/rsr/views/project.py index ad0ec73cd8..ea53c61a78 100644 --- a/akvo/rsr/views/project.py +++ b/akvo/rsr/views/project.py @@ -95,7 +95,8 @@ def directory(request): # Build page page = request.GET.get('page') - page, paginator, page_range = pagination(page, sorted_projects, 10) + limit = request.GET.get('limit', settings.PROJECT_DIRECTORY_DEFAULT_SIZE) + page, paginator, page_range = pagination(page, sorted_projects, limit) # Get the current org filter for typeahead org_filter = request.GET.get('organisation', '') @@ -111,7 +112,8 @@ def directory(request): page.object_list = page.object_list.prefetch_related( 'publishingstatus', 'recipient_countries', - 'sectors' + 'sectors', + 'budget_items', ).select_related( 'primary_organisation', 'last_update' diff --git a/akvo/settings/10-base.conf b/akvo/settings/10-base.conf index d6f7249a16..3860462012 100644 --- a/akvo/settings/10-base.conf +++ b/akvo/settings/10-base.conf @@ -58,8 +58,6 @@ AUTH_USER_MODEL = 'rsr.User' MIDDLEWARE_CLASSES = ( 'akvo.rsr.middleware.HostDispatchMiddleware', - # 'akvo.rsr.middleware.DefaultLanguageMiddleware', - # 'akvo.rsr.middleware_old.PagesLocaleMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', diff --git a/akvo/settings/30-rsr.conf b/akvo/settings/30-rsr.conf index 3d5b8bce8d..7c01a8f9d3 100644 --- a/akvo/settings/30-rsr.conf +++ b/akvo/settings/30-rsr.conf @@ -23,6 +23,7 @@ DECIMALS_DECIMAL_PLACES = 2 WORDPRESS_NEWS_CATEGORY = 13 PROJECT_UPDATE_TIMEOUT = 20 +PROJECT_DIRECTORY_DEFAULT_SIZE = 10 RSR_CACHE_SECONDS = CACHES['default']['TIMEOUT'] diff --git a/akvo/utils.py b/akvo/utils.py index ba51d99efd..448828deed 100644 --- a/akvo/utils.py +++ b/akvo/utils.py @@ -17,8 +17,8 @@ from django.conf import settings from django.contrib.auth.models import Group -from django.core.mail import EmailMultiAlternatives -from django.core.mail import get_connection +from django.core.cache import cache +from django.core.mail import EmailMultiAlternatives, get_connection from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.db.models import get_model from django.http import HttpResponse @@ -293,13 +293,27 @@ def codelist_value(model, instance, field, version=settings.IATI_VERSION): :return: String of the codelist instance """ value = getattr(instance, field, None) - if value: - try: - objects = getattr(model, 'objects') - return objects.get(code=value, version__code=version) - except model.DoesNotExist: - return value - return '' + if not value: + return '' + + key = u'{}-{}-{}'.format(model.__name__, value, version) + result = cache.get(key) + if result is not None: + return result + + try: + objects = getattr(model, 'objects') + result = objects.get(code=value, version__code=version) + + except model.DoesNotExist: + result = value + + else: + # Update the cache only if the required data is in the DB! + cache.set(key, result) + + finally: + return result def codelist_name(model, instance, field, version=settings.IATI_VERSION): @@ -311,14 +325,9 @@ def codelist_name(model, instance, field, version=settings.IATI_VERSION): :param version: String of version (optional) :return: String of the codelist instance """ - value = getattr(instance, field, None) - if value: - try: - objects = getattr(model, 'objects') - return objects.get(code=value, version__code=version).name - except model.DoesNotExist: - return value - return '' + + value = codelist_value(model, instance, field, version) + return value.name if hasattr(value, 'name') else value def check_auth_groups(group_names): diff --git a/scripts/deployment/pip/requirements/3_testing.txt b/scripts/deployment/pip/requirements/3_testing.txt index 975719e50b..bc39dd4962 100644 --- a/scripts/deployment/pip/requirements/3_testing.txt +++ b/scripts/deployment/pip/requirements/3_testing.txt @@ -1,10 +1,3 @@ -# Requirements for building splinter -lxml==2.3.6 - -# Requirements for building lettuce -fuzzywuzzy==0.1 -sure==1.0.6 - # Modules used for app testing nose==1.2.1 mox==0.5.3 @@ -18,5 +11,9 @@ fake-factory==0.7.2 ipaddress==1.0.17 six==1.10.0 + # Modules for linting flake8==3.0.4 + +# django_perf_rec +django_perf_rec==2.0.0