Γράφοντας το πρώτο σας Django app, μέρος 3

Ο οδηγός αυτός ξεκινά εκεί που τελειώνει ο οδηγός 2. Συνεχίζουμε με την εφαρμογή μας (σκοπός μας είναι φτιάξουμε ένα σύστημα ψηφοφορίας όπου θα γράφουμε τις ερωτήσεις και τι πιθανές επιλογές και ο κόσμος θα μπορεί να ψηφίζει). Αυτή τη φορά θα επικεντρωθούμε στην προβολή της ιστοσελίδας μας στο ευρύ κοινό, στα “views”.

Επισκόπηση

Ένα view είναι ένας «τύπος» μιας ιστοσελίδας (Web page) σε ένα Django application το οποίο, σε γενικές γραμμές, εξυπηρετεί μια συγκεκριμένη συνάρτηση και έχει ένα συγκεκριμένο template. Για παράδειγμα, ένα blog application, μπορεί να περιέχει τα ακόλουθα views:

  • Blog homepage – προβάλει τα μερικά από τα τελευταία entries (καταχωρήσεις).
  • Σελίδα «λεπτομερειών» μιας entry (καταχώρησης) – μια permalink σελίδα για ένα συγκεκριμένο entry.
  • Σελίδα archive ανά ημερολογιακό χρόνο – προβάλει όλους τους μήνες που έχουν καταχωρήσεις για το δοθέντα έτος.
  • Σελίδα archive ανά ημερολογιακό μήνα – προβάλει όλες τις μέρες που έχουν καταχωρήσεις για το δοθέντα μήνα.
  • Σελίδα archive ανά ημερολογιακή μέρα – προβάλει όλα τα entries μια συγκεκριμένη μέρα.
  • Προσθήκη σχολίου – χειρίζεται τα σχόλια πάνω στα posts για ένα συγκεκριμένο entry.

Στη δική μας εφαρμογή, θα έχουμε τα ακόλουθα τέσσερα views:

  • Αρχική σελίδα («index») ερωτήσεων – προβάλει τις τελευταίες ερωτήσεις.
  • Σελίδα «λεπτομερειών» μιας ερώτησης – προβάλει μια ερώτηση, χωρίς αποτελέσματα, αλλά με μια φόρμα (η οποία θα περιέχει τις επιλογές της ερώτησης) προς ψήφιση.
  • Σελίδα «αποτελεσμάτων» μιας ερώτησης – προβάλει τα αποτελέσματα μιας συγκεκριμένης ερώτησης.
  • Προσθήκη ψήφου – χειρίζεται τη ψήφο μιας συγκεκριμένης επιλογής μιας συγκεκριμένης ερώτησης.

Στο Django, οι ιστοσελίδες όπως επίσης και άλλα περιεχόμενα εξυπηρετούνται από τα views. Κάθε view αντιπροσωπεύεται από μια απλή Python συνάρτηση (ή μέθοδο, στην περίπτωση των class-based views). Το Django θα διαλέξει το view ανάλογα το URL το οποίο ζητήθηκε (πιο συγκεκριμένα, το μέρος του URL μετά το domain name).

Ίσως μέχρι τώρα έχετε δει στην διαδικτυακή σας περιήγηση μερικές ομορφιές όπως «ME2/Sites/dirmod.asp?sid=&type=gen&mod=Core+Pages&gid=A6CD4967199A42D9B65B1B». Ίσως θα σας χαροποιήσει το γεγονός ότι το Django χρησιμοποιεί (και το κάνει) πολύ πιο κομψά URL patterns εν συγκρίσει με αυτά.

Ένα URL pattern (μοτίβο) είναι απλά μια γενική φόρμα ενός URL – για παράδειγμα: /newsarchive/<year>/<month>/.

