+ محدودیت زمان: ۱ ثانیه
+ محدودیت حافظه: ۲۵۶ مگابایت
----------
در پایین [وبلاگهای «بله»،](https://blog.bale.ai/?utm_source=bale&utm_medium=main_website&utm_campaign=button) کاربرها میتوانند نظرات خود را بنویسند. در پایین یکی از وبلاگها تعدادی نظر ثبت شده ولی به دلیل تغییرات شرایط آبوهوایی این اطلاعات برای یکی از پستها بهم ریخته است.
میدانیم در این پست، $n$ نظر توسط کاربران ثبت شده است (ترتیب نظرات را نمیدانیم). همچنین برای هر نظر میدانیم توسط چه کسی و در چه لحظهای ثبت شده، همچنین محتوای آن نظر را میدانیم. برخی از نظرات پاسخی به نظرات دیگر است. آنها را باید به نحوی که در نمونهها میبینید، با فاصله از سمت چپ صفحه نشان دهید.
فرض کنید عرض صفحهی پیامها برابر $L$ است. یعنی هر پیام در یک جعبه قرار میگیرد که حداکثر $L$ کاراکتر در آن قرار دارد.
در قسمت بالا سمت چپ نظر، نام فرستنده و پایین سمت راست نظر زمان ارسال نشان داده میشود.
حال از شما میخواهیم صفحهی نظرات را بازسازی کنید.
## نکات جعبه حاوی نظر
<details class="blue">
<summary>
**سطر اول جعبه**
</summary>
----------
سطر اول حاوی نامِ کاربر است. ابتدا و انتهای این سطر کاراکتر `+` چاپ کنید. بقیه سطر حاوی $L$ کاراکتر `-` است ولی باید بعد از اولین `-`، نام کاربر را جایگزین `-`ها کنید. برای مثال اگر $L=15$ و نام کاربر برابر `Amin` باشد سطر اول بهصورت زیر میشود.
```
+-Amin----------+
```
</details>
<details class="blue">
<summary>
**سطرهای میانی جعبه**
</summary>
----------
در سطرهای میانی باید نظر کاربر را بنویسید، ابتدا و انتهای هر سطر که ایجاد میکنید باید `+` باشد و بین دو `+` به اندازهی $L$ کاراکتر فاصله باشد. در این فاصله باید پیام کاربر را بنویسید.
اگر بین کلمات چندین فاصله یا *space* وجود داشت، آنها را یکی در نظر بگیرید و شما به اندازهی یک واحد فاصله دهید. همچنین فاصله یا *space*های اول و آخر متن را در نظر نگیرید.
همهی عبارتها را به ترتیب در سطرها بنویسید اگر کلمهای در انتهای یک سطر جا نمیشد، آن کلمه را کاملاً به سطر بعدی منتقل کنید. تضمین میشود مقدار $L$ به اندازهی کافی بزرگ باشد که بتواند یک کلمه را کاملاً در خود جا بدهد.
برای مثال اگر $L = 15$ و متن نظر کاربر برابر عبارت زیر باشد
```
" I am Your User. Can You Help Me. "
```
شما باید به صورت زیر بنویسید
```
+I am Your User.+
+Can You Help +
+Me. +
```
</details>
<details class="blue">
<summary>
**سطر پایانی جعبه**
</summary>
----------
سطر آخر حاوی زمان ارسال نظر است. ابتدا و انتهای این سطر کاراکتر `+` چاپ کنید. بقیه سطر حاوی $L$ کاراکتر `-` است ولی باید قبل از آخرین `-`، زمان ارسال نظر را جایگزین `-`ها کنید. برای مثال اگر $L=15$ و زمان ارسال نظر برابر `00:13:47` باشد سطر آخر بهصورت زیر میشود.
```
+------00:13:47-+
```
</details>
## انواع نظرات
<details class="green">
<summary>
**نوع`send`**
</summary>
----------
```
send <username> hh:mm:ss "<comment>"
```
بهجای `<username>` یک رشته از حروف کوچک یا بزرگ یا ارقام یا `,`، `.` یا `;` است. طول این رشته حداکثر ۱۰ است.
$$0 \leq hh \leq 23, \quad 0 \leq mm \leq 59, \quad 0 \leq ss \leq 59$$
بهجای `<comment>` یک رشته شامل تعدادی کلمه مشابه `<username>` است و کلمههای آن با تعدادی فاصله یا *space* از هم جدا شده است.
برای مثال
```
send Amin 00:13:47 " I am Your User. Can You Help Me. "
```
</details>
<details class="green">
<summary>
**نوع`reply`**
</summary>
----------
```
reply <username> <index> hh:mm:ss "<comment>"
```
بهجای `<username>` یک رشته از حروف کوچک یا بزرگ یا ارقام یا `,`، `.` یا `;` است. طول این رشته حداکثر ۱۰ است.
بهجای `<index>` شمارهی سطر نظری را نشان میدهد که این نظر به آن جواب داده است. شمارهها را از ۱ تا $n$ به ترتیب ورودی در نظر بگیرید.
$$0 \leq hh \leq 23, \quad 0 \leq mm \leq 59, \quad 0 \leq hh \leq 59$$
بهجای `<comment>` یک رشته شامل تعدادی کلمه مشابه `<username>` است و کلمههای آن با تعدادی فاصله یا *space* از هم جدا شده است.
برای مثال (ریپلای آخر به نظر `Amir` است.)
```
send Ali 00:20:00 "second"
send Amir 00:10:00 "first"
send Hossein 00:30:00 "third"
reply Amin 2 00:40:00 " I am Your User. Can You Help Me? "
```
</details>
## نکات مربوط به ریپلای
<details class="yellow">
<summary>
**فاصله نظرات از چپ**
</summary>
----------
اگر نظری پاسخ یک نظر دیگر باشد باید قبل از هر سطر جعبهی آن ۴ کاراکتر `<` را اضافه کنید. اگر همان نظر پاسخ نظر دیگری باشد باید ۸ کاراکتر `<` را اضافه کنید و...
برای مثال اگر ورودی به این صورت باشد:
```
3 15
send A 00:10:00 "Hi"
reply B 1 00:20:00 "Hi"
reply C 2 00:30:00 "Hi"
```
خروجی به صورت زیر است:
```
+-A-------------+
+Hi +
+------00:10:00-+
>>>>+-B-------------+
>>>>+Hi +
>>>>+------00:20:00-+
>>>>>>>>+-C-------------+
>>>>>>>>+Hi +
>>>>>>>>+------00:30:00-+
```
</details>
<details class="yellow">
<summary>
**ترتیب نظرات**
</summary>
----------
شما باید نظرها را به ترتیب زمان ارسال مرتب کنید. همچنین پاسخ هر نظر باید زیر آن نظر باشد. اینکه پاسخ هر نظر باید زیر آن نظر باشد، به ترتیب زمان الویت دارد.
برای مثال اگر ورودی به این صورت باشد:
```
3 15
send D 00:40:00 "Hi"
send A 00:10:00 "Hi"
send B 00:20:00 "Hi"
reply C 2 00:30:00 "Hi"
```
خروجی به این صورت میشود:
```
+-A-------------+
+Hi +
+------00:10:00-+
>>>>+-C-------------+
>>>>+Hi +
>>>>+------00:30:00-+
+-B-------------+
+Hi +
+------00:20:00-+
+-D-------------+
+Hi +
+------00:40:00-+
```
</details>
# زیرمسئلهها
| زیرمسئلهها | توضیح | امتیاز |
|:---:|:---:|:---:|
| ۱ | فقط یک پیام را به درستی نشان دهید. | ۳۰ |
| ۲ | چند پیام را به درستی نشان دهید. | ۷۵ |
| ۳ | یک پیام و یک پاسخ به آن را به درستی نشان دهید. |۳۰ |
| ۴ | چند پیام و چند پاسخ به آن را به درستی نشان دهید. تضمین میشود به پاسخها پاسخی داده نشود. | ۷۵ |
| ۵ | هیچ شرط خاصی نداریم. | ۹۰ |
# ورودی
در سطر اول ورودی، دو عدد صحیح $n$ و $L$ با فاصله از هم داده میشود. $n$ نشان دهندهی تعداد دستورها و $L$ عرض صفحه است.
$$1 \leq n \leq 100, \quad \quad 15 \leq L \leq 30$$
در $n$ سطر بعدی در هر سطر یک دستور مطابق با ساختاری که در بالا گفته شده، داده میشود.
ورودیها لزوماً به ترتیب زمان نیستند ولی تضمین میشود ریپلای یک پیام دیرتر از پیام آمده باشد. همچنین هیچ وقت زمان دو پیام برابر نیست.
# خروجی
خروجی شما باید دقیقاً مثل خروجیهای نمونه و طبق خواستهی سوال باشد.
# مثالها
## ورودی نمونه ۱
```
4 15
send amin 00:00:18 "that was so good I liked it"
send mahla 00:10:19 "Is it rated"
reply romina 2 00:10:30 "yes it is rated"
send arshia 00:00:50 "I love quera contests"
````
## خروجی نمونه ۱
```
+-amin----------+
+that was so +
+good I liked it+
+------00:00:18-+
+-arshia--------+
+I love quera +
+contests +
+------00:00:50-+
+-mahla---------+
+Is it rated +
+------00:10:19-+
>>>>+-romina--------+
>>>>+yes it is rated+
>>>>+------00:10:30-+
````
## ورودی نمونه ۲
```
3 20
send Amin 00:00:18 "That Was So gOod I Liked It"
reply Mahla 1 00:10:19 "Is It Rated"
reply Romina 2 00:10:30 "Yes It Is RaTeD"
````
## خروجی نمونه ۲
```
+-Amin---------------+
+That Was So gOod I +
+Liked It +
+-----------00:00:18-+
>>>>+-Mahla--------------+
>>>>+Is It Rated +
>>>>+-----------00:10:19-+
>>>>>>>>+-Romina-------------+
>>>>>>>>+Yes It Is RaTeD +
>>>>>>>>+-----------00:10:30-+
````
## ورودی نمونه ۳
```
4 15
reply user2 4 00:30:00 " u p "
reply user3 4 00:40:00 " u p "
reply user4 1 00:50:00 " u p "
send user1 00:10:00 " u p "
````
## خروجی نمونه ۳
```
+-user1---------+
+u p +
+------00:10:00-+
>>>>+-user2---------+
>>>>+u p +
>>>>+------00:30:00-+
>>>>>>>>+-user4---------+
>>>>>>>>+u p +
>>>>>>>>+------00:50:00-+
>>>>+-user3---------+
>>>>+u p +
>>>>+------00:40:00-+
````
<details class="red">
<summary>
**لینکهای مفید برای حل سوال**
</summary>
----------
+ [قالب صورت سؤال](https://quera.org/course/assignments/2693/problems/8773)
+ [نحوهی کار با ورودی و خروجی](https://quera.org/course/assignments/2693/problems/8774)
+ [خطاها](https://quera.org/course/assignments/2693/problems/8776)
</details>
نظرات وبلاگ - پیادهسازی
**کوئریهای شما باید روی _MySQL_ قابل اجرا باشند.** برای چک کردن سینتکس کد *SQL* خود میتوانید از **[این وبسایت](https://www.eversql.com/sql-syntax-check-validator/)** کمک بگیرید.
در این سوال بخشی از پایگاهداده یک پیامرسان در اختیار شما قرار داده شده است، این بخش از پایگاهداده شامل اطلاعات حدود ۱۰۰۰ کاربر و ۲۵۰۰ گروه میباشد.
# جزئیات پایگاهداده
دادههای اولیه برای **تست نهایی** را از [این لینک](/contest/assignments/68130/download_problem_initial_project/234190/) دانلود کنید.
<details class="yellow">
<summary>
**توضیحات در مورد دادههای اولیه**
</summary>
در فایل *Messenger.zip* فایلی به اسم initial.sql وجود دارد.
با اجرای این فایل، همه جداول و سطرهایی که برای **تست نهایی** مورد استفاده قرار میگیرد در پایگاهدادهای به `Messenger`، ایجاد میشوند.
</details>
<details class="grey">
<summary>
**توضیحات جداول پایگاهداده**
</summary>
ساختار جداول بهشرح زیر است:
۱.**جدول `users`**: از این جدول برای نگهداری اطلاعات کاربران استفاده میشود. ساختار این جدول بهصورت زیر است:
|نام ستون|نوع|تعریف|
|:-------|:--|----:|
|`id`|`INT`|شناسه کاربر (کلید اصلی)|
|`phone`|`VARCHAR(20)`|شماره موبایل کاربر (یکتا)|
|`email`|`VARCHAR(256)`|ایمیل کاربر (یکتا)|
|`name`|`VARCHAR(128)`|نام و نامخانوادگی کاربر|
|`password`|`VARCHAR(128)`|رمز عبور کاربر (هَش شده)|
|`reported_num`|`int`|تعداد دفعاتی که کاربر گزارش (*report*) شده|
|`created_at`|`datetime`|تاریخ عضویت|
|`updated_at`|`datetime`|تاریخ بهروز رسانی حساب|
|`is_suspend`|`VARCHAR(1)`|آیا کاربر تعلیق شدهاست (مقدار یک) یا خیر (مقدار صفر)|
۲.**جدول `block_list`**: از این جدول برای نگهداری اطلاعات افراد بلاک (مسدود) شده استفاده میشود. ساختار این جدول بهصورت زیر است:
| نام ستون| نوع| تعریف|
| --------- | ------------ | ------------ |
| `id`| `INT`| شناسهی جدول (کلید اصلی) |
| `user_id` | `INT`| شناسه کاربر بلاک کننده|
| `blocked_user_id` | `INT`| شناسه کاربر بلاک شده|
۳.**جدول `group_conversations`**: از این جدول برای نگهداری اطلاعات گروهها استفاده میشود. ساختار این جدول بهصورت زیر است:
|نام ستون|نوع|تعریف|
|:-------|:--|----:|
|`id`|`INT`|شناسه گروه (کلید اصلی)|
|`string_id`|`VARCHAR(20)`|آدرس (*url*) گروه (یکتا)|
|`onwer_id`|`INT`|شناسه کاربر مالک گروه|
|`name`|`VARCHAR(50)`|نام گروه|
۴.**جدول `group_conversation_users`**: از این جدول برای نگهداری اطلاعات کاربران هر گروه استفاده میشود. ساختار این جدول بهصورت زیر است:
|نام ستون|نوع|تعریف|
|:-------|:--|----:|
|`id`|`INT`|شناسه جدول (کلید اصلی)|
|`group_id`|`INT`|شناسه گروه|
| `user_id` | `INT`| شناسه کاربر حاضر در گروه|
|`is_admin`|`VARCHAR(1)`|آیا کاربر ادمین گروه هست (مقدار ۱) یا خیر (مقدار ۰)|
</details>
# مطلوبات🔗
کوئریهایی بنویسید که مطلوبات خواسته شده در هر قسمت را انجام دهد:
۱. همانطور که در توضیحات جداول گفته شده در جدول کاربران (`users`) ستونی به نام `is_suspend` وجود دارد که نشان میدهد یک کاربر تعلیق شده است یا خیر، اگر به این ستون پایگاهداده دقت کنید، متوجه میشوید که این مقدار برای تمامی رکوردها **صفر** است. حال شما باید کوئری بنویسید که این ستون را برای کاربرانی که ۵ بار یا بیشتر گزارش (ستون `reported_num`) شدهاند، برابر **یک** قرار دهد.
۲. لیستی از کاربران و تعداد بلاک شدن آنها توسط کاربران دیگر.
</details>
<details class="blue">
<summary>
*توضیحات مورد مطلوب دوم*
</summary>
نام هر کاربر را در ستونی به نام `blocked_user_name` و تعداد کاربرانی که این کاربر را بلاک کرده اند را در ستونی با نام `block_count` نمایش دهید. همچنین توجه داشته باشید خروجی شما باید بر اساس تعداد بلاکها (ستون `block_count`) به صورت **نزولی** مرتب شود و در صورتی که این مقدار برابر بود بر اساس نام کاربر به صورت **صعودی** مرتب شود.
3 سطر اول خروجی شما باید به شکل زیر باشد.
| blocked_user_name | block_count |
| :---: | :---: |
| Sandra Garcia | 8 |
| Bryan Waters | 7 |
| Danielle Henry | 7 |
</details>
۳. لیستی از کاربران و کسانی که با آنها در گروهی مشترک هستند و نام آن گروه.
</details>
<details class="blue">
<summary>
*توضیحات مورد مطلوب سوم*
</summary>
نام کاربر اول را در ستونی به نام `first_user` و نام کاربر دوم را در ستونی با نام `second_user` و نام گروهی که کاربران در آن مشترک هستند را در ستونی با نام `mutual_group` قرار دهید و به ترتیب ابتدا کابر اول سپس کاربر دوم و در نهایت براساس نام گروه به صورت **صعودی** مرتب شود.
توجه داشته باشید شناسه کاربر اول باید همیشه از کاربر دوم بیشتر باشد و همچنین اجرای این کوئری ممکن است کمی زمانگیر باشد.
3 سطر اول خروجی شما باید به شکل زیر باشد.
| first_user | second_user |mutual_group|
| :---: | :---: |:---:|
|Aaron Foster| Anna Little | Thompson, Sullivan and Turner |
|Aaron Foster | Ashley Scott | Anderson-Hill |
|Aaron Foster | Benjamin Porter | Carrillo-Parker |
</details>
۴. پس از اعمال کوئری اول ستون `is_suspend` دارای مقادیر معنی دار است، حال ساختاری را بر روی پایگاهداده خود پیاده کنید که هنگامی که تعداد گزارش شدن یک کاربر (ستون `reported_num`) به ۵ میرسد (بهروزرسانی میشود) کاربر تعلیق شود (مقدار ستون `is_suspend` **یک** شود) و بر عکس، اگر کاربری تعداد گزارش شدنش کمتر از ۵ شد از تعلیق در بیاید (مقدار ستون `is_suspend` **صفر** شود).
<details class="red">
<summary>
توجه
</summary>
به روش ارسال پاسخ قسمت چهارم توجه داشته باشید. همچنین توجه داشته باشید برای دریافت امتیاز این قسمت باید امتیاز قسمت اول (مطلوب اول همین سوال) را دریافت کرده باشید.
</details>
# **روش ارسال پاسخ**
در یک فایل با نام `code.sql` کد خود را قرار دهید و آن را فشرده (`zip` ) کنید و در سایت بارگذاری نمایید. کد شما باید به صورت زیر باشد. (نام فایل zip مهم نیست).
```sql
-- Section1
your first query here
-- Section2
your second query here
-- Section3
your third query here
-- Section4
Delimiter $$
your fourth query here
$$
```
چالش اطلاعات - دیتابیس
**توجه کنید که سؤال «سامانه بلاگ بَله آباد» را باید با دقیقاً یکی از زبانهای *Python ،PHP ،Golang* یا *Node.js* حل کنید. در صورتی که تمایل دارید سؤال را با *Python* حل کنید، میتوانید از طریق این بخش اقدام به حل کنید. خروجی نهایی در در زبانهای مختلف تفاوتی ندارد و میتوانید زبان مورد نظر را با توجه به دانش خود انتخاب کنید.**
----------
*سلیب* پس از ترجمه نقشه متوجه شد که گنج جایی در آبادی « *بله آباد* » است که در پشت کوههای جزیره پنهان شده است. او سریعاً حرکت کرد و هنگامی که به پشت کوههای « *بله آباد* » رسید، متوجه شده که مردمان این آبادی، سامانهای شبیه سیستم بلاگ *medium* ندارند. برای *سلیب* این موقعیت خوبیبود تا به جای جستوجوی شبانه روزی به دنبال گنج، از این طریق ثروتی کسب کند و بیخیال گنج بشود. پس سریعا درخواست پیادهسازی این سامانه را با مشخصاتی که در ادامه توضیح میدهیم را از شما دارد :).
# جزئیات پروژه
پروژهی اولیه را از [این لینک](/contest/assignments/68130/download_problem_initial_project/234186/) دانلود کنید.
در این سؤال، ما با دو بخش کلی در ارتباط هستیم، بخش کاربران و بخش بلاگها. در ادامه به بیان جزئیات هر بخش میپردازیم.
<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` ریکوئست میزنیم.
برای نصب نیازمندیهای پایتون از `requirements.txt` استفاده کنید.
</details>
+ نیازی به *persistent* بودن دادهها نیست! اما برای دریافت امتیاز کامل مسئله باید دیتاها را *persistent* کنید.
+ شما مجاز به تغییر یا ارسال `docker-compose.yml`و داکرفایل دلخواه نیستید.
+ سرویس شما باید روی پورت `80` آدرس `localhost` قابل دسترسی باشد.
+ توصیه میکنیم در `entry.sh` خود *API*تان را روی `0.0.0.0:80` اجرا کنید.
+ **فراموش نکنید که حتما فایل** `requirements.txt` **را در دایرکتوری** `medium` **بسازید و تمامی نیازمندیهای پروژهتان را درون آن بنویسید.**
## نحوه ارسال پاسخ
شما میتوانید تمامی محتوای موجود در پوشهی `medium` را تغییر دهید و هر فایلی که میخواهید اضافه یا کم کنید.
```text
medium
├── <mark class="purple" title="نام این فایلها و فولدرها اهمیتی ندارند"> ... </mark>
├── requirements.txt
└── entry.sh
```
توجه کنید که نام فایل کد شما برای سیستم داوری اهمیتی ندارد و این خود شما هستید که در `entry.sh` از نام آن برای اجرای پروژه استفاده میکنید.
در نهایت این پوشه را _ZIP_ کرده و ارسال کنید. توجه کنید که پس از _extract_ کردن فایل _ZIP_ شما، باید پوشهی `medium` را ببینیم که درون آن فایلهای پروژه وجود دارد.
سامانه بلاگ بَله آباد - Python
**توجه کنید که سؤال «سامانه بلاگ بَله آباد» را باید با دقیقاً یکی از زبانهای *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
**توجه کنید که سؤال «سامانه بلاگ بَله آباد» را باید با دقیقاً یکی از زبانهای *Python ،PHP ،Golang* یا *Node.js* حل کنید. در صورتی که تمایل دارید سؤال را با *Golang* حل کنید، میتوانید از طریق این بخش اقدام به حل کنید. خروجی نهایی در در زبانهای مختلف تفاوتی ندارد و میتوانید زبان مورد نظر را با توجه به دانش خود انتخاب کنید.**
----------
*سلیب* پس از ترجمه نقشه متوجه شد که گنج جایی در آبادی « *بله آباد* » است که در پشت کوههای جزیره پنهان شده است. او سریعاً حرکت کرد و هنگامی که به پشت کوههای « *بله آباد* » رسید، متوجه شده که مردمان این آبادی، سامانهای شبیه سیستم بلاگ *medium* ندارند. برای *سلیب* این موقعیت خوبیبود تا به جای جستوجوی شبانه روزی به دنبال گنج، از این طریق ثروتی کسب کند و بیخیال گنج بشود. پس سریعا درخواست پیادهسازی این سامانه را با مشخصاتی که در ادامه توضیح میدهیم را از شما دارد :).
# جزئیات پروژه
پروژهی اولیه را از [این لینک](/contest/assignments/68130/download_problem_initial_project/234188/) دانلود کنید.
در این سؤال، ما با دو بخش کلی در ارتباط هستیم، بخش کاربران و بخش بلاگها. در ادامه به بیان جزئیات هر بخش میپردازیم.
<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` ریکوئست میزنیم.
برای نصب نیازمندیهای گولنگ از `go.mod` استفاده کنید.
</details>
+ نیازی به *persistent* بودن دادهها نیست! اما برای دریافت امتیاز کامل مسئله باید دیتاها را *persistent* کنید.
+ شما مجاز به تغییر یا ارسال `docker-compose.yml`و داکرفایل دلخواه نیستید.
+ سرویس شما باید روی پورت `80` آدرس `localhost` قابل دسترسی باشد.
+ توصیه میکنیم در `entry.sh` خود *API*تان را روی `0.0.0.0:80` اجرا کنید.
+ **فراموش نکنید که حتما فایل** `go.mod` **را در دایرکتوری** `medium` **بسازید و تمامی نیازمندیهای پروژهتان را درون آن بنویسید.**
## نحوه ارسال پاسخ
شما میتوانید تمامی محتوای موجود در پوشهی `medium` را تغییر دهید و هر فایلی که میخواهید اضافه یا کم کنید.
```text
medium
├── <mark class="purple" title="نام این فایلها و فولدرها اهمیتی ندارند"> ... </mark>
├── go.mod
└── entry.sh
```
توجه کنید که نام فایل کد شما برای سیستم داوری اهمیتی ندارد و این خود شما هستید که در `entry.sh` از نام آن برای اجرای پروژه استفاده میکنید.
در نهایت این پوشه را _ZIP_ کرده و ارسال کنید. توجه کنید که پس از _extract_ کردن فایل _ZIP_ شما، باید پوشهی `medium` را ببینیم که درون آن فایلهای پروژه وجود دارد.
سامانه بلاگ بَله آباد - Golang
**توجه کنید که سؤال «سامانه بلاگ بَله آباد» را باید با دقیقاً یکی از زبانهای *Python ،PHP ،Golang* یا *Node.js* حل کنید. در صورتی که تمایل دارید سؤال را با *Node.js* حل کنید، میتوانید از طریق این بخش اقدام به حل کنید. خروجی نهایی در در زبانهای مختلف تفاوتی ندارد و میتوانید زبان مورد نظر را با توجه به دانش خود انتخاب کنید.**
----------
*سلیب* پس از ترجمه نقشه متوجه شد که گنج جایی در آبادی « *بله آباد* » است که در پشت کوههای جزیره پنهان شده است. او سریعاً حرکت کرد و هنگامی که به پشت کوههای « *بله آباد* » رسید، متوجه شده که مردمان این آبادی، سامانهای شبیه سیستم بلاگ *medium* ندارند. برای *سلیب* این موقعیت خوبیبود تا به جای جستوجوی شبانه روزی به دنبال گنج، از این طریق ثروتی کسب کند و بیخیال گنج بشود. پس سریعا درخواست پیادهسازی این سامانه را با مشخصاتی که در ادامه توضیح میدهیم را از شما دارد :).
# جزئیات پروژه
پروژهی اولیه را از [این لینک](/contest/assignments/68130/download_problem_initial_project/234189/) دانلود کنید.
در این سؤال، ما با دو بخش کلی در ارتباط هستیم، بخش کاربران و بخش بلاگها. در ادامه به بیان جزئیات هر بخش میپردازیم.
<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` ریکوئست میزنیم.
برای نصب نیازمندیهای نودجیاس از `package.json` استفاده کنید.
</details>
+ نیازی به *persistent* بودن دادهها نیست! اما برای دریافت امتیاز کامل مسئله باید دیتاها را *persistent* کنید.
+ شما مجاز به تغییر یا ارسال `docker-compose.yml`و داکرفایل دلخواه نیستید.
+ سرویس شما باید روی پورت `80` آدرس `localhost` قابل دسترسی باشد.
+ توصیه میکنیم در `entry.sh` خود *API*تان را روی `0.0.0.0:80` اجرا کنید.
+ **فراموش نکنید که حتما فایل** `package.json` **را در دایرکتوری** `medium` **بسازید و تمامی نیازمندیهای پروژهتان را درون آن بنویسید.**
## نحوه ارسال پاسخ
شما میتوانید تمامی محتوای موجود در پوشهی `medium` را تغییر دهید و هر فایلی که میخواهید اضافه یا کم کنید.
```text
medium
├── <mark class="purple" title="نام این فایلها و فولدرها اهمیتی ندارند"> ... </mark>
├── package.json
└── entry.sh
```
توجه کنید که نام فایل کد شما برای سیستم داوری اهمیتی ندارد و این خود شما هستید که در `entry.sh` از نام آن برای اجرای پروژه استفاده میکنید.
در نهایت این پوشه را _ZIP_ کرده و ارسال کنید. توجه کنید که پس از _extract_ کردن فایل _ZIP_ شما، باید پوشهی `medium` را ببینیم که درون آن فایلهای پروژه وجود دارد.