> سالها پیش، **باقر، ممجواد و مصطفی** سه **دانشجوی جوان مهندسی کامپیوتر** دانشگاه صنعتی شریف، **آغازگری** بودند بر **آغازگرانی** که بعدها **کوئرا** *(Quera)* نام گرفت...
اکنون، **بعد از سالها تجربه و پیشرفت و کوئراگری،** این سه **آغازگر،** که **آغازگرِ آغازگران کوئرا** بودند، تصمیم گرفتند تا همزمان با شروع سری جدید [**#المپیکفناوری پردیس**](https://quera.org/events/techolympics-0407) با آغازگری جدیدی، **ویژگی جدید مدیریت نسخهی کوئرا** *( Quera's Version Control)* را برای درسنامهها و سوالات توسعه دهند. **این ویژگی جدید در کوئرا،** باعث میشود تا برخلاف گذشته، هر درسنامه بتواند **چندین نسخهی مختلف** داشته باشد و **نسخهی فعالی** *(Active Version)* برای کاربران نمایش داده شود، سیستم مدیریت نسخه کوئرا به گونهای طراحی میشود که **هویت ایجادکنندهی هر نسخه** نیز همانند **طراحان و مربیان کالجهای کوئرا کالج،** ثبت و نمایش داده شود تا مسیر تغییرات **شفاف** شود و سابقهی تمامی مشارکتکنندگان در توسعهی محتواها، سوالات و درسنامهها در کوئرا به خوبی حفظ شود.
نسخهبندی درسنامهها و سوالات، قرار است تا بر اساس استاندارد **_SemVer_ (Semantic Versioning یا نسخهبندی معنایی)** انجام میشود و **فرمت** `MAJOR.MINOR.PATCH` برای مدیریت آنها وجود داشته باشد. اولین نسخهی هر درسنامه و سوال با شمارهٔ `0.1.0` آغاز میشود. هنگام ایجاد تغییرات جدید، مشارکتکنندگان میتوانند نوع افزایش نسخه را مشخص کند: افزایش `MAJOR`، `MINOR` یا `PATCH`. کاربارن بعدها هنگام خواندن این سوالات و درسنامهها میتوانند روند تغییرات و مشارکتکنندگان آنها را ببینند و همچنین نسخهی فعال را تغییر داده تا بتوانند نسخه مورد نظر خودشان از آن سوال یا درسنامه را مطالعه کنند. از آنجایی که این سه آغازگر قرار است به زودی و در مجموعه برنامههای استخدامی ترتیب داده شده در فینال مسابقات، سخنرانیهای آغازگرانه داشته باشند، در قالب این سوال از شما میخواهند تا بخشی از سیستم مدیریت نسخه کوئرا را آغازگری کنید!

# **پروژهی اولیه**
برای دانلود پروژهی اولیه روی [این لینک](/problemset/assignments/4367/download_problem_initial_project/316828/) کلیک کنید.
<details class="yellow">
<summary>**ساختار فایلها**</summary>
```
release-manager/
├── accounts
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── templates
│ │ └── accounts
│ │ ├── login.html
│ │ └── signup.html
│ ├── __init__.py
│ ├── apps.py
│ ├── forms.py
│ ├── models.py
│ ├── urls.py
│ └── views.py
├── core
│ ├── forms
│ │ ├── __init__.py
│ │ ├── boundfields.py
│ │ └── renderers.py
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── lms
│ ├── fixtures
│ │ └── data.json
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── templates
│ │ └── lms
│ │ ├── lesson_confirm_delete.html
│ │ ├── lesson_detail.html
│ │ ├── lesson_form.html
│ │ ├── lesson_list.html
│ │ ├── my_lessons.html
│ │ ├── release_detail.html
│ │ ├── release_form.html
│ │ └── release_list.html
│ ├── __init__.py
│ ├── apps.py
│ ├── <mark class="yellow" title="شما باید این فایل را تکمیل کنید">forms.py</mark>
│ ├── <mark class="yellow" title="شما باید این فایل را تکمیل کنید">mixins.py</mark>
│ ├── <mark class="yellow" title="شما باید این فایل را تکمیل کنید">models.py</mark>
│ ├── urls.py
│ └── views.py
├── static
│ ├── css
│ │ └── markdown.css
│ └── imgs
│ └── quera.png
├── templates
│ └── base.html
├── tests
│ ├── __init__.py
│ └── sample_tests.py
├── utils
│ ├── markdown.py
│ ├── <mark class="yellow" title="شما باید این فایل را تکمیل کنید">models.py</mark>
│ ├── semver.py
│ └── utility.py
├── db.sqlite3
├── 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="blue">
<summary>**فایل `models.py`**</summary>
باید دو مدل **`Lesson` (درسنامه)** و **`Release` (نسخه)** را مطابق زیر پیادهسازی کنید. در ادامه توضیحات مربوط به نحوهی پیادهسازی هر یک از این مدلها آورده شدهاست.
## **مدل درسنامه** (`Lesson`)
هر درسنامه شامل اطلاعاتی از جمله کلید خارجی به نویسندهی آن، رشتهای حاوی آخرین نسخهی فعال درسنامه و فیلدهای تاریخ-زمان برای نگهداری زمان ایجاد و آخرین زمان ویرایش میباشد.
| نام | نوع |
| :--------------: | ------------------------------------------: |
| `author` | کلید خارجی به مدل کاربر |
| `active_version` | رشته با طول حداکثر ۲۰ کاراکتر |
| `created_at` | تاریخ-زمان (ثبت خودکار زمان ایجاد شیء) |
| `updated_at` | تاریخ-زمان (ثبت خودکار زمان آخرین ویرایش) |
+ با حذف کاربر باید تمام درسنامههای مربوط به آن حذف شوند.
+ با استفاده از فیلد `lessons` از طرف مدل `User` باید به توان به درسنامههای مربوط به آن دسترسی پیدا کرد.
+ فیلد `active_version` رشتهای با فرمت `MAJOR.MINOR.PATCH` (سه عدد که با `.` از هم جدا شدند) است و مقدار پیشفرض آن برابر با رشتهی خالی است.
+ ترتیب پیشفرض این مدل بر اساس فیلد `updated_at` بهصورت نزولی است و درسنامههایی که جدیدتر ویرایش شدهاند بالاتر قرار میگیرند.
+ رشتهی نمایشی پیشفرض این مدل به فرمت زیر است:
```
Lesson #<mark title="شناسهی درسنامه">ID</mark> by <mark title="نام کاربری نویسنده" class="green">USERNAME</mark>
```
### **متد** `get_active_release`
اگر مقدار فیلد `active_version` خالی بود، خروجی متد برابر `None` است؛ در غیر اینصورت سه جزء اصلی نسخه را از آن استخراج کرده و شیء مربوطه از مدل نسخه (`Release`) متناظر با آن را برمیگرداند.
```python lms/models.py
def get_active_release(self):
pass
```
### **متد** `set_active_release`
مقدار فیلد `active_version` را با توجه به شیء نسخهی دریافتشده به فرمت `MAJOR.MINOR.PATCH` **بهروزرسانی** کرده و **ذخیره** میکند؛ بهگونهای که زمان آخرین ویرایش (`updated_at`) نیز بهروزرسانی شود.
```python lms/models.py
def set_active_release(self, release: 'Release'):
pass
```
## **مدل نسخه** (`Release`)
کلید اصلی مدل نسخه باید یک **کلید مرکب (Composite Primary Key)** از چهار جزء باشد: `(lesson, major, minor, patch)`.
| نام | نوع |
| :----------: | ----------------------------------------------: |
| `lesson` | ارجاع به `Lesson` |
| `major` | عدد صحیح نامنفی |
| `minor` | عدد صحیح نامنفی |
| `patch` | عدد صحیح نامنفی |
| `label` | رشتهای با حداکثر طول ۲۵۵ |
| `title` | رشتهای با حداکثر طول ۲۵۵ |
| `content` | متن طولانی |
| `color` | رشتهای با حداکثر طول ۷ |
| `created_at` | تاریخ-زمان (ثبت خودکار زمان ایجاد) |
+ با حذف هر درسنامه (`Lesson`) باید نسخههای (`Release`) مرتبط با آن نیز حذف شوند.
+ با استفاده از فیلد `releases` از طرف مدل `Lesson` میتوان به نسخههای مربوط به آن درسنامه دسترسی پیدا کرد.
+ فیلد `content` از محتوای *Markdown* پشتیبانی میکند؛ بنابراین یک متن راهنمای آن برابر `"Markdown content"` است:
+ فیلد `color` بهطور پیشفرض (در صورت خالی بودن) یک رنگ تصادفی هگز با استفاده از تابع `random_hex_color` موجود در فایل`utils/utility.py` میسازد و در خود ذخیره میکند.
+ ترتیب پیشفرض این مدل از بزرگ به کوچک بر اساس *(major, minor, patch)* و سپس زمان ایجاد (`created_at`) است.
+ رشتهی نمایشی پیشفرض این مدل به فرمت زیر است:
```
<mark class="red" title="عنوان نسخه">Title</mark> [<mark class="violet" title="مقدار عددی فیلد major">MAJOR</mark>.<mark class="pink" title="مقدار عددی فیلد minor">MINOR</mark>.<mark class="blue" title="مقدار عددی فیدل patch">PATCH</mark>]
```
### **متد**`to_semver`
این متد، یک نمونهٔ از دیتاکلاس `SemVer` (موجود در فایل از `utils/semver.py`) با دادههای معتبر ساخته و برگردانده میشود که حاوی `(major, minor, patch)` نسخهی فعلی است (از این متد در ویوها استفاده خواهد شد).
```python lms/models.py
def to_semver(self) -> SemVer:
pass
```
### **متد**`version_str`
این متد، رشتهای به فرمت `MAJOR.MINOR.PATCH` و حاوی اطلاعات صحیح را برمیگرداند.
```python lms/models.py
def version_str(self):
pass
```
</details>
<details class="green">
<summary>**فایل `utils/models.py`**</summary>
در این فایل یک چارچوب عمومی برای **حذف نرم** *(Soft Delete)* پیادهسازی میکنیم تا بهجای حذف دائمی رکوردها، زمان حذف آنها را در فیلد `deleted_at` ذخیره کنیم. بدین ترتیب، کوئریهای عادی رکوردهای حذفشده را نمیبینند و در صورت نیاز میتوان آنها را **بازیابی** یا **بهطور دائمی حذف** کرد.
## **کلاس** `SoftDeleteQuerySet`
یک `QuerySet` سفارشی که عملیاتهای حذف یا بازیابی دستهای و فیلترهای کمکی را فراهم میکند.
```python utils/models.py
class SoftDeleteQuerySet(QuerySet):
pass
```
### **متد** `delete`
بهجای حذف رکوردها، مقدار `deleted_at` را برای همهٔ نتایج روی `timezone.now()` بهروزرسانی میکند (حذف نرم کوئریست).
```python
def delete(self):
pass
```
### **متد** `hard_delete`
با فراخوانی این متد باید رکوردها از پایگاهداده به طور کامل با مکانیزم پیشفرض جنگو پاک شوند (حذف دائمی کوئریست).
```python
def hard_delete(self):
pass
```
### **متد** `restore`
فیلد `deleted_at` را برای همهی نتایج `None` میکند (بازیابی کوئریست).
```python
def restore(self):
pass
```
### **متد** `alive`
فقط رکوردهایی که حذف نشدهاند را برمیگرداند (`deleted_at` در آنها `None` است).
```python
def alive(self):
pass
```
### **متد** `dead`
فقط رکوردهای حذفشده را برمیگرداند (دارای مقدار در `deleted_at`).
```python
def dead(self):
pass
```
## **کلاس** `SoftDeleteManager`
یک `Manager` پیشفرض که **همواره رکوردهای حذفنشده** را برمیگرداند و متدهایی برای دسترسی به رکوردهای حذفشده، بازیابی و حذف دائمی فراهم میکند. این منیجر بهصورت پیشفرض با متد `.all()` فقط نتایج حذفنشده را برمیگرداند (`deleted_at` برابر `None` است).
```python utils/models.py
class SoftDeleteManager(models.Manager):
pass
```
### **متد** `with_deleted`
یک کوئریست شامل **همه** رکوردها (موجود و حذفشده) را برمیگرداند.
```python
def with_deleted(self):
pass
```
### **متد** `dead`
یک کوئریست حاوی تنها رکوردهای حذفشده را برمیگرداند.
```python
def dead(self):
pass
```
### **متد** `restore`
بازیابی دستهای همهی رکوردها (روی کوئریست برگرداندهشده توسط `with_deleted()` اعمال میشود).
```python
def restore(self):
pass
```
### **متد** `hard_delete`
با فراخوانی این متد باید رکوردهای انتخاب شده از پایگاهداده به طور کامل با مکانیزم پیشفرض جنگو پاک شوند.
```python
def hard_delete(self):
pass
```
## **کلاس** `SoftDeleteBaseModel`
کلاسی **انتزاعی *(Abstract)*** که فیلد و رفتارهای حذف نرم را به مدلهایی که از آن ارثبری میکنند اضافه میکند.
```python utils/models.py
class SoftDeleteBaseModel(models.Model):
pass
```
### **فیلد** `deleted_at`
این فیلد از نوع `DateTimeField` است و میتواند خالی باشد. وظیفهی آن نگهداری زمان حذف شدن رکورد است.
### **فیلد** `objects`
این فیلد از یک شیء از کلاس `SoftDeleteManager` استفاده میکند و منیجر پیشفرض مدلها است.
### **متد** `delete`
این متد وظیفهی حذف نرمِ شیء را بر عهده دارد. مقداردهی `deleted_at` با مقدار زمان کنونی آپدیت شود.
```python
def delete(self):
pass
```
### **متد** `hard_delete`
این متد وظیفهی حذف واقعی رکورد از پایگاهداده را بر عهده دارد و رکورد موردنظر را به صورت کامل حذف میکند.
```python
def hard_delete(self):
pass
```
### **متد** `restore`
این متد وظیفهی بازیابی شیء را برعهده دارد که با تنظیم `deleted_at` با مقدار `None` و ذخیره آن اینکار را انجام میدهد؛ در نهایت همان شیء بازیابیشده را برمیگرداند.
```python
def restore(self):
pass
```
پس از پیادهسازی این کلاس، هر مدلی که میخواهید حذف نرم داشته باشد، کافی است از `SoftDeleteBaseModel` ارثبری کند. در این حالت، رفتارهای بالا بهصورت خودکار در دسترس خواهند بود.
</details>
<details class="violet">
<summary>**فایل `mixins.py`**</summary>
## میکسین مالکیت (`OwnerRequiredMixin`)
در این فایل یک کلاس `Mixin` با نام `OwnerRequiredMixin` بسازید که فقط به **مالک درسنامه** اجازهی دسترسی بدهد. این میکسینکلاس در ویوهایی که شیء (از نوع `Lesson` یا `Release`) را میخوانند استفاده شدهاست و باید عملیات مربوط به کنترل دسترسی کاربران را بهدرستی انجام دهد.
```python lms/mixins.py
class OwnerRequiredMixin(UserPassesTestMixin):
def test_func(self):
pass
```
+ اگر شیء از نوع **درسنامه *(Lesson)*** باشد، فقط در صورتی اجازهی دسترسی صادر شود که **شناسهی نویسندهی درسنامه** با شناسهٔ کاربر فعلی برابر باشد.
+ اگر شیء از نوع **نسخه *(Release)*** بود، فقط در صورتی اجازهی دسترسی داده شود که **نویسندهی درسنامهی مربوط به آن نسخه** با کاربر فعلی برابر باشد.
+ اگر شیء ورودی از هیچکدام از انواع بالا نبود، نیازی به انجام عملیات اعتبارسنجی نیست.
**نکته:** این میکسین باید با مکانیزم استاندارد مجوزها در جنگو پیادهسازی شود تا در صورت صدق نکردن شرایط بالا، اجازهی دسترسی داده نشود و ریسپانسی با کد وضعیت ۴۰۳ برگردانده شود.
</details>
<details class="pink">
<summary>**فایل `forms.py`**</summary>
در این فایل باید دو عدد فرم با استفاده از `ModelForm` پیادهسازی کنید. متد سازندهی فرمها در پروژهی اولیه قرار داده شده و **نباید** آن را تغییر دهید. سایر بخشهای این فرمها، از جمله تعریف فیلدها و شخصیسازی ویجتهای نمایشی بر عهدهی شماست.
## **فرم** `ReleaseForm`
هدف از این فرم، ساخت نسخهی اولیه است و بدون دخالت کاربر است؛ نسخهی اولیه در ویوها همواره روی `0.1.0` تنظیم میشود. از طرفی تفاوت آن با فرم بعدی در همین مورد است؛ بهطوری که در این فرم نسخه به صورت خودکار مقداردهی میشود اما در فرم دوم، نحوهی افزایش نسخه را کاربر مشخص میکند. بنابراین برای پیادهسازی فرم بعدی، ابتدا باید این فرم را بهدرستی پیادهسازی کرده باشید.
### **فیلد** `title`
+ **نوع فیلد:** ورودی متنی با پاسخ کوتاه
+ **برچسب *(label)*:**
```
عنوان
```
+ **متن *placeholder*:**
```
مثال: معرفی اولیهٔ درسنامه
```
+ **اجباری/اختیاری:** اجباری
+ **پیام خطا:**
پیام خطای خالی بودن:
```
وارد کردن عنوان الزامی است.
```
پیام خطای طول غیرمجاز:
```
عنوان بیش از حد بلند است.
```
### **فیلد** `content`
+ **نوع فیلد/ویجت:** ورودی متنی با پاسخ بلند (`Textarea`)
+ **برچسب *(label)*:**
```
محتوا (Markdown)
```
+ **متن راهنما *(help text)*:**
```
از Markdown برای قالببندی تیترها، کد و فهرستها استفاده کنید.
```
+ **متن *placeholder*:**
```
محتوای نسخه را با Markdown بنویسید…
```
+ **اجباری/اختیاری:** اجباری
+ **پیام خطا:**
پیام خطای نبود مقدار:
```
محتوای نسخه نمیتواند خالی باشد.
```
#### فیلد `color`
+ **نوع فیلد/ویجت:** رنگ به فرمت کد هگزادسیمال (`ColorInput`)
+ **برچسب *(label)*:**
```
رنگ
```
+ **متن راهنما *(help text)*:**
```
یک رنگ هگز مانند #22C55E انتخاب کنید.
```
+ **متن *placeholder*:**
```
محتوای نسخه را با Markdown بنویسید…
```
+ **اجباری/اختیاری:** اختیاری
+ **پیام خطا:**
پیام خطای مقدار نامعتبر:
```
قالب رنگ نامعتبر است (مثلاً #22C55E).
```
#### فیلد `label`
+ **نوع فیلد/ویجت:** ورودی متنی با پاسخ کوتاه
+ **برچسب *(label)*:**
```
برچسب
```
+ **متن راهنما *(help text)*:**
```
نوع تغییرات این نسخه را مشخص کنید (feature، fix یا breaking).
```
+ **متن *placeholder*:**
```
مثال: feature / fix / breaking
```
+ **اجباری/اختیاری:** اجباری
+ **پیام خطا:**
پیام خطای نبود مقدار:
```
برچسب را وارد کنید (مثلاً feature یا fix).
```
پیام طول زیاد:
```
برچسب بیش از حد بلند است.
```
### **فیلد** `make_active`
فیلد جدیدی که خودتان باید به فرم موردنظر اضافه کنید و جزء فیلدهای مدل `Release` نیست.
+ **نوع فیلد/ویجت:** چکباکس (`BooleanField`)
+ **برچسب *(label)*:**
```
فعالسازی پس از ذخیره؟
```
+ **متن راهنما *(help text)*:**
```
پس از ذخیره، این نسخه بهعنوان نسخهٔ فعال نمایش داده میشود.
```
+ **اجباری/اختیاری:** اجباری
+ **پیشفرض:** فعال (`True`)
## **فرم** `NewVersionForm`
این فرم، نصخهٔ جدید را همراه با نوع افزایش نسخه مشخص میکند؛ درواقع فیلدهای این فرم مانند فیلدهای `ReleaseForm` است بهعلاوهی یک انتخابگر برای **نوع افزایش نسخه**. برای پیادهسازی این فرم باید از فرم `ReleaseForm` ارثبری کنید.
### **فیلد** `bump`
+ **نوع فیلد/ویجت:** انتخابی (`ChoiceField`) تک گزینه بههمراه ویجت `Select` و کلاس `"input"`
+ **برچسب *(label)*:**
```
نوع افزایش نسخه
```
+ **متن راهنما *(help text)*:**
```
نوع نسخهگذاری مطابق Semantic Versioning انتخاب شود.
```
+ **اجباری/اختیاری:** اجباری
+ **پیام خطا:**
پیام خطای نبود مقدار:
```
انتخاب نوع افزایش نسخه الزامی است.
```
+ **گزینهها:** گزینههای این فیلد بهصورت لیستی از تاپلهای دو عضوی به نام `BUMP_CHOICES` در فایل `utils/utility.py` قرار داده شدهاند. که در آن
+ مقدار `patch` به معنای «تغییرات جزئی بدون شکستن سازگاری» است.
+ مقدار `minor` به معنای «افزودن قابلیت بدون شکستن سازگاری» است.
+ مقدار `major` به معنای «تغییرات بزرگ/احتمالاً ناسازگار» است.
</details>


# **زیرمسئلهها**
سیستم داوری برای این سوال به **زیرمسئلههای** زیر برای نمرهدهی تقسیمبندی شده است که میتوانید **امتیاز** مربوط به هر کدام را در جدول زیر مشاهده کنید. **زیرمسئلههای** این جدول **ابتدا بر اساس اولویت و پیشنیازی پیادهسازی** و سپس **بر اساس امتیاز** آنها مرتبسازی شدهاند. **لذا پیشنهاد میشود در پیادهسازی از زیرمسئلهی ابتدایی آغاز کنید.**
| **زیرمسئله** | **امتیاز** |
| ----------------------------------------------- | :----: |
| **مدلهای** `Lesson` و `Release` | `75` |
| **مکانیزم** `Soft Delete` | `100` |
| **میکسین** `OwnerRequiredMixin` | `25` |
| **فرم** `ReleaseForm` | `70` |
| **فرم** `NewVersionForm` | `30` |
# **آنچه باید آپلود کنید**
+ **توجه**: پس از اعمال تغییرات، کل پروژه را _Zip_ کرده و آپلود کنید. **همانند پروژهی اولیه در فایل زیپشده نباید کد در پوشهی دیگری قرار بگیرد در غیر این صورت سیستم داوری فایل را شناسایی نکرده و نمرهای دریافت نخواهید کرد.**
+ **توجه:** تنها فایلهایی که در **ساختار پروژه** مشخص شدهاند، در سیستم داوری **مورد پذیرش** قرار خواهد گرفت و سایر تغییرات در سایر فایلها **بیتأثیر** خواهند بود.
+ **توجه:** متنهای نمونه در مدلها و فرمها باید **دقیقاً** برابر مقادیر گفتهشده باشند؛ در غیر این صورت نمرهی کامل دریافت نخواهید کرد.