Για να μεταβούμε από ένα URL σε ένα view, το Django χρησιμοποιεί κάτι το οποίο είναι γνωστό ως “URLconfs”. Ένα URLconf αντιστοιχεί URL μοτίβα (τα οποία περιγράφονται με regular expressions) σε views. Αυτά τα είδαμε περιληπτικά στον Οδηγό 1.

Ο συγκεκριμένος οδηγός προσφέρει βασικές οδηγίες στη χρήση των URLconfs. Επίσης, περισσότερες πληροφορίες μπορείτε να βρείτε στο module django.urls.

Γράφοντας περισσότερα views

Ας προσθέσουμε τώρα μερικά views παραπάνω, στο αρχείο polls/views.py. Αυτά τα views είναι ελαφρώς διαφορετικά, επειδή παίρνουν και όρισμα (argument):

polls/views.py
def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)

def results(request, question_id):
    response = "You're looking at the results of question %s."
    return HttpResponse(response % question_id)

def vote(request, question_id):
    return HttpResponse("You're voting on question %s." % question_id)

Συνδέστε τα καινούργια αυτά views με το αρχείο polls.urls προσθέτοντας τον παρακάτω κώδικα στη κλήση της συνάρτησης url():

polls/urls.py
from django.conf.urls import url

from . import views

urlpatterns = [
    # ex: /polls/
    url(r'^$', views.index, name='index'),
    # ex: /polls/5/
    url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
    # ex: /polls/5/results/
    url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'),
    # ex: /polls/5/vote/
    url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]

Ρίξτε μια ματιά με τον browser σας στη σελίδα, «/polls/34/». Με το που πατήσετε enter, το GET request που κάνατε για αυτό το URL θα κάνει trigger το Django project σας, θα γίνει σκανάρισμα των URLs, θα ταιριάξει το δεύτερο URL pattern το οποίο με τη σειρά του θα καλέσει τη μέθοδο detail() και η τελευταία θα προβάλει το μήνυμα μαζί με το ID που εισήχθη στο URL (το response το οποίο επιστρέφει η detail συνάρτηση). Τώρα δοκιμάστε το «/polls/34/results/» και το «/polls/34/vote/» – αυτά τα URLs θα εμφανίσουν μια σελίδα placeholder για τα αποτελέσματα και τις ψηφοφορίες αντίστοιχα.

Όπως είπαμε και πιο πάνω όταν κάποιος κάνει request μια σελίδα από το website σας – ας πούμε την «/polls/34/», τότε το Django θα φορτώσει το mysite.urls Python module επειδή δείχνει σε αυτό η ρύθμιση ROOT_URLCONF. Βρίσκει μέσα μια μεταβλητή με το όνομα urlpatterns και βλέπει-σκανάρει μία-μία τις regular expressions με τη σειρά που είναι δηλωμένες. Αφού βρει αυτή που ταιριάζει στο '^polls/', κόβει το string που ταίριαξε ("polls/") και στέλνει το υπόλοιπο – "34/" – στο “polls.urls” URLconf για περαιτέρω επεξεργασία. Εκεί, ταιριάζει το r'^(?P<question_id>[0-9]+)/$', με αποτέλεσμα να κληθεί το detail() view ως εξής:

detail(request=<HttpRequest object>, question_id='34')

Το κομμάτι question_id='34' προκύπτει από το (?P<question_id>[0-9]+). Χρησιμοποιώντας παρενθέσεις γύρω από ένα μοτίβο, «αιχμαλωτίζουμε» το κείμενο (text) που ταίριαξε (βάση του μοτίβου) και το στέλνουμε ως όρισμα (argument) στη συνάρτηση view (π.χ detail). Το ?P<question_id> ορίζει το όνομα που θα χρησιμοποιηθεί για να αναγνωρίσουμε το κείμενο που ταίριαξε. Τέλος, το [0-9]+ είναι μια regular expression η οποία ταιριάζει ακολουθίες ψηφίων (π.χ ένας αριθμός).

