Changeset - 5fa226284bcb
[Not reviewed]
1 7 0
Ben Sturmfels (bsturmfels) - 3 months ago 2024-03-20 03:54:54
ben@sturm.com.au
Delegate management of canonical URLs to Apache

This middleware is mostly redundant:

* redirecting to canonical URLs can be done more simply in Apache
* appending a forward slash is a default in CommonMiddleware now
* we're no longer using Squid cache

May need to update Apache to strip/redirect trailing "index.html".
8 files changed with 4 insertions and 85 deletions:
0 comments (0 inline, 0 general)
TODO.md
Show inline comments
 
# To-do
 

	
 
* remove `ForceCanonicalHostnameMiddleware` by ensuring canonical redirect and HTTPS redirect is done by Apache
 
* ask Denver about why so many license files
 
* serve a 400 in Apache for a hostname we don't explicitly support
 
* use `<detail>` elements for supporter page hidden sections, rather than complex jQuery - or consider Alpine.js
 
* replace `internalNavigate` with inline flexbox layout
 
* add tests for main pages returning 200
 

	
 

	
 
# Done
 

	
 
* remove `ForceCanonicalHostnameMiddleware` by ensuring canonical redirect and HTTPS redirect is done by Apache
 
* standardise settings to replace `settings.py` and `djangocommonsettings.py`
 
  with `settings/prod.py` and move `SECRET_KEY` to an environment variable
 
* migrate to Django 4.2 LTS
 
* review `apache2` directory - may be unused
 
* add deployment script that runs migrations and collects static files
 
* switch `ParameterValidator` to use `SECRET_KEY` if possible to minimize
 
  non-standard settings
 
* install staticfiles app
conservancy/blog/views.py
Show inline comments
...
 
@@ -69,93 +69,90 @@ def custom_index(request, queryset, *args, **kwargs):
 
    except EmptyPage:
 
        # If page is out of range (e.g. 9999), deliver last page of results.
 
        blog_entries = paginator.page(paginator.num_pages)
 

	
 
    extra_context['blog_entries'] = blog_entries
 

	
 
    return render(request, 'blog/entry_list.html', extra_context)
 

	
 
def techblog_redirect(request):
 
    """Redirect from the old 'techblog' to the new blog
 
    """
 

	
 
    path = request.path[len('/technology'):]
 
    if path == '/blog/':
 
        path += "?author=bkuhn"
 

	
 
    return relative_redirect(request, path)
 

	
 
def query(request):
 
    """Page to query the blog based on authors and tags
 
    """
 

	
 
    if request.GET:
 
        d = request.GET.copy()
 
        if 'authors' in d.getlist('all'):
 
            d.setlist('author', []) # remove author queries
 
        if 'tags' in d.getlist('all'):
 
            d.setlist('tag', []) # remove tag queries
 
        d.setlist('all', []) # remove "all" from the query string
 

	
 
        base_url = '/blog/'
 
        if 'rss' in d:
 
            base_url = '/feeds/blog/'
 
            d.setlist('rss', []) # remove it
 

	
 
        query_string = d.urlencode()
 

	
 
        return relative_redirect(request, '{}{}{}'.format(base_url, '?' if query_string else '', query_string))
 

	
 
    else:
 
        authors = sorted(Person.objects.filter(currently_employed=True,
 
                                               entry__isnull=False).distinct(),
 
                         key=last_name)
 
        tags = EntryTag.objects.all().order_by('label')
 
        return render(request, 'blog/query.html', {'authors': authors, 'tags': tags})
 

	
 
def relative_redirect(request, path):
 
    from django import http
 
    from django.conf import settings
 

	
 
    host = request.get_host()
 
    if settings.FORCE_CANONICAL_HOSTNAME:
 
        host = settings.FORCE_CANONICAL_HOSTNAME
 

	
 
    url = "{}://{}{}".format(request.is_secure() and 'https' or 'http', host, path)
 
    return http.HttpResponseRedirect(url)
 

	
 
