کانتست تمام گشت و به پایان رسید المپیک ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ما همچنان در اوّلِ وصفِ سوال مانده‌ایم...

با رسیدن به آخرین سوال از مسابقات پایتون جنگوی سری دوم #المپیک‌فناوری پردیس، علی غرغرو (Ali GhorGhorooo) که از ماه‌ها قبل از شروع این سری مسابقات، با غر‌های فراوانی مانند "من می‌دونمممممممممممممممم ما موفق نمی‌شیمممممممممممممنقش بسزایی در پیشبرد این سری از مسابقات داشته است، می‌خواهد تا با توسعه‌ی یک فرم‌ساز پویا (Dynamic Form Builder) جدید برای کوئرا، نظرسنجی جامع و کاملی از تمام شرکت‌کنندگان این مسابقات در مورد تمام بخش‌ها، از جمله محتوا و سطح سوالات تا داستان‌های عجیب غریب و طولانی سوالات مسابقه و کیفیت برگزاری مسابقه حضوری فینال انجام دهد.

فرم‌هایی که این فرم‌سازی پویا قرار است تا برای نظرسنجی از شرکت‌کنندگان بسازد، باید قابلیت‌های پیشرفته‌ای مانند ثبت زمان پاسخ‌دهی کاربران، تولید فرم‌ها از روی مدل داده‌ها و پردازش پاسخ‌ها را داشته باشد تا علی غرغرو در نهایت بتواند با تحلیل و بررسی داده‌های نظرسنجی انجام شده و زدن غر‌های بهتر و بیشتر، سری سوم مسابقات #المپیک‌فناوری پردیس که قرار است در سال آینده برگزار شود را به صورت هر چه بهتری برگزار کند. از آن‌جایی که سر او همچنان با گفتن "من می‌دونمممممممممممممممم ما موفق نمی‌شیممممممممممممم" برای تیم برگزاری مسابقه فینال، حسابی شلوغ است، شما قرار است تا در این سوال به پیاده‌سازی بخش‌هایی از این فرم‌ساز جدید کوئرا بر اساس توضیحات داده شده بپردازید.

توضیح تصویر

پروژه‌ی اولیه

برای دانلود پروژه‌ی اولیه روی این لینک کلیک کنید.

ساختار فایل‌ها

form-builder/
├── config
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── forms
│   ├── migrations
│   │   ├── __init__.py
│   │   └── 0001_initial.py
│   ├── templatetags
│   │   ├── __init__.py
│   │   └── form_tags.py
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── forms.py
│   ├── middleware.py
│   ├── models.py
│   ├── urls.py
│   ├── utils.py
│   └── views.py
├── static
│   ├── css
│   │   └── style.css
│   └── js
│       └── main.js
├── templates
│   ├── forms
│   │   ├── field_create.html
│   │   ├── field_delete.html
│   │   ├── field_edit.html
│   │   ├── form_analytics.html
│   │   ├── form_builder.html
│   │   ├── form_create.html
│   │   ├── form_delete.html
│   │   ├── form_detail.html
│   │   ├── form_edit.html
│   │   ├── form_list.html
│   │   ├── form_responses.html
│   │   ├── form_success.html
│   │   ├── response_delete.html
│   │   └── response_detail.html
│   └── base.html
├── manage.py
└── requirements.txt

راه‌اندازی پروژه

برای اجرای پروژه، باید پایتون و ابزار pip را از قبل نصب کرده باشید.

  • ابتدا پروژه‌ی اولیه را دانلود و از حالت فشرده خارج کنید.

  • در پوشه‌ی اصلی پروژه، یک محیط مجازی پایتون (venv) ایجاد و فعال کنید:

python -m venv venv
source venv/bin/activate  # در ویندوز: venv\Scripts\activate
  • دستور زیر را برای نصب نیازمندی‌ها در پوشه‌ی اصلی پروژه اجرا کنید:
pip install -r requirements.txt
  • برای اجرای پروژه Django دستور زیر را در مسیر پوشه‌ی اصلی پروژه اجرا کنید:
python manage.py runserver

