فرم در جنگو

آخرین چیزی که می‌خواهیم در وب سایت مان انجام دهیم ایجاد یک راه خوب برای اضافه کردن و ویرایش پست‌های وبلاگ است. ادمین جنگو خوب است، اما سخت است که آن را زیبا یا سفارشی کنیم. با فرم‌ها ما کنترل کاملی بر روی صفحه خود داریم. تقریباً می‌توانیم هرکاری که بخواهیم انجام دهیم!

ویژگی فرم‌ها در جنگو این است که ما می‌توانیم یکی را از ابتدا تعریف کنیم یا از ModelFormاستفاده کنیم که نتیجه فرم را به صورت مدل ذخیره می‌کند.

این دقیقا همان چیزی است که ما می‌خواهیم انجام دهیم: ما یک فرم را برای مدل Post ایجاد خواهیم کرد.

مانند هر بخش مهمی از جنگو، فرم‌ها فایل خود را دارند: forms.py.

ما باید یک فایل با این نام در پوشه blog ایجاد کنیم.

blog
   └── forms.py

خوب، اجازه دهید آن را در ویرایشگر کد باز کنیم و خطوط زیر را تایپ کنیم:

blog/forms.py

from django import forms

from .models import Post

class PostForm(forms.ModelForm):

    class Meta:
        model = Post
        fields = ('title', 'text',)

باید در ابتدا فرم‌های جنگو را فراخوانی کنیم (from django import forms) سپس مدل مربوط به Post را نیز فراخوانی کنیم(from .models import Post).

PostForm، همانطور که احتمالاً حدس زده اید، نام فرم ما است. ما باید به جنگو بگوییم که این فرم یک نوع ModelForm است (بنابراین جنگو برای ما چند کار جادویی انجام خواهد داد) - forms.ModelForm مسئول انجام این کارها است.

بعد از این، ما class Meta را داریم، جایی که به جنگو می‌گویم که کدام مدل باید برای ایجاد این فرم (model = Post) استفاده شود.

در نهایت می‌توانیم بگوییم که کدام بخش‌ (بخش‌ها) باید در فرم ما باشد. در این سناریو ما تنها title و text را می‌خواهیم نشان دهیم - author باید فردی باشد که در حال حاضر وارد شده است (شما!) و created_date تاریخ ایجاد یک پست جدید است که باید به طور خودکار تنظیم شود، درست است؟

و همین! تمام مواردی که اکنون باید انجام دهیم، استفاده از فرم در یک view و نمایش آن در تمپلیت است.

بنابراین یک بار دیگر یک لینک، یک URL، یک view و یک تمپلیت ایجاد خواهیم کرد.

لینک به یک صفحه با فرم

قبل از آنکه لینک را اضافه کنیم، لازم است آیکونی را به عنوان دکمه برای این لینک اضافه کنیم. برای این دوره آموزشی فایل file-earmark-plus.svg را دانلود کنید و آن را در پوشه blog/templates/blog/icons/ ذخیره کنید

نکته: برای دانلود فایل SVG منو زمینه‌ای مربوط به آن را باز کنید (معمولاً به کمک راست کلیک بر روی آن) و بعد گزینه "Save link as" را انتخاب کنید. در پنجره‌ای که باز می‌شود آدرس محلی که می‌خواهید فایل را ذخیره کنید، مشخص کنید، به پوشه djangogirls که پروژه جنگو شما در آن است بروید و پوشه blog/templates/blog/icons/ را پیدا کنید و فایل را در آن ذخیره کنید.

وقت آن است که فایل blog/templates/blog/base.html را در ویرایشگر متن خود باز کنید. حالا می‌توانیم از فایل آیکون در تمپلیت base به شکل زیر استفاده کنیم. در عنصر div که در بخش header قرار دارد، لینکی قبل از عنصر h1 اضافه خواهیم کرد:

blog/templates/blog/base.html

<a href="{% url 'post_new' %}" class="top-menu">
    {% include './icons/file-earmark-plus.svg' %}
</a>

