1 - فراهم آوردن امکان ورود داده برای کاربران

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

شما همچنین یک سیستم احراز هویّت کاربری پیاده‌سازی می‌کنید. یک صفحه‌ی ثبت نام برای کاربران می‌سازید تا کاربران حساب‌های کاربری خود را بسازند و سپس دسترسی به برخی از صفحه‌ها را تنها به کاربران واردشده محدود می‌کنید. پس از آن برخی از توابع نما را تغییر می‌دهید تا کاربران بتوانند داده‌های خود را ببینند. شما خواهید آموخت که چگونه داده‌های کاربران را امن و مطمئن نگه دارید.

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

در حال حاضر تنها ابرکاربر می‌تواند داده‌ها را از طریق سایت مدیر وارد کند. هدف ما این است که کاربران با سایت مدیریّت تعاملی نداشته باشند پس از ابزارهای فرم‌سازی جانگو کمک می‌گیریم تا به کاربران اجازه‌ی وارد کردن داده‌ها را بدهیم.

2 - افزودن موضوع‌های جدید

بیایید کار را برای فراهم آوردن امکان افزودن یک موضوع جدید شروع کنیم. افزودن صفحه‌های مبتنی بر فرم از بسیاری جهات شبیه به صفحه‌هایی کار می‌کنند که پیش از این ساخته‌ایم: یک URL تعیین می‌کنیم، یک تابع نما و یک قالب می‌نویسیم. تنها تفاوت اساسی اضافه کردن ماژول جدید forms.py است که فرم‌ها را در بر می‌گیرد.

مدل فرم موضوع

هر صفحه‌ای به کاربران اجازه‌ی وارد کردن و ثبت اطّل168 اعات را می‌دهد فرم نامیده می‌شود حتّی اگر شبیه به آن نباشد. زمانی که کاربران اطّلاعاتی را وارد می‌کنند باید درستی نوع داده‌های آن‌ها را اعتبارسنجی کنیم تا مطمئن شویم که داده‌ها، مانند کدی که در کار سرور ما وقفه ایجاد می‌کنند، مخرّب نیستند. سپس باید اطّلاعات معتبر را پردازش کرده و در جای مناسبی در پایگاه داده ذخیره کنیم. جانگو انجام بیشتر این کارها را خودکار می‌کند.

در جانگو ساده‌ترین راه برای ساختن یک فرم استفاده ا169 ز فرمِ مدل است که به کمک داده‌ی مدل‌هایی که در فصل ۱۸ام تعریف کرده‌ایم به طور خودکار فرمی را تولید می‌کند. اوّلین فرم خود را در فایل forms.py بسازید که در همان پوشه‌ی حاوی فایل models.py قرار می‌گیرد:

1
2
3
4
5
6
7
8
9
from django import forms

from .models import Topic

class TopicForm(forms.ModelForm):
    class Meta:
        model = Topic
        fields = ['text']
        labels = {'text': ''}

ابتدا ماژول forms و مدل مورد نظر خود، به نام Topic، را وارد می‌کنیم. در ۱ کلاسی به نام TopicForm تعریف می‌کنیم که از forms.ModelForm ارث می‌برد.

ساده‌ترین نسخه‌ی ModelForm از یک کلاس داخلی به نام Meta تشکیل شده است که به جانگو می‌گوید فرم را بر اساس کدام مدل بنا کند و کدام فیلدها را در آن بیاورد. در ۲ فرمی از مدل Topic می‌سازیم و تنها فیلد text را در آن می‌آوریم (۳). کد در ۴ به جانگو می‌گوید که برای فیلد text برچسبی تولید نکند.

آدرس new_topic

URLای که برای یک صفحه‌ی جدید باید کوتاه و گویا باشد. هرگاه کاربر بخواهد موضوع جدیدی اضافه کند باید او را به http://localhost:8000/new_topic/ بفرستیم. الگوی URL برای صفحه‌ی new_topic به صورت زیر است که آن را به learning_logs/urls.py اضافه می‌کنیم:

1
2
3
4
5
6
7
--snip--

urlpatterns = [
    --snip--
    # Page for adding a new topic
    path('new_topic/', views.new_topic, name='new_topic'),
]

این الگو درخواست‌ها را به تابع نمای new_topic() می‌نویسیم که در ادامه آن را می‌نویسیم.

تابع نمای new_topic()

تابع new_topic() باید دو وضعیّت مختلف را مدیریّت کند: یکی درخواست‌های اوّلیّه‌ی صفحه‌ی new_topic (که در آن‌ها باید یک فرم خالی نمایش دهد) و دیگری پردازش هر داده‌ای که در فرم ثبت می‌شود. پس از این که داده‌های ثبت‌شده پردازش شد، باید کاربر به صفحه‌ی topics برگردانده شود:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from django.shortcuts import render, redirect

from .models import Topic
from .forms import TopicForm

--snip--

