mirror of
https://github.com/postgres/pgweb.git
synced 2025-08-01 15:54:53 +00:00
339 lines
11 KiB
Python
339 lines
11 KiB
Python
from django.shortcuts import render_to_response
|
|
from django.http import HttpResponse, Http404, HttpResponseRedirect
|
|
from django.http import HttpResponseNotModified
|
|
from django.template import TemplateDoesNotExist, loader
|
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
|
from django.contrib import messages
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from django.db.models import Count
|
|
from django.db import connection, transaction
|
|
from django.utils.http import http_date, parse_http_date
|
|
from django.conf import settings
|
|
|
|
from datetime import date, datetime
|
|
import os
|
|
import re
|
|
import urllib
|
|
|
|
from pgweb.util.decorators import ssl_required, ssl_optional, cache, nocache
|
|
from pgweb.util.contexts import NavContext
|
|
from pgweb.util.helpers import simple_form, PgXmlHelper, HttpServerError
|
|
from pgweb.util.moderation import get_all_pending_moderations
|
|
from pgweb.util.misc import get_client_ip, is_behind_cache, varnish_purge
|
|
from pgweb.util.sitestruct import get_all_pages_struct
|
|
|
|
# models needed for the pieces on the frontpage
|
|
from pgweb.news.models import NewsArticle
|
|
from pgweb.events.models import Event
|
|
from pgweb.quotes.models import Quote
|
|
from models import Version, ImportedRSSItem
|
|
|
|
# models needed for the pieces on the community page
|
|
from pgweb.survey.models import Survey
|
|
|
|
# models and forms needed for core objects
|
|
from models import Organisation
|
|
from forms import OrganisationForm, MergeOrgsForm
|
|
from django.template.context import RequestContext
|
|
|
|
# Front page view
|
|
@cache(minutes=10)
|
|
def home(request):
|
|
news = NewsArticle.objects.filter(approved=True)[:7]
|
|
events = Event.objects.select_related('country').filter(approved=True, training=False, enddate__gte=date.today).order_by('enddate', 'startdate')[:5]
|
|
try:
|
|
quote = Quote.objects.filter(approved=True).order_by('?')[0]
|
|
except:
|
|
# if there is no quote available, just ignore error
|
|
quote = None
|
|
versions = Version.objects.filter(supported=True)
|
|
planet = ImportedRSSItem.objects.filter(feed__internalname="planet").order_by("-posttime")[:7]
|
|
|
|
traininginfo = Event.objects.filter(approved=True, training=True).extra(where=("startdate <= (CURRENT_DATE + '6 Months'::interval) AND enddate >= CURRENT_DATE",)).aggregate(Count('id'), Count('country', distinct=True))
|
|
# can't figure out how to make django do this
|
|
curs = connection.cursor()
|
|
curs.execute("SELECT * FROM (SELECT DISTINCT(core_organisation.name) FROM events_event INNER JOIN core_organisation ON org_id=core_organisation.id WHERE startdate <= (CURRENT_DATE + '6 Months'::interval) AND enddate >= CURRENT_DATE AND events_event.approved AND training AND org_id IS NOT NULL) x ORDER BY random() LIMIT 3")
|
|
trainingcompanies = [r[0] for r in curs.fetchall()]
|
|
|
|
return render_to_response('index.html', {
|
|
'title': 'The world\'s most advanced open source database',
|
|
'news': news,
|
|
'events': events,
|
|
'traininginfo': traininginfo,
|
|
'trainingcompanies': trainingcompanies,
|
|
'quote': quote,
|
|
'versions': versions,
|
|
'planet': planet,
|
|
}, RequestContext(request))
|
|
|
|
# Community main page (contains surveys and potentially more)
|
|
def community(request):
|
|
s = Survey.objects.filter(current=True)
|
|
try:
|
|
s = s[0]
|
|
except:
|
|
s = None
|
|
planet = ImportedRSSItem.objects.filter(feed__internalname="planet").order_by("-posttime")[:7]
|
|
return render_to_response('core/community.html', {
|
|
'survey': s,
|
|
'planet': planet,
|
|
}, NavContext(request, 'community'))
|
|
|
|
# List of supported versions
|
|
def versions(request):
|
|
return render_to_response('support/versioning.html', {
|
|
'versions': Version.objects.filter(tree__gt=0).filter(testing=0),
|
|
}, NavContext(request, 'support'))
|
|
|
|
re_staticfilenames = re.compile("^[0-9A-Z/_-]+$", re.IGNORECASE)
|
|
# Generic fallback view for static pages
|
|
def fallback(request, url):
|
|
if url.find('..') > -1:
|
|
raise Http404('Page not found.')
|
|
|
|
if not re_staticfilenames.match(url):
|
|
raise Http404('Page not found.')
|
|
|
|
try:
|
|
t = loader.get_template('pages/%s.html' % url)
|
|
except TemplateDoesNotExist:
|
|
try:
|
|
t = loader.get_template('pages/%s/en.html' % url)
|
|
except TemplateDoesNotExist:
|
|
raise Http404('Page not found.')
|
|
|
|
# Guestimate the nav section by looking at the URL and taking the first
|
|
# piece of it.
|
|
try:
|
|
navsect = url.split('/',2)[0]
|
|
except:
|
|
navsect = ''
|
|
return HttpResponse(t.render(NavContext(request, navsect)))
|
|
|
|
# Edit-forms for core objects
|
|
@ssl_required
|
|
@login_required
|
|
def organisationform(request, itemid):
|
|
return simple_form(Organisation, itemid, request, OrganisationForm,
|
|
redirect='/account/edit/organisations/')
|
|
|
|
# robots.txt
|
|
def robots(request):
|
|
if not is_behind_cache(request):
|
|
# If we're not serving this through one of our Varnish caches, we allow *nothing* to be indexed
|
|
return HttpResponse("User-agent: *\nDisallow: /\n", content_type='text/plain')
|
|
else:
|
|
# Regular website
|
|
return HttpResponse("""User-agent: *
|
|
Disallow: /admin/
|
|
Disallow: /account/
|
|
Disallow: /docs/devel/
|
|
Disallow: /list/
|
|
Disallow: /search/
|
|
Disallow: /message-id/raw/
|
|
Disallow: /message-id/flat/
|
|
|
|
Sitemap: http://www.postgresql.org/sitemap.xml
|
|
""", content_type='text/plain')
|
|
|
|
|
|
# Sitemap (XML format)
|
|
@cache(hours=6)
|
|
def sitemap(request):
|
|
resp = HttpResponse(content_type='text/xml')
|
|
x = PgXmlHelper(resp)
|
|
x.startDocument()
|
|
x.startElement('urlset', {'xmlns': 'http://www.sitemaps.org/schemas/sitemap/0.9'})
|
|
pages = 0
|
|
for p in get_all_pages_struct():
|
|
pages+=1
|
|
x.startElement('url', {})
|
|
x.add_xml_element('loc', 'http://www.postgresql.org/%s' % urllib.quote(p[0]))
|
|
if len(p) > 1 and p[1]:
|
|
x.add_xml_element('priority', unicode(p[1]))
|
|
if len(p) > 2 and p[2]:
|
|
x.add_xml_element('lastmod', p[2].isoformat() + "Z")
|
|
x.endElement('url')
|
|
x.endElement('urlset')
|
|
x.endDocument()
|
|
return resp
|
|
|
|
# dynamic CSS serving, meaning we merge a number of different CSS into a
|
|
# single one, making sure it turns into a single http response. We do this
|
|
# dynamically, since the output will be cached (for all non-SSL users, which
|
|
# is the vast majority) anyway.
|
|
_dynamic_cssmap = {
|
|
'base': ['media/css/global.css',
|
|
'media/css/layout.css',
|
|
'media/css/text.css',
|
|
'media/css/navigation.css',
|
|
'media/css/table.css',
|
|
'media/css/iefixes.css'],
|
|
'docs': ['media/css/global.css',
|
|
'media/css/table.css',
|
|
'media/css/text.css',
|
|
'media/css/docs.css'],
|
|
}
|
|
@ssl_optional
|
|
@cache(hours=6)
|
|
def dynamic_css(request, css):
|
|
if not _dynamic_cssmap.has_key(css):
|
|
raise Http404('CSS not found')
|
|
files = _dynamic_cssmap[css]
|
|
resp = HttpResponse(content_type='text/css')
|
|
|
|
# We honor if-modified-since headers by looking at the most recently
|
|
# touched CSS file.
|
|
latestmod = 0
|
|
for fn in files:
|
|
try:
|
|
stime = os.stat(fn).st_mtime
|
|
if latestmod < stime:
|
|
latestmod = stime
|
|
except OSError:
|
|
# If we somehow referred to a file that didn't exist, or
|
|
# one that we couldn't access.
|
|
raise Http404('CSS (sub) not found')
|
|
if request.META.has_key('HTTP_IF_MODIFIED_SINCE'):
|
|
# This code is mostly stolen from django :)
|
|
matches = re.match(r"^([^;]+)(; length=([0-9]+))?$",
|
|
request.META.get('HTTP_IF_MODIFIED_SINCE'),
|
|
re.IGNORECASE)
|
|
header_mtime = parse_http_date(matches.group(1))
|
|
# We don't do length checking, just the date
|
|
if int(latestmod) <= header_mtime:
|
|
return HttpResponseNotModified(content_type='text/css')
|
|
resp['Last-Modified'] = http_date(latestmod)
|
|
|
|
for fn in files:
|
|
with open(fn) as f:
|
|
resp.write("/* %s */\n" % fn)
|
|
resp.write(f.read())
|
|
resp.write("\n")
|
|
|
|
return resp
|
|
|
|
@nocache
|
|
def csrf_failure(request, reason=''):
|
|
resp = render_to_response('errors/csrf_failure.html', {
|
|
'reason': reason,
|
|
})
|
|
resp.status_code = 403 # Forbidden
|
|
return resp
|
|
|
|
# Basic information about the connection
|
|
@cache(seconds=30)
|
|
def system_information(request):
|
|
return render_to_response('core/system_information.html', {
|
|
'server': os.uname()[1],
|
|
'behind_cache': is_behind_cache(request),
|
|
'cache_server': is_behind_cache(request) and request.META['REMOTE_ADDR'] or None,
|
|
'client_ip': get_client_ip(request),
|
|
})
|
|
|
|
@ssl_required
|
|
def system_information_ssl(request):
|
|
return render_to_response('core/system_information.html', {
|
|
'server': os.uname()[1],
|
|
'behind_cache': False,
|
|
'cache_server': None,
|
|
'client_ip': get_client_ip(request),
|
|
})
|
|
|
|
# Sync timestamp for automirror. Keep it around for 30 seconds
|
|
# Basically just a check that we can access the backend still...
|
|
@cache(seconds=30)
|
|
def sync_timestamp(request):
|
|
s = datetime.now().strftime("%Y-%m-%d %H:%M:%S\n")
|
|
r = HttpResponse(s, content_type='text/plain')
|
|
r['Content-Length'] = len(s)
|
|
return r
|
|
|
|
# List of all unapproved objects, for the special admin page
|
|
@login_required
|
|
@user_passes_test(lambda u: u.is_staff)
|
|
def admin_pending(request):
|
|
return render_to_response('core/admin_pending.html', {
|
|
'app_list': get_all_pending_moderations(),
|
|
}, RequestContext(request))
|
|
|
|
# Purge objects from varnish, for the admin pages
|
|
@login_required
|
|
@user_passes_test(lambda u: u.is_staff)
|
|
def admin_purge(request):
|
|
if request.method == 'POST':
|
|
url = request.POST['url']
|
|
if url == '':
|
|
return HttpResponseRedirect('.')
|
|
varnish_purge(url)
|
|
messages.info(request, "Purge completed: '^%s'" % url)
|
|
return HttpResponseRedirect('.')
|
|
|
|
# Fetch list of latest purges
|
|
curs = connection.cursor()
|
|
curs.execute("SELECT added, completed, consumer, mode, expr FROM varnishqueue.queue q LEFT JOIN varnishqueue.consumers c ON c.consumerid=q.consumerid ORDER BY added DESC")
|
|
latest = curs.fetchall()
|
|
|
|
return render_to_response('core/admin_purge.html', {
|
|
'latest_purges': latest,
|
|
}, RequestContext(request))
|
|
|
|
@ssl_required
|
|
@csrf_exempt
|
|
def api_varnish_purge(request):
|
|
if not request.META['REMOTE_ADDR'] in settings.VARNISH_PURGERS:
|
|
return HttpServerError("Invalid client address")
|
|
if request.method != 'POST':
|
|
return HttpServerError("Can't use this way")
|
|
n = int(request.POST['n'])
|
|
curs = connection.cursor()
|
|
for i in range(0, n):
|
|
expr = request.POST['p%s' % i]
|
|
curs.execute("SELECT varnish_purge_expr(%s)", (expr, ))
|
|
return HttpResponse("Purged %s entries\n" % n)
|
|
|
|
@nocache
|
|
@csrf_exempt
|
|
def api_repo_updated(request):
|
|
if not get_client_ip(request) in settings.SITE_UPDATE_HOSTS:
|
|
return HttpServerError("Invalid client address")
|
|
# Ignore methods and contents, just drop the trigger
|
|
open(settings.SITE_UPDATE_TRIGGER_FILE, 'a').close()
|
|
return HttpResponse("OK")
|
|
|
|
# Merge two organisations
|
|
@login_required
|
|
@user_passes_test(lambda u: u.is_superuser)
|
|
@transaction.atomic
|
|
def admin_mergeorg(request):
|
|
if request.method == 'POST':
|
|
form = MergeOrgsForm(data=request.POST)
|
|
if form.is_valid():
|
|
# Ok, try to actually merge organisations, by moving all objects
|
|
# attached
|
|
f = form.cleaned_data['merge_from']
|
|
t = form.cleaned_data['merge_into']
|
|
for e in f.event_set.all():
|
|
e.org = t
|
|
e.save()
|
|
for n in f.newsarticle_set.all():
|
|
n.org = t
|
|
n.save()
|
|
for p in f.product_set.all():
|
|
p.org = t
|
|
p.save()
|
|
for p in f.professionalservice_set.all():
|
|
p.organisation = t
|
|
p.save()
|
|
# Now that everything is moved, we can delete the organisation
|
|
f.delete()
|
|
|
|
return HttpResponseRedirect("/admin/core/organisation/")
|
|
# Else fall through to re-render form with errors
|
|
else:
|
|
form = MergeOrgsForm()
|
|
|
|
return render_to_response('core/admin_mergeorg.html', {
|
|
'form': form,
|
|
}, RequestContext(request))
|