Pliki statyczne w Django

Pracuję z Django od wersji 0.96 i w projektach, które utrzymuję jest jeszcze wiele zaszłości. Jedną z nich jest sposób składowania wszelakich plików statycznych w katalogu, którego ścieżka zdefiniowana była w settings.py w zmiennej MEDIA_ROOT. Tymczasem od wersji 1.3 developerzy Django postanowili rozróżnić pliki statyczne – czyli wszelkiego rodzaju javascript-y, css-y, grafiki itp. stałe elementy składające się na tzw. layout – od mediów, pod którą to nazwą powinniśmy rozumieć wszelkiego rodzaju pliki uploadowane.

Na marginesie dodam, że aplikacja django-staticfiles była już wcześniej dostępna, ale integralną częścią frameworka stała się dopiero od wersji 1.3.

Jak skonfigurować ustawienia mediów?

Zacznę od mediów bo sprawa jest stara, prosta i wielokrotnie opisywana.

Załóżmy, że struktura naszej aplikacji wygląda tak:

project_root/
    manage.py
    mysite/
        __init__.py
        settings.py
        urls.py
        wsgi.py 
    news/
        __init__.py
        models.py
        urls.py
        views.py
    /media
        /users
	    user_avatar.jpg

UWAGA! Od wersji 1.4 zmieniła się podstawowa struktura projektu w ten sposób, że podstawowa konfiguracja oraz główny plik z urls.py trafiły do podkatalogu z nazwą projektu – w tym wypadku „mysite”.

Mediów dotyczą dwie zmienne w settings.py

MEDIA_ROOT – przechowuje pełną ścieżkę do katalogu z mediami. Do tego folderu będą trafiały pliki wrzucane przez użytkowników z poziomu strony www. Stanowi on bazową ścieżkę dla parametru „upload_to” pól FileField i ImageField.

MEDIA_ROOT = '/home/user/project_root/media'

Jeśli zmienimy fizyczne położenie mediów (bo np. serwer produkcyjny czyta pliki statyczne tylko z podkatalogu public_html) to będziemy musieli zmienić MEDIA_ROOT.

MEDIA_ROOT = '/home/user/project_root/public_html/media'

ale MEDIA_URL i wszystkie odwołania w szablonach pozostaną takie same.

MEDIA_URL – bazowy adres URL, pod którym dostępne będą pliki mediów.

MEDIA_URL = '/media/'

Przy takiej kongfiguracji do plików mediów będziemy odwoływać się następująco

<img src="/media/images/user_avatar.jpg" alt="{{ user.username }}" />

MEDIA_ROOT – Może mieć formę adresu względnego – najczęściej używaną na serwerze developerskim, albo adresu bezwzględnego, przydatnej na serwerach produkcyjnych

MEDIA_URL='http://static.domain.pl/media/'

Zmiana url-a wiąże się z koniecznością zmiany wszystkich odwołań w szablonach dlatego warto od razu przewidzieć taką możliwość i przekazać do szablonu zmienną MEDIA_ROOT i posługiwać się nią zamiast hardkodować adresy do plików mediów.

<img src="{{ MEDIA_URL }}images/user_avatar.jpg" alt="{{ user.username }}" />

Przy okazji wspomnę, że obiekty formularzy FileField i ImageField rzutowane do tekstu lub unicode-a zwracają względną ścieżkę do pliku

{{ user.avatar }} # users/user_avatar.jpg

metoda url zwraca pełen url zawierający w sobie MEDIA_URL

{{ user.avatar.url }} # /media/users/user_avatar.jpg (albo http://static.domain.pl/media/users/user_avatar.jpg)
<img src="{{ user.avatar.url }}" alt="{{ user.username }}" />

UWAGA! Jeśli dla FileField lub ImageField dopuścimy możliwość wstawiania null-i (null=True, blank=True, default=None), w szablonie zawsze dobrze jest sprawdzać czy mamy do czynienia z plikiem czy null-em.

<img src="{% if user.avatar %}{{ user.avatar.url }}{% endif %}" alt="{{ user.username }}" />

a metoda path zwraca pełną ścieżkę do pliku zawierającą w sobie MEDIA_ROOT

{{ user.avatar.path }} # /home/user/project_root/public_html/media/users/user_avatar.jpg

Z plikami statycznymi wiąże się więcej zagadnień.

Jaki jest modelowy sposób składowania plików statycznych w Django?

Przede wszystkim zmienna STATIC_ROOT pełni rolę analogiczną do MEDIA_ROOT, ale tylko na serwerze produkcyjnym. Wbudowany – developerski – serwer Django w celu odnalezienia położenia plików statycznych korzysta z tzw. finderów zdefiniowanych w STATICFILES_FINDERS oraz STATICFILES_DIRS – ale po kolei.

Pliki statyczne należy trzymać w podkatalogu „./static” każdej aplikacji

project_root/
    manage.py
    mysite/
        __init__.py
        settings.py
        urls.py
        wsgi.py
        static/ - pliki wspólne dla całej strony
            /css
                screen.css
            /js
                jquery.js 
    musicplayer/
        __init__.py
        models.py
        urls.py
        views.py
        static/ - pliki statyczne aplikacji
            /js
                jplayer.js
            /images
                play.gif

Ścieżkę każdego z podkatalogów powinniśmy dodać do krotki zmiennej STATICFILES_DIRS w settings.py

STATICFILES_DIRS = (
    "/home/project_root/mysite/static",
    "/home/project_root/musicplayer/static",
)

Jeśli zdefiniujemy zmienną STATIC_URL następująco:

STATIC_URL = '/static/'

Adresy plików w szablonach będą wyglądały tak:

<img src="/static/images/play.gif" alt="play" />

