1. Temat projektu
System zarządzania treścią witryny internetowej dla frameworka Django
2. Harmonogram realizacji zadania
Wrzesień 2009 i wcześniej (tj. przed ustaleniem tematu)
- Przeprowadzenie eksperymentów mających na celu określenie wymagań dla systemu
Wrzesień i Listopad 2009
- Zapoznanie się z dokumentacją frameworka Django
- Tworzenie projektu systemu (modelowanie z użyciem diagramów przypadków użycia i klas języka UML)
Grudzień 2009
- Implementacja
Styczeń 2010
- Testowanie systemu
- Poprawa znalezionych błędów
- Sporządzenie dokumentacji systemu
3. Charakterystyka funkcjonalna systemu (specyfikacja wymagań)
3.1. Przeznaczenie systemu i podstawowe informacje o odbiorcy
System służy zarządzaniu treścią witryny internetowej.
Z założenia ma być to system uniwersalny, mogący, jako uzupełnienie samego frameworka Django,
stanowić bazę dla tworzenia witryn w których istotną rolę odgrywają artykuły.
W szczególności wszelkich stron informacyjnych, portali z newsami, blogów, itp.
Użycie frameworka Django gwarantuje jednak, w razie konieczności, możliwość rozbudowy całości
o inne elementy specyficzne dla konkretnej witryny.
Tworząc system wygodnie było jednak tworzyć go dla konkretnego zastosowania.
Aby móc go testować tworzyłem więc go jako system zarządzania treścią własnej strony (http://jaboja.pl/).
Wybór był o tyle korzystny, z punktu widzenia wszechstronności tworzonego systemu,
że strona owa, przed jego wdrożeniem, stanowiła nieco chaotyczny zbiór luźno powiązanych
ze sobą aplikacji znajdujących się na kilku serwerach i pisanych w różnych
językach programowania (PHP, Python oraz statyczne dokumenty HTML i XSLT).
Konieczność połączenia tych elementów pozwala już na etapie projektu
przyjąć pewne uogólnione założenia, czyniące strukturę systemu bardziej elastyczną.
Ponieważ jednak chciałem położyć na ową elastyczność szczególny nacisk,
przeanalizowałem dodatkowo strukturę kilku innych stron internetowych.
Wyniki poniżej.
3.2. Ogólne zadania systemu
Porównałem główne funkcjonalności 4 witryn internetowych, do których kodów źródłowych miałem dostęp
(z wyjątkiem ostatniej, której wybór wynikał z tego, że często z niej korzystam):
- mojej własnej strony
- strony parafii w Kłodawie
- strony PTOP „Salamandra”
- strony duszpasterstwa akademickiego Don Bosco
jaboja.pl | parafia | Salamandra | DA | |
---|---|---|---|---|
Strony statyczne | • | • | • | • |
Strony z zakładkami | • | • | ||
Blog news | • | • | • | • |
Galeria | • | 1 | • | • |
Linki | • | • | ||
Kalendarz | • | |||
Forum | • | |||
Serwisy tematyczne | • |
3.3. Podstawowe założenia realizacyjne (metody, języki i platformy implementacji)
Jak już zostało zasugerowane w temacie projektu, jako język i platformę implementacyjną wybrałem
język Python i napisany w nim framework Django. Wybór podyktowany jest dużą ilością gotowych elementów,
jakie daje tu programiście sam framework i język (np. automatyczne generowanie penalu administracyjnego),
jak również wydajność samego języka, która przy odpowiednim wdrożeniu jest lepsza od powszechnie
stosowanego PHP.
Istotną dla projektu zaletą frameworka Django była też duża łatwość zastępowania elementów
wbudowanych we framework własnymi. W kontekście tego, co zrealizowałem mam tu na myśli
middleware ładowania szablonów i tłumaczenia adresów URL na widoki. Oba te elementy,
można zastąpić własnymi ich implementacjami dodając jedynie odpowiednie wpisy w pliku
konfiguracyjnym projektu. Nie trzeba w tym celu stosować żadnych hacków w kodzie samego Django.
Co jeszcze istotne, własne procedury nie muszą implementować całości funkcji oryginału,
wystarczy, że zawierają elementy istotne z punktu widzenia implementowanych aplikacji
(w tym przypadku tłumaczenie adresów URL artykułów oraz ładowanie szablonów z bazy danych).
Jeśli któraś z nich zakończy się niepowodzeniem, sterowanie przekazywane jest do kolejnego
zdefiniowanego w konfiguracji middleware.
W ten sposób przy ładowaniu szablonów wpierw podejmowana jest próba załadowania ich
przez zaimplementowany przeze mnie moduł, następnie zaś przez wbudowany we framework.
W przypadku tłumaczenia adresów URL artykułów sytuacja jest odwrotna.
Wpierw wywoływane są standardowe procedury frameworka. Jeśli nie znajdą one widoku
dla danego adresu URL, sterowanie przekazywane jest do mojej aplikacji artykułów
(jest to zachowanie zaczerpnięte z aplikacji flatpages na której bazowałem).
4. Model systemu
4.1. Powiązania pomiędzy użytkownikami i realizowanymi na ich rzecz funkcjami systemu (diagram przypadków użycia UML)
4.2. Struktura modułów i klas projektu
Dla rozróżnienia pliki wygenerowane automatycznie przez Django i potem nie edytowane, oznaczono kursywą.
- articles — aplikacja artykułów (zasadnicza część systemu zarządzania treścią)
- admin — definicja formularzy do zarządzania artykułami w panelu administracyjnym
- class ArticleForm(forms.ModelForm)
- class ArticleTabForm(forms.ModelForm)
- class ArticleAdmin(admin.ModelAdmin)
- class ArticleTabAdmin(admin.ModelAdmin)
- middleware — handler odpowiadający za obsługę wywołań, dla których nie ma definicji w pliku urls.py, przez aplikację artykułów. Dzięki niemu ścieżka w URL artykułu może być dowolna,
- class ArticleFallbackMiddleware(object)
- process_response(self, request, response)
- class ArticleFallbackMiddleware(object)
- models — definicja modeli (tj. stroktury bazy danych) dla tabeli artykułów i tabeli zakładek (mogących być elementem artykułu)
- class Article(models.Model)
- class Tab(models.Model)
- urls — przepisanie adresów URL na domyślny widok z views.py. Z racji użycia middleware zwykle niewykorzystywane
- views — domyślny widok. Podstawowa część aplikacji artykułów
- article(request, url)
Article view. Models: `articles.articles` Templates: Uses the template defined by the ``template_name`` field, or `articles/default.html` if template_name is not defined. Context: article `articles.articles` object
- article(request, url)
- admin — definicja formularzy do zarządzania artykułami w panelu administracyjnym
- templates — aplikacja loadera szablonów (dzięki niej szablony pobierane są z bazy, a nie z plików)
- manage — standardowy skrypt do zarządzania projektem (np. synchronizacji bazy danych)
Szczegóły w dokumentacji Django:
http://docs.djangoproject.com/en/dev/ref/django-admin/ - settings — plik konfiguracyjny projektu. Należy w nim wprowadzić zmiany przy instalacji systemu na serwerze, aby odpowiadały docelowej konfiguracji.
Szczegóły w dokumentacji Django:
http://docs.djangoproject.com/en/dev/topics/settings/ - urls — definicja przepisań adresó URL na widoki Django.
4.3. Diagram klas modeli
Definicje klas modeli, na podstawie których generowana jest struktura bazy danych.
Article | |
---|---|
┌1 │ ├∞ ├∞ │ │ │ │ │ │ │ │ │ │ |
+ id : int autoincrement + url : CharField(max_length=100, db_index=True) + url_parent : ForeignKey('self', null=True, blank=True) + content : TextField(blank=True) + template_name : CharField(max_length=70, blank=True) + date_created : DateTimeField(auto_now_add=True) |
│ | |
│ | Tab |
│ ├∞ └∞ |
+ id : int autoincrement + where : ForeignKey(Article) + index : IntegerField |
Template | |
+ id : int autoincrement + name : CharField(max_length=100, db_index=True) + code : TextField(blank=False) |
4.4. Opis funkcji systemu
Podstawowym zadaniem systemu jest wyświetlanie znajdujących się w bazie danych artykułów
(oraz ich edycja, ale to realizują mechanizmy wchodzące w skład frameworka, więc nie będę ich tu bliżej omawiał).
Oprócz aplikacji artykułów w skład CMSa wchodzi jeszcze loader szablonów, ładujący je z bazy danych.
Ostatecznie jest to jednak tylko dodatek do głównej części systemu, którą jest wspomniana aplikacja artykułów.
Aplikacja artykułów bazuje na znajdującej się w oryginalnej dystrybucji Django aplikacji flatpages.
W stosunku do oryginału zostało jednak dodanych wiele ulepszeń. Przede wszystkim storny mają teraz hierarchiczną strukturę.
Logiczna struktura katalogów jest więc już nie tyle efektem nadania odpowiednich nazw stronom (np. "a/b", "a/c"),
lecz znajduje odzwierciedlenie w bazie danych. Każdemu segmentowi odpowiada bowiem jeden artykuł.
Jest to również korzystne z punktu widzenia użytkownika witryny opartej o opisywany CMS, gdyż wymusza stosowanie
struktury dokumentów nie posiadającej luk w postaci katalogów w ścieżce URL, które same w sobie nie
zawierają treści, a wpisanie ich adresu generuje błąd 404 lub podobny.
Ostatecznie przetwarzanie rządania HTTP przez framework i moje aplikacje ma następujący przebieg:
- Wywołanie przez framework standardowego tłumaczenia adresów URL Django, które może np. dopasować adresy panelu administracyjnego
- Wywołanie przez framework middleware tłumaczenia adresów aplikacji articles, które tłumaczy je na jedyny widok tejże aplikacji
- Przetwarzanie rządania przez ów widok. Pobiera on z bazy kolejne elementy ścieżki, aż do ostatniego, na podstawie którego generuje odpowiedź
- Rządanie wygenerowania strony z użyciem szablonu (wywoływane przez widok)
- Wywołanie mojego loadera szablonów, pobierającego je z bazy danych
- Ewentualne wywołanie standardowych loaderów szablonów z systemu plików, jeśli poprzedni zawiódł
- Zwrócenie przez framework odpowiedzi wygenerowanej przez widok (względnie strony z błędem, jeśli nastąpi wyjątek).
5. Opis implementacji widoku dla aplikacji articles
5.1. Przetwarzanie adresu URL
Najistotniejszym elementem projektu jest definicja widoku dla aplikacji articles.
Widok ten składa się z jednej funkcji znajdującej dopasowanie dla adresu URL i kontrolującej generowanie strony z odpowiednim artykułem.
Podczas przetwarzania adresu URL najpierw ucinany jest końcowy ukośnik (jeśli jest obecny), aby formy „adres” i „adres/” traktować jako równoważne.
Następnie adres URL jest dzielony na segmenty względem znaku ukośnika. Uzyskuje się tym sposobem listę logicznych nazw katyalogów w ścieżce,
oraz nazwę samego pliku strony jako ostatni segment.
Nazwy katalogów przetwarzamy nieco odmiennie niż samą nazwę pliku, tak więc w kolejnym kroku iterujemy wpierw po liście segmentów od jej początku do elementu przedostatniego, a następnie osobno przetwarzamy sam ostatni element. Przetwarzanie nazw katalogów sprowadza się do próby wczytania obiektu z bazy danych, które realizujemy funkcją get_object_or_404, tak więc jeśli jakiś katalog nie ma odpowiadającego mu artykułu w bazie danych, zwracany jest błąd HTTP 404 – „Nie znaleziono strony”. W samym rekordzie potrzebne jest nam tylko pole `id`, pozwalające znaleźć artykuł będący w adresie URL jego potomkiem, co jest niezbędne do pobrania tego potomnego artykułu w kolejnej iteracji.
Po przetworzeniu nazw katalogów wczytujemy z bazy rekord odpowiadający samemu artykułowi. Wykorzystujemy tu ostatni, wcześniej pominięty, segment adresu URL, oraz pobrany w ostatniej iteracji `id` katalogu w którym znajduje się artykuł. Pewną różnicą w tym etapie w stosunku do wcześniejszych iteracji jest to, że w przypadku nie znalezienia rekordu w bazie, przed zwróceniem strony błędu 404, sprawdzamy czy nazwa pliku nie zawiera rozszerzenia „.html”, a jeśli tak, to ponawiamy próbę pobrania artykułu z bazy korzystając z nazwy pliku pozbawionej tego rozszerzenia.
Wszystkie te operacje dają taki efekt, że dla przykładowego artykułu „sprawozdanie” w bazie danych przechowujemy jego nazwę nie zwierającą ani rozszerzenia, ani końcowego ukośnika, jednak użytkownik wpisując adres strony może użyć wszystkich trzech wariantów:
- w formie nazwy pliku bez rozszerzenia – „http://jaboja.pl/sprawozdanie”
- w formie nazwy pliku z rozszerzeniem – „http://jaboja.pl/sprawozdanie.html”
- w formie nazwy katalogu – „http://jaboja.pl/sprawozdanie/”
5.2. Generowanie strony artykułu
Generowanie bazuje w dużej mierze na gotowych mechanizmach frameworka. Rola widoku sprowadza się tu generalnie do przekazania danych pobranych z bazy do odpowiednich funkcji frameworka. Framework używa następnie szablonu, który też należy zdefiniować, aby wygenerować kod HTML strony, i przesyła odpowiedź do przeglądarki.
Zanim jednak dane zostaną przekazane do funkcji obsługującej szablony aplikacja sprawdza jeszcze, czy użytkownik ma uprawnienia do przeczytania artykułu. Artykuł może być dostępny bądź dla wszystkich odwiedzających, bądź tylko dla zalogowanych użytkowników (czyli tylko dla redakcji portalu). W tym drugim przypadku, jeżeli użytkownik nie jest zalogowany, generujemy przekierowanie na stronę logowania.
Gdy mamy pewność, że użytkownik jest uprawniony do czytania artykułu, określamy nazwę szablonu jakiego użyjemy do generowania kodu HTML. Szablon może być określony poprzez jedno z pól rekordu bazy danych. Jeśli jest ono puste użyty zostaje szablon domyślny – „articles/default.html”.
5.3. Kod źródłowy widoku dla aplikacji articles
from cms.articles.models import Article
from django.template import loader, RequestContext
from django.shortcuts import get_object_or_404
from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.conf import settings
from django.core.xheaders import populate_xheaders
from django.utils.safestring import mark_safe
from tagging.models import Tag
DEFAULT_TEMPLATE = 'articles/default.html'
def article(request, url):
"""
Widok artykułu.
Modele: `articles.articles`
Szablony: Używa szablonu określonego w polu ``template_name``,
lub `articles/default.html` jeśli szablonu nie określono.
Kontekst:
article
obiekt `articles.articles`
"""
# Dzielimy ścieżkę adresu URL na segmenty i znajdujemy dla każdego odpowiedni
# rekord w bazie danych. W ten sposób przechodzimy przez zdefiniowane w bazie
# drzewo dokumentów aż do bezpośredniego rodzica żądanego artykułu.
segments = url.strip('/').split('/')
parent = None
for segment in segments[:-1]:
f = get_object_or_404(Article, url__exact=segment,
url_parent__id__exact=parent,
sites__id__exact=settings.SITE_ID)
parent = f.id
# Wykonujemy podobną operację dla ostatniego elementu ścieżki, tj. żądanego
# artykułu. Jeśli żądana ścieżka kończy się rozszerzeniem ".html", a w bazie
# brak dokładnego dopasowania, sprawdzamy też wariant bez tego rozszerzenia.
segment = segments[-1]
try:
f = get_object_or_404(Article, url__exact=segment,
url_parent__id__exact=parent,
sites__id__exact=settings.SITE_ID)
except Http404:
if segment.endswith('.html'):
f = get_object_or_404(Article, url__exact=segment[:-5],
url_parent__id__exact=parent,
sites__id__exact=settings.SITE_ID)
else:
raise
# Jeśli artykuł jest dostępny tylko dla zarejestrowanych użytkowników (redaktorów),
# a użytkownik nie jest zalogowany, przekierowujemy go na stronę logowania.
if f.registration_required and not request.user.is_authenticated():
from django.contrib.auth.views import redirect_to_login
return redirect_to_login(request.path)
if f.template_name:
t = loader.select_template((f.template_name, DEFAULT_TEMPLATE))
else:
t = loader.get_template(DEFAULT_TEMPLATE)
# Aby nie trzeba było za każdym razem używać w szablonie filtru "|safe",
# oznaczamy tytuł i treść jako bezpieczną (tj. nie wymagającą konwersji
# znaków specjalnych na encje HTML).
f.title = mark_safe(f.title)
f.content = mark_safe(f.content)
# Tworzymy potrzebne obiekty i zwracamy przeglądarce odpowiedź
# (lub innymi słowy, rendrujemy stronę z artykułem)
c = RequestContext(request, {
'article': f,
'subnodes': f.article_set.order_by('-date_created'),
'tags': Tag.objects.get_for_object(f)
})
response = HttpResponse(t.render(c))
populate_xheaders(request, response, Article, f.id)
return response
6. Instrukcja użytkowania systemu
6.1. Przykład edycji artykułu „sprawozdanie”
Ponieważ system oparty jest o framework Django, a sama edycja artykułów i szablonów stron odbywa się poprzez
panel administracyjny generowany przez sam framework, obsługa moich aplikacji nie różni się szczególnie od zarządzania
innymi treściami w Django. Należy jedynie pamiętać, że za artykuły odpowiada aplikacja Articles, a za
szablony – Templates. Poniżej prezentuję przykładową edycję niniejszej dokumentacji poprzez ów panel administracyjny.
Panel dostępny jest pod adresem http://nazwa-domeny/admin/, czyli w przypadku mojej testowej instalacji http://jaboja.pl/admin/. Po wejściu na w/w stronę musimy podać login i hasło, po czym wyświetlona zostaje strona główna panelu administracyjnego, widoczna poniżej:
Aby przejść do edycji artykułów musimy w sekcji Articles (tj. odpowiadającej aplikacji artykułów) wybrać link Articles
(odpowiadający edycji danych zgromadzonych w tabeli bazy danych wykorzystywanej przez model Articles wspomnianej aplikacji). Drugi link (Tabs) pozwala definiować strony wyświetlane w kartach wewnątrz innych artykułów. Mamy tu też do dyspozycji skróty do dodawania nowego obiektu (tj. odpowiednio nowego artykułu i nowej definicji karty), oraz do zmiany istniejących, który jest tożsamy z kliknięciem na nazwę modelu.
Po wybraniu linku Articles, lub znajdującego się obok niego skrótu Change, zostanie wyświetlona lista wszystkich znajdujących się w bazie artykułów. Jeśli jest ich zbyt dużo lista podzielona zostanie na kilka stron. Aby ułatwić nawigację po nich w definicji panelu administracyjnego dla modelu Articles aktywowana została wyszukiwarka – przeszukująca pola `url` (ostatni segment adresu URL strony – tj. nazwa pliku bez rozszerzenia), oraz `title` (tytuł artykułu). Aby wyszukać niniejszą dokumentację posłużymy się wyszukiwarką wpisując nazwę pliku „sprawozdanie”. Widać to na kolejnych ekranach:
W ten sposób trafiamy na stronę edycji artykułu, gdzie można wprowadzić stosowne zmiany:
7. Podsumowanie i wnioski (problemy projektowe i realizacyjne — cele realizowane i niezrealizowane, wskazanie możliwych kierunków rozbudowy systemu)
Zasadniczą część pierwotnych zamierzeń udało się zrealizować. Niepodważalnie system działa i widać
efekty tego działania w praktyce — na przykład w postaci niniejszej strony.
Nie oznacza to jednak ostatecznego końca prac i braku perspektyw rozwoju. System posiada bowiem z jednej
strony pewne mankamenty, które wartoby usunąć, z drugiej zaś jest wiele elementów, które jeszcze warto by dodać,
aby dodatkowo zwiększyć jego funkcjonalność. Poniżej wymieniam dwa najistotniejsze:
- Brak jest walidacji definiowanych segmentów ścieżki adresu URL. Jeśli użytkownik omyłkowo
wprowadzi niepoprawne dane, zdefiniowany artykuł nie będzie dostępny na stronie.
Stanie się tak w dwu przypadkach:- Gdy zdefiniujemy dwa artykuły o identycznym adresie URL wyświetlany będzie tylko jeden z nich – ten który znajdzie się jako pierwszy w wyniku zapytania do bazy danych.
- Gdy poszczególne segmenty ścieżki adresu URL przypisane są (poprzez mechanizm sites) do różnych domen artykuł nie będzie dostępny pod żadną z nich.
Aby usunąć ten mankament należałoby dodać odpowiednie klucze unique do bazy danych i ewentualnie zmodyfikować
formularz edycji artykułu, aby nie pozwalał na ustawienie niepoprawnych wartości. - Mechanizm zakładek zrealizowany jest obecnie w oparciu o JavaScript i dynamiczny HTML. Strona co prawda działa przy jego braku,
jednak rozwiązanie nie jest szczególnie eleganckie, o tyle, że strona wyświetlana w zakładce ma dwa adresy URL, swój własny,
oraz adres strony z zakładkami. Warto zastanowić się nad wyświetlaniem strony z zakładkami po wprowadzeniu adresu URL
prowadzącego do treści zakładki. Wymagałoby to jednak pewnych zmian w projekcie aplikacji articles.
8. Bibliografia
- Django documentation, Django Software Foundation, http://docs.djangoproject.com/en/1.1/ (dostęp: 19.01.2010).
- Python Package Index : Index of Packages Matching 'django', Python Software Foundation, http://pypi.python.org/pypi?%3Aaction=search&term=django&submit=search (dostęp: 08.12.2009).