توجه داشته باشید که ما می‌خواهیم نام ویو جدید را post_new بگذاریم. آیتم SVG icon توسط Bootstrap Icons ارائه می‌شود و علامتی مانند یک صفحه به همراه یک علامت به اضافه، نشان خواهد داد. ما از یک هدایت‌کننده جنگو یا Django template directive، به اسم include استفاده می‌کنیم. این هدایت‌کننده محتوای یک فایل را به تمپلیت جنگو تزریق می‌کند. مرورگر وب به خوبی می‌تواند این نوع از محتوا را بدون پردازش‌های اضافه، مدیریت کند.

شما می‌توانید تمام آیکون‌های بوتسترپ را از اینجا دانلود کنید. فایل را از حالت زیپ خارج کنید و همه تصاویر را در پوشه‌ای به نام icons در داخل پوشه blog/templates/blog/ قرار دهید. به این روش شما می‌توانید به آیکونی مانند pencil-fill.svg از طریق آدرس blog/templates/blog/icons/pencil-fill.svg، دسترسی داشته باشید

بعد از اصلاح این خط، فایل شما باید به این شکل باشد:

blog/templates/blog/base.html

{% load static %}
<!DOCTYPE html>
<html>
    <head>
        <title>Django Girls blog</title>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
        <link href='//fonts.googleapis.com/css?family=Lobster&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
        <link rel="stylesheet" href="{% static 'css/blog.css' %}">
    </head>
    <body>
        <header class="page-header">
            <div class="container">
                <a href="{% url 'post_new' %}" class="top-menu">
                    {% include './icons/file-earmark-plus.svg' %}
                </a>
                <h1><a href="/">Django Girls Blog</a></h1>
            </div>
        </header>
        <main class="content container">
            <div class="row">
                <div class="col">
                    {% block content %}
                    {% endblock %}
                </div>
            </div>
        </main>
    </body>
</html>

پس از ذخیره و بازخوانی صفحه http://127.0.0.1:8000 شما با یک خطای شناخته شده NoReverseMatch مواجه خواهید شد، درست است؟ بسیار عالی!

آدرس اینترنتی

فایل blog/urls.py را باز کرده و کد زیر را به آن اضافه می‌کنیم:

blog/urls.py

path('post/new/', views.post_new, name='post_new'),

و در نهایت کد ما بصورت زیر خواهد بود:

blog/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('', views.post_list, name='post_list'),
    path('post/<int:pk>/', views.post_detail, name='post_detail'),
    path('post/new/', views.post_new, name='post_new'),
]

از آنجایی که ویوی post_new را پیاده سازی نکرده ایم، هنگامی که سایت را ریفرش کنیم با خطای AttributeError مواجه خواهیم شد. پس باید ویوی مربوطه را نیز بسازیم:

ویو post_new

زمان آن است که فایل blog/views.py را در ویرایشگر کد باز کنید و خطوط زیر را در ردیف from ها اضافه کنید:

blog/views.py

from .forms import PostForm

و حالا view ما:

blog/views.py

def post_new(request):
    form = PostForm()
    return render(request, 'blog/post_edit.html', {'form': form})

برای ایجاد یک فرم جدیدPost، ما باید()PostForm را فراخوانی کنیم و آن را به تمپلیت منتقل کنیم. ما به این view باز خواهیم گشت، اما در حال حاضر، بیایید به سرعت یک تمپلیت برای این فرم ایجاد کنیم.

تمپلیت

لازم است یک فایل به نام post_edit.htmlدر پوشه blog/templates/blogبسازیم و آن را در ویرایشگر کد بازکنیم. برای آنکه یک فرم کار کند به چند چیز نیاز داریم:

  • ما باید فرم را نمایش دهیم. مثلاً می‌توانیم این کار را با {{ form.as_p }} انجام دهیم.
  • خط بالا باید درون تگ فرم HTML پیچیده شود: <form method="POST">... </form>.
  • ما به یک دکمه ذخیره کردن نیاز داریم. این کار را با یک دکمه HTML انجام می‌دهیم: <button type="submit">Save</button>.
  • و در نهایت، درست بعد از باز شدن تگ <form ...> باید {% csrf_token %} را اضافه کنیم. نوشتن این عبارت بسیار مهم است، زیرا فرم‌های شما را امن می‌کند! اگر شما این تکه را فراموش کنید، جنگو هنگام تلاش برای ذخیره کردن فرم پیغام خطا می‌دهد:

صفحه خطا CSRF

خوب، بگذار ببینیم HTML در فایل post_edit.html چطور باید باشد:

blog/templates/blog/post_edit.html

{% extends 'blog/base.html' %}

{% block content %}
    <h2>New post</h2>
    <form method="POST" class="post-form">{% csrf_token %}
        {{ form.as_p }}
        <button type="submit" class="save btn btn-default">Save</button>
    </form>
{% endblock %}

حالا زمان به روزرسانی پروژه است! بله فرم شما نمایش داده می‌شود!

فرم جدید

اما یک دقیقه صبر کنید وقتی چیزی را در فیلدهای title و text تایپ کنید و سعی کنید آن را ذخیره کنید، چه اتفاقی خواهد افتاد؟

هیچ چی! ما یک بار دیگر در همان صفحه هستیم و متن ما رفته است... و هیچ پست جدیدی اضافه نشده است. پس چه اتفاقی افتاد؟

جواب این است: هیچ چیز. ما باید در view خودمان کمی کارهای بیشتری انجام دهیم.

ذخیره فرم

یکبار دیگر فایل blog/views.py را در ویرایشگر کد باز کنید. در حال حاضر آنچه که در ویو post_new داریم این هاست:

blog/views.py

def post_new(request):
    form = PostForm()
    return render(request, 'blog/post_edit.html', {'form': form})

هنگامی که فرم را ارسال می‌کنیم، ما به همان ویوی قبل باز می‌گردیم، اما این بار ما در request داده‌های بیشتری داریم، به ویژه در request.POST (نامگذاری request.POST هیچ ارتباطی با "پست" وبلاگ ندارد و به معنی آن است که ما در حال ارسال اطلاعات هستیم). به یاد می آورید که چگونه در فایل HTML، تعریف <form> دارای متغیر method="POST" بود؟ تمام فیلدهای فرم اکنون در request.POST قرار دارند. شما نباید عبارت POST را در اینجا به چیز دیگری تغییر دهید (تنها مقدار معتبر دیگر برای method مقدار GET است، اما الان وقت آن را نداریم تا تفاوت آن‌ها را توضیح دهیم).

بنابراین در view، ما دو موقعیت جداگانه برای رسیدگی داریم: اول اینکه زمانی که ما برای اولین بار به صفحه می‌رویم و یک فرم خالی را می‌خواهیم، و دوم، وقتی با اطلاعات نوشته شده درون فرم، به view برمی‌گردیم. بنابراین ما نیاز به اضافه کردن یک شرط داریم (ما از ifبرای این منظور استفاده می‌کنیم):

blog/views.py

if request.method == "POST":
    [...]
else:
    form = PostForm()

وقت آن است که نقاط [...] را پر کنید. اگر مقدار method برابر با POST باشد، ما می خواهیم که با اطلاعات درون فرم PostForm را بسازیم، درست است؟ ما این کار را به صورت زیر انجام خواهیم داد:

blog/views.py

form = PostForm(request.POST)

قدم بعدی این است که بررسی کنید که آیا فرم صحیح است (تمام فیلدهای لازم تنظیم شده و مقادیر نادرست وارد نشده است). ما این کار را با form.is_valid() انجام می‌دهیم.

ما بررسی می کنیم که آیا فرم معتبر است و اگر چنین باشد، می توانیم آن را ذخیره کنیم!

blog/views.py

if form.is_valid():
    post = form.save(commit=False)
    post.author = request.user
    post.published_date = timezone.now()
    post.save()

