milo noir

Hogyan készült? 4. rész

Egy blogban keresni több módon is lehet. Az egyik legegyszerűbb, ha témakör alapján kategóriákat hozunk létre, majd ezekkel felcímkézzük a bejegyzéseinket. Ezután a kategóriákat vagy címkéket felsoroljuk mondjuk egy oldalsávban és ezekre kattintva egy szűrést végzünk a posztok listáján. Először nézzük meg hogyan lehet megcsinálni a címkézést, aztán pedig, hogy hogyan tudjuk velük a szűrést intézni!

Címkézés

A legegyszerűbb megoldás, ha egy egyszerű CharField mezőt hozzácsapunk a meglévő modellünkhöz:

1
2
3
4
5
6
7
8
from django.db import models

class Story(models.Model):
    title = models.CharField(max_length=100)
    slug = models.SlugField()
    category = models.CharField(max_length=50)
    content = models.TextField(blank=True)
    created = models.DateTimeField(default=datetime.datetime.now)

Ezzel csak egy probléma van: maximum 1 db kategóriát tudunk hozzárendelni egy bejegyzéshez. Szerencsére létezik már több kész megoldás a problémára, ezekből az egyik a django-taggit, amit én is használok. Sajnos a django-taggit önmagában nem tartalmaz template tageket, azonban később jól jöhetnek, így én javaslom a django-taggit-templatetags telepítését is. Nincs különösebb varázslat, a Taggit elintéz mindent. Először is hozzá kell adni az appokhoz a settings.py-ban:

1
2
3
4
5
6
INSTALLED_APPS = (
    ...
    'taggit',
    'taggit_templatetags',
    ...
)

A modellünkbe importáljuk be a TaggableManager osztályt és cseréljük le vele az egyszerű category mezőt (nevezzük inkább tags-nek mostantól):

1
2
3
4
5
6
7
from taggit.managers import TaggableManager
from django.db import models


class Story(models.Model):

tags = TaggableManager()

Ha ez megvan rögtön futtassunk is egy migrálást, hogy az adatbázisunk sémája frissüljön:

1
python manage.py migrate

Innentől kezdve az admin oldalon vesszővel elválasztva bármennyi címkét hozzárendelhetünk a postjainkhoz. A tagek egyszerűen megjeleníthetőek az oldalon az alábbi template részlet segítségével:

1
2
3
{% for tag in story.tags.all %}
    <a href="{% url 'blog-tagged' tag.slug %}">{{ tag.name }}</a>
{% endfor %}

A fenti példában a story template object egy darab posztra mutat, a blog-tagged URL-ről a szűrésnél lesz szó. Minden tag-nek van egy name mezője, ami a konkrét megjelenítendő címke neve lesz és egy slug mezője, ami ugyanazt a szerepet játsza, mint a saját Story modellünkben. A story.tags.all lekérdezi az összes, adott story-hoz tartozó taget és egy listában adja vissza.

Szűrés

Szűrésnél az utolsó lekérdezést fordítjuk meg: az adott taghez milyen story-k tartoznak? Ehhez már saját view-t és URL patternt kell írnunk. A view:

1
2
3
4
5
6
7
8
9
from django.shortcuts import render_to_response, get_object_or_404
from django.template.context import RequestContext
from taggit.models import Tag
from models import Story

def tagged(request, slug):
    tag = get_object_or_404(Tag, slug=slug)
    story_list = Story.objects.filter(tags__name__in=[tag.name])
    return render_to_response('story_list.html', locals(), context_instance=RequestContext(request))

Először egy get_object_or_404 hívással a címke egyedi slug attribútuma alapján kiválasztjuk a megfelelő taget, majd az összes Story közül kiszűrjük azokat, amelyekben ez megtalálható. Végül az egészet egy erre alkalmas template-tel a kész HTML válasszá rendereljük.

A context is a “variable name” -> “variable value” mapping that is passed to a template.

A template renders a context by replacing the variable “holes” with values from the context and executing all block tags.

A URL pattern:

1
url(r'^tags/(?P<slug>[-\w]+)/$', 'tagged', name='blog-tagged')