Oczywiście tu podobnie jak w mediach warto przekazać do szablonu zmienną STATIC_URL,

<img src="{{ STATIC_URL }}images/play.gif" alt="play" />

bo na serwerze produkcyjnym możemy się zdecydować na podanie bezwzględnego url-a zamiast względnej ścieżki.

STATIC_URL = 'http://static.domain.pl/static/'

Czasami chcielibyśmy aby media aplikacji – dajmy na to musicplayer – znalazły się w podkatalogu o tej samej nazwie. Nie musimy w tym celu zmieniać struktury plików lecz wystarczy dodać prefix do ścieżki w STATICFILES_DIRS.

STATICFILES_DIRS = (
    "/home/project_root/mysite/static",
    ('musicplayer', "/home/project_root/musicplayer/static"),
)

Fizyczne położenie pliku się nie zmieniło, ale zmiana konfiguracji pozwala traktować plik jakby się znajdował w dodatkowym podkatalogu.

<img src="/static/musicplayer/images/play.gif" alt="play" />

Aby całość zadziałała musimy jeszcze wskazać Django narzędzia wyszukiwania plików tzw. findery.

STATICFILES_FINDERS = (
    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
)

Ponieważ w dwóch różnych podkatalogach /static możemy mieć plik o tej samej nazwie, może się zdarzyć że jeden „nadpisze” drugi i trzeba będzie wyszukać wszystkie lokalizacje w jakich może znajdować się plik o danej nazwie.

Warto wtedy skorzystać z komendy findstatic

$ python manage.py findstatic css/base.css admin/js/core.js
#/home/special.polls.com/core/static/css/base.css
#/home/polls.com/core/static/css/base.css
#/home/polls.com/src/django/contrib/admin/media/js/core.js

Pliki statyczne na serwerze developerskim

Django nie udostępnia plików statycznych i mediów z automatu gdyż byłoby to niebezpieczne na serwerze produkcyjnym. Dlatego też także w środowisku developerskim trzeba zadbać aby serwer wiedział gdzie szukać plików poprzez dodanie odpowiednich reguł do urls.py

from django.conf.urls import patterns, include, url
from django.conf import settings
from django.conf.urls.static import static
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
 
urlpatterns = patterns('',
    # Uncomment the next line to enable the admin:
    url(r'^admin/', include(admin.site.urls)),
    ...
)
 
if settings.DEBUG:
    urlpatterns += staticfiles_urlpatterns()   
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Pliki statyczne na serwerze produkcyjnym

Nie będę opisywał jak skonfigurować Apache 2, Nginx czy Lighttpd. Przyjmijmy, że dowolny serwer umożliwia serwowanie plików statycznych z poziomu podkatalogu /public_html.

W pierwszej kolejności należy odpowiednio ustawić zmienną STATIC_ROOT w settings.py

STATIC_ROOT = os.path.join(PROJECT_ROOT, 'public_html/static')

Następnie należało by odpowiednio do ustawień STATICFILES_DIRS przekopiować (lub dowiązać) pliki i katalogi.

project_root/
    public_html/
        static/ - pliki wspólne dla całej strony
            /css
                screen.css
            /js
                jquery.js 
	    musicplayer/ - pliki statyczne aplikacji
                /js
                    jplayer.js
                /images
                    play.gif

Może to być nieco pracochłonne dlatego też wielu developerów do tej pory preferowało trzymanie statyków w jednym wspólnym katalogu. Po opublikowaniu strony na serwerze produkcyjnym wystarczyło tylko przekopiować lub przenieść statiki do katalogu publicznego oraz zmienić ustawienia STATIC_ROOT.

Od wersji 1.4 mamy do dyspozycji komendę „collectstatic”, który całą robotę z kopiowaniem plików statycznych robi za nas. W trakcie developmentu możemy trzymać pliki statyczne w wielu podkatalogach a po wrzuceniu na live wystarczy tylko wywołać komendę –

python manage.py collectstatic

i mamy przekopiowane wszystkie pliki statyczne łącznie z tymi związanymi z panelem administracyjnym. Dodanie na końcu opcji -l albo –link,

python manage.py collectstatic -l

tworzy dowiązania symboliczne zamiast kopiować pliki.

Staticfiles w starych projektach

Przy podnoszeniu starych projektów do nowej wersji Django nie jesteśmy zmuszani do wydzielania mediów i relokacji plików statycznych. Dalej możemy trzymać wszystko w jednym katalogu, ale trzeba zadbać o odpowiednią konfigurację.

MEDIA_ROOT = '/home/user/project_root/static'
 
MEDIA_URL = '/static/'
 
STATICFILES_DIRS = (
    MEDIA_ROOT,
)
 
STATIC_URL = MEDIA_URL
 
STATIC_ROOT = '/home/user/project_root/public_html/static'

UWAGA! Traktuj STATIC_ROOT jak zmienną używaną tylko na serwerze produkcyjnym i nawet jeśli nie wiesz jak powinna ta ścieżka wyglądać docelowo wskarz na katalog, który nie jest jednym z katalogów zdefiniowanych w STATICFILES_DIRS

Podusmowanie

Konfiguracja mediów w Django nie jest tak prosta jak to się wydaje doświadczonym developerom. Zagadnienie to nie jest poruszone w podstawowym tutorialu, a dokumentacja choć jedna z najlepszych nie prowadzi za rękę początkującego programistę.

Choć staticfiles wraz z komendą collectstatic to bardzo wygodne rozwiązanie ułatwiające trzymanie porządku w projekcie, dodatkowo utrudnia zagadnienie zarządzania plikami statycznymi. Mam nadzieję, że swoim wpisem nie gmatwam dodatkowo całej sprawy, i że przyda się on komuś rozpoczynającemu dopiero pracę z frameworkiem Django.