اساساً، ما دو چیز در اینجا داریم: فرم را با form.save ذخیره می‌کنیم سپس یک نویسنده برای مطلب تعیین می‌کنیم (از آنجا که فیلد author در فرم موجود نبوده و داشتن نویسنده برای مطلب در PostFormالزامی است). commit=False بدین معنی است که ما نمی‌خواهیم مدل Post را ذخیره کنیم - ما می‌خواهیم ابتدا نویسنده را اضافه کنیم. در بیشتر موارد شما از ()form.save بدون commit=False استفاده می‌کنید، اما در این مورد، ما باید آن را استفاده کنیم. ()post.save تغییرات (اضافه کردن نویسنده) را اعمال می‌کند و یک پست جدید بلاگ ایجاد می‌شود!

سرانجام، خوب بود اگر می‌توانستیم بلافاصله به صفحه جزییات پست جدید post_detail که هم اکنون ساخته‌ایم برویم، درست است؟ برای این کار ما نیاز به فراخوانی دیگری داریم:

blog/views.py

from django.shortcuts import redirect

آن را در ابتدای فایل خود اضافه کنید. و اکنون می‌توان گفت، "به صفحه جزییات پست post_detail برای پست جدید برو":

blog/views.py

return redirect('post_detail', pk=post.pk)

post_detail نام ویویی است که ما می‌خواهیم به آن برویم. به یاد دارید که این view نیاز به متغیر pk دارد؟ برای فرستادن آن به ویو، از pk=post.pk استفاده می‌کنیم، که در آن post جدیدترین پست وبلاگ است!

خوب، ما خیلی صحبت کرده‌ایم، اما احتمالاً می‌خواهید ببینید که کل view چگونه به نظر می‌رسد، درست است؟

blog/views.py

def post_new(request):
    if request.method == "POST":
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user
            post.published_date = timezone.now()
            post.save()
            return redirect('post_detail', pk=post.pk)
    else:
        form = PostForm()
    return render(request, 'blog/post_edit.html', {'form': form})

بیایید ببینیم که آیا کار می‌کند. به صفحه http://127.0.0.1:8000/post/new/ بروید، یک title و یک text اضافه کنید و نتیجه را ببینید! پست جدید وبلاگ اضافه شده است و ما به صفحه post_detail هدایت می‌شویم!

ممکن است متوجه شده باشید که قبل از ذخیره پست، تاریخ انتشار را تنظیم کرده‌ایم. بعدها، ما دکمه publish button را در بخش Django Girls Tutorial: Extensions معرفی می‌کنیم.

این عالیه!

همانطور که اخیراً رابط کاربری ادمین جنگجو را استفاده کردیم، سیستم در حال حاضر فکر می‌کند ما هنوز در سیستم لاگین هستیم. چند موقعیت وجود دارد که می تواند منجر به خروج ما از رابط کاربری مدیریت شود (بسته شدن مرورگر، راه اندازی مجدد دیتابیس و غیره). اگر در هنگام ایجاد یک پست متوجه شدید که خطایی در رابطه با عدم وجود یک کاربر ایجاد شده است، به صفحه مدیریت http://127.0.0.1:8000/admin بروید و دوباره وارد شوید. این مسأله به طور موقت حل خواهد شد. یک تکلیف اضافه برای خانه که بعد از بخش اصلی آموزش، انتظار شما را می‌کشد رفع مشکلات امنیتی وبسایت در بخش Homework: add security to your website! است.

خطا ورود به سیستم

اعتبارسنجی فرم

حالا به شما نشان می‌دهیم که فرم‌ها در جنگو چقدر جالب هستند. یک پست وبلاگ باید فیلدهای title و text را داشته باشد. در مدل Post ما نگفتیم که این فیلدها (به غیر از published_date) الزامی نیستند، بنابراین، جنگو به طور پیش فرض آن‌ها را الزامی می‌داند.

سعی کنید فرم را بدون title و text ذخیره کنید. حدس بزنید چه اتفاقی خواهد افتاد!

اعتبارسنجی فرم

جنگو مواظب است که تمام فیلدهای موجود در فرم ما صحیح پر شده باشند. فوق‌العاده نیست؟

ويرايش فرم

