مقدمه
در این بخش، به بررسی سایر عملیات اساسی مدیریت دادهها CRUD در برنامههای تحت وب، یعنی نحوهی ایجاد (Create)، ویرایش (Update) و حذف (Delete) دادههای یک مدل میپردازیم.
در قسمت پایانی بخش قبلی، فرآیند خواندن (Read) دادهها را بررسی کردیم. در آن مثال، با کلیک روی نام پروژه به مسیر read-project/ هدایت میشدیم و جزئیات هر پروژه نمایش داده میشد. این فرآیند، تنها یکطرفه بود: دادهها از پایگاه داده خوانده و به کاربر نمایش داده میشدند، اما هیچ امکانی برای ارسال یا تغییر داده وجود نداشت.
برای پیادهسازی سایر عملیات CRUD که در بالا اشاره شد، نیازمند یک مکانیزم برای دریافت ورودی از کاربر و ارسال آن به سرور هستیم. در دنیای وب، این وظیفه بهعهدهی فرمها (Forms) است. در فریمورک جنگو نیز، فرمها نقش کلیدی در انتقال دادهها بین تمپلیتها (صفحات HTML) و پایگاه داده ایفا میکنند.
مفهوم فرم در HTML
در HTML، یک Form شامل مجموعهای از فیلدها یا ویجتها (widgets) است که برای دریافت داده از کاربر و ارسال آن به سرور استفاده میشود. فرمها ابزارهایی انعطافپذیر هستند که امکان جمعآوری انواع دادهها را از طریق المانهایی مانند text box، checkbox، radio button، انتخابگر تاریخ و... فراهم میکنند.
علاوه بر جمعآوری داده، فرمها میتوانند دادهها را در قالب درخواستهای POST ارسال کنند و از طریق مکانیزمهای امنیتی مانند توکن CSRF (Cross-Site Request Forgery protection) که امنیت نسبی بالایی دارند از حملات امنیتی رایج جلوگیری میکنند. این توکن بهصورت خودکار توسط جنگو در فرمها قرار داده میشود و اطمینان حاصل میکند که درخواستها واقعا از طرف کاربر معتبر ارسال شدهاند.
اگرچه تاکنون بهصورت مستقیم فرمی ایجاد نکردهایم، اما در رابط مدیریت جنگو (Admin Interface) هنگام افزودن پروژهها، در واقع در پسزمینه از یک فرم مبتنی بر مدل Project استفاده میشد.
سیستم فرم در جنگو
جنگو با فراهم کردن یک چارچوب قدرتمند و انعطافپذیر برای مدیریت فرمها، اجازه میدهد فرمهای دلخواه را تعریف کنیم.این سیستم نهتنها کد HTML مربوط به فرم را بهصورت خودکار تولید میکند، بلکه مراحل اعتبارسنجی دادهها (Validation)، پردازش ورودیها و ذخیرهسازی ایمن در پایگاه داده را نیز تسهیل میکند.
اصلیترین بخش این سیستم، کلاس Form است. جنگو دو نوع فرم ارائه میدهد
-
Form— برای ساخت فرمهای سفارشی که لزوما به یک مدل متصل نیستند. بهعنوان مثال، فرمهای تماس یا جستجو که دادههایشان مستقیما در پایگاه داده ذخیره نمیشوند. -
ModelForm— برای ساخت فرمهایی که مستقیما مبتنی بر یک مدل جنگو بوده و بهصورت خودکار فیلدهای مدل را به المانهای فرم تبدیل میکند و تمامی ویژگیهای مرتبط — از جمله نوع ورودی (ویجت)، برچسبها، مقادیر پیشفرض، محدودیتها و پیامهای خطا — را مدیریت میکند.
در اغلب مواقع، از ModelForm بهویژه هنگام پیادهسازی عملیات CRUD، از ModelForm استفاده میشود، چرا که توسعه را سریعتر، ایمنتر و کمخطاتر میکند.
تعریف یک ModelForm
تعریف ModelForm بسیار ساده است و حداقل به دو بخش نیاز دارد:
-
مدل مرجعی که فرم بر اساس آن ساخته میشود ←
model -
لیستی از فیلدهایی از مدل که باید در فرم نمایش داده شوند. ←
fields
بهطور متعارف، فرمها در یک فایل جداگانه با نام forms.py در دایرکتوری اپلیکیشن تعریف میشوند. این فایل بهصورت پیشفرض در ساختار پروژهی جنگو وجود ندارد و باید بهصورت دستی ایجاد شود.
coreapp/forms.py
from django.forms import ModelForm
from .import models
class ProjectForm(ModelForm):
class Meta:
model = models.Project
fields = ['title', 'area', 'content', 'tags', ]
- کلاس
ProjectFormازModelFormارثبری میکند. - در بخش
Meta، مدل مرجع (Project) و فیلدهای مورد نظر برای نمایش در فرم مشخص شدهاند. - جنگو بهصورت خودکار برای هر فیلد، المان HTML مناسب (ویجت) را بر اساس نوع دادهی تعریفشده در مدل تولید میکند. مثلا یک فیلد
CharFieldبه یک<input type="text">تبدیل میشود، در حالی که یکTextFieldبه یک<textarea>تبدیل میگردد.
ProjectForm) تا با قراردادهای نامگذاری پایتون (PascalCase) هماهنگ باشد.بهعبارتی، فرآیندی که در بالا جریان مییابد بدین صورت است که جنگو model مورد اشاره در دستور کد را فراخوانی کرده و فرمی با فیلدهای مورد اشاره در قسمت fields مبتنی بر نوع دادههای (Data Types) تعریف شده در مدل میسازد.
اگر بخواهیم فرمی ایجاد کنیم که همهی فیلدهای مدل به جز فیلدهای غیرقابل ویرایش را شامل شود، میتوان از عبارت ویژهی __all__ استفاده نمود. این روش بهویژه در مراحل اولیهی توسعه یا برای مدلهای ساده، بسیار کاربردی خواهد بود.
fields = '__all__'
نکتهی مهم دیگر این است که همهی فیلدهای مدل، لزوما در فرم نمایش داده نمیشوند. بهطور خاص:
- فیلدهایی که دارای پارامتر
editable=Falseهستند (مانند فیلدcreatedوupdatedکه معمولا بهصورت خودکار پر میشود). - فیلدهایی که بهصورت خودکار توسط سیستم مدیریت میشوند (مانند فیلد
idیا فیلدهایauto_nowوauto_now_add).
این فیلدها بهطور خودکار از فرم حذف میشوند، چرا که جنگو فرض میکند کاربر نباید مستقیما روی آنها تاثیر بگذارد.
جنگو علاوه بر مشخص کردن فیلدهای مورد نظر در مدل–فرمها با پارامتر fields، امکان حذف انتخابی برخی فیلدها از فرم را نیز فراهم میکند. این کار با استفاده از پارامتر exclude در کلاس Meta انجام میشود. زمانی که از fields = '__all__' استفاده میکنیم، تمامی فیلدهای قابل ویرایش مدل در فرم ظاهر میشوند. اما گاهی نیاز خواهد بود که همهچیز را نشان دهیم، بهجز چند فیلد خاص — مثلا فیلدی که بخواهیم بهصورت خودکار پر شود یا فیلدی که کاربر نباید آن را تغییر دهد. در چنین مواردی، بهجای لیستکردن تمام فیلدهای مورد نظر در fields، میتوان از exclude استفاده نمود
coreapp/forms.py
from django.forms import ModelForm
from .import models
class ProjectForm(ModelForm):
class Meta:
model = models.Project
exclude = ['owner']
⚠️ نمیتوان همزمان از fields و exclude در یک ModelForm استفاده نمود. جنگو اجازهی ترکیب این دو پارامتر را نمیدهد، چرا که ممکن است منجر به تناقض یا رفتارهای غیرمنتظره شود و حتما باید یکی از دو روش را انتخاب کرد
- مشخص کردن فیلدهای مورد نظر با
fields - یا مشخص کردن فیلدهای غیرمورد نظر با
exclude
ایجاد داده – Create
در این بخش، نحوه ایجاد یک صفحه برای ذخیره داده جدید (مثلاً یک پروژه جدید) در پایگاه داده با استفاده از مدل–فرمهای جنگو را بهصورت کامل و گامبهگام توضیح میدهیم. هدف ما این است که با استفاده از یک فرم هوشمند و ایمن، دادههای واردشده توسط کاربر را اعتبارسنجی کرده و در صورت صحیح بودن، در پایگاه داده ذخیره کنیم.
گام اول: ایجاد View برای نمایش فرم
اولین قدم، تعریف یک View در فایل views.py خواهد بود که فرم مربوطه را به کاربر نمایش دهد.
💡 نکته: در این مرحله فقط فرم را نمایش میدهیم. ذخیرهسازی داده در مرحله بعدی (با مدیریت درخواست POST) انجام میشود.
coreapp/views.py
from . import forms
def ProjectCreate(request):
projectForm = forms.ProjectForm()
context = {'form': projectForm }
return render(request, 'forms.html', context)
گام دوم: تعریف مسیر (URL) مربوط به View
حال باید یک الگوی مسیر (URL) برای دسترسی به View ایجاد شده، در فایل urls.py تعریف گردد.
coreapp/urls.py
urlpatterns = [
...,
path('project-add/', views.ProjectCreate, name='ProjectCreate'),
]
گام سوم: ایجاد تمپلیت عمومی برای فرمها
در مرحله پایانی به سراغ ایجاد تمپلیت عمومی forms.html در دایرکتوری ریشه پروژه خواهیم رفت. یکی از مزیتهای جنگو این است که میتوان یک تمپلیت عمومی برای تمام فرمهای ایجاد (Create) و ویرایش (Update) طراحی نمود. که تمامی مدلها را پوشش دهد. این کار از تکرار کد جلوگیری کرده و نیاز به ایجاد تمپلیتهای فرم متعدد برای مدلهای مختلف را — مگر در مواردی خاص — از بین میبرد.
tutorial/templates/forms.html
{% extends 'base.html' %}
{% block content %}
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save</button>
</form>
{% endblock %}
جنگو بهصورت پیشفرض از CSRF (Cross-Site Request Forgery) محافظت میکند. تگ {% csrf_token %} یک توکن امنیتی در فرم قرار میدهد که بدون آن، جنگو هر درخواست POST را رد کرده و خطای 403 Forbidden نمایش میدهد. همواره در هر فرمی که از method="POST" استفاده میکند، میبایست تگ {% csrf_token %} را قرار دهیم.
هنگام استفاده از تگ {{ form.as_p }}، جنگو بهصورت خودکار تمام فیلدهای فرم را بهصورت یک سری تگ <p> (پاراگراف) رندر میکند. هر فیلد در یک تگ <p> جداگانه قرار میگیرد که شامل برچسب (label) و ورودی (input) فیلد مربوطه خواهد بود و زمانی کاربردی خواهد بود که بخواهیم سریعا یک فرم قابل استفاده داشته باشیم (مثلا در مرحله توسعه یا پروتوتایپ) و نیازی به کنترل دقیق روی ظاهر فرم نداشته باشیم.
پس از اتمام مراحل، میتوان نمایشی از فیلدها را برای ایجاد پروژه جدید با رجوع به آدرس project-add/ در مرورگر، مشاهده کرد.