A fenti URL pattern három részből áll:

  1. Egy reguláris kifejezés, amelyre egy beérkező URL request illeszkedhet. Ebben a (P<slug>[-\w]+) rész egy nevesített csoport, amely - és alfanumerikus karakterek kombinációjára illeszkedik. A csoport a slug nevet kapja.
  2. A view neve (lásd a view kódjában a függvény neve is tagged): ezt a view-t kell hívni ha a regexpre van illeszkedő találat.
  3. A name argumentum pedig egy alias, amelyet a template-ben használtunk (lásd fent a template kódját). További értelmezés itt.

Az alábbi template részlettel felsorolható az összes témába vágó poszt:

1
2
3
4
5
6
7
8
9
{% if tag %}
    <p>{{ tag.name }}</p>
{% endif %}
...
{% for story in story_list %}
    <p><a href="{{ story.get_absolute_url }}">{{ story.title }}</a></p>
{% empty %}
    <p>No posts.</p>
{% endfor %}

Hogyan készült? 3. rész

Frissiben találtam megoldást egy problémára, amely pár hete bosszant, ezért erről írok röviden: biztonsági mentés.

Adott ugye a Raspberry Pi, rajta pedig a weboldal beélesítve. Maga a Django kód persze verziókövető rendszerben van, annak nem eshet bántódása. De mi a helyzet az adatbázissal és a statikus tartalmakkal (pl. képek)? Amióta szeptember végén elindítottam az oldalt aggaszt, hogy nincsen backup stratégiám.

Mostanáig! Úgy képzeltem, hogy a nem verziókövetett fájljaimat felsyncelem valamilyen felhő tárhelyre minden nap és akkor nyugodtan alhatok. Ma délután meg is találtam a megoldást ezen az oldalon. Csak annyira kötnék bele, hogy az első wget lépés nálam nem működött, úgyhogy helyette inkább clone-oztam a repot innen, ahogyan az a Getting started részben írva vagyon.

Ha jól csináltam, akkor minden reggel 6 órakor egy újabb backup mentésem lesz a Dropboxomban.

Hogyan készült? 2. rész

Sokat gondolkoztam az első rész óta a folytatás mikéntjén. Temérdek információ fellelhető tutorial és könyv formában arról, hogy hogyan kell egy nagyon alap Django blogot megcsinálni, úgyhogy én nem szeretnék beállni ebbe a sorba. Írásaimban feltételezem, hogy az olvasó már egy ilyen kalauzon végigment, így az alapokkal nem foglalkozom, sokkal inkább olyan feature leírásokat szeretnék közölni, amelyekkel egy blog továbbléphet erről a szintről.

Egy tipikus nagyon alap blog models.py állománya nagyjából így szokott kinézni:

1
2
3
4
5
6
7
from django.db import models

class Story(models.Model):
    title = models.CharField(max_length=100)
    slug = models.SlugField()
    content = models.TextField(blank=True)
    created = models.DateTimeField(default=datetime.datetime.now)

Mint látható egy ez nem egy túlkomplikált dolog. Amire szükség van az egy cím, egy slug mező permalink generáláshoz, maga a bejegyzés tartalma és egy időbélyeg.

A created mezőt az admin felület automatikusan kitölti majd nekünk amikor új bejegyzést kreálunk. A slug-ot a title-ből generáltatjuk - ez a beállítás admin.py-ból látszik:

1
2
3
4
5
6
7
8
from django.contrib import admin
from models import Story

class StoryAdmin(admin.ModelAdmin):
    ...
    prepopulated_fields = {'slug': ('title')}

admin.site.register(Story, StoryAdmin)

Ami a tartalmat illeti, a történet itt már érdekesebbé válik. Ebben a formában a content csak egyszerű szöveget fog visszaadni, ami azért valljuk be, elég kopár. Mondjuk azt, hogy szeretnénk látni ilyesmiket is: címsorokat, félkövér és dőlt betűket, képeket, beágyazott videókat és programkódokat lehetőleg szintaxis kiemeléssel és a sorok megszámozásával. Hívjuk ezt most egyszerűen formázott tartalomnak.

Figyelem! Most érkeztünk el oda, amit a mai poszt mondanivalójának szántam.