حالا ما می‌دانیم که چگونه یک پست جدید اضافه کنیم. اما اگر بخواهیم یک فرم موجود را ویرایش کنیم، چه؟ این کار بسیار شبیه آنچه ما انجام داده‌یم است. بگذارید برخی از چیزهای مهم را سریع بسازیم. (اگر چیزی را درک نمی‌کنید، باید از مربی خود بپرسید یا در فصل‌های قبلی نگاه کنید، زیرا ما قبلاً همه این مراحل را پوشش دادیم.)

ابتدا، اجازه دهید که آیکونی که نمایش دهنده دکمه اصلاح است را ذخیره کنیم. فایل pencil-fill.svg را دانلود کنید و آن را در آدرس blog/templates/blog/icons/ ذخیره کنید.

فایل blog/templates/blog/post_detail.html را در ویرایشگر متن باز کنید و خطوط زیر را به عنصر article اضافه کنید:

blog/templates/blog/post_detail.html

<aside class="actions">
    <a class="btn btn-default" href="{% url 'post_edit' pk=post.pk %}">
      {% include './icons/pencil-fill.svg' %}
    </a>
</aside>

قالب فوق شبیه به این خواهد شد:

blog/templates/blog/post_detail.html

{% extends 'blog/base.html' %}

{% block content %}
    <article class="post">
        <aside class="actions">
            <a class="btn btn-default" href="{% url 'post_edit' pk=post.pk %}">
                {% include './icons/pencil-fill.svg' %}
            </a>
        </aside>
        {% if post.published_date %}
            <time class="date">
                {{ post.published_date }}
            </time>
        {% endif %}
        <h2>{{ post.title }}</h2>
        <p>{{ post.text|linebreaksbr }}</p>
    </article>
{% endblock %}

فایل blog/urls.py را باز کنید و خط زیر را به آن اضافه کنید:

blog/urls.py

    path('post/<int:pk>/edit/', views.post_edit, name='post_edit'),

ما از این تمپلیت blog/templates/blog/post_edit.html مجدداً استفاده خواهیم کرد پس تنها قطعه باقی مانده view است.

فایل blog/views.py را باز کنید و خط زیر را به انتهای این فایل اضافه کنید:

blog/views.py