در پروژههای نهایی (production) که ظاهر کاربری مهم است و یا زمانی که نیاز به نمایش پیغامهای خطا (Errors)، متنهای راهنما (Help Text) یا استایلدهی پیشرفته داشته باشیم، بهتر است فرم عمومی forms.html را بهصورت دستی و با بهرهگیری از حلقه رندر کنیم.
tutorial/templates/forms.html
{% extends 'base.html' %}
{% block content %}
<form method="POST">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
{{ field.label_tag }}
{{ field }}
{% if field.help_text %}
<small>{{ field.help_text }}</small>
{% endif %}
{% if field.errors %}
<div>{{ field.errors }}</div>
{% endif %}
</div>
{% endfor %}
<button type="submit">Save</button>
</form>
{% endblock %}
تا به اینجا، تنها به نمایش تمپلیت فرم برای ایجاد پروژه در مدل Project توسط مدل–فرم ProjectForm پرداختیم. حال در ادامه میبایست به پردازش دادههای ارسالی از طریق فرم و ذخیره آنها در پایگاه داده بپردازیم که یکی از هستههای اصلی کار با فرمها در Django است
گام چهارم: پردازش و ذخیرهسازی دادهها
if request.method == 'POST':
data = forms.ProjectForm(request.POST)
if data.is_valid():
data.save()
return redirect('Projects')
— بررسی نوع درخواست
وقتی کاربر روی Submit فرم کلیک میکند، مرورگر یک درخواست HTTP از نوع POST به سرور ارسال میکند.. در view میبایست در ابتدای کار نوع درخواست ارسالی را توسط if request.method == 'POST' بررسی نمود. این بررسی، اولین سطح از منطق کنترل جریان پردازش و دخیرهسازی دادهها در View میباشد.
request.methodنشان میدهد که این درخواست از چه نوعی است (GE- فقط زمانی دادهها پردازش خواهند شد که درخواست از نوع
POSTباشد (یعنی کاربر، داده ارسال نموده باشد). T،POST،PUTو غیره).اگر درخواست از نوعGETباشد (مثلا وقتی کاربر صفحه را برای اولین بار باز میکند)، این بلوک اجرا نمیشود و معمولا یک فرم خالی نمایش داده میشود.
— ایجاد نمونه فرم با دادههای ارسالی
formData = ProjectForm(request.POST) فقط دادهها را دریافت و به فرم نسبت میدهیم — هنوز هیچ اعتبارسنجی یا ذخیرهسازی انجام نشده است.ProjectFormیک کلاس فرم است که ازforms.ModelFormارثبری کرده و مربوط به مدلProjectاست.request.POSTیک دیکشنری مانند (QueryDict) بوده که تمام دادههای ارسالی از طریق فرم (با متد POST) را در خود دارد.- با ارسال
request.POSTبه سازنده فرمProjectForm(...), جنگو یک نمونه پر شده از فرم ایجاد میکند که دادههای واردشده توسط کاربر را در خود نگه میدارد.
— اعتبارسنجی دادهها
اعتبارسنجی دادهها با متد is_valid() یکی از قدرتمندترین ویژگیهای جنگو است چرا که از خطاهای انسانی (مثل وارد کردن ایمیل نامعتبر) و حملات امنیتی (مثل تزریق داده — Data Injection ) جلوگیری میکند.
- تمام فیلدهای فرم را بر اساس قوانین تعریفشده در مدل یا فرم (مثل
max_length،required،validatorsو غیره) بررسی میکند.- اگر همه دادهها معتبر باشند:
- مقدار
Trueبرگردانده میشود. - دادههای تمیزشده (cleaned data) در ویژگی
formData.cleaned_dataقرار میگیرند.
- مقدار
- اگر حتی یک فیلد نامعتبر باشد:
- مقدار
Falseبرگردانده میشود. - خطاهای مربوطه در
formData.errorsذخیره میشوند و میتوان آنها را در تمپلیت نمایش داد.
- مقدار
- اگر همه دادهها معتبر باشند:
— ذخیره داده در پایگاه داده
اگر فرم معتبر باشد، از آنجا که ProjectForm یک ModelForm است، متد save() بهصورت خودکار:
- یک رکورد — Object — جدید از مدل
Projectایجاد میکند. - مقادیر فیلدها را از
cleaned_dataبه آن Object اختصاص میدهد. - Object را با فراخوانی
save()، در پایگاه داده ذخیره میکند (یک رکورد جدید در جدولprojectایجاد میشود).
forms.Form (نه ModelForm) ارثبری کرده بود، متد save() دیگر وجود نخواهد داشت و باید بهصورت دستی دادهها را در مدل ذخیره نمود.— هدایت مجدد کاربر
پس از ذخیره موفقیتآمیز داده، بهجای بازگرداندن یک صفحه جدید یا نمایش پیام، میتوان کاربر را به یک URL دیگر هدایت کرد. این کار با بهرهگیری از متد redirect , بر اساس الگوی طراحی معروف PRG (Post-Redirect-Get) انجام میشود.
چرا این کار مهم است؟
- جلوگیری از ارسال مجدد تصادفی فرم (اگر کاربر F5 بزند، دوباره داده ذخیره نمیشود).
- رفتار استاندارد و امن در وب.
- بهبود تجربه کاربری (مثلا هدایت به لیست پروژهها پس از ایجاد یک پروژه جدید).
redirect از مقداری که برای متغیر name — در اینجا name="Projects" — برای الگوی URL در لیست urlpatterens=[] تعریف شده، برای فراخوانی URL استفاده میگردد.⚠️ برای استفاده از redirect، میبایست متد آن را از کتابخانه django.shortcuts وارد نمود
from django.shortcuts import render, redirect
def ProjectCreate(request):
projectForm = forms.ProjectForm()
if request.method == 'POST':
data = forms.ProjectForm(request.POST)
if data.is_valid():
data.save()
return redirect('Projects')
context = {'form': projectForm }
return render(request, 'forms.html', context)
if request.method == 'POST':
data = forms.ProjectForm(request.POST)
if data.is_valid():
...
else:
projectForm = forms.ProjectForm()
context = {'form': projectForm }
return render(request, 'forms.html', context)
⚠️ هرگاه فرم شامل آپلود فایل باشد، باید request.FILES را هم به فرم اضافه نماییم
data = ProjectForm(request.POST, request.FILES)
enctype="multipart/form-data" را در اختیار داشته باشد.<form class="" action="" method="POST" enctype="multipart/form-data">
...
</form>
ویرایش داده – Update
فرآیند ویرایش (Update) داده در جنگو بسیار شبیه به فرآیند ایجاد (Create) است، با این تفاوت که در هنگام نمایش فرم ویرایش، فیلدهای آن از قبل با دادههای موجود در پایگاه داده پر شدهاند. این امر به کاربر اجازه میدهد تا دادههای قبلی را مشاده کرده و در صورت نیاز، آنها را تغییر دهد.
برای پیادهسازی این عملکرد، نیاز است تا یک شناسه منحصربهفرد (مانند id) از رکورد مورد نظر دریافت شود تا بتوان دقیقا همان رکورد را از پایگاه داده بازیابی کرده و در تمپلیت فرم نمایش داد. در جنگو، هر مدل بهصورت پیشفرض یک فیلد id از نوع AutoField دارد که منحصربهفرد (unique) و غیرقابل تغییر است — در مدل Project فیلد id از نوع UUIDField تعریف شد — از فیلد id بهعنوان شناسه داده جهت پیادهسازی فرایند ویرایش استفاده خواهیم نمود.
در فایل views.py، یک view با عنوان ProjectUpdate تعریف میکنیم که مسئول نمایش فرم ویرایش و ذخیره تغییرات خواهد بود
coreapp/views.py
def ProjectUpdate(request, uid):
projectObj = models.Project.objects.get(id=uid)
projectData = forms.ProjectForm(instance=projectObj)
if request.method == 'POST':
data = forms.ProjectForm(request.POST, instance=projectObj)
if data.is_valid():
data.save()
return redirect('Projects')
else:
projectData = data
context = { 'form': projectData }
return render(request, 'forms.html', context)
استفاده از پارامتر instance=projectObj هنگام ایجاد فرم، باعث میشود فیلدهای فرم با مقادیر موجود در آن رکورد از پایگاه داده پر شوند. همچنین، هنگام ذخیرهسازی (data.save())، جنگو بهجای ایجاد رکورد جدید، همان رکورد قبلی را بهروزرسانی میکند.
برای دسترسی به view، باید یک الگوی URL آن را در فایل urls.py تعریف کنیم که شناسه (uid) را به عنوان پارامتر دریافت کند
coreapp/urls.py
urlpatterns = [
... ,
path(project-edit/<str:uid>/', views.ProjectUpdate, name='ProjectUpdate'),
]
- از
<str:uid>استفاده کردیم چونidیک مقدار رشتهای تصادفی است. این کار هم امنیت بیشتری فراهم میکند و هم از خطاهای نوع داده جلوگیری میکند. - تعیین عنوان مسیر (
name='ProjectUpdate') برای استفاده در تمپلیتها با تگ{% url %}ضروری است.
در تمپلیت نمایش جزئیات پروژه (در اینجا read-project.html)، یک لینک ویرایش اضافه میکنیم که شناسه پروژه را به مسیر ویرایش منتقل کند.
<a href="{% url 'ProjectUpdate' project.id %}"> Edit Project </a>
- این لینک، کاربر را به آدرسی همانند
/project-edit/550b6632-e552-4ffe-8762-62af20db79ec/هدایت میکند، که در آن view مربوطه، پروژه باid=550b6632-e552-4ffe-8762-62af20db79ecبارگذاری و در فرم نمایش داده میشود. - تعیین عنوان مسیر (
name='ProjectUpdate') برای استفاده در تمپلیتها با تگ{% url %}ضروری است.
بدین ترتیب در این فرایند، کاربر به لیست پروژهها (/projects/) رفته و روی عنوان یک پروژه کلیک نموده و به صفحه جزئیات (/read-project/...) منتقل میشود. سپس در آنجا، بر روی دکمه "Edit Project" کلیک کرده و به صفحهای با آدرس /project-edit/<id>/ هدایت میشود که فرمی با دادههای فعلی پروژه نمایش داده خواهد شد. در نهایت پس از اعمال تغییرات و ارسال فرم، دادهها بهروزرسانی شده و کاربر به لیست پروژهها بازمیگردد.

حذف داده – Delete
برای حذف یک رکورد از مدل در جنگو، مطابق روال استاندارد، ابتدا باید یک تابع (view) اختصاصی در فایل views.py ایجاد کنیم. همانند عملیات ویرایش، فرآیند حذف نیز همیشه مربوط به یک رکورد خاص است؛ بنابراین نیاز داریم تا یک شناسهٔ منحصربهفرد — معمولا فیلد id — را بهعنوان پارامتر ورودی دریافت کنیم. این فیلد بهصورت پیشفرض در هر مدل Django وجود دارد، مقدار آن منحصربهفرد (unique=True) بوده و پس از ایجاد قابل تغییر نیست.
در فایل coreapp/views.py، تابعی به نام ProjectDelete تعریف میکنیم که مسئول بازیابی و حذف پروژه مورد نظر از پایگاه داده خواهد بود.
coreapp/views.py
def ProjectDelete(request, uid):
projectObj = models.Project.objects.get(id = uid)
if request.method == 'POST':
projectObj.delete()
return redirect('Projects')
- عملیات حذف فقط در پاسخ به درخواست
POSTانجام میشود تا از حذف تصادفی یا از طریق لینک ساده (مثلا با کلیک یا پیمایش خودکار) جلوگیری شود.
مطابق روال همیشگی، برای دسترسی به view، یک الگوی URL تعریف میکنیم که شناسه پروژه (uid) را بهعنوان پارامتر دریافت کند.
coreapp/urls.py
urlpatterns = [
...,
path('project-delete/<str:uid>/', views.ProjectDelete, name='ProjectDelete'),
]
در تمپلیت نمایش جزئیات پروژه (read-project.html)، یک فرم و گزینه حذف تعریف میکنیم که با بهرهگیری از ویژگی action فرم، شناسه پروژه را برای حذف به الگوی مسیر تعریف شده هدایت خواهد کرد.
coreapp/templates/project-read.html
<form action="{% url 'ProjectDelete' project.id %}" method="post">
{% csrf_token %}
<button type="submit">Delete</button>
</form>
- حضور تگ
{% csrf_token %}برای جلوگیری از حملات CSRF (Cross-Site Request Forgery) الزامی است. بدون آن، درخواستPOSTتوسط جنگو رد خواهد شد. - ویژگی
actionدر تگ<form>مشخص میکند که دادههای فرم (وقتی کاربر روی دکمهtype="submit"کلیک میکند) به کجا ارسال شوند. در صورتی که آدرسی برای ویژگیactionفرم تعیین نشده باشد -action=""- باشد یا وجود نداشته باشد (<form method="post">)، دادهها به همان آدرسی که فرم در آن قرار دارد ارسال خواهند شد.
فرآیند حذف از دید کاربر به این صورت خواهد بود که، کاربر به لیست پروژهها (/projects/) مراجعه کرده و بر روی عنوان یک پروژه کلیک کرده و به صفحه جزئیات آن (/read-project/...) هدایت میشود. در آن صفحه، با انتخاب گزینه حذف «Delete»، یک درخواست POST به آدرس /project-delete/<id>/ ارسال شده و پروژه از پایگاه داده حذف و کاربر به لیست پروژهها بازمیگردد.