def new_topic(request):
    """Add a new topic."""
    if request.method != 'POST':
        # No data submitted; create a blank form.
        form = TopicForm()
    else:
        # POST data submitted; process data.
        form = TopicForm(data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topics')

    # Display a blank or invalid form.
    context = {'form': form}
    return render(request, 'learning_logs/new_topic.html', context)

تابع redirect را وارد می‌کنیم و از آن برای بازگردانی کاربر به صفحه‌ی topics پس از این که موضوع خود را ثبت کرد استفاده می‌کنیم. این تابع نام یک نما را می‌گیرد و کاربر را به آن هدایت می‌کند. علاوه بر این فرمی را که هم‌اکنون نوشتیم، TopicForm، وارد می‌کنیم.

درخواست‌های GET و POST

دو نوع درخواست اصلی که هنگام ساختن برنامه‌های تحت وب استفاده می‌کنیم درخواست‌های GET و POSTاند. درخواست‌های GET را در صفحه‌هایی استفاده می‌کنید که تنها می‌خواهند داده‌هایی را از سرور بگیرند. از درخواست‌های POST زمانی به کار می‌آیند که کاربر بخواهد از طریق یک فرم اطّلاعاتی را ثبت کند. ما در تمام فرم‌های خود از متد POST برای پردازش تمام فرم‌های خود استفاده خواهیم کرد. (چند نوع درخواست دیگر هم وجود دارد امّا از آن‌ها در این پروژه استفاده نمی‌کنیم.)

تابع new_topic() شیء درخواست را به عنوان پارامتر می‌پذیرد. وقتی کاربران در ابتدا این صفحه را درخواست می‌کنند مرورگر آن‌ها یک درخواست GET خواهد فرستاد. زمانی که کاربران فرم را پر کرده و ثبت می‌کنند مرورگر آن‌ها یک درخواست POST ثبت می‌کند. بسته به نوع درخواست ما خواهیم فهمید که کاربران درخواست فرم خالی دارند (یک درخواست GET) یا از ما می‌خواهند که فرم تکمیل‌شده را پردازش کنیم (یک درخواست POST).

آزمونی که در ۱ نوشتیم مشخّص می‌کند که درخواست از نوع GET است یا POST. اگر متد درخواست POST نباشد پس به احتمال زیاد GET خواهد بود پس باید یک فرم خالی برگردانیم (اگر هم نوع درخواست چیز دیگری جز این دو باشد، برگرداندن فرم خالی به لحاظ امنیّتی مشکلی ندارد.) یک نمونه از TopicForm می‌سازیم (۲) و آن را به متغیّر form نسبت می‌دهیم و فرم را در قالب دیکشنری context به قالب می‌فرستیم (۷). چون هنگام نمونه‌سازی TopicForm هیچ نشان‌وندی تعیین نکرده‌ایم جانگو یک فرم خالی می‌سازد که کاربر می‌تواند آن را پر کند.

اگر متد درخواست POST باشد بخش else اجرا می‌شود و داده‌های ثبت‌شده در فرم را پردازش می‌کند. نمونه‌ای از TopicForm می‌سازیم (۳) و داده‌هایی را که کاربر وارد کرده و در request.POST ذخیره شده، به آن می‌دهیم. شیء form که برگردانده می‌شود حاوی اطّلاعاتی است که کاربر ثبت کرده است.

تا زمانی که اطّلاعات ثبت‌شده اعتبارسنجی نشده باشد آن‌ها را در پایگاه داده ذخیره نخواهیم کرد (۴). متد is_valid() بررسی می‌کند که آیا تمام فیلدهای اجباری پر شده‌اند یا نه (تمام فیلدها به صورت پیش‌فرض اجباری‌اند) و آیا داده‌های وارد شده با نوع داده‌ی مورد انتظار انطباق دارند یا نه—برای مثال آیا چنان‌که در models.py در فصل ۱۸ام تعیین کردیم، طول text کمتر از ۲۰۰ حرف است یا نه. اگر همه چیز معتبر باشد save() را فرامی‌خوانیم (۵) که داده‌های دریافتی از فرم را در پایگاه داده می‌نویسد.

به محض ذخیره‌سازی داده‌ها در پایگاه داده می‌توانیم صفحه را ترک کنیم. از redirect() برای بازگرداندن مرورگر کاربر به صفحه‌ی topics استفاده می‌کنیم که کاربر باید بتواند در آن فهرستی موضوعاتی را که همان لحظه وارد کرده ببیند.

متغیّر context در انتهای تابع نما تعریف می‌شود و صفحه به کمک قالب new_topic.html ساخته می‌شود که در ادامه آن را ایجاد می‌کنیم. این کد خارج از تمام عبارت‌های if قرار می‌گیرد؛ زمانی اجرا می‌شود که یک فرم خالی ایجاد شده یا اطّلاعات ثبت‌شده در فرم معتبر نیستند. فرم نامعتبر چند پیام خطای پیش‌فرض نیز دربرمی‌گیرد تا به کاربر در ثبت داده‌های قابل قبول کمک کند.

قالب new_topic

بیایید قالبی جدید به نام new_topic.html بسازیم تا فرم ساخته‌شده را نمایش دهیم.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{% extends "learning_logs/base.html" %}

{% block content %}
  <p>Add a new topic:</p>

  <form action="{% url 'learning_logs:new_topic' %}" method='post'>
    {% csrf_token %}
    {{ form.as_p }}
    <button name="submit">Add topic</button>
  </form>

{% endblock content %}

این قالب base.html را گسترش می‌دهد پس ساختار آن مشابه دیگر صفحه‌های Learning Log خواهد بود. در ۱ یک فرم HTML تعریف می‌کنیم. نشان‌وند action به مرورگر نشانی ارسال اطّلاعات ثبت‌شده در فرم را به مرورگر می‌دهد؛ در این مورد آن را به تابع نمای new_topic() می‌فرستیم. نشان‌وند method به مرورگر می‌گوید که داده‌ها را به صورت یک درخواست POST ثبت کند.

جانگو به کمک تگ قالب {% csrf_token %} مانع دسترسی غیرمجاز نفوذگران به سرور از طریق فرم می‌شود.170 (این نوع حمله را سرقت درخواست بین سایتی می‌نامند.) در ۳ فرم را نمایش می‌دهیم؛ اینجا می‌توانید ببینید که جانگو انجام برخی از کارها مانند نمایش یک فرم را چه‌قدر ساده می‌کند. کافی است متغیّر قالب {{ form.as_p }} را اضافه کنیم تا جانگو تمام فیلدهای مورد نیاز برای نمایش خودکار فرم بسازد. عبارت as_p به جانگو می‌گوید که تمام عنصرهای فرم را به عنوان یک روش ساده و منظّم برای نمایش فرم، به شکل پاراگراف دربیاورد.

جانگو دکمه‌ی ثبت را در فرم‌ها ایجاد نمی‌کند پس در ۴ یکی تعریف می‌کنیم.

ایجاد پیوند به صفحه‌ی new_topic

در ادامه پیوندی را به صفحه‌ی new_topic در صفحه‌ی topics ایجاد می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{% extends "learning_logs/base.html" %}

{% block content %}
  <p>Topics</p>
  <ul>
    --snip--
  </ul>

  <a href="{% url 'learning_logs:new_topic' %}">Add a new topic</a>

{% endblock content %}

پیوند را پس از فهرست موضوعات موجود قرار دهید. شکل ۱۹-۱ فرم حاصل را نمایش می‌دهد. از این فرم برای افزودن چند موضوع استفاده کنید.

![][image47]

شکل ۱۹-۱: صفحه‌ی اضافه کردن موضوع جدید

3 - افزودن مطالب جدید

همانگونه که کاربران می‌توانند موضوع جدیدی اضافه کنند، باید امکان افزودن مطالب جدید هم وجود داشته باشد. باز هم یک URL تعریف می‌کنیم، یک تابع نما و قالب می‌نویسیم و پیوندی به صفحه ایجاد می‌کنیم. امّا اوّل اجازه دهید کلاسی به forms.py اضافه کنیم.

فرم مدل موضوع

ما باید فرمی در ارتباط با مدل Entry بسازیم امّا این بار نسبت به TopicForm کمی بیشتر آن را شخصی می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from django import forms

from .models import Topic, Entry

class TopicForm(forms.ModelForm):
    --snip--

class EntryForm(forms.ModelForm):
    class Meta:
        model = Entry
        fields = ['text']
        labels = {'text': 'Entry:'}
        widgets = {'text': forms.Textarea(attrs={'cols': 80})}

عبارت import را به‌روز می‌کنیم تا Entry را هم مانند Topic وارد کنیم. کلاس جدیدی به نام EntryForm می‌سازیم که از forms.ModelForm ارث می‌برد. کلاس EntryForm کلاسی داخلی به نام Meta دارد که مدلی را که بر آن بنا شده و فیلدهایی را که باید در فرم بیاورد فهرست می‌کند. دوباره به فیلد text یک برچسب خالی می‌دهیم (۱).

در ۲ مشخّصه‌ی widgets را اضافه می‌کنیم. ویجت یا ابزاره نوعی عنصر فرم HTML اس171 ت، مانند یک ورودی متن یک‌خطّی، ورودی متنی چندخطّی یا یک فهرست پایین‌افتادنی. با اضافه کردن مشخّصه‌ی widgets می‌توانید ویجت‌های انتخابی پیش‌فرض جانگو را تغییر دهید. وقتی به جانگو می‌گوییم که از یک forms.Textarea استفاده کند ویجت ورودی فیلد text را شخصی می‌کنیم تا عرض آن به جای ۴۰ ستونِ پیش‌فرض، به اندازه‌ی ۸۰ ستون باشد. این کار فضایی مناسب برای نوشتن یک مطلب معنادار ایجاد می‌کند.

آدرس new_entry

موضوعات جدید باید در رابطه با موضوع خاصّی باشند پس نیاز داریم که نشان‌وند topic_id را در URL مربوط به اضافه کردن مطلب جدید بیاوریم. این URLایست که آن را به learning_logs/urls.py اضافه می‌کنیم:

1
2
3
4
5
6
7
--snip--

urlpatterns = [
    --snip--
    # Page for adding a new entry
    path('new_entry/<int:topic_id>/', views.new_entry, name='new_entry'),
]

هر آدرسی به شکل http://localhost:8000/new_entry/id/ با این الگوی URL مطابق خواهد بود که در آن id شماره‌ای برابر با ID موضوع خواهد بود. کد <int:topic_id> مقداری عددی را استخراج و آن را به متغیّر topic_id نسبت می‌دهد. زمانی که آدرسی با الگوی فوق درخواست شد جانگو آن را به همراه شماره‌ی موضوع به تابع نمای new_entry() می‌فرستد.

تابع نمای new_entry()

تابع نمای new_entry بسیار شبیه به تابع اضافه کردن موضوع جدید است. این کد را به فایل views.py اضافه کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from django.shortcuts import render, redirect

from .models import Topic
from .forms import TopicForm, EntryForm

--snip--

def new_entry(request, topic_id):
    """Add a new entry for a particular topic."""
    topic = Topic.objects.get(id=topic_id)

    if request.method != 'POST':
        # No data submitted; create a blank form.
        form = EntryForm()
    else:
        # POST data submitted; process data.
        form = EntryForm(data=request.POST)
        if form.is_valid():
            new_entry = form.save(commit=False)
            new_entry.topic = topic
            new_entry.save()
            return redirect('learning_logs:topic', topic_id=topic_id)

    # Display a blank or invalid form.
    context = {'topic': topic, 'form': form}
    return render(request, 'learning_logs/new_entry.html', context)

عبارت import را برای اضافه کردن EntryForm تغییر می‌دهیم. در تعریف new_entry() یک پارامتر topic_id وجود دارد که مقدار دریافت شده از URL در آن ذخیره می‌شود. موضوع مربوطه را برای ساختن صفحه و پردازش داده‌های فرم نیاز دارید پس از در ۱ از topic_id استفاده می‌کنیم تا شیء موضوع درست را دریافت کنیم.

در ۲ بررسی می‌کنیم که آیا متد درخواست GET است یا POST. اگر GET باشد کد داخل if اجرا می‌شود و نمونه‌ای خالی از EntryForm می‌سازد (۳).

اگر نوع درخواست POST باشد با ساختن یک نمونه از EntryForm و انتقال داده‌های POST داخل شیء request به آن، داده‌ها را پردازش می‌کنیم (۴). سپس اعتبار فرم را بررسی می‌کنیم. اگر معتبر بود باید پیش از ذخیره کردن شیء در پایگاه داده مشخّصه‌ی topic آن را تعیین کنیم. زمانی که save() را صدا می‌کنیم نشان‌وند commit=False را به آن می‌دهیم (۵) تا جانگو بداند باید برای موضوع جدید شیء جدیدی ایجاد کند و آن را به new_entry نسبت دهد امّا فعلاً آن را در پایگاه داده ذخیره نکند. مقدار مشخّصه‌ی new_entry را برابر با موضوعی قرار می‌دهیم که در ابتدای تابع از پایگاه داده خواندیم (۶). سپس save() را بدون هیچ نشان‌وندی فرامی‌خوانیم تا موضوع را به همراه موضوع مرتبط با آن در بانک اطّلاعاتی ذخیره کنیم.

فراخوانی redirect() در ۷ به دو نشان‌وند نیاز دارد—نام نمایی که می‌خواهیم به آن بازگردیم و نشان‌وندی که آن تابع نما نیاز دارد. اینجا به topic() برمی‌گردیم پس به نشان‌وند topic_id نیاز داریم. این نما صفحه‌ی موضوعی را می‌سازد که کاربران در رابطه با آن موضوعی ایجاد کرده‌اند و آن‌ها باید بتوانند موضوع جدید خود را در فهرست موضوعات ببینند.

در پایان تابع یک دیکشنری context ایجاد می‌کنیم و صفحه را به کمک قالب new_entry.html ایجاد می‌کنیم. این کد زمانی اجرا خواهد شد که فرم خالی درخواست شده باشد یا فرم ثبت‌شده معتبر نباشد.

قالب new_entry

چنانکه در کد زیر می‌بینید قالب new_entry شبیه به قالب new_topic است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{% extends "learning_logs/base.html" %}

{% block content %}
  <p><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></p>
  <p>Add a new entry:</p>
  <form action="{% url 'learning_logs:new_entry' topic.id %}" method='post'>
    {% csrf_token %}
    {{ form.as_p }}
    <button name='submit'>Add entry</button>
  </form>

{% endblock content %}

موضوع را در بالای صفحه نمایش می‌دهیم (۱) تا کاربر بتواند ببیند که درباره‌ی چه موضوعی مطلب می‌نویسد. موضوع همچنین به عنوان پیوندی برای بازگشت به صفحه‌ی اصلی آن موضوع هم به کار می‌رود.

نشان‌وند action فرم مقدار topic_id داخل URL را هم دربرمی‌گیرد تا تابع نما بتواند بین مطلب جدید و موضوع مرتبط با آن ارتباط برقرار کند (۱). به جز این قالب کاملاً شبیه به new_topic.html است.

ایجاد پیوند به صفحه‌ی new_entry

در گام بعدی باید از هر صفحه‌ی موضوع در قالب مربوط به موضوعات پیوندی به صفحه‌ی new_entry ایجاد کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{% extends "learning_logs/base.html" %}

{% block content %}
  <p>Topic: {{ topic }}</p>
  <p>Entries:</p>
  <p>
    <a href="{% url 'learning_logs:new_entry' topic.id %}">Add new entry</a>
  </p>
  <ul>
  --snip--
  </ul>

{% endblock content %}

پیوندِ اضافه کردن موضوعات جدید را درست پیش از نمایش موضوعات قرار می‌دهیم زیرا در این صفحه افزودن مطلب جدید متداول‌ترین عمل است. شکل ۱۹-۲ صفحه‌ی new_entry را نشان می‌دهد. اکنون کاربران می‌توانند هر تعداد موضوع که خواستند اضافه کنند و برای هر موضوع نیز هر اندازه مطلب که دوست دارند بنویسند. صفحه‌ی new_entry را با افزودن چند مطلب به برخی از موضوعاتی که ایجاد کرده‌اند امتحان کنید.

![][image48]

شکل ۱۹-۲: صفحه‌ی new_entry

4 - ویرایش مطلب‌ها

اکنون صفحه‌ای می‌سازیم تا کاربران بتوانند از طریق آن موضوعات خود را ویرایش کنند.

آدرس edit_entry

URL صفحه باید شماره‌ی مطلب مورد ویرایش را انتقال دهد. این learning_logs/urls.py است:

1
2
3
4
5
6
7
--snip--

urlpatterns = [
    --snip--
    # Page for editing an entry.
    path('edit_entry/<int:entry_id>/', views.edit_entry, name='edit_entry'),
]

شماره‌ی انتقال‌یافته از طریق URL (برای مثال http://localhost:8000/edit_entry/1/) در پارامتر entry_id ذخیره می‌شود. این الگوی URL درخواست‌های منطبق با خود را به تابع نمای edit_entry() می‌فرستد.

تابع نمای edit_entry()

زمانی که صفحه‌ی edit_entry یک درخواست GET دریافت می‌کند تابع edit_entry() فرمی برای ویرایش مطلب برمی‌گرداند. وقتی یک درخواست POST به همراه متن ویرایش‌شده بگیرد آن را در پایگاه داده ذخیره می‌کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from django.shortcuts import render, redirect

from .models import Topic, Entry
from .forms import TopicForm, EntryForm

--snip--

def edit_entry(request, entry_id):
    """Edit an existing entry."""
    entry = Entry.objects.get(id=entry_id)
    topic = entry.topic

    if request.method != 'POST':
        # Initial request; pre-fill form with the current entry.
        form = EntryForm(instance=entry)
    else:
        # POST data submitted; process data.
        form = EntryForm(instance=entry, data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topic', topic_id=topic.id)

    context = {'entry': entry, 'topic': topic, 'form': form}
    return render(request, 'learning_logs/edit_entry.html', context)

ابتدا مدل Entry را وارد می‌کنیم. در ۱ شیء موضوعی که کاربر می‌خواهد ویرایش کند و موضوع مرتبط با آن را دریافت می‌کنیم. در داخل if که برای درخواست‌های GET اجرا می‌شود نمونه‌ای از EntryForm با نشان‌وند instance=entry می‌سازیم (۲). این نشان‌وند به جانگو می‌گوید که فرمی بسازد که با اطّلاعات شیء فعلی موضوع از پیش پر شده باشد. کاربران قادر هستند که داده‌های موجود را ببینند و آن را اصلاح کنند.

برای پردازش درخواست POST نشان‌وندهای instance=entry و data=request.POST را انتقال می‌دهیم (۳). این نشان‌وندها به جانگو می‌گویند که یک نمونه‌ی فرم بر اساس اطّلاعات مربوط به شیء مطلب فعلی بسازد و آن را با هر داده‌ای مرتبطی از request.POST به‌روز کند. سپس اعتبار فرم را بررسی می‌کنیم؛ اگر چنین بود save() را بدون هیچ نشان‌وندی فرامی‌خوانیم چون موضوع از قبل با موضوع درست مرتبط شده است (۴). سپس کاربر را به صفحه‌ی topic برمی‌گردانیم که کاربران در آن می‌توانند نسخه‌ی به‌روزشده‌ی مطلبی را که ویراسته‌اند ببینند (۵).

اگر یک فرم ابتدایی برای ویرایش مطلب نشان دهیم یا فرم ثبت‌شده نامعتبر باشد یک دیکشنری context درست می‌کنیم و صفحه را به کمک قالب edit_entry.html می‌سازیم.

قالب edit_entry

در ادامه قالب edit_entry.html را می‌سازیم که مشابه new_entry.html است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{% extends "learning_logs/base.html" %}

{% block content %}
  <p><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></p>
  <p>Edit entry:</p>
  <form action="{% url 'learning_logs:edit_entry' entry.id %}" method='post'>
    {% csrf_token %}
    {{ form.as_p }}
    <button name="submit">Save changes</button>
  </form>

{% endblock content %}

نشان‌وند action در ۱ فرم را برای پردازش به تابع edit_entry() می‌فرستد. شماره‌ی مطلب را به عنوان نشان‌وندی در تگ {% url %} قرار می‌دهیم تا تابع نما بتواند شیء موضوع درست را تغییر دهد. عنوان دکمه‌ی ثبت را Save changes می‌گذاریم که به کاربر گوشزد کند در حال ویرایش مطلب است نه ایجاد مطلب جدید (۲).

ایجاد پیوند به صفحه‌ی edit_entry

باید به هر مطلبی در صفحه‌ی موضوعات پیوندی به صفحه‌ی edit_entry ایجاد کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
--snip--

  {% for entry in entries %}
    <li>
      <p>{{ entry.date_added|date:'M d, Y H:i' }}</p>
      <p>{{ entry.text|linebreaks }}</p>
      <p>
        <a href="{% url 'learning_logs:edit_entry' entry.id %}">Edit entry</a>
      </p>
    </li>

--snip--

پس از این که تاریخ و متن هر موضوع نمایش داده شد پیوند ویرایش را قرار می‌دهیم. از تگ قالب {% url %} و مشخّصه‌ی ID مطلب کنونی در حلقه (entry.id)، برای تعیین URL برای الگوی URL با نام edit_entry استفاده می‌کنیم. متن Edit Entry پیوند پس از هر مطلب در صفحه ظاهر می‌شود. شکل ۱۹-۳ نشان می‌دهد که صفحه‌ی موضوعات با این پیوندها چگونه به نظر می‌رسد.

![][image49]

شکل ۱۹-۳: اکنون هر مطلب پیوندی برای ویرایش دارد.

Learning Log اکنون بیشتر قابلیّت‌های مورد نیاز را دارد. کاربران می‌توانند موضوع‌ها و مطلب‌ها را اضافه کنند و هر مجموعه از مطالب را که بخواهند بخوانند. در بخش بعدی یک سیستم ثبت نام برای کاربران پیاده می‌کنیم تا هر کس بتواند در سایت حساب بسازد و موضوع‌ها و مطالب خود را ایجاد کند.

5 - خودآزمایی

۱۹-۱- بلاگ: پروژه‌ی جانگوی جدیدی به نام Blog راه بیندازید. برنامه‌ای به نام blogs در پروژه ایجاد کند و مدلی به نام BlogPost بسازید. مدل باید فیلدهایی مانند title، text و date_added داشته باشد. ابرکاربری برای پروژه بسازید و از طریق سایت مدیر چند نوشته‌ی کوتاه اضافه کنید. صفحه‌ی خانه‌ای بسازید که تمام نوشته‌ها را به ترتیب زمانی نشان می‌دهد.

یک فرم برای نوشتن مطالب جدید و یکی دیگر برای ویرایش نوشته‌های موجود بسازید. فرم‌های خود را پر کنید تا مطمئن شوید که کار می‌کنند.

6 - برپاکردن حساب‌های کاربری

در این بخش یک سیستم ثبت نام و احراز هویّت برای کاربران به راه می‌ندازیم تا افراد بتوانند حسابی ایجاد کنند و به سیستم وارد یا از آن خارج شوند. برنامه‌ی جدیدی می‌سازیم تا تمام قابلیّت‌های مربوط به کار کردن با کاربران را در بر بگیرد. ما تا جای ممکن از سیستم احراز هویّت پیش‌فرض جانگو برای انجام کارها استفاده می‌کنیم. همچنین مدل Topic را کمی تغییر می‌دهیم تا هر موضوع بتواند مختصّ کاربری خاص باشد.

برنامه users

کار را با ساختن برنامه‌ای به نام users به کمک فرمان startapp شروع می‌کنیم:

1
2
3
4
5
6
7
(ll_env)learning_log$ python manage.py startapp users

(ll_env)learning_log$ ls
db.sqlite3 learning_log learning_logs ll_env manage.py users

(ll_env)learning_log$ ls users
__init__.py admin.py apps.py migrations models.py tests.py views.py

این دستور پوشه‌ای جدید به نام users می‌سازد (۱) که ساختاری مشابه با برنامه‌ی learning_logs دارد (۲).

افزودن کاربران به settings.py

باید برنامه‌ی جدید خود را به INSTALLED_APPS در settings.py اضافه کنیم؛ مانند زیر:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
--snip--

INSTALLED_APPS = [
    # My apps
    'learning_logs',
    'users',
    # Default django apps.
    --snip--
]

--snip--

جانگو اکنون برنامه‌ی users را به پروژه‌ی کلی اضافه می‌کند.

افزودن URLهای مربوط به users

حال فایل اصلی urls.py را تغییر می‌دهیم تا URLهایی را که برای برنامه‌ی users ایجاد می‌کنیم در بر بگیرد:

1
2
3
4
5
6
7
8
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('users/', include('users.urls')),
    path('', include('learning_logs.urls')),
]

خطّی را برای افزودن فایل urls.py از پوشه‌ی users اضافه می‌کنیم. این خط هر URLای را که با کلمه‌ی users شروع می‌شود تشخیص می‌دهد، مانند http://localhost:8000/users/login/.

7 - صفحه‌ی ورود

ما ابتدا صفحه‌ی ورود را پیاده‌سازی می‌کنیم. چون از نمای پیش‌فرض login که جانگو فراهم کرده استفاده می‌کنیم، الگوی URL این برنامه کمی متفاوت به نظر می‌آید. یک فایل urls.py جدید در پوشه‌ی learning_log/users/ بسازید و این‌ها را به آن اضافه کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
"""Defines URL patterns for users"""

from django.urls import path, include

app_name = 'users'

urlpatterns = [
    # Include default auth urls.
    path('', include('django.contrib.auth.urls')),
]

تابع path و سپس تابع include را وارد می‌کنیم تا برخی از URLهای پیش‌فرض احراز هویّت تعریف‌شده در جانگو را اضافه کنیم. این URLهای پیش‌فرض شامل چند الگوی اسم‌دار URL مانند 'login' و 'logout'اند. مقدار متغیّر app_name را 'users' می‌گذاریم تا جانگو بتواند بین این URLها و URLهای دیگر برنامه تفاوت قایل شود (۱). حتّی این URLهای پیش‌فرض جانگو هم اگر در فایل urls.py مربوط به برنامه‌ی users آورده شوند، از طریق فضای نام users در دسترس خواهند بود.

الگوی صفحه‌ی ورود به چنین URLای می‌خورد: http://localhost:8000/users/login (۲). زمانی که جانگو این URL را می‌خواند واژه‌ی users به آن می‌گوید که داخل users/urls.py را نگاه کن و login مشخّص می‌کند که درخواست‌ها را به نمای پیش‌فرض login جانگو بفرست.

قالب login

زمانی که کاربر صفحه‌ی ورود را درخواست می‌کند جانگو از یک تابع نمای پیش‌فرض استفاده می‌کند امّا ما باید همچنان قالبی برای این صفحه ارائه کنیم. نماهای پیش‌فرض احراز هویّت به دنبال قالب‌ها در پوشه‌ای به نام registration می‌گردند بنابراین باید آن پوشه را ایجاد کنیم. داخل پوشه‌ی learning_logs/users/ پوشه‌ای به نام templates بسازید و داخل آن پوشه‌ی دیگری به نام registration ایجاد کنید. این قالب login.html است و باید آن را در learning_logs/users/templates/registration ذخیره کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{% extends "learning_logs/base.html" %}

{% block content %}
  {% if form.errors %}
    <p>Your username and password didn't match. Please try again.</p>
  {% endif %}

  <form method="post" action="{% url 'users:login' %}">
    {% csrf_token %}
    {{ form.as_p }}
    <button name="submit">Log in</button>
    <input type="hidden" name="next"
      value="{% url 'learning_logs:index' %}" />
  </form>

{% endblock content %}

این قالب base.html را گسترش می‌دهد تا مطمئن باشیم که شکل و قیافه‌ی آن شبیه بقیه‌ی صفحات سایت است. توجّه کنید که قالب یک برنامه می‌تواند از قالب برنامه‌ی دیگری ارث ببرد.

اگر مشخّصه‌ی errors فرم مقدار داشته باشد در ۱ پیام خطایی نمایش می‌دهیم که بیان می‌کند نام کاربری و گذرواژه‌ی واردشده با هیچ یک از اطّلاعات موجود در پایگاه داده تطابق ندارد.

چون می‌خواهیم نمای ورود فرم را پردازش کند، نشان‌وند action را برابر با URL صفحه‌ی ورود قرار می‌دهیم (۲). نمای ورود فرمی را به قالب می‌فرستد و نمایش فرم (۳) و افزودن دکمه‌ی ثبت (۴) بر عهده‌ی ماست. در ۵ یک عنصر فرم مخفی اضافه می‌کنیم، 'next'؛ نشان‌وند value به جانگو می‌گوید که پس از ورود موفّق کاربر، او را به کدام صفحه هدایت کند. در این مورد کاربران را به صفحه‌ی خانه برمی‌گردانیم.

8 - پیوند دادن به صفحه‌ی ورود

حال پیوند ورود به base.html اضافه می‌کنیم تا در هر صفحه نمایش داده شود. چون نمی‌خواهیم وقتی کاربر وارد سیستم شده بود این پیوند را ببیند، آن را داخل یک تگ {% if %} می‌گذاریم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<p>
  <a href="{% url 'learning_logs:index' %}">Learning Log</a> -
  <a href="{% url 'learning_logs:topics' %}">Topics</a> -
  {% if user.is_authenticated %}
    Hello, {{ user.username }}.
  {% else %}
    <a href="{% url 'users:login' %}">Log in</a>
  {% endif %}
</p>

{% block content %}{% endblock content %}

در سیستم احراز هویّت جانگو در هر قالب یک متغیّر user وجود دارد که همیشه مشخّصه‌ی is_authenticated آن مقدار دارد: اگر کاربر وارد شده باشد True وگرنه False است. این مشخّصه به شما کمک می‌کند که به کاربران احرازِ هویّت‌شده یک پیام و به دیگر کاربران پیام دیگری نشان دهید.

اینجا به کاربران واردشده یک پیام خوش‌آمدگویی نشان می‌دهیم (۱). کاربران احراز هویّت‌شده یک مشخّصه‌ی username هم دارند که می‌توانیم از آن برای شخصی کردن پیام استفاده کنیم و به کاربر خاطرنشان کنیم که وارد شده است (۲). در ۳ پیوندی در اختیار کاربرانی که هنوز وارد نشده‌اند قرار می‌دهیم تا به صفحه‌ی ورود بروند.

استفاده از صفحه‌ی ورود

ما پیش از این یک حساب کاربری ساخته‌ایم پس بیایید با آن وارد شویم تا ببینیم آیا صفحه به درستی کار می‌کند یا نه. به آدرس http://localhost:8000/admin/ بروید. اگر همچنان به عنوان یک مدیر در سیستم حضور دارید به دنبال پیوند خروج بگردید و روی آن کلیک کنید.

اکنون که از سیستم خارج شده‌اید به http://localhost:8000/users/login/ بروید. باید صفحه‌ای مانند صفحه‌ی ورود نشان‌داده‌شده در شکل ۱۹-۴ ببینید. نام کاربری و گذرواژه‌ای که قبلاً ساخته‌اید وارد کنید. باید به صفحه‌ی اوّل برگردانده شوید. عنوان صفحه‌ی خانه باید پیامی خوش‌آمدی با نام کاربری شما نمایش دهد.

![][image50]

شکل ۱۹-۴: صفحه‌ی ورود

9 - خروج

باید راهی برای خروج از سایت در اختیار کاربران بگذاریم. پیوندی در base.html قرار دهیم که کاربران را از سیستم خارج می‌کند؛ زمانی که کاربران روی آن کلیک می‌کنند به صفحه‌ای خواهند رفت که تایید می‌کند آن‌ها از سیستم خارج شده‌اند.

افزودن یک پیوند خروج به base.html

پیوند خروج را به base.html اضافه می‌کنیم تا در هر صفحه وجود داشته باشد. آن را داخل {% if user.is_authenticated %} قرار می‌دهیم تا تنها کاربرانی که قبلاً وارد شده‌اند بتوانند آن را ببینند:

1
2
3
4
5
6
7
--snip--

  {% if user.is_authenticated %}
    Hello, {{ user.username }}.
    <a href="{% url 'users:logout' %}">Log out</a>
  {% else %}
    --snip--

الگوی URL اسم‌دار پیش‌فرض برای خروج 'logout' است.

صفحه‌ی تایید خروج

کاربران می‌خواهند بدانند که آیا خروج آن‌ها موفّق بوده یا نه، در نتیجه نمای پیش‌فرض خروج صفحه را به کمک قالب logged_out.html تولید می‌کند که آن را اکنون می‌سازیم. اینجا صفحه‌ی ساده‌ای می‌سازیم که نشان می‌دهد کاربر خارج شده است. این فایل را در templates/registration، همان‌جایی که login.html را قرار دادید، ذخیره کنید:

1
2
3
4
5
6
{% extends "learning_logs/base.html" %}

{% block content %}
  <p>You have been logged out. Thank you for visiting!</p>

{% endblock content %}

هیچ چیز دیگری در این صفحه نمی‌خواهیم زیرا base.html پیوندی بازگشت به صفحه‌ی خانه یا صفحه‌ی ورود را در اختیار می‌گذارد و کاربران می‌توانند به هرکدام که خواستند بروند.

شکل ۱۹-۵ صفحه‌ی خارج‌شده را نشان می‌دهد که به کاربری نشان داده می‌شود که روی پیوند Log out کلیک کرده است. شکل و شیوه‌ی صفحه بسیار مختصر است زیرا فعلاً روی ساختن سایتی که به درستی کار کند تمرکز کرده‌ایم. زمانی که مجموعه‌ی ویژگی‌های مورد نیاز ما به درستی پیاده شد شکل سایت را تغییر می‌دهیم تا حرفه‌ای‌تر به نظر بیاید.

![][image51]

شکل ۱۹-۵: صفحه‌ی خارج‌شده تایید می‌کند که کاربر با موفقیّت خارج شده است.

10 - صفحه‌ی ثبت نام

در ادامه صفحه‌ای می‌سازیم تا کاربران بتوانند از طریق آن ثبت نام کنند. از UserCreationForm پیش‌فرض جانگو استفاده می‌کنیم امّا تابع نما و قالب خود را می‌نویسیم.

URL ثبت نام

کد زیر الگوی URL صفحه‌ی ثبت نام را فراهم می‌آورد که دوباره در users/urls.py قرار می‌گیرد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
"""Defines URL patterns for users"""

from django.urls import path, include

from . import views

app_name = 'users'

urlpatterns = [
    # Include default auth urls.
    path('', include('django.contrib.auth.urls')),
    # Registration page.
    path('register/', views.register, name='register'),
]

ماژول views را از users وارد می‌کنیم. آن را برای نوشتن نمای صفحه‌ی ثبت نام نیاز داریم. الگوی صفحه‌ی ثبت نام URLای مانند http://localhost:8000/users/register/ را می‌پذیرد و درخواست‌ها را به تابع register() که الان خواهیم نوشت، می‌فرستیم.

تابع نمای register()

تابع نمای register() باید وقتی صفحه‌ی ثبت نام برای اوّلین بار درخواست می‌شود، یک فرم خالی ثبت نام نمایش دهد و پس از تکمیل و ثبت آن اطّلاعات آن را پردازش کند. اگر ثبت نام موفّقیّت‌آمیز بود این تابع باید همچنین کاربر جدید را وارد کند. کد زیر را به users/views.py اضافه کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from django.shortcuts import render, redirect

from django.contrib.auth import login
from django.contrib.auth.forms import UserCreationForm

def register(request):
    """Register a new user."""
    if request.method != 'POST':
        # Display blank registration form.
        form = UserCreationForm()
    else:
        # Process completed form.
        form = UserCreationForm(data=request.POST)
        if form.is_valid():
            new_user = form.save()
            # Log the user in and then redirect to home page.
            login(request, new_user)
            return redirect('learning_logs:index')

    # Display a blank or invalid form.
    context = {'form': form}
    return render(request, 'registration/register.html', context)

تابع‌های render() و redirect() را وارد می‌کنیم. سپس تابع login() را وارد می‌کنیم تا در صورت ثبت نام موفّق کاربر او را وارد سایت کنیم. ما همچنین UserCreationForm پیش‌فرض را وارد می‌کنیم. در تابع register() بررسی می‌کنیم که آیا به یک درخواست POST پاسخ می‌دهیم یا نه. اگر چنین نباشد نمونه‌ای از UserCreationForm را بدون هیچ داده‌ی اوّلیّه‌ای می‌سازیم (۱).

اگر به درخواست POST پاسخ می‌دادیم یک نمونه از UserCreationForm را بر اساس داده‌های ثبت‌شده ایجاد می‌کنیم (۲). بررسی می‌کنیم که آیا داده‌ها معتبرند یا نه (۳)—در این مورد، آیا نام کاربری شامل حرف‌های مناسب است، دو گذرواژه با یکدیگر برابرند و این که کاربر هنگام ثبت داده‌های خود هیچ خرابکاری‌ای انجام ندهد.

اگر داده‌های ثبت‌شده معتبر باشند متد save() فرم را فرامی‌خوانیم تا نام کاربری و معادل درهم‌ریخته‌ی گذرواژه را در پایگاه داده ذخیره می‌کنیم (۴). متد save() شیء کاربر جدیدی را که ساخته‌ایم برمی‌گرداند و ما آن را به new_user نسبت می‌دهیم. زمانی که اطّلاعات کاربران ذخیره شد آن‌ها را به صدا کردن تابع login() و دادن شیءهای request و new_user به سایت وارد می‌کنیم (۵) که یک نشست معتبر برای کاربر جدید ایجاد می‌کند. در نهایت کاربر را به صفحه‌ی خانه برمی‌گردانیم (۶) که در آن یک پیام شخصی در عنوان دیده می‌شود که به کاربران می‌گوید ثبت نام آن‌ها موفق بوده است.

در پایان تابع صفحه را می‌سازیم که می‌تواند یک فرم خالی یا فرم ثبت‌شده با اطّلاعات نامعتبر باشد.

قالب register

اکنون قالبی برای صفحه‌ی ثبت نام می‌سازیم که شبیه به صفحه‌ی ورود خواهد بود. حتماً آن را در همان پوشه‌ی حاوی login.html ذخیره کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{% extends "learning_logs/base.html" %}

{% block content %}
  <form method="post" action="{% url 'users:register' %}">
    {% csrf_token %}
    {{ form.as_p }}
    <button name="submit">Register</button>
    <input type="hidden" name="next" value="{% url 'learning_logs:index' %}" />
  </form>

{% endblock content %}

دوباره از متد as_p استفاده می‌کنیم تا جانگو تمام فیلدهای فرم را به همراه پیام‌های خطای مربوط به درست پر نشدن آن، به درستی نمایش دهد.

پیوند دادن به صفحه‌ی ثبت نام

در ادامه کدِ نمایش پیوند صفحه‌ی ثبت نام را برای کاربرانی که هنوز وارد نشده‌اند اضافه می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
--snip--

  {% if user.is_authenticated %}
    Hello, {{ user.username }}.
    <a href="{% url 'users:logout' %}">Log out</a>
  {% else %}
    <a href="{% url 'users:register' %}">Register</a> -
    <a href="{% url 'users:login' %}">Log in</a>
  {% endif %}

--snip--

اکنون کاربرانی که وارد شده‌اند یک پیام شخصی و یک پیوند خروج می‌بینند. کاربرانی که هنوز وارد نشده‌اند پیوندی به صفحه‌ی ثبت نام و یک پیوند برای ورود می‌بینند. صفحه‌ی ثبت نام را با ساختن چند حساب کاربری با نام‌های کاربری مختلف امتحان کنید.

در بخش بعدی برخی از صفحه‌ها را محدود می‌کنیم تا تنها در دسترس کاربرانی باشند که ثبت نام کرده‌اند و مطمئن خواهیم شد که هر موضوع به کاربر خاصّی تعلّق دارد.

نکته
سیستم ثبت نامی که ما برپا کرده‌ایم به هر کس اجازه می‌دهد که هر تعداد حساب کاربری که بخواهد بسازد. امّا برخی سیستم‌ها از کاربران می‌خواند که هویّت خود را باید پاسخ دادن به یک ایمیل تایید کنند. انجام چنین کاری باعث می‌شود که سیستم نسبت به سیستم ساده‌ی ما، تعداد کمتری حساب کاربری هرز ایجاد کند. با این حال زمانی که ساختن برنامه‌ها را می‌آموزید تمرین کردن با یک سیستم ثبت نام ساده مثل این بسیار عالی خواهد بود.

11 - خودآزمایی

۱۹-۲: حساب‌های کاربری بلاگ: یک سیستم احراز هویّت و ثبت نام برای کاربران پروژه‌ی Blog که در تمرین ۱۹-۱ شروع کرده‌اید، بسازید. مطمئن شوید که کاربرانِ واردشده نام کاربری خود را در جایی از صفحه می‌بینند و کاربران ثبت نام نشده پیوندی به صفحه‌ی ثبت نام می‌بینند.

12 - فراهم آوردن امکان در اختیار گرفتن داده‌ها برای کاربران

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

مدل Topic را تغییر می‌دهیم تا هر موضوع به یک کاربر خاص تعلّق داشته باشد. چنین کاری مطالب را هم مدیریّت خواهد کرد زیرا هر مطلب به یک موضوع خاص مرتبط است. کار را به محدود کردن دسترسی به صفحات خاص آغاز می‌کنیم.

محدود کردن دسترسی با @login_required

جانگو به کمک شکل‌دهنده‌ی @login_required محدود کردن دسترسی به صفحه‌های خاص را تنها برای کاربران172 واردشده ساده می‌کند. یک شکل‌دهنده راهنمایی است که درست پیش از تعریف تابع قرار داده می‌شود و پایتون آن را به پیش از اجرای تابع در آن اعمال می‌کند تا نحوه‌ی رفتار تابع را تغییر دهد. اجازه دهید به یک مثال نگاهی بیندازیم.

محدود کردن دسترسی به صفحه‌ی موضوعات

هر موضوع به یک کاربر تعلّق دارد پس تنها کاربرانی که ثبت نام کرده‌اند می‌توانند صفحه‌ی موضوعات را درخواست کنند. کد زیر را به learning_logs/views.py اضافه کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from django.shortcuts import render, redirect

from django.contrib.auth.decorators import login_required

from .models import Topic, Entry

--snip--

@login_required
def topics(request):
    """Show all topics."""
    --snip--

ابتدا تابع login_required() را وارد می‌کنیم. با افزودن یک علامت @ به ابتدای تابع login_required()، آن را به عنوان یک شکل‌دهنده به تابع نمای topics() اضافه می‌کنیم. در نتیجه پایتون می‌فهمد که کد داخل login_required() را پیش از کد topics() اجرا کند.

کد داخل login_required() بررسی می‌کند که آیا کاربری وارد شده است یا نه و جانگو تنها در صورتی کد topics() را اجرا می‌کند که کاربری وارد شده باشد. اگر کاربر وارد نشده باشد، به صفحه‌ی ورود هدایت خواهد شد.

برای این که بازگردانی کاربر درست کار کند باید settings.py را طوری تغییر دهیم که جانگو بداند صفحه‌ی ورود کجاست. خط زیر را به آخر settings.py اضافه کنید:

1
2
3
4
--snip--

# My settings
LOGIN_URL = 'users:login'

حال اگر کاربری که هویّت او احراز نشده صفحه‌ای را درخواست کند که با @login_required محافظت شده جانگو او را به آدرسی می‌فرستد که در LOGIN_URL در فایل settings.py تعیین شده است.

این تنظیمات را می‌توانید با خارج شدن از یکی از حساب‌های کاربری و رفتن به صفحه‌ی خانه امتحان کنید. روی پیوند Topics کلیک کنید که باید شما را به صفحه‌ی ورود رهنمون کند. سپس با یکی از حساب‌های کاربری وارد شوید و در صفحه‌ی خانه دوباره روی پیوند Topics کلیک کنید. باید به صفحه‌ی موضوعات دسترسی داشته باشید.

ایجاد محدودیّت در سراسر سایت

جانگو ایجاد محدودیّت در دسترسی را بسیار ساده می‌کند امّا شما باید تصمیم بگیرید که از کدام صفحه‌ها باید محافظت کنید. بهترین راه این است که ابتدا صفحه‌هایی را که نیازی به محدودیّت ندارد تعیین کنید و سپس تمام صفحه‌های دیگر پروژه را محدود کنید. اصلاح محدودیّت‌های بیش از حد ساده است و از این که صفحه‌های حسّاس را بدون محافظت رها کنید، کم‌خطرتر است.

در Learning Log صفحه‌ی خانه و صفحه‌ی ثبت نام را محدود نمی‌کنیم. دسترسی به دیگر صفحه‌ها محدود به کاربران خواهد بود.

این learning_logs/views.py است که در آن شکل‌دهنده‌ی @login_required به تمام نماها به جز index() اعمال می‌شود:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
--snip--

@login_required
def topics(request):
    --snip--

@login_required
def topic(request, topic_id):
    --snip--

@login_required
def new_topic(request):
    --snip--

@login_required
def new_entry(request, topic_id):
    --snip--

@login_required
def edit_entry(request, entry_id):
    --snip--

سعی کنید پس از خروج از سایت به تمام صفحه‌های آن دسترسی پیدا کنید: شما به صفحه‌ی ورود برگردانده می‌شوید. نمی‌توانید روی پیوندها به صفحه‌هایی مانند new_topic کلیک کنید. امّا اگر آدرس https://localhost:8000/new_topic/ را وارد کنید به صفحه‌ی ورود هدایت می‌شوید. دسترسی شما به هر آدرسی که به طور عمومی در دسترس است امّا حاوی داده‌های خصوصی کاربران است، باید محدود باشد.

13 - ایجاد ارتباط بین داده‌ها و کاربران

در ادامه باید بین داده‌ها و کاربری که آن را ثبت کرده ارتباط برقرار کنیم. کافی است تنها داده‌ها در بالاترین سطح سلسله‌مراتب را به یک کاربر متّصل کنیم و داده‌های دیگر در سطح‌های پایین‌تر به دنبال آن خواهند آمد. برای مثال در Learning Log موضوع‌ها بالاترین سطح اطّلاعات در برنامه‌اند و تمام مطلب‌ها به یک موضوع وابسته‌اند. مادامی‌که هر موضوع به یک کاربر خاص تعلّق داشته باشد می‌توانیم مالکیّت هر مطلب را در پایگاه داده ردگیری کنیم.

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

اصلاح مدل Topic

تغییرات models.py تنها در دو خط انجام می‌پذیرد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from django.db import models

from django.contrib.auth.models import User

class Topic(models.Model):
    """A topic the user is learning about."""
    text = models.CharField(max_length=200)
    date_added = models.DateTimeField(auto_now_add=True)
    owner = models.ForeignKey(User, on_delete=models.CASCADE)

    def __str__(self):
        """Return a string representation of the model."""
        return self.text

class Entry(models.Model):
    --snip--

مدل User را از django.contrib.auth وارد می‌کنیم. سپس فیلدی به نام owner به Topic اضافه می‌کنیم که یک ارتباط کلید خارجی به مدل User برقرار می‌کند. اگر کاربری حذف شود تمام موضوع‌های مرتبط با آن نیز پاک خواهد شد.

14 - انتقال پایگاه داده

اکنون که می‌خواهیم پایگاه داده را منتقل کنیم، جانگو آن را تغییر می‌دهد تا بتواند بین هر موضوع و یک کاربر ارتباطی برقرار کند. برای انجام انتقال جانگو باید بداند موضوعاتی را که قبلاً ایجاد شده‌اند به کدام کاربر ربط دهد. ساده‌ترین رویکرد این است که برای شروع تمام موضوع‌های موجود را به یک کاربر نسبت دهیم—مثلاً ابرکاربر. امّا ابتدا باید شماره‌ی آن کاربر را بدانیم.

بیایید به شماره‌ی تمام کاربرانی که تا الان ساخته‌ایم نگاهی بیندازیم. یک نشست پوسته‌ی جانگو اجرا کنید و دستورهای زیر را وارد کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
(ll_env)learning_log$ python manage.py shell

>>> from django.contrib.auth.models import User

>>> User.objects.all()

<QuerySet [<User: ll_admin>, <User: eric>, <User: willie>]>

>>> for user in User.objects.all():
...     print(user.username, user.id)
...

ll_admin 1
eric 2
willie 3

>>>

در ۱ مدل User را داخل نشست پوسته وارد می‌کنیم. سپس به تمام کاربرانی که تا کنون ساخته‌ایم نگاهی می‌اندازیم (۲). خروجی سه کاربر را نشان می‌دهد: ll_admin، eric و willie.

در ۳ در لیست کاربران حلقه می‌زنیم و نام کاربری و شماره‌ی هر کاربر را چاپ می‌کنیم. زمانی که جانگو درباره‌ی کاربری سوال می‌کند که قرار است تمام موضوع‌های موجود به او نسبت یابد، یکی از این سه شماره را وارد می‌کنیم.

انتقال پایگاه داده

حال که شماره‌ی کاربران را می‌دانیم می‌توانیم پایگاه داده را انتقال دهیم. زمانی که می‌خواهیم این کار را انجام دهیم پایتون دو گزینه پیش روی ما می‌گذارد: یا مدل Topic را موقّتاً به یکی از کاربران نسبت دهیم یا خودمان در فایل models.py یک حالت پیش‌فرض تعیین کنیم تا پایتون بر اساس آن عمل کند. گزینه‌ی ۱ را انتخاب کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
(ll_env)learning_log$ python manage.py makemigrations learning_logs

You are trying to add a non-nullable field 'owner' to topic without a default;
we can't do that (the database needs something to populate existing rows).

Please select a fix:

1) Provide a one-off default now (will be set on all existing rows with a
   null value for this column)

2) Quit, and let me add a default in models.py

