> **کانتست** تمام گشت و به پایان رسید **المپیک** ما همچنان در **اوّلِ وصفِ سوال ماندهایم...**
با رسیدن به **آخرین سوال** از [**مسابقات پایتون جنگوی**](https://quera.org/events/techolympics-python-0407) سری دوم [**#المپیکفناوری پردیس،**](https://quera.org/events/techolympics-0407) **علی غرغرو** *(Ali GhorGhorooo)* که از **ماهها قبل از شروع این سری مسابقات،** با غرهای فراوانی مانند "_من میدونمممممممممممممممم ما موفق نمیشیممممممممممممم_"، **نقش بسزایی** در پیشبرد این سری از مسابقات داشته است، میخواهد تا با توسعهی یک **فرمساز پویا** *(Dynamic Form Builder)* **جدید** برای کوئرا، **نظرسنجی جامع و کاملی** از تمام شرکتکنندگان این مسابقات در مورد **تمام بخشها،** از جمله *محتوا و سطح سوالات تا داستانهای عجیب غریب و طولانی سوالات مسابقه و کیفیت برگزاری مسابقه حضوری فینال* انجام دهد.
**فرمهایی** که این **فرمسازی پویا** قرار است تا برای نظرسنجی از شرکتکنندگان بسازد، باید قابلیتهای پیشرفتهای مانند *ثبت زمان پاسخدهی کاربران، تولید فرمها از روی مدل دادهها و پردازش پاسخها* را داشته باشد تا **علی غرغرو** در نهایت بتواند با **تحلیل** و **بررسی دادههای نظرسنجی** انجام شده و **زدن غرهای بهتر و بیشتر،** سری سوم **مسابقات #المپیکفناوری پردیس** که قرار است در سال آینده برگزار شود را به صورت هر چه بهتری برگزار کند. از آنجایی که سر او **همچنان با گفتن** "_من میدونمممممممممممممممم ما موفق نمیشیممممممممممممم_" برای تیم برگزاری مسابقه فینال، حسابی شلوغ است، **شما قرار است تا در این سوال به پیادهسازی بخشهایی از این فرمساز جدید کوئرا بر اساس توضیحات داده شده بپردازید.**

# **پروژهی اولیه**
برای دانلود پروژهی اولیه روی [این لینک](/problemset/assignments/4367/download_problem_initial_project/316829/) کلیک کنید.
<details class="yellow">
<summary>**ساختار فایلها**</summary>
```
form-builder/
├── config
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── forms
│ ├── migrations
│ │ ├── __init__.py
│ │ └── 0001_initial.py
│ ├── templatetags
│ │ ├── __init__.py
│ │ └── <mark class="yellow" title="شما باید این فایل را تکمیل کنید">form_tags.py</mark>
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── <mark class="yellow" title="شما باید این فایل را تکمیل کنید">forms.py</mark>
│ ├── <mark class="yellow" title="شما باید این فایل را تکمیل کنید">middleware.py</mark>
│ ├── <mark class="yellow" title="شما باید این فایل را تکمیل کنید">models.py</mark>
│ ├── urls.py
│ ├── utils.py
│ └── <mark class="yellow" title="شما باید این فایل را تکمیل کنید">views.py</mark>
├── 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
```
</details>
<details class="grey">
<summary>**راهاندازی پروژه**</summary>
برای اجرای پروژه، باید **پایتون و ابزار** _pip_ را از قبل نصب کرده باشید.
- ابتدا **پروژهی اولیه** را دانلود و از حالت فشرده خارج کنید.
- در پوشهی اصلی پروژه، یک **محیط مجازی پایتون** _(venv)_ ایجاد و فعال کنید:
```bash terminal terminal
python -m venv venv
source venv/bin/activate # در ویندوز: venv\Scripts\activate
```
- دستور زیر را برای **نصب نیازمندیها** در پوشهی اصلی پروژه اجرا کنید:
```bash terminal terminal
pip install -r requirements.txt
```
- برای **اجرای پروژه** *Django* دستور زیر را در مسیر پوشهی اصلی پروژه اجرا کنید:
```bash terminal terminal
python manage.py runserver
```
در صورت اجرای موفق، **یک لینک** در خروجی نمایش داده میشود که میتوانید **آن را در مرورگر باز کنید.**
- برای **اجرای مایگریشنها** و ایجاد جداول پایگاه داده، دستور زیر را اجرا کنید:
```bash terminal terminal
python manage.py migrate
```
- برای **بارگذاری دادههای نمونه** از **فایل** *fixture،* دستور زیر را اجرا کنید:
```bash terminal terminal
python manage.py loaddata data.json
```
</details>
# **جزئیات پروژه**
<details class="green">
<summary>**فایل `models.py`**</summary>
در این فایل تعدادی مدل وجود دارد که دو تا از آنها را شما باید پیادهسازی کنید؛ در تصویر زیر نمودار ER مربوط به آن جهت درک بهتر روابط میان این مدلها نمایش داده شده است.
|  |
| :-: |
| نمودار *Entity Rlationship Diagram (ERD)* مربوط به مدلها |
در این قسمت باید دو مدل `Form` و `Field` را به شکل گفته شده پیادهسازی کنید.
<details class="green">
<summary>**مدل `Form`**</summary>
این مدل وظیقهی نگهداری فرمهای ساخته شده توسط کاربران را دارد و اطلاعات مربوط به هر فرم را در خود ذخیره کرده تا در ادامه با استفاده از مدل `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__`
خروجی این متد رشتهای به فرمت زیر میباشد:
```
<mark class="green" title="مقدار فیلد title">TITLE</mark>
```
### **متد** `get_response_count()`
در این متد باید تعداد پاسخهای مرتبط با فرم را از طریق رابطهی میان مدلهای `Form` و `Response` برگردانید.
### **متد** `get_completion_rate()`
نرخ تکمیل فرم را برمیگرداند؛ نرخ تکمیل که عددی ببین `0` و `100` است، از تقسیم تعداد پاسخها بر تعداد بازدیدهای فرم ضربدر `100` بدست میآید. همچنین برای دریافت تعداد ویوهای مربوط به هر فرم باید از رابطهی موجود میان مدلهای `Form` و `FormView` استفاده کرد.
</details>
<details class="green">
<summary>**مدل `Field`**</summary>
این مدل وظیفهی نگهداری اطلاعات مربوط به سوالات یک فرم را دارد و باید حاوی فیلدهای زیر باشد.
| فیلد | نوع داده | توضیحات |
| ------------- | ----------------- | -------------------------- |
| `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` میتواند از یک فیلد انتخابی است که مقادیر مجاز برای آن به شکل زیر است:
```python
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__`
خروجی این متد رشتهای به فرمت زیر میباشد:
```
<mark class="green" title="مقدار فیلد title از مدل form مربوطه">FORM_TITLE</mark> - <mark title="مقدار فیلد label">LABEL</mark>
```
### **متد** `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` را دارد. خروجی این متد یک لیست خواهد بود که در صورت وجود خطا حاوی این خطاها میباشد.
در صورتی که فیلد اجباری است اما مقدار ورودی خالی است (یعنی مقدار دهی نشده است) یک لیست تکعضوی حاوی رشتهی زیر را برگردانید:
```
"<mark title="مقدار فیلد label مدل">{label}</mark> 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 <mark title="مقدار این فیلد">{min_value}</mark>.
```
و اگر ورودی از نوع عددی بود و از مراحل قبل عبور کردید، این بار همین کار را برای فیلد `max_value` انجام دهید و بررسی کنید مقدار ورودی کمتر مساوی مقدار این فیلد باشد و در غیر این صورت خطای زیر را اضافه کنید:
```
Value must be at most <mark title="مقدار این فیلد">{max_value}</mark>.
```
+ `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 <mark title="مقدار این فیلد">{min_length}</mark> characters long.
```
در غیر اینصورت اگر طول کاراکترها از `max_length` بیشتر است پیغام خطای زیر را اضافه کنید:
```
Must be at most <mark title="مقدار این فیلد">{max_length}</mark> characters long.
```
در آخر، در صورت مقداردهی فیلد `regex_pattern` باید الگوی موجود در این فیلد را با مقدار ورودی تطبیق داده و در صورت عدم تطابق، اگر فیلد `custom_error_message` مقداردهی شده بود آن را به لیست ارورها اضافه کنید و در غیر اینصورت خطای زیر را اضافه کنید:
```
Invalid format.
```
در نهایت پس از انجام اعتبارسنجیهای بالا لیست خطاها را برگردانید.
</details>
</details>
<details class="blue">
<summary>**فایل `forms.py`**</summary>
در این فایل شما وظیفهی تکمیل یک کلاس به نام `DynamicForm` را دارید.
این فرم وظیفه دارد از روی مدلهای ذخیرهشده (`Form` و `Field`) بهطور پویا یک فرم *HTML* مناسب برای نمایش به کاربر بسازد. تابع سازندهی کلاس (`__init__()`) از قبل پیادهسازی شده و شما تنها کافیست منطق مربوط به اعتبارسنجی و ذخیرهی مدل در پایگاهداده را در متدهای `clean()` و `save()` پیادهسازی کنید.
### **متد** `clean`
از این متد برای اعتبارسنجی دادههای فرم بعد از پر شدن توسط کاربر استفاده میکنیم.
در این متد ابتدا با صدا زدن متد `clean` کلاس والد اعتبارسنجی پیشفرض را انجام دهید و دیکشنری `cleaned_data` که برمیگرداند را در یک متغیر ذخیره کنید. در مرحلهی بعد روی همهی فیلدهای مرتبط با فرم پیمایش کرده و مقدار هر فیلد را از `cleaned_data` بگیرید؛ سپس با استفاده از متد `validate_value(...)` که پیشتر در مدل `Field` پیادهسازی کردید، ابتدا ورودی کاربر را بررسی کرده و در صورتی که لیست خطای برگردانده شده خالی نبود آن را با به لیست خطاهای فرم فعلی به همراه نام فیلد اضافه کنید:
```python
self.add_error(<mark title="نام فیلد">field_name</mark>, <mark class="red" title="یکی از خطاها">error</mark>)
```
در انتها دیکشنری `cleaned_data` را برگردانید.
### **متد** `save`
از این متد برای ذخیرهسازی پاسخهای ارسالشده توسط کاربر در مدل `Response` استفاده میکنیم و هدف از آن جداسازی منطق ذخیرهی این شیء از ویوهاست. خروجی این متد در صورت معتبر بودن دادهها و ذخیرهی اطلاعات کاربر، شیء `Response` ساخته شده است؛ و در غیر این صورت خروجی آن `None` خواهد بود.
ابتدا بررسی کنید که فرم معتبر است یا نه. اگر معتبر نبود، هیچ چیزی ذخیره نکنید و یک مقدار `None` برگردانید. در صورتی که فرم معتبر باشد، یک شیء `Response` جدید با استفاده از پارامترهای ارسالی برای فرم فعلی ایجاد کنید و آن را ذخیره کنید.
سپس باید روی تمام اشیاء فیلدهای مربوط به فرم فعلی پیمایش کنید و دادههای هر فیلد را ذخیره کنید؛ برای انجام اینکار ابتدا باید دادهها را از دیکشنری `cleaned_data` بخوانید؛ برای اینکار نیز به مقدار `name` هر فیلد در متد `__init__()` نوشته شده بود نیاز دارید. پس ابتدا نامی که مقدار فیلد با آن در این دیشکنری نگهداری میشود را تشکیل دهید و سپس از دیکشنری مقدار آن را بخوانید.
در صورتی که مقداری برای آن فیلد وجود داشت بسته به نوع فیلد یک شیء از مدل `ResponseData` ایجاد کنید و در پایگاهداده ذخیره کنید:
* اگر فیلد از نوع `file` بود آن را در ستون `file` از شیء `ResponseData` ذخیره کنید.
* در غیر اینصورت آن را به رشته تبدیل کرده و در ستون `value` ذخیره کنید (توجه کنید که اگر فیلد از نوع `checkbox` و مقدار آن از نوع لیست باشد باید آن را به *JSON String* تبدیل کنید)
در نهایت شیء `Response` ایجادشده را برگردانید.
</details>
<details class="purple">
<summary>**فایل `middlewares.py`**</summary>
شما باید یک **میدلور *(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: <mark title="ارور مربوطه">{error}</mark>
```
### **متد** `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: <mark title="ارور مربوطه">{error}</mark>
```
### **متد** `_calculate_completion_time`
این متد وظیفهی محاسبهی مدت زمانی را بر عهده دارد که کاربر صرف پر کردن یک فرم کرده است. برای انجام این کار، ابتدا شناسهی فرم از پارامترهای URL استخراج میشود. سپس در `session` به دنبال زمانی که به عنوان شروع پر کردن همان فرم ثبت شده است جستجو میکنیم. در صورت وجود، اختلاف بین زمان فعلی و آن زمان به عنوان مدت زمان تکمیل فرم (بر حسب ثانیه) محاسبه میشود. این مقدار در `session` و تحت کلید `form_completion_time` ذخیره خواهد شد تا در بخشهای بعدی سیستم مورد استفاده قرار گیرد. در نهایت برای جلوگیری از باقی ماندن دادههای غیرضروری، مقدار ذخیرهشدهی اولیهی شروع فرم از دیکشنری `form_start_times` حذف میشود.
</details>
<details class="pink">
<summary>**فایل `views.py`**</summary>
در این فایل شما وظیفهی پیادهسازی کلاس `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` برگردد)، همان صفحهی فرم دوباره رندر میشود و خطاهای اعتبارسنجی در کنار فیلدها نمایش داده خواهند شد تا کاربر بتواند اصلاحات لازم را انجام دهد.
</details>
<details class="grey">
<summary>**فایل `templatetags/form_tags.py`**</summary>
در این فایل یک سری **فیلترهای سفارشی *(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`) یک آیکون منحصر به فرد در نظر گرفته شده است:
```python
{
'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'` بازگردانده شود.
</details>

# **زیرمسئلهها**
سیستم داوری برای این سوال به **زیرمسئلههای** زیر برای نمرهدهی تقسیمبندی شده است که میتوانید **امتیاز** مربوط به هر کدام را در جدول زیر مشاهده کنید. **زیرمسئلههای** این جدول **ابتدا بر اساس اولویت و پیشنیازی پیادهسازی** و سپس **بر اساس امتیاز** آنها مرتبسازی شدهاند. **لذا پیشنهاد میشود در پیادهسازی از زیرمسئلهی ابتدایی آغاز کنید.**
| زیرمسئله | امتیاز |
| ----------------------------- | ------ |
| مدل `Form` | 50 |
| مدل `Field` | 55 |
| فرم `DynamicForm` | 90 |
| میدلور `FormAccessMiddleware` | 90 |
| ویو `FormSubmissionView` | 135 |
| فیلترها | 30 |
# **آنچه باید آپلود کنید**
+ **توجه**: پس از اعمال تغییرات، کل پروژه را _Zip_ کرده و آپلود کنید. **همانند پروژهی اولیه در فایل زیپشده نباید کد در پوشهی دیگری قرار بگیرد در غیر این صورت سیستم داوری فایل را شناسایی نکرده و نمرهای دریافت نخواهید کرد.**
+ **توجه:** تنها فایلهایی که در **ساختار پروژه** مشخص شدهاند، در سیستم داوری **مورد پذیرش** قرار خواهد گرفت و سایر تغییرات در سایر فایلها **بیتأثیر** خواهند بود.
+ **توجه:** متنهای نمونه در مدلها و فرمها باید **دقیقاً** برابر مقادیر گفتهشده باشند؛ در غیر این صورت نمرهی کامل دریافت نخواهید کرد.