Jelen esetben egy WYSIWYG tartalomszerkesztő nem áll rendelkezésre, ígyhát ahhoz, hogy formázott tartalmat kapjunk a szöveget tele kell pakolnunk HTML tagekkel és CSS kódokkal. Ettől egyel jobb, ha csak HTML tageljük a szöveget és a CSS-t valahonnan source-oljuk. A legjobb azonban, ha egy text-to-HTML fordítót használunk, pl. a Markdownt. A Markdown szintaxisa egyszerűen elsajátítható, hasonló a Wikiéhez, de attól sokkal átgondoltabb és felhasználóbarátabb (szerintem).

Felmerül a kérdés, hogy mikor történik meg a text-HTML konverzió? Erre két megoldás kínálkozik:

  • A content mező tartalmazza a mindenkori Markdown formátumú szöveget, amiből minden lekéréskor generáltatjuk a HTML kódot; vagy
  • Fenntartunk egy másik mezőt a HTML kódnak is, amit minden szerkesztés után frissítünk.

Az első megoldással az a probléma, hogy számítási feladattal terheljük a szerverünket, a másodikkal pedig az, hogy duplikálva tároljuk a kontentot, tehát az adatbázisunk nagyobbra hízik. Az R-Pi esetében tegyük fel, hogy az utóbbi a kisebbik rossz. Ezek után a models.py úgy módosul, hogy felveszünk egy újabb nem szerkeszthető content mezőt (az eredeti content nevét is megváltoztatjuk a readability szellemében)...:

1
2
3
4
5
6
...
class Story(models.Model):
    ...
    markdown_content = models.TextField(blank=True)
    html_content = models.TextField(editable=False)
    ...

... majd ezt úgy töltetjük ki az admin felülettel, hogy a szerkesztés végeztével (mentéskor) ráhívjuk a Markdownt a markdown_content tartalmára - szintén a models.py-ban (természetesen a Markdownt nem felejtjük el importálni). Ehhez a Model class save metódusát írjuk felül:

1
2
3
4
5
6
7
from markdown import markdown
...
class Story(models.Model):
    ...
    def save(self, *args):  # yes, we override it
        self.html_content = markdown(text=self.markdown_content, extensions=['codehilite(linenums = True)'])
        super(Story, self).save()  # don’t forget to call super()

A codehilite(linenums = True) rész lesz felelős a kontentben megjelenő programkódok szintaxisának színezéséért és sorszámozásáért. Ahhoz, hogy ez működjön két dolgot kell megtennünk:

  1. Telepítsük a Pygments csomagot.
  2. Készítsünk vagy szerezzünk be egy CSS fájlt a codehilite számára, amit linkelünk a template-ünkben. Pl.: <link rel="stylesheet" type="text/css" href="./codehilite.css">

Már csak egy dolog van hátra: default felállásban a Django semmilyen változóból származó HTML kódot nem hajlandó értelmezni biztonsági okokból, ezért külön szólnunk kell neki, hogy mi megbízunk a html_content tartalmában és legyen szíves azt HTML kódnak látni. Ezt valamelyik vonatkozó template fájlban tehetjük meg a safe flag segítségével. Például:

1
2
3
<p id="story-body">
    {{ story.html_content|safe }}
</p>

Ezzel kész is vagyunk, most már mindenféle formázási lehetőség birtokában vagyunk, amit a Markdown támogat.

Az urls.py és views.py, valamint a template-ek tartalma a fenti feladat megoldása szempontjából irreleváns, bármelyik tutorialban választ kaphatunk rá. Éppen ezért itt nem is szerepeltetem.

Hogyan készült? 1. rész

Ahogy arról már korábban megemlékeztem, nagyjából 2 évvel ezelőtt jött az ihlet, hogy én bizony itthon beüzemelek egy nagyon kicsi PC-t és azon lakik majd az én weblapom. Mielőtt ezt kitaláltam volna, mindenféle ingyenes és olcsó hosting szolgáltatásokat böngésztem. Teljesen fogalom nélkül voltam, de azt tudtam, hogy nem akarok sokat rákölteni és a lehető legkevesebb hirdetést szeretném látni az oldalamon. Ekkoriban hallottam először a bankkártya méretű, alacsony fogyasztású számítógépekről. Rövidesen kiderült, hogy mindent meg tudok valósítani saját magam.