در صورت اجرای موفق، یک لینک در خروجی نمایش داده می‌شود که می‌توانید آن را در مرورگر باز کنید.

  • برای اجرای مایگریشن‌ها و ایجاد جداول پایگاه داده، دستور زیر را اجرا کنید:
python manage.py migrate
  • برای بارگذاری داده‌های نمونه از فایل fixture، دستور زیر را اجرا کنید:
python manage.py loaddata data.json

جزئیات پروژه

فایل models.py

در این فایل تعدادی مدل وجود دارد که دو تا از آن‌ها را شما باید پیاده‌سازی کنید؛ در تصویر زیر نمودار ER مربوط به آن جهت درک بهتر روابط میان این مدل‌ها نمایش داده شده است.

ERD
نمودار Entity Rlationship Diagram (ERD) مربوط به مدل‌ها

در این قسمت باید دو مدل Form و Field را به شکل گفته شده پیاده‌سازی کنید.

مدل Form

این مدل وظیقه‌ی نگه‌داری فرم‌های ساخته شده توسط کاربران را دارد و اطلاعات مربوط به هر فرم را در خود ذخیره کرده تا در ادامه با استفاده از مدل Field بتوانیم سوالات برای این فرم ایجاد کنیم و آن را برای پاسخ‌دهی به اشتراک بگذاریم.

فیلد نوع داده توضیحات
title CharField بیشترین طول مجاز ۲۰۰ کاراکتر
description TextField توضیحات فرم (اختیاری)
success_message TextField پیام موفقیت بعد از ارسال
allow_multiple_submissions BooleanField اجازه چند بار پاسخ دادن به یک کاربر (پیشفرض: False)
submission_limit PositiveIntegerField حداکثر تعداد پاسخ‌های مجاز (اختیاری)
expires_at DateTimeField تاریخ انقضای فرم (اختیاری)
is_active BooleanField فعال یا غیرفعال بودن فرم (پیشفرض True)
created_by ForeignKey کاربری (User) که فرم را ساخته
created_at DateTimeField تاریخ ایجاد فرم (خودکار)
updated_at DateTimeField آخرین بروزرسانی (خودکار)
  • باید بتوان از طریق مدل User با استفاده از ویژگی created_forms به فرم‌های ساخته شده توسط کاربر دسترسی داشت و همچنین در صورت حذف کاربر فرم‌های مربوط به آن نیز حذف شوند.
  • مقدار پیش‌فرض برای فیلد success_message برابر با رشته‌ی زیر است:
Thank you for your submission!

ویژگی is_expired

بررسی می‌کند آیا فرم منقضی شده است؛ برای این‌کار باید بررسی کنید زمان فعلی از زمان منقضی شدن فرم کمتر باشد.

ویژگی is_submission_limit_reached

بررسی می‌کند آیا محدودیت پاسخ‌ها پر شده است یا خیر؛ برای این‌کار باید از ارتباط میان این مدل با مدل Response استفاده کنید و در صورتی که متغیر submission_limit برای این فرم مقداردهی شده است، چک کنید آیا تعداد پاسخ‌ها بیشتر مساوی مقدار این فیلد باشد؛ در غیر این‌صورت False برگردانید.

ویژگی can_accept_submissions

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

متد‌ __str__

خروجی این متد رشته‌ای به فرمت زیر می‌باشد:

TITLE

متد‌ get_response_count()

در این متد باید تعداد پاسخ‌های مرتبط با فرم را از طریق رابطه‌ی میان مدل‌های Form و Response برگردانید.

متد‌ get_completion_rate()

نرخ تکمیل فرم را برمی‌گرداند؛ نرخ تکمیل که عددی ببین 0 و 100 است، از تقسیم تعداد پاسخ‌ها بر تعداد بازدید‌های فرم ضرب‌در 100 بدست می‌آید. همچنین برای دریافت تعداد ویو‌های مربوط به هر فرم باید از رابطه‌ی موجود میان مدل‌های Form و FormView استفاده کرد.

مدل Field

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