Επειδή, στην ουσία, τα URL patterns είναι regular expressions, στην πραγματικότητα δεν υπάρχουν όρια για το τι μπορεί να επιτευχθεί με αυτά. Επίσης δεν υπάρχει λόγος να προσθέσετε άσχημες URL καταλήξεις όπως .html – εκτός και αν το θέλετε. Αν ναι, τότε θα κάνετε κάτι σαν:

url(r'^polls/latest\.html$', views.index),

Αλλά μην το κάνετε. Είναι ανόητο και μη επαγγελματικό.

Γράφοντας views οι οποίες κάνουν κάτι

Κάθε view είναι υπεύθυνο στο να κάνει ένα από δύο πράγματα: να επιστρέφει ένα HttpResponse object το οποίο περιέχει το περιεχόμενο για την σελίδα που ζητήθηκε ή να κάνει raise κάποιο exception όπως το Http404. Τα υπόλοιπα εξαρτώνται από εσάς.

Το view σας μπορεί να διαβάσει records από την βάση δεδομένων ή όχι. Μπορεί να χρησιμοποιήσει ένα template system όπως αυτό του Django – ή κάποιο τρίτο Python template system – ή όχι. Μπορεί να παράγει κάποιο PDF αρχείο, να εξάγει XML, να δημιουργεί κάποιο ZIP αρχείο, να εξάγει JSON δεδομένα, βασικά μπορεί να κάνει τα πάντα χρησιμοποιώντας οποιεσδήποτε βιβλιοθήκες Python επιθυμείτε.

Το μόνο που απαιτείται από το Django είναι ένα HttpResponse ή μια exception.

Επειδή είναι βολικό, θα χρησιμοποιήσουμε το database API του Django, το οποίο καλύψαμε στον Οδηγό 2. Παρακάτω βλέπετε ένα καινούργιο view με το όνομα index(), το οποίο εμφανίζει τις τελευταίες 5 ερωτήσεις (που υπάρχουν στην βάση δεδομένων), διαχωρισμένες με κόμμα, βάση της ημερομηνίας που καταχωρήθηκαν (δημιουργήθηκαν):

polls/views.py
from django.http import HttpResponse

from .models import Question


def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    output = ', '.join([q.question_text for q in latest_question_list])
    return HttpResponse(output)

# Leave the rest of the views (detail, results, vote) unchanged

Υπάρχει ένα πρόβλημα εδώ, όμως: ο σχεδιασμός της σελίδας (design) είναι γραμμένος στο view. Αν χρειαστεί να αλλάξετε τον τρόπο εμφάνιση της σελίδας σας, θα χρειαστεί να επεξεργαστείτε αυτόν τον Python κώδικα. Ας κάνουμε κάτι καλύτερο. Ας χρησιμοποιήσουμε το template system του Django για να διαχωρίσουμε το design από τον Python κώδικα δημιουργώντας ένα template που το view μπορεί να χρησιμοποιήσει.

Για αρχή, δημιουργήστε ένα φάκελο με το όνομα templates κάτω από τον φάκελο polls. Το Django θα κοιτάξει για templates εκεί.

Η ρύθμιση TEMPLATES του project σας περιγράφει πως το Django θα φορτώνει και θα κάνει render τα templates. Η προεπιλεγμένη ρύθμιση στο αρχείο settings παραμετροποιεί το DjangoTemplates backend του οποίου η ρύθμιση APP_DIRS είναι ρυθμισμένη στο True. Έτσι, το DjangoTemplates ψάχνει για ένα υπό-φάκελο «templates» σε κάθε εφαρμογή (app) που είναι περασμένη στη ρύθμιση INSTALLED_APPS.

Μέσα στο φάκελο templates που μόλις δημιουργήσατε, δημιουργήστε άλλο έναν φάκελο με το όνομα polls και μέσα σε αυτό το φάκελο ένα HTML αρχείο με το όνομα index.html. Με άλλα λόγια, το template σας θα πρέπει να βρίσκεται στο polls/templates/polls/index.html. Λόγω του μηχανισμού λειτουργίας του φορτωτή template (template loader) app_directories όπως αναφέρθηκε παραπάνω, μπορείτε να αναφερθείτε σε αυτό το template μέσα στο Django ως polls/index.html.