class BlogYearArchiveView(YearArchiveView):
 
    make_object_list = True
 
    allow_future = True
 
    extra_context = {}
 
    
 
    def get_context_data(self, **kwargs):
 
        context = super().get_context_data(**kwargs)
 
        context.update(self.extra_context)
 
        return context
 

	
 
class BlogMonthArchiveView(MonthArchiveView):
 
    allow_future = True
 
    extra_context = {}
 
    
 
    def get_context_data(self, **kwargs):
 
        context = super().get_context_data(**kwargs)
 
        context.update(self.extra_context)
 
        return context
 

	
 
class BlogDayArchiveView(DayArchiveView):
 
    allow_future = True
 
    extra_context = {}
 
    
 
    def get_context_data(self, **kwargs):
 
        context = super().get_context_data(**kwargs)
 
        context.update(self.extra_context)
 
        return context
 

	
 
class BlogDateDetailView(DateDetailView):
 
    allow_future = True
 
    extra_context = {}
 
    
 
    def get_context_data(self, **kwargs):
 
        context = super().get_context_data(**kwargs)
 
        context.update(self.extra_context)
 
        return context
conservancy/local_context_processors.py
Show inline comments
 
from datetime import datetime as DateTime
 

	
 
from django.conf import settings
 

	
 
from .fundgoal.models import FundraisingGoal
 

	
 
SITE_FUNDGOAL = 'cy2023-end-year-match'
 

	
 
def fundgoal_lookup(fundraiser_sought):
 
    try:
 
        return FundraisingGoal.objects.get(fundraiser_code_name=fundraiser_sought)
 
    except FundraisingGoal.DoesNotExist:
 
        # we have no object!  do something
 
        return None
 

	
 
def sitefundraiser(request):
 
    return {
 
        'datetime_now': DateTime.now(),
 
        'sitefundgoal': fundgoal_lookup(SITE_FUNDGOAL),
 
    }
 

	
 
if settings.FORCE_CANONICAL_HOSTNAME:
 
    _HOST_URL_VAR = {'host_url': 'https://' + settings.FORCE_CANONICAL_HOSTNAME}
 
    def host_url(request):
 
        return _HOST_URL_VAR
 
else:
 
    def host_url(request):
 
        return {'host_url': request.build_absolute_uri('/').rstrip('/')}
 
def host_url(request):
 
    return {'host_url': request.build_absolute_uri('/').rstrip('/')}
conservancy/middleware.py
Show inline comments
 
deleted file
conservancy/podjango/views.py
Show inline comments
...
 
@@ -60,56 +60,53 @@ def custom_index(request, queryset, *args, **kwargs):
 
    tags = []
 
    if 'tag' in request.GET:
 
        tags = [get_object_or_404(CastTag, slug=tag)
 
                for tag in request.GET.getlist('tag')]
 
        extra_context['tags'] = tags
 
        queryset = queryset.filter(OR_filter('tags', tags))
 

	
 
    if authors or tags:
 
        query_string = '&'.join(['author=%s' % a.username for a in authors]
 
                                + ['tag=%s' % t.slug for t in tags])
 
        extra_context['query_string'] = query_string
 

	
 
    else:
 
        date_list = queryset.dates(date_field, 'year')
 
        extra_context['date_list'] = date_list
 

	
 
    # TODO
 
    return render(request, 'podjango/cast/cast_list.html', {'object_list': queryset})
 

	
 

	
 