Select an option: 1

Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do
e.g. timezone.now

Type 'exit' to exit this prompt

>>> 1

Migrations for 'learning_logs':
  learning_logs/migrations/0003_topic_owner.py
- Add field owner to topic

(ll_env)learning_log$

کار را با صدور فرمان makemigrations در ۱ آغاز می‌کنیم. در خروجی ۲، جانگو اعلام می‌کند که در حال افزودن یک فیلد اجباری (non-nullable) به مدل موجود (topic) هستیم و هیچ مقدار پیش‌فرضی تعیین نشده است. جانگو به ما دو گزینه می‌دهد (۳): می‌توانیم همین الان یک مقدار پیش‌فرض اعلام کنیم یا خارج شویم و مقدار پیش‌فرضی در models.py تعیین کنیم. در ۴ گزینه‌ی یکم را انتخاب کرده‌ایم. جانگو سپس مقدار پیش‌فرض را از ما می‌پرسد (۵).

برای ربط دادن به کاربر مدیر اوّلیّه، ll_admin، شماره‌ی کاربر ۱ را وارد کردم (۶). می‌توانید شماره‌ی هر کاربری را وارد کنید؛ حتماً نباید ابرکاربر باشد. سپس جانگو به کمک این مقدار پایگاه داده را منتقل کرده و فایل 0003_topic_owner.py را می‌سازد که فیلد owner را به مدل Topic اضافه می‌کند.