فیلد نوع داده توضیحات
form ForeignKey ارجاع به فرم (Form) که این فیلد متعلق به آن است
label CharField عنوان فیلد (حداکثر ۲۰۰ کاراکتر)
field_type CharField نوع فیلد
help_text TextField متن راهنما برای فیلد (اختیاری)
placeholder CharField متن پیش‌فرض داخل فیلد (اختیاری)
is_required BooleanField آیا پر کردن فیلد اجباری است یا خیر (پیشفرض: False)
order PositiveIntegerField ترتیب نمایش فیلد در فرم (پیشفرض: 0)
options JSONField گزینه‌ها برای فیلدهای چند گزینه‌ای (پیشفرض: لیست خالی)
min_length PositiveIntegerField حداقل طول متن (اختیاری)
max_length PositiveIntegerField حداکثر طول متن (اختیاری)
min_value FloatField حداقل مقدار (اختیاری)
max_value FloatField حداکثر مقدار (اختیاری)
regex_pattern CharField الگوی regex برای اعتبارسنجی (اختیاری)
custom_error_message CharField پیام خطای دلخواه (اختیاری)
  • فیلد field_type می‌تواند از یک فیلد انتخابی است که مقادیر مجاز برای آن به شکل زیر است:
FIELD_TYPES = [
    ('text', 'Text'),
    ('email', 'Email'),
    ('number', 'Number'),
    ('textarea', 'Textarea'),
    ('select', 'Select'),
    ('radio', 'Radio'),
    ('checkbox', 'Checkbox'),
    ('file', 'File Upload'),
    ('date', 'Date'),
    ('url', 'URL'),
    ('phone', 'Phone'),
]
  • options فقط برای فیلدهای چند گزینه‌ای (select, radio, checkbox) استفاده می‌شود و انتخاب‌های مجاز آن را نشان می‌دهد.
  • min_length و max_length برای فیلدهای متنی کاربرد دارند و درصورت مقدار دهی یک بازه‌ی مشخص برای طول کاراکترهای آن فیلد تعیین می‌شود.
  • min_value و max_value برای فیلدهای عددی کاربرد دارند و درصورت مقدار دهی یک بازه‌ی مشخص برای مقدار آن فیلد تعیین می‌شود.

متد‌ __str__

خروجی این متد رشته‌ای به فرمت زیر می‌باشد:

FORM_TITLE - LABEL

متد‌ clean()

در این متد باید صحت مقادیر داده شده را برای ایجاد یک نمونه از مدل Field را بررسی کنید.

برای این کار ابتدا بررسی کنید اگر این فیلد از یکی از انواع فیلدهای انتخابی است و مقدار options خالی یا برابر لیست خالی است، یک ValidationError با پیغام زیر پرتاب کنید:

Choice fields must have at least one option.

در مرحله‌ی بعد، در صورت مقداردهی فیلد‌های min_value و max_value بایستی بررسی کنید مقدار max_value همواره بزرگتر مساوی مقدار min_value باشد و در غیر این صورت یک ValidationError با پیغام زیر پرتاب کنید:

Minimum value cannot be greater than maximum value.

در ادامه همین اعتبارسنجی را برای فیلدهای min_legth و max_length نیز انجام دهید و در صورت وجود خطا باید حاوی پیغام زیر باشد:

Minimum length cannot be greater than maximum length.

در نهایت در صورت مقداردهی فیلد regex_pattern، بررسی کنید آیا الگوی وارد شده یک الگوری Regex معتبر است یا خیر. در صورت نامعتبر بودن یک ValidationError با پیغام زیر پرتاب کنید:

Invalid regex pattern.

متد‌ validate_value(value)

این متد وظیفه‌ی اعتبارسنجی مقدار ورودی (value) برای یک نمونه از مدل Field را دارد. خروجی این متد یک لیست خواهد بود که در صورت وجود خطا حاوی این خطاها می‌باشد.

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

"{label} is required."

اگر پاسخ به فیلد اختیاری است و مقدار ورودی نیز خالی داده نشده، یک لیست خالی برگردانید و اجرای تابع را متوقف کنید تا سایر اعتبارسنجی ها روی این فیلد انجام نشود.

