W części drugiej przedstawię widoki związane z blogiem. W odróżnieniu od modeli, zawarta tutaj logika jest znacznie prostsza (poza paginacją). W pewnych sytuacjach mogłaby być prostsza, ale przerabiają blog z oryginalnego Django chciałem zachować URL-e i interfejs z widoków generycznych korzystających z dat.
Poniżej wymagane importy dla views.py, których raczej tłumaczyć nie trzeba...
from google.appengine.ext import db
from weblog.models import Entry, Comment, EntryArchive, Tag, Comment
from weblog.forms import CommentForm
from django.views.generic import date_based
from django.http import Http404, HttpResponseRedirect
from django.views.generic.list_detail import object_list
from django.views.generic.simple import direct_to_template
from django.views.decorators.http import require_POST
from django.utils.translation import ugettext_lazy as _, ugettext
from django.views.decorators.cache import cache_page
import datetime, time
Wyświetlenie głównej strony z 5 ostatnimi wpisami raczej nie odstaje niczym od standardowego Django...
@cache_page(60)
def entry_archive_index(request):
latest = Entry.all().order('-pub_date').fetch(5)
return direct_to_template(request, 'entry_archive.html', {'latest':latest})
Wiele stron w tym przykładzie stosuje bardzo krótki cache pełnostronicowy. Przy tak niewielkim ruchu ma to nawet działanie odwrotne do założeń cachowania (więcej zapisów niż odczytów). Niemniej warto być przygotowanym na większy ruch.
Wyświetlanie listy miesięcy z wpisami w danym roku używa modelu EntryArchive i faktu, że filtr nierówności działa jak przechodzenie po słowniku. Ponieważ lista może mieć maksymalnie 12 wpisów, można ją pobrać bez martwienia się. Pamiętaj, że standardowe Django w tym miejscu używa sztuczek SQL do pobrania danych. My dzięki denormalizacji mamy ułatwiony odczyt...
def entry_archive_year(request, year):
year = int(year)
date_list = [datetime.date(int(row.year_month[0:4]), int(row.year_month[4:6]), 1)
for row in EntryArchive.all().filter('year_month >=', str(year)).filter('year_month <', str(year+1)).order('year_month').fetch(12)]
if len(date_list) == 0:
raise Http404
return direct_to_template(request, 'entry_archive_year.html',
{'date_list': date_list, 'year': year})
Pobranie wszystkich wpisów w danym miesiącu wymaga sprawdzenia, czy taki miesiąc istnieje w EntryArchive. Jeśli istnieje pobieramy 100 wpisów (raczej więcej wpisów w miesiącu mieć nie będę ;), których miesiąc jest przodkiem (kluczem nadrzędnym). Tutaj można by wprowadzić paginację, ale tego nie zrobiłem, bo średnia liczba wpisów w miesiącu jest niewielka, a prezentuję tylko ich skróty...
@cache_page(60)
def entry_archive_month(request, year, month):
archive = EntryArchive.get_by_key_name("_%s%s" % (year, month))
if archive is None:
raise Http404
object_list = Entry.all().ancestor(archive).order('-pub_date').fetch(100)
return direct_to_template(request, 'entry_archive_month.html',
{'object_list': object_list, 'month': datetime.date(int(year), int(month), 1)})
Poniższy kod pobierający konkretny wpis mógłby być prostszy, ale chciałem dopasować się do sposobu działania oryginalnego Django, czyli dany slug może pojawić się wielokrotnie, o ile nie jest to ten sam dzień. Ponieważ wyrażenie regularne nie sprawdza wszystkich możliwych sytuacji z datami, kod najpierw waliduje datę, a dopiero później poszukuje wpisu z daną datą i slugiem. Dla pobranego wpisu pobieram jego komentarze, stosując ograniczenie po kluczu przodka...
@cache_page(60)
def entry_detail(request, year, month, day, slug):
try:
date = datetime.date(*time.strptime(year+month+day, '%Y%m%d')[:3])
except ValueError:
raise Http404
object = Entry.all() \
.filter('pub_date >=', datetime.datetime(date.year, date.month, date.day)) \
.filter('pub_date <=', datetime.datetime(date.year, date.month, date.day, 23, 59, 59)) \
.filter('slug =', slug).get()
if object is None:
raise Http404
comment_list = Comment.all().ancestor(object).order('-date').fetch(100)
return direct_to_template(request, 'entry_detail.html', {
'object': object,
'comment_list': comment_list,
})
Pobieranie wpisów dla danego tagu to najbardziej zaawansowany widok. Po pierwsze sprawdzam, czy istnieje tag o podanej nazwie. Mógłbym po odnalezieniu taga wyświetlić użytkownikowi łączną liczbę wpisów (licznik) do poinformowania o łącznej licznie podstron paginacji. Następnie tworzę zapytanie limitujące po określonym tagu. Zauważ, że wpis zawiera listę tagów jako teksty, ale ponieważ jest to specjalny typ listy, umożliwia on stosowanie jego zawartości do limitowania. Mając wpisy z listami ['django', 'python'] i ['django', 'www'], mogę podać, że chcę pobrać wszystkie wpisy zawierające na liście tagów django.
Widok wykorzystuje paginację, która do działania potrzebuje tylko i wyłącznie informacji o dacie publikacji (w DataStore stosowanie offsetów jest mocno ograniczone i niewydajne). Muszę uprzedzić, że poniższy kod paginacji, choć działa, to nie jest najefektywniejszy w przypadku cofania się. Istnieją również inne sposoby tworzenia paginacji. Nowsze rozwiązanie stosujące dodane w listopadzie __key__ zostało opisane tutaj. Moje podejście wymaga unikatowej właściwości daty (co nie jest problemem, bo nie dodaję dwóch wpisów w tej samej sekundzie).
Ogólnie pomysł na paginację jest prosty. Zawsze pobieramy o jeden element więcej niż chcemy wyświetlić. W ten sposób wiemy, że jest jeszcze następna strona. Do generowania adresu następnej serii wykorzystujemy unikatowy identyfikator tego dodatkowego elementu. Po przejściu do nowej serii dodajemy warunek pobierający wpisy od podanego identyfikatora. Adresy wstecz sprawiają nieco więcej problemów. Ponieważ mój sposób nie jest najefektywniejszy (pobiera w pewnej sytuacji 11 wpisów, choć paginacja jest po 5 elementów) i obecnie na Google Groups znajdują się lepsze, nie będę go tu dokładnie opisywał. Pozostawiam jednak sam kod, bo nie jest złożony. Jeśli to możliwe, w przypadku paginacji w GAE warto posiłkować się Ajaksem do pobierania dalszych podstron (patrz chociażby panel administracyjny Google App Engine), bo to najefektywniejsze rozwiązanie.
@cache_page(60)
def entry_tagged_object_list(request, tag):
PAGE_LIMIT = 5
tag = Tag.get_by_key_name(tag)
if tag is None:
raise Http404
entry_list = Entry.all().filter('tags =', tag.name)
if tag.counter > PAGE_LIMIT:
is_paginated = True
cur_page = request.GET.get('page', None)
prev_page = request.GET.get('prev', None)
if cur_page:
try:
cut_point = datetime.datetime.fromtimestamp(int(cur_page))
entry_list = entry_list.filter('pub_date <=', cut_point)
except ValueError:
is_paginated = False
if prev_page:
has_previous = True
previous = prev_page
else:
tmp = Entry.all().filter('tags =', tag.name).filter('pub_date >', cut_point).order('pub_date').fetch(PAGE_LIMIT)
if len(tmp) > 0:
previous = int(time.mktime(tmp[-1].pub_date.timetuple()))+1 # To commodate fractions of seconds
has_previous = True
else:
is_paginated = False
entry_list = entry_list.order('-pub_date').fetch(PAGE_LIMIT+1)
if len(entry_list) > PAGE_LIMIT:
extra = entry_list.pop()
has_next = True
next = int(time.mktime(extra.pub_date.timetuple()))+1 # To commodate fractions of seconds
prev = int(time.mktime(entry_list[0].pub_date.timetuple()))+1 # To commodate fractions of seconds
else:
has_next = False
return direct_to_template(request, 'entry_list.html', locals())
Poniższy widok generuje listę tagów wraz z liczbą wpisów, w których się znajdują...
def all_entry_tags_list(request):
return direct_to_template(request, 'tag_list.html', extra_context={'tags_list': Tag.all().order('name')})
Ostatni fragment kodu odpowiada za zapis komentarza. Niewiele tu z GAE, więc bez dodatkowych opisów...
@require_POST
def comment_preview(request, *args):
form = CommentForm(request.POST)
if form.is_valid():
if 'post' in request.POST:
Comment.post(**form.cleaned_data)
return HttpResponseRedirect('../'+ugettext('posted')+'/')
context = form.cleaned_data
else:
context = {}
context.update({'comment_form': form})
return direct_to_template(request, 'comment_preview.html', extra_context=context)
Podsumowanie
Na tym kończę opis tworzenia niniejszej strony na GAE. Mam nadzieję, że lektura okazała się dla niektórych pomocna i pozwoli uniknąć chociaż niektórych pułapek AppEngine.
Choć ten blog został napisany z użyciem Google App Engine Django Helper, to na przyszłość polecam App Engine Patch. Ten drugi projekt ma znacznie więcej i aktywnie się rozwija, w odróżnieniu od Helpera. App Engine Patch użyję w następnym projekcie dla GAE. Być może i ten blog zostanie przeportowany, bo w dniu wczorajszym App Engine Patch doczekało się uproszczonego admina z Django (oczywiście z podobnymi ograniczeniami co wcześniejszy patch na grupie dyskusyjnej).