حال می‌توانیم عملیّات انتقال را اجرایی کنیم. این دستورها را در یک محیط مجازی فعّال وارد کنید:

1
2
3
4
5
6
7
8
9
(ll_env)learning_log$ python manage.py migrate

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, learning_logs, sessions

Running migrations:
  Applying learning_logs.0003_topic_owner... OK

(ll_env)learning_log$

جانگو انتقال جدید را اعمال می‌کند و نتیجه OK است (۱).

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> from learning_logs.models import Topic

>>> for topic in Topic.objects.all():
...           print(topic, topic.owner)
...

Chess ll_admin
Rock Climbing ll_admin

>>>

Topic را از learning_logs.models وارد می‌کنیم و سپس در تمام موضوعات موجود حلقه می‌زنیم تا هر موضوع و کاربر صاحب آن را چاپ کنیم (۲). می‌توانید ببینید که اکنون تمام موضوعات به کاربر ll_admin تعلّق دارند. (اگر هنگام اجرای این کد به مشکل خوردید یک بار پوسته را ببندید و دوباره باز کنید.)

نکته
شما می‌توانید به جای جابه‌جایی پایگاه داده، به سادگی آن را بازنشانی کنید امّا تمام اطّلاعات موجود در آن را از دست خواهید داد. آموختن نحوه‌ی جابه‌جایی یک پایگاه‌داده با حفظ یکپارچگی داده‌های کاربران تمرین خوبی است. اگر می‌خواهید یک پایگاه داده‌ی تازه راه بیندازید فرمان python manage.py flush را برای بازسازی ساختار پایگاه داده صادر کنید. شما باید یک ابرکاربر جدید بسازید و تمام داده‌هایتان از دست خواهد رفت.