Template namespacing

Ίσως θα αναρωτηθήκατε γιατί βάλαμε το template μέσα σε υπό-φάκελο (polls) και όχι μέσα στο φάκελο polls/templates. Αν το κάναμε έτσι, τότε δεν θα ήταν και τόσο καλή ιδέα. Το Django θα ψάξει και θα βρει το πρώτο template βάση ονόματος. Αν, λοιπόν, εσείς έχετε ένα άλλο template με το ίδιο όνομα σε μία διαφορετική εφαρμογή, τότε το Django δεν θα είναι σε θέση να ξεχωρίσει ποια από τα δύο να χρησιμοποιήσει. Πρέπει καθοδηγήσουμε το Django στην επιλογή του template που θέλουμε και ο ευκολότερος τρόπος είναι να χρησιμοποιήσουμε την τεχνική namespace. Αυτό επιτυγχάνεται βάζοντας τα templates μέσα σε έναν υπό-φάκελο ο οποίος θα έχει το ίδιο όνομα με το όνομα της εφαρμογής.

Βάλτε τον ακόλουθο κώδικα στο template:

polls/templates/polls/index.html
{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

Τώρα, ας ανανεώσουμε το index view μέσα στο αρχείο polls/views.py ούτως ώστε να χρησιμοποιήσει το νέο μας template:

polls/views.py
from django.http import HttpResponse
from django.template import loader

from .models import Question


def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    template = loader.get_template('polls/index.html')
    context = {
        'latest_question_list': latest_question_list,
    }
    return HttpResponse(template.render(context, request))

Αυτός ο κώδικας φορτώνει το template με το όνομα polls/index.html και περνάει σε αυτό ένα context. Το context είναι ένα dictionary το οποίο αντιστοιχεί template variable ονόματα σε Python objects.

Πηγαίνετε στη σελίδα με το browser σας στο «/polls/» και θα πρέπει να δείτε μια λίστα (με κουκίδες) η οποία περιέχει την ερώτηση «What’s up» που φτιάξαμε στον Οδηγό 2. Το link δείχνει στην σελίδα με τις λεπτομέρειες της ερώτησης.

Η συντόμευση: render()

Είναι πολύ συνηθισμένο να φορτώνεται ένα template, να γεμίζεται ένα context και να επιστρέφεται ένα HttpResponse object του οποίου το αποτέλεσμα θα είναι το rendered template. Το Django παρέχει μια συντόμευση για όλα αυτά. Ορίστε πάλι το index() view, γραμμένο από την αρχή:

polls/views.py
from django.shortcuts import render

from .models import Question


def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    context = {'latest_question_list': latest_question_list}
    return render(request, 'polls/index.html', context)

Σημειώστε ότι, αφού χρησιμοποιήσουμε την render() για όλες τις views, δεν χρειάζεται πλέον να κάνουμε import τα loader και HttpResponse. Παραταύτα, θα χρειαστεί να κρατήσετε το HttpResponse αν έχετε ακόμη τις μεθόδους για το``detail``, results και vote view.

Η συνάρτηση render() παίρνει ως πρώτο όρισμα το request object, ως δεύτερο το όνομα του template και ένα dictionary ως τρίτο προαιρετικό. Επιστρέφει ένα HttpResponse object του δηλωθέν template το οποίο template έχει γίνει rendered βάση του δοθέν context.

Raising σφάλμα 404

Σε αυτό το σημείο θα πειράξουμε το view που αφορά στις λεπτομέρειες μιας ερώτησης – τη σελίδα, δηλαδή, η οποία εμφανίζει την ερώτηση για μια συγκεκριμένη ψηφοφορία. Αυτό είναι το view:

polls/views.py
from django.http import Http404
from django.shortcuts import render

from .models import Question
# ...
def detail(request, question_id):
    try:
        question = Question.objects.get(pk=question_id)
    except Question.DoesNotExist:
        raise Http404("Question does not exist")
    return render(request, 'polls/detail.html', {'question': question})

Το νέο concept εδώ είναι: Το view κάνει raise την exception Http404 αν η ερώτηση με το συγκεκριμένο ID δεν υπάρχει (στη βάση δεδομένων).

Θα συζητήσουμε τι μπορείτε να βάλετε μέσα στο template με το όνομα polls/detail.html λίγο αργότερα, αλλά τώρα αν βιάζεστε να δείτε πως θα φαίνεται γράψτε τα ακόλουθα στο αρχείο polls/detail.html:

polls/templates/polls/detail.html
{{ question }}

Αυτό θα σας δώσει μια ιδέα.

Η συντόμευση: get_object_or_404()

Είναι συνηθισμένο να χρησιμοποιείται η μέθοδος get() η οποία να κάνει raise το exception Http404 αν το object δεν υπάρχει. Το Django σας παρέχει μια συντόμευση και γι’ αυτό. Παρακάτω φαίνεται το detail() view, γραμμένο από την αρχή (πάλι):

polls/views.py
from django.shortcuts import get_object_or_404, render

from .models import Question
# ...
def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})