def query(request):
 
    """Page to query the cast based on and tags
 
    """
 

	
 
    if request.GET:
 
        d = request.GET.copy()
 
        if 'authors' in d.getlist('all'):
 
            d.setlist('author', [])  # remove author queries
 
        if 'tags' in d.getlist('all'):
 
            d.setlist('tag', [])  # remove tag queries
 
        d.setlist('all', [])  # remove "all" from the query string
 

	
 
        base_url = '/cast/'
 
        if 'rss' in d:
 
            base_url = '/feeds/cast/'
 
            d.setlist('rss', [])  # remove it
 

	
 
        query_string = d.urlencode()
 

	
 
        return relative_redirect(request, '%s%s%s' % (base_url, '?' if query_string else '', query_string))
 

	
 
    else:
 
        tags = CastTag.objects.all().order_by('label')
 
        return render(request, 'podjango/cast/query.html', {'tags': tags})
 

	
 

	
 
def relative_redirect(request, path):
 
    from django import http
 
    from django.conf import settings
 

	
 
    host = http.get_host(request)
 
    if settings.FORCE_CANONICAL_HOSTNAME:
 
        host = settings.FORCE_CANONICAL_HOSTNAME
 

	
 
    url = "%s://%s%s" % (request.is_secure() and 'https' or 'http', host, path)
 
    return http.HttpResponseRedirect(url)
conservancy/settings/base.py
Show inline comments
 
# Copyright 2005-2008, James Garrison
 
# Copyright 2010, Bradley M. Kuhn
 

	
 
# This software's license gives you freedom; you can copy, convey,
 
# propagate, redistribute, modify and/or redistribute modified versions of
 
# this program under the terms of the GNU Affero General Public License
 
# (AGPL) as published by the Free Software Foundation (FSF), either
 
# version 3 of the License, or (at your option) any later version of the
 
# AGPL published by the FSF.
 
#
 
# This program is distributed in the hope that it will be useful, but
 
# WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero
 
# General Public License for more details.
 
#
 
# You should have received a copy of the GNU Affero General Public License
 
# along with this program in a file in the toplevel directory called
 
# "AGPLv3".  If not, see <http://www.gnu.org/licenses/>.
 

	
 
from pathlib import Path
 

	
 
SITE_ID = 2
 
ROOT_URLCONF = 'conservancy.urls'
 

	
 
REDIRECT_TABLE = {
 
    'www.sf-conservancy.org': 'sfconservancy.org',
 
}
 

	
 
LOGGING = {
 
    'version': 1,
 
    'disable_existing_loggers': False,
 
    'formatters': {
 
        'default': {
 
            'format': '%(asctime)s %(levelname)s %(name)s: %(message)s',
 
            'datefmt': '%Y-%m-%d %H:%M:%S',
 
        },
 
    },
 
    'filters': {
 
        'require_debug_false': {
 
            '()': 'django.utils.log.RequireDebugFalse'
 
        }
 
    },
 
    'handlers': {
 
        'mail_admins': {
 
            'level': 'ERROR',
 
            'filters': ['require_debug_false'],
 
            'class': 'django.utils.log.AdminEmailHandler'
 
        },
 
        'console': {
 
            'level': 'DEBUG',
 
            'class': 'logging.StreamHandler',
 
            'formatter': 'default',
 
        },
 
    },
 
    'loggers': {
 
        'django.request': {
 
            'handlers': ['mail_admins'],
 
            'level': 'ERROR',
 
            'propagate': True,
 
        },
 
        # Avoid email notification on DisallowedHost error.
 
        'django.security.DisallowedHost': {
 
            'handlers': ['console'],
 
            'propagate': False,
 
        },
 
    },
 
    'root': {
 
        'handlers': ['console'],
 
        'level': 'INFO',
 
    },
 
}
 

	
 