15 - محدود کردن موضوعات به کاربران مربوطه

هم‌اکنون اگر وارد سایت شده باشید قادر خواهید بود که صرف نظر از این که چه کسی هستید، تمام موضوع‌ها را ببینید. این وضعیّت را به گونه‌ای تغییر می‌دهیم که کاربران تنها موضوعاتی را ببینند که به آن‌ها تعلّق دارد.

تغییر زیر را در تابع topics() در views.py اعمال کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
--snip--

@login_required
def topics(request):
    """Show all topics."""
    topics = Topic.objects.filter(owner=request.user).order_by('date_added')
    context = {'topics': topics}
    return render(request, 'learning_logs/topics.html', context)

--snip--

زمانی که کاربری وارد می‌شود، شیء درخواست مشخّصه‌ای به نام request.user دارد که اطّلاعات آن کاربر را ذخیره می‌کند. پرس‌وجوی topics = Topic.objects.filter(owner=request.user).order_by('date_added') به جانگو می‌گوید که تنها شیءهای Topicای را از پایگاه داده استخراج کن که مشخّصه‌ی owner آن با کاربر فعلی مطابقت داشته باشد. چون نحوه‌ی نمایش موضوع‌ها را تغییر نداده‌ایم نیازی نیست که قالب صفحه‌ی موضوعات را دست بزنیم.