def post_edit(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if request.method == "POST":
        form = PostForm(request.POST, instance=post)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user
            post.published_date = timezone.now()
            post.save()
            return redirect('post_detail', pk=post.pk)
    else:
        form = PostForm(instance=post)
    return render(request, 'blog/post_edit.html', {'form': form})

این ویو شبیه ویوی post_new ماست، درست است؟ اما نه دقیقاً. در ابتدا ما یک پارامتر pk اضافی از urls ارسال کرده‌ایم. سپس مدل Post را که می‌خواهیم اصلاح کنیم با دستور get_object_or_404(Post, pk=pk) می‌گیریم و بعد از آن یک فرم می‌سازیم و این فرم را به صورت instanceارسال می‌کنیم، هر دو این کارها وقتی اتفاق می‌افتد که فرم را ذخیره می‌کنیم…

blog/views.py

form = PostForm(request.POST, instance=post)

… و هنگامی که یک فرم را به این صورت برای تغییر دادن باز می‌کنیم:

blog/views.py

form = PostForm(instance=post)

بسیار خوب، حالا بیایید امتحان کنیم که آیا کار می‌کند! به صفحه post_detail بروید. باید یک کلید edit در گوشه سمت راست بالا باشد:

کلید Edit

وقتی بر روی آن کلیک کنید یک فرم که با اطلاعات پست وبلاگی ما پر شده است نشان داده می‌شود:

فرم Edit

به راحتی محتوای پست را تغییر دهید و آن را ذخیره کنید!

تبریک! برنامه شما کامل و کامل‌تر می‌شود!

اگر اطلاعات بیشتری در مورد فرم‌ها در جنگو لازم دارید باید مستندات مربوط به آن را در این آدرس بخوانید: https://docs.djangoproject.com/en/2.2/topics/forms/

امنیت

ساختن یک پست جدید فقط با یک کلیک، بسیار فوق العاده است! اما همین الان هرکسی که از صفحه شما بازدید می‌کند می‌تواند به راحتی یک پست جدید بسازد و این احتمالاً چیزی نیست که شما بخواهید. حالا بیایید کاری کنیم که این دکمه فقط برای شما نشان داده شود و کس دیگری آن را نبیند.

فایل blog/templates/blog/base.html را در ویرایشگر کد باز کنید بخش div با نام header و تگ a موجود در آن را پیدا کنید. باید چیزی شبیه به این باشد:

blog/templates/blog/base.html

<a href="{% url 'post_new' %}" class="top-menu">
    {% include './icons/file-earmark-plus.svg' %}
</a>

حالا ما می‌خواهیم یک تگ {% if %} دیگر به این اضافه کنیم که باعث خواهد شد این لینک فقط برای کاربری نشان داده شود که به عنوان ادمین وارد شده باشد که در حال حاضر فقط شما هستید! تگ <a> را به شکل زیر تغییر دهید:

blog/templates/blog/base.html

{% if user.is_authenticated %}
    <a href="{% url 'post_new' %}" class="top-menu">
        {% include './icons/file-earmark-plus.svg' %}
    </a>
{% endif %}

تگ {% if %} باعث خواهد شد که لینک فقط در صورتی به مرورگر ارسال شود که کاربر درخواست کننده صفحه، با حساب کاربری ادمین وارد شده باشد. این کار به طور کامل ساختن پست جدید را محافظت نمی‌کند اما قدم اولیه‌ی خوبی است. مباحث بیشتری از امنیت را در بخش‌های اضافه آموزش پوشش خواهیم داد.

کلید edit را که به صفحه جزییات یک پست اضافه کردیم به یاد دارید؟ حالا همین کار را برای آن صفحه نیز می‌خواهیم انجام دهیم تا دیگران قادر به تغییر محتوای یک پست نباشند.

فایل blog/templates/blog/post_detail.html را باز کنید و خط زیر را پیدا کنید:

blog/templates/blog/post_detail.html

<a class="btn btn-default" href="{% url 'post_edit' pk=post.pk %}">
    {% include './icons/pencil-fill.svg' %}
</a>

آن را به شکل زیر تغییر دهید:

blog/templates/blog/post_detail.html

{% if user.is_authenticated %}
     <a class="btn btn-default" href="{% url 'post_edit' pk=post.pk %}">
        {% include './icons/pencil-fill.svg' %}
     </a>
{% endif %}

از آنجا که احتمالاً هنوز به عنوان ادمین در سیستم لاگین کرده‌اید، اگر صفحه را دوباره بازخوانی کنید، تغییر خاصی را نمی‌بینید. صفحه را در یک مرورگر دیگر یا یک پنجره ناشناس incognito window (در Windows Edge به آن "InPrivate" می‌گویند) بارگذاری کنید. خواهید دید که لینک و آیکون اصلاح پست، نمایش داده نمی‌شود!

یک چیز دیگر: زمان دیپلوی است!

بیایید ببینیم آیا همه این تغییرات در PythonAnywhere کار می‌کند. وقت یک دیپلوی کردن دیگر است!

  • ابتدا کد جدید را کامیت کنید و آن را به GitHub بفرستید:

خط فرمان

$ git status
$ git add .
$ git status
$ git commit -m "Added views to create/edit blog post inside the site."
$ git push

PythonAnywhere command-line

$ cd ~/<your-pythonanywhere-domain>.pythonanywhere.com
$ git pull
[...]

(یادتان باشد که <your-pythonanywhere-domain> را با زیر دامنه اصلی خود در PythonAnywhere عوض کنید البته بدون آکولادها.)

  • در نهایت به صفحه "Web" page بروید (از کلید منو در بالا و سمت راست کنسول استفاده کنید) و کلید Reload را بزنید. آدرس وبلاگ خودتان https://subdomain.pythonanywhere.com را باز کنید تا تغییرات را ببینید.

به نتیجه رسید! تبریک! :)

results matching ""

    No results matching ""