INSTALLED_APPS = [
 
    'django.contrib.auth',
 
    'django.contrib.contenttypes',
 
    'django.contrib.sessions',
...
 
@@ -91,55 +87,54 @@ INSTALLED_APPS = [
 
    'conservancy.fundgoal',
 
    'conservancy.assignment',
 
    'conservancy.fossy',
 
    'conservancy.podjango',
 
    'conservancy.usethesource.apps.UseTheSourceConfig',
 
]
 

	
 
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
 

	
 
BASE_DIR = Path(__file__).resolve().parent.parent
 
TEMPLATES = [
 
    {
 
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
 
        'DIRS': [
 
            BASE_DIR / 'templates',
 
            BASE_DIR / 'static',
 
        ],
 
        'APP_DIRS': True,
 
        'OPTIONS': {
 
            'context_processors': [
 
                'django.template.context_processors.debug',
 
                'django.template.context_processors.request',
 
                'django.contrib.auth.context_processors.auth',
 
                'django.contrib.messages.context_processors.messages',
 
                'conservancy.local_context_processors.host_url',
 
                'conservancy.local_context_processors.sitefundraiser',
 
            ]
 
        }
 
    }
 
]
 

	
 
# Internationalization
 
TIME_ZONE = 'America/New_York'
 
LANGUAGE_CODE = 'en-us'
 

	
 
STATIC_URL = '/static/'
 
STATIC_ROOT = BASE_DIR.parent / 'collected_static'
 
STATICFILES_DIRS = [
 
    BASE_DIR / 'static',
 
]
 

	
 
MEDIA_URL = '/media/'
 

	
 
MIDDLEWARE = [
 
    'django.middleware.common.CommonMiddleware',
 
    'django.contrib.sessions.middleware.SessionMiddleware',
 
    'django.contrib.auth.middleware.AuthenticationMiddleware',
 
    'django.contrib.messages.middleware.MessageMiddleware',
 
    'conservancy.middleware.ForceCanonicalHostnameMiddleware',
 
]
 

	
 
USETHESOURCE = {
 
    'SENDER': 'compliance@sfconservancy.org',
 
    'LIST_RECIPIENT': 'ccs-review@lists.sfconservancy.org',
 
}
conservancy/settings/dev.py
Show inline comments
 
from .base import *
 

	
 
DEBUG = True
 
ALLOWED_HOSTS = ['*']
 

	
 
FORCE_CANONICAL_HOSTNAME = False
 

	
 
DATABASES = {
 
    'default': {
 
        'NAME': 'conservancy-website.sqlite3',
 
        'ENGINE': 'django.db.backends.sqlite3',
 
    }
 
}
 

	
 
SECRET_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
 

	
 
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
conservancy/settings/prod.py
Show inline comments
 
import json
 

	
 
from django.core.exceptions import ImproperlyConfigured
 

	
 
from .base import *
 

	
 
DEBUG = False
 
ALLOWED_HOSTS = ['www.sfconservancy.org', 'sfconservancy.org']
 

	
 
FORCE_CANONICAL_HOSTNAME = 'sfconservancy.org'
 

	
 
ADMINS = [
 
    ('Bradley M. Kuhn', 'sysadmin@sfconservancy.org'),
 
    ('Ben Sturmfels', 'sysadmin+conservancy@sturm.com.au'),
 
]
 

	
 
MANAGERS = [
 
    ('Bradley M. Kuhn', 'sysadmin@sfconservancy.org'),
 
]
 

	
 
DATABASES = {
 
    'default': {
 
        'NAME': '/var/lib/www/database/conservancy-website.sqlite3',
 
        'ENGINE': 'django.db.backends.sqlite3',
 
    }
 
}
 

	
 
# Apache/mod_wsgi doesn't make it straightforward to pass environment variables
 
# to Django (can't use the Apache config).
 
with open(BASE_DIR.parent / 'secrets.json') as f:
 
    secrets = json.load(f)
 

	
 
def get_secret(secrets, setting):
 
    try:
 
        return secrets[setting]
 
    except KeyError:
 
        raise ImproperlyConfigured(f'Missing secret \'{setting}\'')
 

	
 
SECRET_KEY = get_secret(secrets, 'SECRET_KEY')
 

	
 
SESSION_COOKIE_SECURE = True
0 comments (0 inline, 0 general)