برای اطمینان از عملکرد این کد با کاربری که تمام موضوع‌های فعلی را به او نسبت دادید وارد شوید و به صفحه‌ی موضوع‌ها بروید. باید تمام موضوعات را ببینید. حال از سیستم خارج شوید و با یک کاربر دیگر وارد شوید. صفحه‌ی موضوعات باید خالی باشد.

16 - محافظت از موضوع‌های یک کاربر

ما هنوز دسترسی به صفحه‌های موضوعات را محدود نکرده‌ایم در نتیجه هر کاربری که ثبت نام کرده باشد می‌تواند تعدادی آدرس به شکل http://localhost:8000/topics/1/ را امتحان کند تا بالاخره به یکی از صفحه‌ها با شماره‌ی موجود دست یابد.

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

این مشکل با انجام یک بررسی پیش از دریافت مطالب درخواستی در تابع نمای topic() برطرف می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from django.shortcuts import render, redirect

from django.contrib.auth.decorators import login_required
from django.http import Http404

--snip--

@login_required
def topic(request, topic_id):
    """Show a single topic and all its entries."""
    topic = Topic.objects.get(id=topic_id)
    # Make sure the topic belongs to the current user.
    if topic.owner != request.user:
        raise Http404

    entries = topic.entry_set.order_by('-date_added')
    context = {'topic': topic, 'entries': entries}
    return render(request, 'learning_logs/topic.html', context)

