*سلیب* پس از ترجمهی نقشه متوجه شد که گنج جایی در آبادی *«بلهآباد»* است که در پشت کوههای جزیره پنهان شدهاست. او سریعاً حرکت کرد و هنگامیکه به پشت کوههای *«بلهآباد»* رسید، متوجه شد که مردمان این آبادی، سامانهای شبیه سیستم بلاگ *Medium* ندارند. برای *سلیب* این موقعیت خوبی بود تا به جای جستوجوی شبانهروزی به دنبال گنج، از این طریق ثروتی کسب کند و بیخیال گنج بشود؛ پس درخواست پیادهسازی این سامانه را با مشخصاتی که در ادامه توضیح میدهیم، از شما دارد. :)
# جزئیات پروژه
پروژهی اولیه را از [این لینک](/contest/assignments/68134/download_problem_initial_project/234212/) دانلود کنید.
در این سؤال، ما با دو بخش کلی در ارتباط هستیم: بخش کاربران و بخش بلاگها. در ادامه جزئیات هر بخش را بیان میکنیم.
<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/` | لایک کردن بلاگ |
| `GET /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` کنید، امتیاز بیشتری از مسئله دریافت میکنید.
**شما تنها مجاز به استفاده از ایمیجهای موجود در [این لینک](https://gitlab.com/qio/standard/container_registry/?orderBy=UPDATED&sort=desc) در داکر فایل و داکر کامپوز خود هستید.**
# نکات تکمیلی
<details class="blue">
<summary>
نصب نیازمندیها و اجرا
</summary>
برای حل این سؤال میتوانید از هر زبان و هر تکنولوژیای که میخواهید استفاده کنید. بهصورتیکه در یک پوشه به نام `medium` کد برنامه را بنویسید. توجه کنید که حتماً باید `Dockerfile` مربوط به پروژهی خود را برای ما ارسال کنید.
</details>
+ نیازی به *persistent* بودن دادهها نیست! اما برای دریافت امتیاز کامل مسئله باید دیتاها را *persistent* کنید.
+ سیستم داوری `docker-compose.yml` تحویلی شما را که در خارج از فولدر `medium` قرار دارد، با دستور `docker-compose up --build` اجرا میکند.
```yaml docker-compose.yml yaml
version: "3"
# add your services here
```
+ شما مجاز به تغییر یا ارسال `docker-compose.yml` دلخواه هستید.
+ **نام سرویس و کانتیر کد سرور شما در فایل** `docker-compose.yml` **حتما باید برابر با** `medium` **باشد.**
+ سرویس شما باید روی پورت `80` آدرس `localhost` قابل دسترسی باشد.
+ توصیه میکنیم خود *API*تان را روی `0.0.0.0:80` اجرا کنید.
+ ورژن فایل داکرکامپوز شما باید حتما برابر با `3` باشد.
## نحوهی ارسال پاسخ
شما میتوانید تمامی محتوای موجود در پوشهی `medium` را تغییر دهید و هر فایلی که میخواهید اضافه یا کم کنید.
```text
├── medium
├──├── <mark class="purple" title="نام این فایلها و فولدرها اهمیتی ندارند."> [ALL_YOUR_PROJECT] </mark> # or main.go somefile.js anyfile.php name.any ...
├──├── Dockerfile
└── docker-compose.yml
```
توجه کنید که نام فایل کد شما برای سیستم داوری اهمیتی ندارد و این خود شما هستید که در داکر فایل و داکر کامپوزتان از نام آن برای اجرای پروژه استفاده میکنید.
در نهایت پوشه `medium` را به همراه `docker-compose.yml` _ZIP_ کرده و ارسال کنید. توجه کنید که پس از _extract_ کردن فایل _ZIP_ شما، باید فایل `docker-compose.yml` پوشهی `medium` را ببینیم که درون آن `Dockerfile` وجود دارد.