اذهب إلى المحتوى

التعامل مع الاستمارات Forms والعروض العامة والملفات الساكنة في Django


محمد طاهر5

يفتقر تطبيق الاقتراعات الذي نعمل على إنشائه إلى آلية جيّدة للتصويت على الأسئلة التي يتم عرضها للمستخدم، لذا يجب علينا توفير استمارة تتيح للمستخدم التصويت على الإجابة التي يرغب بها. إضافة إلى ذلك، سنتعرف في هذا الدرس على العروض العامة التي تهدف إلى اختصار الوقت والجهد عبر التخلّص من العمليات المتكررة بصورة دائمة، مثل جلب البيانات من قاعدة البيانات وعرض النتائج في صفحة مستقلة. وفي نهاية الدرس سنتعرف على الملفات الساكنة وسنضيف بعض التنسيقات إلى تطبيق الاقتراعات.

main.png

إنشاء استمارة بسيطة

يفترض أن يكون المستخدم قادرًا على اختيار إحدى الإجابات الخاصة بسؤال معين، ولكن قالب detail بشكله الحالي لا يوفّر هذا اﻷمر، لذا سنحتاج إلى إضافة استمارة إلى هذا القالب. توجّه إلى الملف polls/detail.html في مجلد القوالب templates ثم أضف إليه الشيفرة التالية:

<h1>{{ question.question_text }}</h1>

{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}

<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
{% for choice in question.choice_set.all %}
    <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
    <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br />
{% endfor %}
<input type="submit" value="صوّت" />
</form>

يعمل السطر الأول على جلب نص السؤال ووضعه داخل وسم <h1> لعرضه بشكل واضح وكبير.

في السطر الثاني يتم التحقق مما إذا كان المتغير error_message يحمل قيمة أم ﻻ، فإن كان كذلك يتم عرض هذه القيمة وإلا فلا، سنستخدم هذا المتغير بعد قليل في العرض المسؤول عن التصويت، حيث سيحمل هذا المتغير قيمة نصية وهي عبارة عن رسالة نخبر المستخدم فيها أنّه لم يقم باختيار أي إجابة من الإجابات المعروضة.

في السطر الثالث أنشأنا استمارة يتم توليد قيمة الحدث action فيه بصورة ديناميكية، وذلك باستخدام الوسم url والمسار vote والمتغير question.id والذي يمثّل الرقم المعرف للسؤال، وستكون طريقة إرسال الحدث هي POST، وبما أننا نعتمد هذه الطريقة، فعلينا الانتباه إلى حماية المعلومات من تزوير الطلب عبر المواقع Cross-site request forgery والمعروف اختصارًا بـ CSRF. ولكن لا تقلق، يوفّر Django نظامًا سهل الاستخدام لتجنّب هذه المشكلة. باختصار، يجب عليك وضع الوسم {% csrf_tocken %} في أي استمارة تعتمد طريقة POST ﻹرسال اﻷحداث.

تعمل الحلقة for على إضافة زر اختيار Radio button إلى الإجابات الخاصة بالسؤال الذي اختاره المستخدم، ويتم جلب هذه الإجابات من باستخدام العبارة question.choice_set.all.

والآن لنقم بإنشاء العرض الذي سيتحكم في البيانات المرسلة إلى الخادوم.

أعتقد أن سير العمل في Django قد أصبح واضحًا في هذه المرحلة، ففي البداية نقوم بتعريف المسار، ثم نربط هذا المسار بالعرض الذي سيكون مسؤولًا عن القيام بأي شيء نرغب به، وبعد ذلك نربط هذا العرض بالقالب الذي سيظهر النتائج.

وقد قمنا فعلًا بالخطوة الأولى من سلسلة الخطوات هذه في الدرس الرابع من هذه السلسلة عندما بدأنا الحديث عن المسارات، حيث يتضمن ملف polls/urls.py المسار التالي:

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

هذا المسار مرتبط بالعرض view والذي قمنا بإنشائه في الدرس نفسه:

def vote(request, question_id):
    return HttpResponse("أنت تصوت على السؤال %s." % question_id)

لنقم الآن بكتابة الشيفرة المسؤولة عن عملية التصويت ضمن هذا العرض، لذا توجّه إلى ملف views.py وأضف الشيفرة التالية في بداية الملف:

from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from .models import Choice, Question

ثم عدّل دالة vote لتصبح بالشكل التالي:

def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        return render(request, 'polls/detail.html', {'question':question, "error_message": "لم تقم بالتصويت على السؤال"})
    else:
        selected_choice.votes += 1
        selected_choice.save()
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

لنتكلم عن الشيفرة السابقة بشيء من التفصيل، فهناك عدد من الأمور الجديدة فيها:

  • request.POST هو عنصر شبيه بالقاموس يتيح الوصول إلى البيانات المرسلة من الاستمارة بواسطة اسم المفتاح، وفي حالتنا هذا فإن request.POST['choice'] سوف يرجع قيمة المعرّف الخاص بالجواب الذي اختاره المستخدم، وتكون هذه القيمة على هيئة سلسلة نصية.
    وجدير بالذكر أن Django يقدّم عنصرًا آخر باسم request.GET ويستخدم عندما تكون طريقة إرسال البيانات في الاستمارة هي GET.

  • يطلق العنصر request.POST['choice] خطأً مفتاحيًا KeyError وهذا النوع من الأخطاء يُطلق عندما لا يكون المفتاح المطلوب (ضمن القاموس) موجودًا ضمن مجموعة المفاتيح المتوفرة، وفي تطبيقنا ينطلق هذا الخطأ عندما ﻻ يختار المستخدم أي إجابة من الإجابات المعروضة أمامه، ويعمل على إعادة المستخدم إلى صفحة التصويت مرة أخرى، ولكن هذه المرة مع إضافة العبارة: “لم تقم بالتصويت على السؤال” إلى المتغير error_message، وبعد أن أصبح هذا المتغير يحمل قيمة معينة، سيقوم قالب detail بعرض الرسالة في المكان المناسب.

  • إن تمت عملية التصويت بنجاح يتم إضافة صوت واحد إلى الأصوات الخاصة بالإجابة المختارة من قبل المستخدم، وقد استخدمنا الصنف HttpResponseRedirect بدلًا من الصنف HttpResponse لإعادة توجيه المستخدم إلى صفحة النتائج. هذا الصنف في الواقع هو أحد الأصناف الفرعية للصنف HttpResponse ويأخذ معاملًا واحدًا وهو عنوان URL الذي ستتم إعادة التوجيه إليه. والهدف من استخدام هذا الصنف هو تجنّب إرسال البيانات مرتين في حال ضغط المستخدم على زر الرجوع في المتصفح، وينصح باستخدام هذا الصنف في كل مرة يتم فيها التعامل بنجاح مع بيانات POST، وهذا الأمر ليس خاصًا بـ Django فقط، بل هو من الممارسات الجيدة في مجال تطوير الويب باستخدام أي لغة برمجية.

  • استخدمنا الدالة reverse() في مشيّد الصنف HttpResponseRedirect لنتجنّب الإدخال اليدوي للمسار الذي نرغب بإعادة توجيه المستخدم إليه. يأخذ المعامل الأول في هذه الدالة اسم العرض أو اسم نمط URL المطلوب إعادة التوجيه إليه. في هذا المثال سيتم توجيه المستخدم إلى المسار الذي يحمل اسم polls:results، ولكن هذا المسار يتضمن متغيّرًا، لذا سنخبر Django بأن يأخذ قيمته من المتغيّر question.id وذلك من خلال المعامل args.
    بمعنى أنّه لو اختار المستخدم السؤال الذي يحمل الرقم 3، فإن الصنف HttpResponseRedirect سيعيد السلسلة النصية التالية: polls/3/results/ حيث أن قيمة question.id في هذه الحالة هو 3.

بعد أن يقوم المستخدم بالتصويت على سؤال معين، تعمل دالة العرض vote() على إعادة توجيهه إلى صفحة النتائج الخاصة بذلك السؤال، فلنقم إذًا بكتابة دالة العرض الخاصّة بالنتائج. توجّه إلى ملف polls/views.py وعدّل الدالة results لتصبح بالشكل التالي:

def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question': question})

والآن، قم بإنشاء القالب المسؤول عن عرض النتائج. في مجلد templates/polls/ أنشئ الملف results.html، وأضف إليه الشيفرة التالية:

<h1>{{ question.question_text }}</h1>

<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>

<a href="{% url 'polls:detail' question.id %}">صوّت مرة أخرى</a>

والآن توجّه إلى العنوان التالي في متصفحك بعد تشغيل الخادوم الخاص بـ Django:

127.0.0.1:8000/polls/1/
وستحصل على نتيجة مشابهة لهذه:

pic-001.png

وبعد التصويت سيتم توجيهك إلى صفحة النتائج:

pic-002.png

وفي حال عدم التصويت على أي إجابة والضغط على زرّ التصويت، تظهر رسالة الخطأ بالشكل التالي:

pic-003.png

العروض العامة Generic Views

لنلق نظرة على ملف views.py بعد إضافة جميع الدوال إليه:

from django.shortcuts import render, get_object_or_404
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from .models import Choice, Question

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

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

def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question': question})

def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        return render(request, 'polls/detail.html', {'question':question, "error_message": "لم تقم بالتصويت على السؤال"})
    else:
        selected_choice.votes += 1
        selected_choice.save()
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

هل لاحظت أمرًا ما؟ الدالتان detail() وresults() متشابهتان كثيرًا، وفي الواقع الفارق الوحيد بينهما هو القالب الذي يرتبط بكل دالة. إضافة إلى ذلك فإن هاتين الدالتين إضافة إلى دالة index() تقوم بعمل واحد وبسيط، وهو عرض مجموعة من المعلومات على المتصفح.

تمثل هذه العروض حالة شائعة جدًّا في عملية تطوير الويب، وهي الحصول على البيانات من قاعدة البيانات بالاستناد إلى معامل يتم تمريره في عنوان URL في المتصفح، ثم تحميل قالب معين، ثم تصيير ذلك القالب على متصفح الإنترنت.

لقد ذكرنا في بداية هذه السلسلة أنّ إطار العمل Django يركّز على مبدأ عدم التكرار، وقد رأينا هذا بشكل واضح عند استخدامنا للدوال المختصرة render() و get_object_or_404().

كذلك يوفّر Django في حالتنا هذه طريقة مختصرة للتعامل مع هذه العمليات الشائعة، وهي العروض العامّة. تعمل هذه العروض على اختصار الأنماط المتكررة من الشيفرات إلى درجة لا تعود فيها بحاجة إلى كتابة شيفرة بايثون لكتابة التطبيق.

ولاستخدام العروض العامّة يتوجّب علينا إجراء بعض التعديلات البسيطة على شيفرتنا السابقة، ويمكن تلخيص هذه التعديلات بالنقاط التالية:

  1. تعديل أنماط URL.
  2. الاستغناء عن بعض العروض التي لسنا بحاجة إليها بعد الآن.
  3. كتابة عروض جديدة بالاعتماد على عروض Django العامة.

تعديل أنماط URL

توجّه إلى ملف polls/urls.py وعدّله بالشكل التالي:

from django.conf.urls import url

from . import views

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

لاحظ أنّنا استبدلنا <question_id> بـ <pk> في مساري detail و results، وسنتعرف على سبب ذلك بعد قليل.

تعديل العروض

التعديلات التي سنجريها على العروض ستشمل دوال index(), detail(), results() أما دالة vote() فستبقى دون تغيير:

from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.views import generic

from .models import Choice, Question


class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        return Question.objects.order_by('-pub_date')[:5]


class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'


class ResultsView(generic.DetailView):
    model = Question
    template_name = 'polls/results.html'

لاحظ كيف استغنينا بشكل كامل عن الدوال الثلاثة سابقة الذكر لتحلّ محلّها أصناف تحمل أسماءً مشابهة، ونظرًا لاستخدام الأصناف في هذا النوع من العروض، فإنّها تحمل اسم العروض المستندة إلى الأصناف Class based views.

لقد استخدمنا نوعين من أنواع العروض العامة، وهما ListView و DetailView. يلخّص العرض اﻷول عملية عرض جميع العناصر، في حين يلخّص العرض الثاني عملية عرض التفاصيل المرتبطة بعنصر معيّن.

يحتاج كل عرض عامٍّ إلى التعرّف على النموذج الذي سيتعامل معه، ويتم التصريح عن ذلك في المعامل model. إلى جانب ذلك، يطلب العرض DetailView أن يتم إسناد قيمة المفتاح الرئيسي المأخوذة من عنوان URL إلى متغير يحمل الاسم pk، لهذا السبب قمنا بتبديل question_id بالاسم الجديد في ملف المسارات urls.py.

يبحث العرض DetailView بصورة افتراضية عن القالب المرتبط به في المسار <app name>/<model_name>_detail.html، وهذا يعني أنّ هذا العرض سيستخدم القالب polls/question_detail.html بشكل تلقائي. ويمكن تجاوز هذه القيمة التلقائية من خلال تعيين اسم القالب باستخدام المعامل template_name. ينطبق الأمر ذاته على العرض ListView والذي سيستخدم بشكل تلقائي القالب polls/question_list.html.

بقي أن نشير إلى أنّنا كنا نزوّد كل قالب بمتغيرات السياق التي سيستخدمها، وفي مثالنا هذا كانت المتغيرات هي question و latest_question_list.
عند استخدام العروض العامّة يتم تزويد العرض DetailView بالمتغير question تلقائيًا، وذلك ﻷننا نستخدم النموذج Question، حيث يستطيع Django تحديد اسم مناسب لمتغيرات السياق بناء على النموذج الذي يرتبط بالعرض العام. ويمكن تجاوز القيمة التلقائية هذه بسهولة وذلك بإسناد اسم المتغير الذي نرغب به إلى المعامل context_object_name، وبهذا نخبر Django بأنّنا نرغب باستخدام هذا الاسم بدلًا من الاسم الذي سينشئه بصورة تلقائية وهو في هذه الحالة question_list.

كما يمكنك الإبقاء على الاسم الافتراضي question_list إن كنت ترغب في ذلك، ولكن هنا عليك تغيير اسم المتغير في أي مكان يرد فيه ضمن القوالب.

الملفات الساكنة Static Files

يبدو تطبيقنا بشعًا أليس كذلك؟ لنقم إذًا بإضافة بعض التنسيقات البسيطة إليه، ولنتعرف على مفهوم الملفات الساكنة.

إلى جانب ملفات HTML التي ينشئها الخادوم، تحتاج تطبيقات الويب بشكل عام إلى بعض الملفات اﻹضافية - مثل الصور وملفات جافا سكربت وملفات CSS - لعرض صفحات الويب بشكل منسّق ومرتّب، ويطلق Django على هذه الملفات اسم الملفات الساكنة.

سنبدأ أولًا بإنشاء مجلد باسم static في مجلد polls الرئيسي، ثم أنشئ داخل المجلد static مجلّدًا جديدًا باسم polls. بمعنى آخر يجب أن يكون مسار ملف CSS بالشكل التالي: polls/static/polls/style.css. كما هو الحال مع القوالب، فإن Django يبحث بنفس الطريقة عن الملفات الساكنة، واستخدام هذه المجلدات هو أمر تنظيمي يتيح لـ Django التمييز بين الملفات الساكنة الخاصة بكل تطبيق.

والآن توجّه من خلال محرّر النصوص إلى ملف style.css وأضف إليه الشيفرة التالية:

li a {
    color: green;
    text-decoration: none;
    font-size: 1.3em;
}

والآن توجّه قالب index.html وأضف الشيفرة التالية في بداية الملف:

{% load staticfiles %}

<link rel="stylesheet" type="text/css" href="{% static 'polls/style.css' %}" />

يعمل الوسم load staticfiles في بداية الملف على تحميل الوسم static المسؤول بدوره عن توليد عنوان URL المطلق للملف الساكن المطلوب.

والآن أعد تحميل الصفحة الرئيسية وستلاحظ أن الأسئلة قد تلونت باللون اﻷخضر.

ولإضافة صورة للخلفية قم بإنشاء مجلد images في نفس المجلد الذي يحتوي الملف style.css ثم ضع فيه الصورة التي ترغب في جعلها خلفية للصفحة، وعلى فرض استخدام صورة باسم background.gif أضف الشيفرة التالية إلى ملف style.css:

body {
    background: white url("images/background.gif") no-repeat right bottom;
}

أعد تحميل الصفحة الرئيسية، وستلاحظ أن الصورة قد أصبحت خلفية للصفحة.

من الجدير بالذكر أنّه لا يمكن استخدام الوسم {% static %} استخدام داخل الملفات الساكنة والتي لا يتم توليدها بواسطة Django، لذا يجب استخدام المسارات النسبية وليست المطلقة لربط الملفات الساكنة بعضها ببعض.

ختامًا

تعرّفنا في هذا الدرس على كيفية استخدام نماذج الاستبيان في Django بصورة مبسطة، واستخدمنا بعد ذلك العروض العامّة التي تختصر الكثير من الوقت والجهد، وفي النهاية تعرّفنا على مفهوم الملفات الساكنة وكيفية استخدامها في تطبيقات Django. في الدرس القادم وهو الدرس اﻷخير ضمن هذه السلسلة، سنتحدّث بشيء من التفصيل عن لوحة التحكم التي يتم إنشاؤها بصورة تلقائية مع كل مشروع في Django.

المصدر:
توثيقات Django


تفاعل الأعضاء

أفضل التعليقات

أولاً: شكراً لك وجزاك الله خيراً على هذه المقالة التي كنت أبحث عنها.

 

ثانياً: هل يوجد شرح بالفيديو لهذه المقالة؟ 

وإذا لم يوجد، هل يمكن توجيهي لفيديو يفيدني في هذا؟

وهل ضروري استخدام sql ، أم يمكن الاكتفاء بالموديول تبع الجانجو، واستعمال هذا الكود معه لن يسبب مشكلة؟

رابط هذا التعليق
شارك على الشبكات الإجتماعية

بتاريخ On 17‏/1‏/2023 at 10:07 قال Sam Sham:

أولاً: شكراً لك وجزاك الله خيراً على هذه المقالة التي كنت أبحث عنها.

 

ثانياً: هل يوجد شرح بالفيديو لهذه المقالة؟ 

مرحباً سام،

يوجد شروحات عن هذه المقالة و عن الدجانغو في الدورات الخاصة بأكاديمية حسوب، بشكل أساسي في دورة بايثون.

بتاريخ On 17‏/1‏/2023 at 10:07 قال Sam Sham:

وهل ضروري استخدام sql ، أم يمكن الاكتفاء بالموديول تبع الجانجو، واستعمال هذا الكود معه لن يسبب مشكلة؟

إن المودل الخاص بالدجانغو يشكل طبقة وسيطة بينك و بين قاعدة البيانات مما يسهل العمل، من أجل أعمال بسيطة لن تحتاج لكتابة تعليمات sql بنفسك، فقط عليك استعمال المودل، و لكن من أجل استعمالات أكثر تعقيداً قد يكون من الأفضل و الأسرع من ناحية الأداء أن تقوم بكتابة تعليمة sql لوحدك، كما أنه أحياناً تحدث أخطاء في عمليات التهجير لقاعدة البيانات، يمكنك حلها فقط إذا كنت على معرفة بتعليمات ال sql و الجداول و ما إلى هنالك.

تحياتي لك.

رابط هذا التعليق
شارك على الشبكات الإجتماعية



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...