--snip--

پاسخ 404 یک پیام خطای استاندارد است و هنگامی برگردانده می‌شود که محتوای درخواستی روی سرور وجود نداشته باشد. اینجا استثنای Http404 را وارد می‌کنیم (۱) که اگر کاربر موضوعی را درخواست کند و آن وجود نداشته باشد، اعلام می‌شود. پس از دریافت درخواست و پیش از تولید صفحه کاربر مطمئن می‌شویم که کاربر مرتبط با آن موضوع با کاربری که الان وارد شده مطابقت دارد یا نه. اگر کاربر فعلی مالک موضوع درخواستی نبود استثنای Http404 را اعلام می‌کنیم (۲) و جانگو صفحه‌ی خطای 404 را برمی‌گرداند.

اکنون اگر تلاش کنید که مطالب مربوط به موضوع کاربری دیگری را ببینید پیام Page Not Found را از سوی جانگو خواهید دید. در فصل ۲۰ پروژه را جوری تنظیم می‌کنیم که یک صفحه‌ی خطای مناسب نمایش داده شود.

محافظت از صفحه‌ی edit_entry

صفحه‌های edit_entry آدرسی به شکل http://localhost:8000/edit_entry/entry_id/ هستند که entry_id یک عدد است. بیایید از این صفحه محافظت کنیم تا هیچ‌کس نتواند از این آدرس برای دست یافتن به مطالب کس دیگری استفاده کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
--snip--