Η συνάρτηση get_object_or_404() παίρνει ως πρώτο όρισμα ένα Django model και ένα αυθαίρετο αριθμό από arguments, τα οποία τα δίνει στην μέθοδο get() του manager του μοντέλου (model manager) που περάστηκε ως πρώτο όρισμα. Η συνάρτηση αυτή κάνει raise το exception Http404 αν το object δεν υπάρχει.

Φιλοσοφία

Γιατί όμως χρησιμοποιούμε την βοηθητική συνάρτηση (helper function) get_object_or_404() αντί να κάνουμε αυτόματα το catch του exception ObjectDoesNotExist σε ένα υψηλότερο επίπεδο; Επίσης, γιατί να βάλουμε το model API να κάνει raise το exception Http404 αντί του exception ObjectDoesNotExist;

Η απάντηση είναι ίδια και στις δύο ερωτήσεις. Επειδή αυτό θα δεσμεύσει το επίπεδο του model με το επίπεδο του view. Ένας από τους πρωταρχικούς στόχους του Django είναι να διατηρηθεί η ανεξαρτητοποίηση και η μη σύνδεση μεταξύ οντοτήτων (το λεγόμενο loose coupling). Μερικά ελεγχόμενα couplings παρουσιάζονται στο module django.shortcuts.

Υπάρχει επίσης η συνάρτηση get_list_or_404(), η οποία λειτουργεί πανομοιότυπα με την get_object_or_404() – με τη μόνη διαφορά ότι χρησιμοποιεί την μέθοδο filter() αντί της get(). Η get_list_or_404() κάνει raise την exception Http404 αν η λίστα είναι κενή (άδεια).

Χρησιμοποιώντας ένα template system

Πίσω στο detail() view της εφαρμογής μας. Έχοντας το context variable με το όνομα question, το template polls/detail.html θα δείχνει κάπως έτσι:

polls/templates/polls/detail.html
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>

Το template system χρησιμοποιεί την σύνταξη της τελείας για να ψάχνει μέσα στα variable attributes. Στο παράδειγμα του {{ question.question_text }}, το Django θα ψάξει πρώτα μέσα στο dictionary του object question. Αν δεν βρει τίποτα (δεν υπάρχει, δηλαδή, το key question_text``στο υποτιθέμενο dictionary), προσπαθεί να βρει το ``question_text σαν attribute του object – το οποίο στη συγκεκριμένη περίπτωση ισχύει. Αν όμως δεν υπήρχε, τότε θα έψαχνε σε μια λίστα να το βρει.

