**توجه کنید که سؤال «سامانه بلاگ بَله آباد» را باید با دقیقاً یکی از زبانهای *Python ،PHP ،Golang* یا *Node.js* حل کنید. در صورتی که تمایل دارید سؤال را با *PHP* حل کنید، میتوانید از طریق این بخش اقدام به حل کنید. خروجی نهایی در در زبانهای مختلف تفاوتی ندارد و میتوانید زبان مورد نظر را با توجه به دانش خود انتخاب کنید.**
----------
*سلیب* پس از ترجمه نقشه متوجه شد که گنج جایی در آبادی « *بله آباد* » است که در پشت کوههای جزیره پنهان شده است. او سریعاً حرکت کرد و هنگامی که به پشت کوههای « *بله آباد* » رسید، متوجه شده که مردمان این آبادی، سامانهای شبیه سیستم بلاگ *medium* ندارند. برای *سلیب* این موقعیت خوبیبود تا به جای جستوجوی شبانه روزی به دنبال گنج، از این طریق ثروتی کسب کند و بیخیال گنج بشود. پس سریعا درخواست پیادهسازی این سامانه را با مشخصاتی که در ادامه توضیح میدهیم را از شما دارد :).
# جزئیات پروژه
پروژهی اولیه را از [این لینک](/contest/assignments/68130/download_problem_initial_project/234187/) دانلود کنید.
در این سؤال، ما با دو بخش کلی در ارتباط هستیم، بخش کاربران و بخش بلاگها. در ادامه به بیان جزئیات هر بخش میپردازیم.
<details class="blue">
<summary> **بخش کاربران** </summary>
برای این بخش نیاز است تا تعدادی *REST API* شامل *endpoint* های زیر باید پیادهسازی شود:
| آدرس | عنوان |
| :------------------------ | ----------------------: |
| `GET /` | بررسی *up* بودن سرویس |
| `POST /auth/signup/` | ثبتنام |
| `POST /auth/login/` | ورود به حساب کاربری |
| `POST /auth/logout/` | خروج از حساب کاربری |
در این *API* هر کاربر باید یک توکن داشته باشد. این توکن برای هر کاربر ثابت است.
## پیادهسازی *endpoint* های موردنیاز بخش کاربر
**در همهی _endpoint_ ها، پاسخ باید بهصورت _JSON_ باشد.**
**اکیداً توصیه میگردد برای پیادهسازی بخش کاربر از _JWT_ استفاده کنید.**
اطلاعات ورودی بهصورت `application/x-www-form-urlencoded` به *endpoint* ها ارسال میشوند.
<details class="red">
<summary> **بررسی *up* بودن سرویس** </summary>
پاسخ این *endpoint* باید بهصورت زیر باشد:
+ کد وضعیت: `200`
+ بدنه: `{"message":Welcome to Medium API}`
</details>
<details class="red">
<summary> **ثبتنام** </summary>
سه پارامتر `username` و `password` و `email` به این *endpoint* ارسال میشوند. در صورتی که حداقل یکی از پارامترهای `username` و `password` ارسال نشده باشد یا برابر با رشتهی خالی باشد، پاسخ باید بهصورت زیر باشد:
هر دو پارامتر `username` و `password` خالی:
+ کد وضعیت: `400`
+ بدنه:
```json json json
{
"error": {
"username": ["This field may not be blank."],
"password": ["This field may not be blank."],
}
}
```
پارامتر `username` خالی:
+ کد وضعیت: `400`
+ بدنه:
```json json json
{
"error": {
"username": ["This field may not be blank."]
}
}
```
پارامتر `password` خالی:
+ کد وضعیت: `400`
+ بدنه:
```json json json
{
"error": {
"password": ["This field may not be blank."]
}
}
```
اگر کاربری با نام کاربری واردشده از قبل موجود باشد، پاسخ باید بهصورت زیر باشد:
+ کد وضعیت: `400`
+ بدنه:
```json json json
{
"error": {
"username": ["A user with that username already exists."],
}
}
```
در غیر اینصورت، کاربر باید ساخته شود، یک توکن یکتا برایش تولید شود و پاسخ بهصورت زیر باشد:
+ کد وضعیت: `201`
+ بدنه:
```json json json
{
"refresh": <mark class="yellow" title="رفرش توکن کاربر ساخته شده.">"[REFRESH_TOKEN]"</mark>,
"access": <mark class="yellow" title="اکسس توکن کاربر ساخته شده.">"[ACCESS_TOKEN]"</mark>,
}
```
</details>
<details class="red">
<summary> **ورود به حساب کاربری** </summary>
دو پارامتر `username` و `password` باید به این *endpoint* ارسال شوند. در صورتی که حداقل یکی از این پارامترها ارسال نشده باشد یا برابر با رشتهی خالی باشد، پاسخ باید بهصورت زیر باشد:
+ کد وضعیت: `401`
+ بدنه: `{"error": "Invalid credentials"}`
اگر نام کاربری یا رمز عبور نادرست باشد، پاسخ باید بهصورت زیر باشد:
+ کد وضعیت: `401`
+ بدنه: `{"error": "Invalid credentials"}`
در غیر اینصورت، پاسخ باید بهصورت زیر باشد:
+ کد وضعیت: `200`
+ بدنه:
```json json json
{
"refresh": <mark class="yellow" title="رفرش توکن کاربر ساخته شده.">"[REFRESH_TOKEN]"</mark>,
"access": <mark class="yellow" title="اکسس توکن کاربر ساخته شده.">"[ACCESS_TOKEN]"</mark>,
}
```
</details>
<details class="red">
<summary> **خروج از حساب کاربری** </summary>
تنها پارامتر `refresh` باید به این *endpoint* ارسال شود. در صورتی که این پارامتر ارسال نشده باشد یا برابر با رشتهی خالی باشد یا حتی مقدار درستی نداشته باشد، پاسخ باید بهصورت زیر باشد:
+ کد وضعیت: `400`
+ بدنه: `{}`
در غیر اینصورت، پاسخ باید بهصورت زیر باشد:
+ کد وضعیت: `205`
+ بدنه: `{}`
</details>
</details>
<details class="blue">
<summary> **بخش بلاگ** </summary>
برای این بخش نیاز است تا تعدادی *REST API* شامل *endpoint* های زیر باید پیادهسازی شود:
| آدرس | عنوان |
| :------------------------ | ----------------------: |
| `GET /blogs/` | دریافت تمامی بلاگهای ثبت شده |
| `POST /blogs/create/` | ایجاد بلاگ جدید |
| `PUT /blogs/<int:pk>/` | آپدیت بلاگ |
| `DELETE /blogs/<int:pk>/` | حذف بلاگ |
| `GET /blogs/<int:pk>/detail/` | مشاهده بلاگ |
| `POST /blogs/<int:pk>/like/` | لایک کردن بلاگ |
| `POST /blogs/analytics/` | اوضاع کلی حساب نویسنده |
## پیادهسازی *endpoint* های موردنیاز بخش کاربر
**در همهی _endpoint_ ها، پاسخ باید بهصورت _JSON_ باشد.**
اطلاعات ورودی بهصورت `application/x-www-form-urlencoded` به *endpoint* ها ارسال میشوند.
<details class="red">
<summary> **دریافت تمامی بلاگهای ثبت شده** </summary>
این *endpoint* نیازمند *authentication* **نیست**.
نیازی به ارسال هیچ پارامتری به این *endpoint* نیست. و در همه حالات باید جواب برابر با مقدار زیر باشد.
+ کد وضعیت: `200`
+ بدنه:
```json json json
[
{
"id": <mark class="yellow" title="آیدی بلاگ">"[BLOG_ID]"</mark>,
"title": <mark class="yellow" title="تایتل بلاگ">"[BLOG_TITLE]"</mark>,
"content": <mark class="yellow" title="محتوای بلاگ">"[BLOG_CONTENT]"</mark>,
"views": <mark class="yellow" title="تعداد ویوهای بلاگ">"[BLOG_VIEWS]"</mark>,
"likes": <mark class="yellow" title="تعداد لایکهای بلاگ">"[BLOG_LIKES]"</mark>,
},
<mark class="blue" title="به ازای تمامی بلاگهای ثبت شده یک رکورد باز گردانده میشود.">...</mark>
]
```
**بلاگهای بازگشت داده شده باید به ترتیب زمان ثبت بلاگ باشند.**
</details>
<details class="red">
<summary> **ایجاد بلاگ جدید** </summary>
این *endpoint* نیازمند *authentication* است. در ریکوئست ارسالی مقدار هدر `Authorization` باید برابر با توکن کاربر باشد (**توکن همواره همراه با `Bearer` ارسال میشود.**).
پارامتر `title` و `content` باید به این *endpoint* ارسال شود. در صورتی که یکی از این پارامترها ارسال نشده باشد یا برابر با رشتهی خالی باشد، پاسخ باید بهصورت زیر باشد:
+ کد وضعیت: `400`
+ بدنه:
```json json json
{
"error": {
"title": ["This field may not be blank."],
"content": ["This field may not be blank."],
}
}
```
در غیر اینصورت، بلاگ باید ثبت شود و پاسخ بهصورت زیر باشد:
+ کد وضعیت: `201`
+ بدنه:
```json json json
{
"id": <mark class="yellow" title="آیدی بلاگ">"[BLOG_ID]"</mark>,
"title": <mark class="yellow" title="تایتل بلاگ">"[BLOG_TITLE]"</mark>,
"content": <mark class="yellow" title="محتوای بلاگ">"[BLOG_CONTENT]"</mark>,
"views": <mark class="yellow" title="تعداد ویوهای بلاگ">"[BLOG_VIEWS]"</mark>,
"likes": <mark class="yellow" title="تعداد لایکهای بلاگ">"[BLOG_LIKES]"</mark>,
}
```
</details>
<details class="red">
<summary> **آپدیت بلاگ** </summary>
این *endpoint* نیازمند *authentication* است. در ریکوئست ارسالی مقدار هدر `Authorization` باید برابر با توکن کاربر باشد (**توکن همواره همراه با `Bearer` ارسال میشود.**).
پارامتر `title` و `content` باید به این *endpoint* ارسال شود. در صورتی که یکی از این پارامترها ارسال نشده باشد یا برابر با رشتهی خالی باشد، پاسخ باید بهصورت زیر باشد:
+ کد وضعیت: `400`
+ بدنه:
```json json json
{
"error": {
"title": ["This field may not be blank."],
"content": ["This field may not be blank."],
}
}
```
در صورتی که `pk` موجود در *URL* در دیتابیس وجود نداشته باشد، پاسخ باید به صورت زیر باشد:
+ کد وضعیت: `404`
+ بدنه: `{}`
در صورتی که یوزر در حال ارسال ریکوئست برابر با نویسنده بلاگ نباشد، پاسخ باید به صورت زیر باشد:
+ کد وضعیت: `403`
+ بدنه: `{}`
در غیر اینصورت، بلاگ باید آپدیت شود و پاسخ بهصورت زیر باشد:
+ کد وضعیت: `200`
+ بدنه:
```json json json
{
"id": <mark class="yellow" title="آیدی بلاگ">"[BLOG_ID]"</mark>,
"title": <mark class="yellow" title="تایتل بلاگ">"[BLOG_TITLE]"</mark>,
"content": <mark class="yellow" title="محتوای بلاگ">"[BLOG_CONTENT]"</mark>,
"views": <mark class="yellow" title="تعداد ویوهای بلاگ">"[BLOG_VIEWS]"</mark>,
"likes": <mark class="yellow" title="تعداد لایکهای بلاگ">"[BLOG_LIKES]"</mark>,
}
```
</details>
<details class="red">
<summary> **حذف بلاگ** </summary>
این *endpoint* نیازمند *authentication* است. در ریکوئست ارسالی مقدار هدر `Authorization` باید برابر با توکن کاربر باشد (**توکن همواره همراه با `Bearer` ارسال میشود.**).
پارامتری به این *endpoint* ارسال نمیشود. در صورتی که `pk` موجود در *URL* در دیتابیس وجود نداشته باشد، پاسخ باید به صورت زیر باشد:
+ کد وضعیت: `404`
+ بدنه: `{}`
در صورتی که یوزر در حال ارسال ریکوئست برابر با نویسنده بلاگ نباشد، پاسخ باید به صورت زیر باشد:
+ کد وضعیت: `403`
+ بدنه: `{}`
در غیر اینصورت، بلاگ باید حذف شود و پاسخ بهصورت زیر باشد:
+ کد وضعیت: `204`
+ بدنه: `{}`
</details>
<details class="red">
<summary> **مشاهده بلاگ** </summary>
این *endpoint* نیازمند *authentication* **نیست**.
نیازی به ارسال هیچ پارامتری به این *endpoint* نیست. و در همه حالات باید جواب برابر با مقادیر زیر باشد:
در صورتی که `pk` موجود در *URL* در دیتابیس وجود نداشته باشد، پاسخ باید به صورت زیر باشد:
+ کد وضعیت: `404`
+ بدنه: `{}`
در غیر اینصورت، بلاگ باید ثبت شود و پاسخ بهصورت زیر باشد:
+ کد وضعیت: `200`
+ بدنه:
```json json json
{
"id": <mark class="yellow" title="آیدی بلاگ">"[BLOG_ID]"</mark>,
"title": <mark class="yellow" title="تایتل بلاگ">"[BLOG_TITLE]"</mark>,
"content": <mark class="yellow" title="محتوای بلاگ">"[BLOG_CONTENT]"</mark>,
"views": <mark class="yellow" title="تعداد ویوهای بلاگ">"[BLOG_VIEWS]"</mark>,
"likes": <mark class="yellow" title="تعداد لایکهای بلاگ">"[BLOG_LIKES]"</mark>,
}
```
توجه داشته باشید که در صورتی که بلاگ مورد نظر وجود داشت و کاربر بدون مشکل میتوانست آن را مشاهده کند، باید به میزان `view` بلاگ یک واحد اضافه کنید **و سپس بلاگ را برگردانید.** یعنی بازدید فعلی کاربر در میزان بازدیدهای بلاگ باز گردانده شده باید **محاسبه شده باشد.**
**نکته ریت لیمیت:** در اینجا نیاز به پیادهسازی ساز و کاری برای پیادهسازی ریت لیمیت داریم. میخواهیم تا هر `device_id` و `ip` بتواند تنها *10* بار **هر بلاگ** را ببیند تا میزان ویوهای هر بلاگ شهودی واقعی از میزان دیده شدن بلاگ بدهد. تا *10* ریکوئست برای گرفتن یک بلاگ از یک `device_id` و `ip` را بدون مشکل باز گردانید و میزان ویو بلاگ را هم اضافه کنید. به محض عبور از *10* نیاز است تا پاسخ برابر زیر باشد و به میزان ویو بلاگ **هیچ عددی اضافه نشود.**
+ کد وضعیت: `429`
+ بدنه: `{}`
</details>
<details class="red">
<summary> **لایک کردن بلاگ** </summary>
این *endpoint* نیازمند *authentication* است. در ریکوئست ارسالی مقدار هدر `Authorization` باید برابر با توکن کاربر باشد (**توکن همواره همراه با `Bearer` ارسال میشود.**).
پارامتری به این *endpoint* ارسال نمیشود. در صورتی که `pk` موجود در *URL* در دیتابیس وجود نداشته باشد، پاسخ باید به صورت زیر باشد:
+ کد وضعیت: `404`
+ بدنه: `{}`
در صورتی که توکن `Authorization` وجود نداشت و در اصل یوزر لاگین نبود، پاسخ باید به صورت زیر باشد:
+ کد وضعیت: `401`
+ بدنه: `{}`
در غیر اینصورت، باید یک عدد به لایکهای بلاگ اضافه شود و پاسخ بهصورت زیر باشد:
+ کد وضعیت: `200`
+ بدنه:
```json json json
{
"id": <mark class="yellow" title="آیدی بلاگ">"[BLOG_ID]"</mark>,
"title": <mark class="yellow" title="تایتل بلاگ">"[BLOG_TITLE]"</mark>,
"content": <mark class="yellow" title="محتوای بلاگ">"[BLOG_CONTENT]"</mark>,
"views": <mark class="yellow" title="تعداد ویوهای بلاگ">"[BLOG_VIEWS]"</mark>,
"likes": <mark class="yellow" title="تعداد لایکهای بلاگ">"[BLOG_LIKES]"</mark>,
}
```
</details>
<details class="red">
<summary> **اوضاع کلی حساب نویسنده** </summary>
این *endpoint* نیازمند *authentication* است. در ریکوئست ارسالی مقدار هدر `Authorization` باید برابر با توکن کاربر باشد (**توکن همواره همراه با `Bearer` ارسال میشود.**).
در صورتی که توکن `Authorization` وجود نداشت و در اصل یوزر لاگین نبود، پاسخ باید به صورت زیر باشد:
+ کد وضعیت: `401`
+ بدنه: `{}`
در غیر اینصورت، باید مجموع تمام `view` و `like` های تمام بلاگهای نویسنده را محسابه کند و پاسخ بهصورت زیر باشد:
+ کد وضعیت: `200`
+ بدنه:
```json json json
{
"total_views": <mark class="yellow" title="مجموع ویوهای همه بلاگهای نویسنده">"int"</mark>,
"total_likes": <mark class="yellow" title="مجموع لایکهای همه بلاگهای نویسنده">"int"</mark>
}
```
</details>
</details>
# نکات تکمیلی
نحوه پیادهسازی کاملا بر عهده خودتان است اما برای دریافت نمره کامل مسئله نیاز است تا تمامی دیتاهای مسئله را `persist` کنید. برای این کار در سیستم داوری ما دیتابیس پستگرسی با مشخصات زیر بالاست:
## دیتابیس `postgres`
هاست: `medium_postgres`
پورت: `5432`
یوزرنیم: `quera`
پسورد: `quera`
دیتابیس: `quera`
<details class="blue">
<summary>
نصب نیازمندیها و اجرا
</summary>
برای حل این سؤال در پوشه `medium` کد برنامه را نوشته و در فایلی به نام `entry.sh` که توسط `sh` اجرا میشود، باید برنامهی خود را اجرا کنید. توجه کنید که ما در سیستم داوری اسکریپت `entry.sh` شما را اجرا میکنیم و پس از آن به آدرس `localhost` ریکوئست میزنیم.
برای نصب نیازمندیهای پیاچپی از `composer.json` استفاده کنید.
</details>
+ نیازی به *persistent* بودن دادهها نیست! اما برای دریافت امتیاز کامل مسئله باید دیتاها را *persistent* کنید.
+ شما مجاز به تغییر یا ارسال `docker-compose.yml`و داکرفایل دلخواه نیستید.
+ سرویس شما باید روی پورت `80` آدرس `localhost` قابل دسترسی باشد.
+ توصیه میکنیم در `entry.sh` خود *API*تان را روی `0.0.0.0:80` اجرا کنید.
+ **فراموش نکنید که حتما فایل** `composer.json` **را در دایرکتوری** `medium` **بسازید و تمامی نیازمندیهای پروژهتان را درون آن بنویسید.**
## نحوه ارسال پاسخ
شما میتوانید تمامی محتوای موجود در پوشهی `medium` را تغییر دهید و هر فایلی که میخواهید اضافه یا کم کنید.
```text
medium
├── <mark class="purple" title="نام این فایلها و فولدرها اهمیتی ندارند"> ... </mark>
├── composer.json
└── entry.sh
```
توجه کنید که نام فایل کد شما برای سیستم داوری اهمیتی ندارد و این خود شما هستید که در `entry.sh` از نام آن برای اجرای پروژه استفاده میکنید.
در نهایت این پوشه را _ZIP_ کرده و ارسال کنید. توجه کنید که پس از _extract_ کردن فایل _ZIP_ شما، باید پوشهی `medium` را ببینیم که درون آن فایلهای پروژه وجود دارد.
سامانه بلاگ بَله آباد - PHP