@login_required
def edit_entry(request, entry_id):
    """Edit an existing entry."""
    entry = Entry.objects.get(id=entry_id)
    topic = entry.topic
    if topic.owner != request.user:
        raise Http404

    if request.method != 'POST':
        --snip--

مطلب و موضوع مربوط به آن را دریافت می‌کنیم. سپس بررسی می‌کنیم که آیا صاحب این موضوع کاربری است که الان وارد شده یا نه؛ اگر چنین نبود استثنای Http404 را اعلام می‌کنیم.

17 - نسبت دادن موضوع‌های جدید به کاربر فعلی

در حال حاضر صفحه‌ی مربوط به اضافه کردن موضوع‌های جدید خراب است زیرا موضوع‌های جدید را به هیچ کاربر خاصّی نسبت نمی‌دهد. اگر تلاش کنید که موضوع جدیدی ایجاد کنید با پیام خطای IntegrityError به همراه NOT NULL constraint failed: learning_logs_topic.owner_id برمی‌خورید. جانگو می‌گوید که شما نمی‌توانید بدون تعیین کردن مقداری برای فیلد owner موضوع جدیدی بسازید.

راه حل این مشکل بسیار سرراست است زیرا از طریق شیء request به کاربر فعلی دسترسی داریم. کد زیر را اضافه کنید که هر موضوع جدید را به کاربر فعلی نسبت می‌دهد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
--snip--

@login_required
def new_topic(request):
    """Add a new topic."""
    if request.method != 'POST':
        # No data submitted; create a blank form.
        form = TopicForm()
    else:
        # POST data submitted; process data.
        form = TopicForm(data=request.POST)
        if form.is_valid():
            new_topic = form.save(commit=False)
            new_topic.owner = request.user
            new_topic.save()
            return redirect('learning_logs:topics')

    # Display a blank or invalid form.
    context = {'form': form}
    return render(request, 'learning_logs/new_topic.html', context)

--snip--

ابتدا form.save() صدا می‌کنیم و نشان‌وند commit=False را به آن می‌دهیم زیرا پیش از ذخیره کردن موضوع جدید در پایگاه داده باید آن را تغییر دهیم (۱). سپس مشخّصه‌ی owner موضوع جدید را برابر با کاربر فعلی می‌گذاریم (۲). در نهایت save() را صدا می‌کنیم و نمونه‌ی موضوعی که تعریف کرده‌ایم به آن می‌دهیم (۳). حال موضوع تمام داده‌های مورد نیاز را دارد و با موفقّیت ذخیره خواهد شد.

شما باید بتوانید هر تعداد موضوع جدید را برای هر تعداد کاربر که بخواهید ایجاد کنید. هر کاربر تنها به داده‌های خود دسترسی خواهد داشت؛ چه بخواهد داده‌ها را ببیند، چه داده‌ی جدیدی وارد کند یا داده‌های قدیمی را ویرایش کند.

18 - خودآزمایی

۱۹-۳- بازآرایی: در views.py دو جا وجود دارد که می‌خواهیم مطمئن شویم کاربری مالک یک موضوع با کاربری فعلی تطابق دارد یا نه. کد این بررسی را در تابعی به نام check_topic_owner() قرار دهید و آن را در جای مناسب صدا کنید.

۱۹-۴- محافظت از new_entry: در حال حاضر یک کاربر می‌تواند با وارد کردن یک URL با شماره‌ی موضوع متعلّق به کاربر دیگری، یک موضوع جدید به دفترچه‌ی آموخته‌های او اضافه کند. با بررسی مرتبط بودن کاربر فعلی با موضوع مطلب پیش از ذخیره‌ی مطلب جدید، از این دستکاری جلوگیری کنید.

۱۹-۵- بلاگ محافظت‌شده: در پروژه‌ی Blog خود مطمئن شوید که هر پست وبلاگ به یک کاربر خاص اختصاص دارد. اطمینان حاصل کنید که تمام پست‌های در دسترس عموم‌اند امّا تنها کاربران ثبت نام کرده می‌توانند پست‌های جدیدی اضافه یا قبلی‌ها را ویرایش کنند. در نمایی که به کاربران اجازه‌ی ویرایش پست‌هایشان را می‌دهد پیش از پردازش فرم مطمئن شوید که کاربران در حال ویرایش پست خود هستند.

19 - خلاصه

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

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

در نهایت آموختید که با تغییر دادن تابع‌های نما چگونه اطمینان حاصل کنید که یک کاربر تنها داده‌ی مربوط به خود را می‌بیند. داده‌های مناسب را به کمک متد filter() دریافت کردید و مالک داده‌ی درخواستی را با کاربر واردشده‌ی فعلی مقایسه کردید.

شاید همیشه در ابتدا واضح نباشد که چه داده‌ای را باید در اختیار همه بگذارید و چه داده‌ای را محافظت کنید امّا این مهارت با تمرین به دست می‌آید. تصمیم‌هایی که در این فصل برای امن کردن داده‌های کاربران خود گرفتیم نشان داد که چرا هنگام ساختن یک پروژه کار کردن با دیگران فکر خوبی است: خواستن از شخص دیگری برای نگاه انداختن به پروژه‌ی شما احتمال یافتن نقاط آسیب‌پذیر آن را افزایش می‌دهد.

اکنون شما یک پروژه‌ی کاملاً کارآمد دارید که روی ماشین محلی‌تان اجرا می‌شود. در فصل پایانی شکل و شیوه‌ی Learning Log را تغییر می‌دهید تا زیبا به نظر بیاید و پروژه را روی یک سرور قرار می‌دهید تا هر کس که به اینترنت دسترسی دارد بتواند ثبت نام کند و یک حساب کاربری بسازد.