Hardver

Választásom az akkoriban nagyon népszerű Raspberry Pi-re esett, azon belül is a combosabb B variánsra. A gépen egy 700 MHz-es ARM proci dolgozik, 512 MB RAM-mal megtámogatva. Található rajta 2 db USB 2.0 port, egy Ethernet csati, egy kompozit video kimenet, egy 3.5-es jack audio out, egy SDHC memóriakártya-foglalat és egy micro USB bemenet a delejnek. Itt jegyezném meg, hogy egy közönséges mobiltelefon töltőről üzemeltethető, amely képes 700 mA-t produkálni 5 V-on. Ha ezekkel az adatokkal számolunk, akkor folyamatos üzem mellett ez évi kb. 30 kWh fogyasztást jelent. A gépet kitben vásároltam az eBay-ről, ezért átlátszó műanyag tokozást is kaptam hozzá. Mivel Kínából jött, szép piros a NYÁK színe. Ezen kívül vettem még egy Class 10-es SanDisk SDHC kártyát 16 GB tárhellyel (azt hiszem szintén eBay-ről) és egy noname micro USB töltőt, ami a Pi-t táplálja. Az egész konstrukciót így megúsztam 16-17 ezer JMF-ből.

Raspberry Pi

Op. rendszer

Az üres SD kártyára fel kellett varázsolnom valamilyen operációs rendszert. Ma már rendelhető Málnáéktól előre installált kártya is, de azért ez nem agysebészet. Végigkövettem a leírást, majd a kártyáról bootoltam be a Pi-t. Első indításkor összedugtam a TV-mmel, csatlakoztattam bele billentyűzetet és egeret. A NOOBS (New Out Of the Box Software) felkínál néhány lehetőséget, csak rá kell bökni melyik oprendszert szeretnénk telepíteni. Mivel Ubuntu-féle Linuxokkal érzem magam a legjobban, a nagyon ajánlott Raspbian OS-t installáltam, ami végülis egy ARM-ra optimalizált Debian. Miután a Raspbian működőképessé vált felraktam rá egy SSH-t (meg VNC-t is, bár azt nem szoktam használni) és osztottam neki statikus IP-t (mert ugye közben rácsatlakoztattam az otthoni routerünkre). Innentől a TV-re, billentyűzetre és egérre többé már nem volt szükség.

Szoftver

Megszámlálhatatlanul sok fórumot és blogot áttúrtam, hogy megtaláljam az optimális software stacket. Két kiindulási pontom volt: Raspberry Pi és Django (Python 2.7 alapon). Ezek mellé kellett találnom gyors és lightweight adatbázist és webszervert. SQL? NoSQL? A Django teszt célokra és kisebb projektekhez az SQLite3-at ajánlja. Az igazság az, hogy egy blog kiszolgálásához bőven elég ez is. MySQL-t vagy Postgret telepíteni emiatt teljesen felesleges. Ha később mégsem jönne be, akkor MongoDB-vel fogok próbálkozni.

Ha webszerver, akkor a legtöbb embernek az Apache ugrik be legelőször. Kicsit talán ágyúval verébre, de nem lehetetlen. Én azért ettől lájtosabbat szerettem volna. Elolvastam néhány leírást, így lett aztán Nginx-Gunicorn kombó. Ebben a felállásban az Nginx HTTP proxyként a beérkező requesteket továbbítja egy statikus könyvtár vagy valamilyen applikáció (Gunicorn) felé. A Gunicorn egy socketen keresztül összeköti az Nginx-et a blogmotorral és ezen keresztül szolgálja ki a dinamikus tartalmakat.

A blogmotor maga pedig egy Django applikáció egy virtualenv-ben. A virtuális környezet különválasztja a blogot a rendszer Pythontól; alapja egy 2.7-es Python és egy Django 1.6-os installáció, melyekhez minden szükséges egyéb library is telepítve van.

Nagy vonalakban így áll össze az egész rendszer.