Ο τρόπος για να καλείτε μεθόδους φαίνεται στο βρόγχο επανάληψης {% for %}: το question.choice_set.all μεταφράζεται ως Python κώδικας question.choice_set.all(), το οποίο επιστρέφει ένα iterable από Choice objects και είναι βολικό προς χρήση (στην παρούσα) με τον βρόγχο επανάληψης {% for %} (το οποίο αναφέρεται και ως template tag).

Δείτε το άρθρο templates για περισσότερα σχετικά με τα templates.

Αφαίρεση των hardcoded URLs στα templates

Θυμηθείτε ότι πριν, δηλώσαμε στην HTML το link για την ερώτηση στο template polls/index.html ως:

<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>

Το πρόβλημα με τα hardcoded URLs (αλλά και γενικά με την hardcoded προσέγγιση), είναι ότι από ένα σημείο και μετά γίνεται δύσκολο (αν όχι ακατόρθωτο) να αλλάξετε όλα τα URLs στα projects σας τα οποία χρησιμοποιούν πολλά templates. Ωστόσο, εφόσον έχετε ονοματήσει τα URLs σας μέσα στη εκάστοτε συνάρτηση url() στο αρχείο polls.urls, μπορείτε να αναφέρεστε σε αυτά χρησιμοποιώντας το template tag {% url %}:

<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

Ο τρόπος με τον οποίο λειτουργεί αυτό το template tag είναι κοιτώντας τα URLs που έχετε ορίσει στο module polls.urls. Μπορείτε να δείτε ακριβώς σε ποιο URL έχει οριστεί το όνομα “detail” παρακάτω:

...
# the 'name' value as called by the {% url %} template tag
url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
...

Αν θέλετε να αλλάξετε το URL αυτού του view σε κάτι άλλο (ίσως στο polls/specifics/12/), τότε αντί να ψάχνετε όλα τα templates που είχατε δηλώσει το παλιό URL προκειμένου να το αντικαταστήσετε με το καινούργιο, πολύ απλά πειράζετε ένα αρχείο (polls/urls.py) και όλα τα υπόλοιπα ανανεώνονται αυτόματα, χάρη στο template tag {% url %}:

...
# added the word 'specifics'
url(r'^specifics/(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
...

Χρησιμοποιώντας namespacing στα ονόματα των URL

Το project του οδηγού αυτού έχει μόνο μια εφαρμογή, την polls. Σε πραγματικά Django projects, ίσως να έχετε πέντε, δέκα, είκοσι ή περισσότερα apps. Πως μπορεί να ξέρει και να διαφοροποιεί το Django τα ονόματα των URLs ανάμεσα σε τόσα apps; Για παράδειγμα, η εφαρμογή polls έχει ένα detail view και ίσως ένα άλλο app (στο ίδιο project) να έχει το ίδιο όνομα για ένα δικό του view. Όταν το Django συναντήσει το template tag {% url 'detail' %} πως θα ξέρει σε ποιο view να αναφερθεί;

Η απάντηση έρχεται με την προσθήκη namespaces στο URLconf. Στο αρχείο polls/urls.py, προσθέστε μια μεταβλητή app_name ούτως ώστε να θέσετε το namespace για αυτό το app:

polls/urls.py
from django.conf.urls import url

from . import views

app_name = 'polls'
urlpatterns = [
    url(r'^$', views.index, name='index'),
    url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
    url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'),
    url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]

Τώρα, πηγαίνετε και αλλάξτε το αρχείο ``polls/index.html``από:

polls/templates/polls/index.html
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

στο:

polls/templates/polls/index.html
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>

Όταν αισθανθείτε άνετα με τη συγγραφή των views, διαβάστε το τέταρτο μέρος αυτού του οδηγού για να μάθετε το πώς να διαχειρίζεστε απλές φόρμες και τα Django generic views.

Back to Top