اما در غیر این صورت بر اساس نوع فیلد (field_type) اعتبارسنجی‌های زیر را انجام دهید:

  • email: مقدار ورودی را با الگوی [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ تطابق داده و در صورت عدم تطابق خطای زیر را به لیست خطاها اضافه کنید:
Please enter a valid email address.
  • number: در این‌صورت سه مرحله اعتبارسنجی باید انجام دهید و هر کجا به خطا برخوردید دیگر ادامه ندهید؛ در مرحله‌ی اول، چک کنید مقدار داده شده از نوع عددی باشد و در غیر این صورت ارور زیر را به لیست ارورها اضافه کنید:
Please enter a valid number.

اگر ورودی از نوع عددی بود و فیلد min_value مقداردهی شده بود باید بررسی شود مقدار ورودی بیشتر مساوی مقدار این فیلد باشد و در غیر این‌صورت خطای زیر به لیست خطاها اضافه شود:

Value must be at least {min_value}.

و اگر ورودی از نوع عددی بود و از مراحل قبل عبور کردید، این بار همین کار را برای فیلد max_value انجام دهید و بررسی کنید مقدار ورودی کمتر مساوی مقدار این فیلد باشد و در غیر این صورت خطای زیر را اضافه کنید:

Value must be at most {max_value}.
  • url: باید مقدار آدرس داده شده را بررسی کنیم تا یک URL معتبر باشد؛ برای این‌کار با استفاده از الگوی زیر این مورد را بررسی کنید:
^https?://(?:[-\w.])+(?:[:\d]+)?(?:/(?:[\w/_.])*(?:\?(?:[\w&=%.])*)?(?:#(?:\w*))?)?$

در صورت عدم تطابق پیغام خطای زیر را به لیست خطاها اضافه کنید:

Please enter a valid URL.
  • phone: برای بررسی معتبر بود شماره تلفن وارد شده باید ابتدا آن را با الگوی ^\+?[1-9][0-9]{7,14}$ تطبیق داده و در صورت عدم مطابقت پیغام خطای زیر را اضافه کنید:
Please enter a valid phone number.
  • select یا radio: در صورتی که مقدار ورودی داده‌شده در فیلد options که مقادیر مجاز برای فیلدهای انتخابی است وجود نداشت، پیغام خطای زیر را اضافه کنید:
Please select a valid option.
  • checkbox: برای فیلدهای از نوع چند انتخابی که ورودی از نوع لیست است، باید تمام اعضای لیست را بررسی کنید و در صورتی که حداقل یکی از اعضای لیست ورودی جز مقادیر مجاز برای این فیلد نبود خطای زیر را به لیست خطاها اضافه کنید:
Please select valid options.

در ادامه باید مقادیر max_length و min_length را بررسی کنید؛ در صورت مقداردهی باید مقدار ورودی را با توجه به مقادیر موجو در این متغیر ها بررسی کنید تا در بازه‌ی مورد نظر باشند. در صورتی که طول کاراکترهای ورودی از min_legth کمتر است خطای زیر را اضافه کنید:

Must be at least {min_length} characters long.

در غیر این‌صورت اگر طول کاراکترها از max_length بیشتر است پیغام خطای زیر را اضافه کنید:

Must be at most {max_length} characters long.

در آخر، در صورت مقداردهی فیلد regex_pattern باید الگوی موجود در این فیلد را با مقدار ورودی تطبیق داده و در صورت عدم تطابق، اگر فیلد custom_error_message مقداردهی شده بود آن را به لیست ارورها اضافه کنید و در غیر این‌صورت خطای زیر را اضافه کنید:

Invalid format.

در نهایت پس از انجام اعتبارسنجی‌های بالا لیست خطاها را برگردانید.

فایل forms.py

در این فایل شما وظیفه‌ی تکمیل یک کلاس به نام DynamicForm را دارید.

این فرم وظیفه دارد از روی مدل‌های ذخیره‌شده (Form و Field) به‌طور پویا یک فرم HTML مناسب برای نمایش به کاربر بسازد. تابع سازنده‌ی کلاس (__init__()) از قبل پیاده‌سازی شده و شما تنها کافیست منطق مربوط به اعتبارسنجی و ذخیره‌ی مدل در پایگاه‌داده را در متدهای clean() و save() پیاده‌سازی کنید.

متد clean

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

در این متد ابتدا با صدا زدن متد clean کلاس والد اعتبارسنجی پیش‌فرض را انجام دهید و دیکشنری cleaned_data که برمی‌گرداند را در یک متغیر ذخیره کنید. در مرحله‌ی بعد روی همه‌ی فیلدهای مرتبط با فرم پیمایش کرده و مقدار هر فیلد را از cleaned_data بگیرید؛ سپس با استفاده از متد validate_value(...) که پیش‌تر در مدل Field پیاده‌سازی کردید، ابتدا ورودی کاربر را بررسی کرده و در صورتی که لیست خطای برگردانده شده خالی نبود آن را با به لیست خطاهای فرم فعلی به همراه نام فیلد اضافه کنید:

self.add_error(field_name, error)

در انتها دیکشنری cleaned_data را برگردانید.

متد save

از این متد برای ذخیره‌سازی پاسخ‌های ارسال‌شده توسط کاربر در مدل Response استفاده می‌کنیم و هدف از آن جداسازی منطق ذخیره‌ی این‌ شیء از ویو‌هاست. خروجی این متد در صورت معتبر بودن داده‌ها و ذخیره‌ی اطلاعات کاربر، شیء Response ساخته‌ شده است؛ و در غیر این صورت خروجی آن None خواهد بود.

ابتدا بررسی کنید که فرم معتبر است یا نه. اگر معتبر نبود، هیچ چیزی ذخیره نکنید و یک مقدار None برگردانید. در صورتی که فرم معتبر باشد، یک شیء Response جدید با استفاده از پارامترهای ارسالی برای فرم فعلی ایجاد کنید و آن را ذخیره کنید.

سپس باید روی تمام اشیاء فیلدهای مربوط به فرم فعلی پیمایش کنید و داده‌های هر فیلد را ذخیره کنید؛ برای انجام این‌کار ابتدا باید داده‌ها را از دیکشنری cleaned_data بخوانید؛ برای این‌کار نیز به مقدار name هر فیلد در متد __init__() نوشته شده بود نیاز دارید. پس ابتدا نامی که مقدار فیلد با آن در این دیشکنری نگه‌داری می‌شود را تشکیل دهید و سپس از دیکشنری مقدار آن را بخوانید.

در صورتی که مقداری برای آن فیلد وجود داشت بسته به نوع فیلد یک شیء از مدل ResponseData ایجاد کنید و در پایگاه‌داده ذخیره کنید:

  • اگر فیلد از نوع file بود آن را در ستون file از شیء ResponseData ذخیره کنید.
  • در غیر این‌صورت آن را به رشته تبدیل کرده و در ستون value ذخیره کنید (توجه کنید که اگر فیلد از نوع checkbox و مقدار آن از نوع لیست باشد باید آن را به JSON String تبدیل کنید)

در نهایت شیء Response ایجادشده را برگردانید.

فایل middlewares.py

شما باید یک میدلور (Middleware) بنویسید که زمان شروع و پایان پر کردن فرم را ثبت کرده و در نهایت مدت پاسخ‌دهی کاربر را ذخیره کند تا علی غرغرو بتواند آمار زمان پاسخ‌دهی کاربران را دریافت کند.

کلاس FormAccessMiddleware

این کلاس وظیفه دارد مدت زمان اجرای هر درخواست را محاسبه کند و علاوه بر آن، مدت زمانی که یک کاربر برای پر کردن و ارسال فرم صرف می‌کند را نیز ثبت نماید. برای رسیدن به این هدف، سه متد اصلی باید پیاده‌سازی شوند: process_request، process_response و _calculate_completion_time.

متد process_request

این متد پیش از آن‌که درخواست به view برسد اجرا می‌شود. در اولین گام باید زمان شروع پردازش درخواست در ویژگی‌ای به نام _form_access_start_time در شیء request ذخیره شود. این زمان بعداً برای محاسبه‌ی مدت کل اجرای درخواست مورد استفاده قرار خواهد گرفت.

در ادامه لازم است بررسی شود که آیا درخواست مربوط به مشاهده‌ی یک فرم است یا خیر. این حالت زمانی رخ می‌دهد که نام ویو برابر form_detail باشد و متد درخواست نیز GET باشد. اگر چنین شرایطی برقرار بود، باید زمان شروع پر کردن فرم در شیء session ذخیره شود تا بتوان در لحظه‌ی ارسال فرم، مدت زمان تکمیل آن را محاسبه کرد. برای این منظور ابتدا باید کلیدی به نام form_start_times در session ایجاد شود (در صورتی که وجود نداشت) و مقدار اولیه‌ی آن یک دیکشنری خالی قرار گیرد. سپس شناسه‌ی فرم از پارامترهای URL استخراج شده و پس از تبدیل به رشته، به عنوان کلید در این دیکشنری قرار داده می‌شود. مقدار متناظر با آن نیز برابر زمان فعلی خواهد بود.

در نهایت اگر هرگونه خطایی در طول اجرای این بخش رخ داد، باید با استفاده از logger پیام خطا (error) در لاگ ثبت شود تا فرآیند قابل پیگیری باشد.

Error in FormAccessMiddleware: {error}

متد process_response

این متد پس از اجرای view و پیش از ارسال پاسخ به مرورگر فراخوانی می‌شود. در این مرحله ابتدا باید مدت زمان اجرای کل درخواست محاسبه شود. این کار از طریق مقایسه‌ی زمان فعلی با مقداری که در متد process_request ذخیره شده انجام می‌شود. اگر این مدت زمان بیش از مقدار آستانه‌ای باشد که در تنظیمات پروژه و در متغیر FORM_REQUEST_SLOW_THRESHOLD مشخص شده است، لازم است یک پیام هشدار در سطح warning در لاگ ثبت شود.

گام بعدی مربوط به حالتی است که کاربر یک فرم را ارسال کرده است. اگر نام ویو برابر form_submit، متد درخواست POST و وضعیت پاسخ نیز 302 باشد (که نشان‌دهنده‌ی موفقیت و ریدایرکت است)، در این صورت باید متد _calculate_completion_time فراخوانی شود تا زمان تکمیل فرم محاسبه و در session ذخیره گردد.

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

همچنین اگر هرگونه خطایی در طول اجرای این بخش رخ داد، باید با استفاده از logger پیام خطا (error) در لاگ ثبت شود تا فرآیند قابل پیگیری باشد.

Error in FormAccessMiddleware: {error}

متد _calculate_completion_time

این متد وظیفه‌ی محاسبه‌ی مدت زمانی را بر عهده دارد که کاربر صرف پر کردن یک فرم کرده است. برای انجام این کار، ابتدا شناسه‌ی فرم از پارامترهای URL استخراج می‌شود. سپس در session به دنبال زمانی که به عنوان شروع پر کردن همان فرم ثبت شده است جستجو می‌کنیم. در صورت وجود، اختلاف بین زمان فعلی و آن زمان به عنوان مدت زمان تکمیل فرم (بر حسب ثانیه) محاسبه می‌شود. این مقدار در session و تحت کلید form_completion_time ذخیره خواهد شد تا در بخش‌های بعدی سیستم مورد استفاده قرار گیرد. در نهایت برای جلوگیری از باقی ماندن داده‌های غیرضروری، مقدار ذخیره‌شده‌ی اولیه‌ی شروع فرم از دیکشنری form_start_times حذف می‌شود.

فایل views.py

در این فایل شما وظیفه‌ی پیاده‌سازی کلاس FormSubmissionView را دارید که وظیفه‌ی مدیریت ارسال‌های کاربران برای پاسخ‌دهی به یک فرم را دارد.

کلاس FormSubmissionView

این کلاس برای مدیریت فرآیند ارسال فرم توسط کاربر طراحی شده است. هر بار که کاربری یک فرم را پر کرده و دکمه‌ی ارسال را می‌زند، منطق پردازش داده‌ها از طریق این ویو انجام می‌شود. از آنجا که کلاس از DetailView ارث‌بری می‌کند، وظیفه‌ی نمایش جزئیات یک فرم (از جمله فیلدهای پویا) را بر عهده دارد، اما بخش اصلی و مهم آن در متد post پیاده‌سازی شده است.

متد post

این متد زمانی اجرا می‌شود که یک درخواست POST به سمت سرور ارسال گردد، یعنی همان لحظه‌ای که کاربر داده‌های فرم را برای ذخیره‌سازی ارسال می‌کند. در ادامه گام‌های مهم این متد شرح داده شده است.

ابتدا شیء فرم مورد نظر از دیتابیس واکشی می‌شود تا بتوان بررسی‌ها و اعتبارسنجی‌ها روی آن اعمال گردد. سپس اولین شرط مهم این است که بررسی شود آیا فرم همچنان امکان دریافت پاسخ دارد یا خیر. اگر فرم بسته شده باشد (از طریق ویژگی can_accept_submissions)، کاربر با پیام خطای زیر مواجه شده و دوباره به صفحه‌ی جزئیات همان فرم هدایت خواهد شد:

This form is no longer accepting submissions.

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

مسئله‌ی بعدی مربوط به جلوگیری از ارسال چندباره‌ی یک فرم توسط یک کاربر است. اگر فرم به گونه‌ای تنظیم شده باشد که فقط یک بار قابلیت ارسال داشته باشد (allow_multiple_submissions=False)، آنگاه باید بررسی کنیم آیا کاربر (چه لاگین کرده و چه ناشناس) قبلاً پاسخی برای این فرم ثبت کرده است یا خیر.

  • اگر کاربر وارد شده باشد، جستجو بر اساس فیلد submitted_by انجام می‌شود.
  • اگر کاربر ناشناس باشد، بررسی با استفاده از کلید سشن (session_key) صورت می‌گیرد.

اگر چنین پاسخی وجود داشته باشد، ارسال مجدد متوقف می‌شود و پیامی به کاربر نمایش داده خواهد شد:

You have already submitted this form.

در ادامه یک نمونه از DynamicForm ساخته می‌شود که با توجه به مدل فرم و داده‌های ارسالی (POST و FILES) مقداردهی شده است. این فرم پویا وظیفه‌ی اعتبارسنجی مقادیر هر فیلد را بر عهده دارد. اگر داده‌ها معتبر باشند، مرحله‌ی ذخیره‌سازی شروع می‌شود.

در زمان ذخیره‌سازی، ابتدا مقدار مدت زمانی که کاربر صرف تکمیل فرم کرده از سشن خوانده می‌شود (کلیدی به نام form_completion_time که در Middleware محاسبه شده بود). اگر چنین مقداری وجود داشته باشد، به صورت یک timedelta به تابع save فرستاده می‌شود. متد save در نهایت یک پاسخ جدید در دیتابیس ایجاد کرده و داده‌های مربوط به هر فیلد را ذخیره می‌کند.

پس از ذخیره موفق، داده‌ی form_completion_time از سشن حذف می‌شود تا برای درخواست‌های بعدی باقی نماند. سپس اطلاعاتی درباره‌ی referrer (یعنی صفحه‌ای که کاربر از آنجا به فرم آمده است) جمع‌آوری شده و در جدول مربوط به بازدید فرم‌ها (FormView) ذخیره می‌شود. در اینجا منطق ثبت بازدید به گونه‌ای است که اگر کاربر ناشناس باشد، بازدید بر اساس کلید سشن منحصربه‌فرد در نظر گرفته می‌شود و اگر کاربر وارد سیستم شده باشد، بر اساس حساب کاربری او ذخیره خواهد شد.

در پایان کاربر به صفحه‌ی موفقیت (form_success) هدایت می‌شود. اما اگر داده‌های فرم معتبر نباشند (یعنی is_valid=False برگردد)، همان صفحه‌ی فرم دوباره رندر می‌شود و خطاهای اعتبارسنجی در کنار فیلدها نمایش داده خواهند شد تا کاربر بتواند اصلاحات لازم را انجام دهد.

فایل templatetags/form_tags.py

در این فایل یک سری فیلترهای سفارشی (Custom Template Filters) برای جنگو تعریف شده‌اند. این فیلترها را می‌توان در تمپلیت‌ها استفاده کرد تا داده‌ها را قبل از نمایش به کاربر پردازش و تغییر دهند. در ادامه به معرفی و شرح عملکرد هر یک از این فیلترها می‌پردازیم.

فیلتر render_markdown(text)

این فیلتر برای تبدیل متن‌هایی که به فرمت Markdown نوشته شده‌اند به کد HTML استفاده می‌شود. زمانی که کاربر یک متن را با نشانه‌گذاری‌های مارک‌داون وارد می‌کند، این فیلتر باید به کمک کتابخانه‌ی markdown آن را پردازش کرده و نسخه‌ی HTML آن را تولید کند. در نهایت متن تبدیل‌شده با mark_safe علامت‌گذاری شود تا جنگو آن را به عنوان HTML معتبر رندر کند.

فیلتر extract_field_id(field_name)

این فیلتر وظیفه دارد شناسه‌ی عددی یک فیلد را از نام آن (که در فرم DynamicForm ایجاد می‌کردیم) استخراج کند.

به عنوان مثال، اگر نام فیلد چیزی شبیه به field_123 باشد، این فیلتر مقدار 123 را برمی‌گرداند. برای انجام این کار باید به دنبال الگوی field_ به همراه عدد بگردید. اگر چنین الگویی پیدا شد، همان بخش عددی استخراج و بازگردانده شود؛ در غیر این صورت یک رشته‌ی خالی برگرداندید.

فیلتر get_field_type_icon(field_type)

این فیلتر به ازای هر نوع فیلد، یک کلاس آیکون از کتابخانه‌ی Font Awesome برمی‌گرداند. ورودی این تابع یک رشته خواهد بود که برای هر یک از انواع مجاز برای فیلدها (field_type) یک آیکون منحصر به فرد در نظر گرفته شده است:

{
    'text': 'fas fa-font',
    'email': 'fas fa-envelope',
    'number': 'fas fa-hashtag',
    'textarea': 'fas fa-align-left',
    'select': 'fas fa-list',
    'radio': 'fas fa-dot-circle',
    'checkbox': 'fas fa-check-square',
    'file': 'fas fa-file-upload',
    'date': 'fas fa-calendar',
    'url': 'fas fa-link',
    'phone': 'fas fa-phone',
}

اگر هم ورودی تابع یکی از موارد بالا نبود، مقدار پیش‌فرض 'fas fa-question' بازگردانده شود.

خروجی سوال

زیرمسئله‌ها

سیستم داوری برای این سوال به زیرمسئله‌های زیر برای نمره‌دهی تقسیم‌بندی شده است که می‌توانید امتیاز مربوط به هر کدام را در جدول زیر مشاهده کنید. زیرمسئله‌های این جدول ابتدا بر اساس اولویت و پیشنیازی پیاده‌سازی و سپس بر اساس امتیاز آن‌ها مرتب‌سازی شده‌اند. لذا پیشنهاد می‌شود در پیاده‌سازی از زیرمسئله‌ی ابتدایی آغاز کنید.

زیرمسئله امتیاز
مدل Form 50
مدل Field 55
فرم DynamicForm 90
میدلور FormAccessMiddleware 90
ویو FormSubmissionView 135
فیلترها 30

آنچه باید آپلود کنید

  • توجه: پس از اعمال تغییرات، کل پروژه را Zip کرده و آپلود کنید. همانند پروژه‌ی اولیه در فایل زیپ‌شده نباید کد در پوشه‌ی دیگری قرار بگیرد در غیر این صورت سیستم داوری فایل را شناسایی نکرده و نمره‌ای دریافت نخواهید کرد.

  • توجه: تنها فایل‌هایی که در ساختار پروژه مشخص شده‌اند، در سیستم داوری مورد پذیرش قرار خواهد گرفت و سایر تغییرات در سایر فایل‌ها بی‌تأثیر خواهند بود.

  • توجه: متن‌های نمونه‌ در مدل‌ها و فرم‌ها باید دقیقاً برابر مقادیر گفته‌شده باشند؛ در غیر این صورت نمره‌ی کامل دریافت نخواهید کرد.

ارسال پاسخ برای این سؤال
فایلی انتخاب نشده است.