Repository: rework-union/django-rework (language: python)

Issue:
400 bad request response format 
Response status code 400.

Response body contains `error_message`, `error_code`.

for example:
```python
return Response({
    'error_code': 'point_not_enough',
    'error_message': 'User's point not enough!',
}, status=status.HTTP_400_BAD_REQUEST)
```


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG b/CHANGELOG
index eaee793..0ea9eae 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -3,7 +3,14 @@
 # This project adheres to [Semantic Versioning](http://semver.org/).
 # includes Added / Changed / Fixed
 
-## [0.5.2] UNRELEASED
+## [0.5.3] UNRELEASED
+
+## [0.5.2] 2023-03-03
+### Added
+- 400 bad request response format
+### Changed
+- Upgraded all dependency packages
+- Drop Python 3.7 support
 
 ## [0.5.1] 2023-02-11
 ### Fixed
diff --git a/README.md b/README.md
index 5e9d67a..f6690f8 100644
--- a/README.md
+++ b/README.md
@@ -6,24 +6,24 @@
 [![Django](https://img.shields.io/pypi/djversions/django-rework)](https://www.djangoproject.com)
 [![License](https://img.shields.io/pypi/l/django-rework)](https://opensource.org/licenses/MIT)
 
-Rapid develop framework base on Django
+Rapid develop framework base on Django, integrated with Django-Ninja.
 
-# _Installation_
+# _Installation_ & Cli commands
 
 ## Requirements
 
-- Python >= 3.7
+- Python >= 3.8
 - Django >= 3.2
 - Django REST framework >=3.13,<4.0
 
 ## Install django-rework
 ```bash
-python3 -m pip install django-rework
+pip install django-rework
 ```
 
-# _Generic CLI Commands_
+## Generic CLI Commands
 
-## Start a new project
+### Start a new project
 
 ```bash
 # It will create project in current dir
@@ -31,24 +31,48 @@ python3 -m pip install django-rework
 rework init pony
 ```
 
-## Add App
+### Add build-in contrib Apps
 
 ```bash
 rework add users
 ```
 
-## Add deployment configurations
+### Add deployment configurations
 
 ```bash
 rework deploy --init
 ```
 
-## Deploy to test or production
+### Deploy to test or production
 
 ```bash
-rework deploy
+rework deploy  # or 'fab -H web1 deploy'
 ```
 
+# _Core docs_
+
+## Custom exceptions
+
+The exception handler must also be configured in your settings, using the EXCEPTION_HANDLER setting key. For example:
+
+```python
+REST_FRAMEWORK = {
+    'EXCEPTION_HANDLER': 'rework.core.views.exception_handler'
+}
+```
+
+Use build-in custom exceptions:
+```python
+from rework.core.exceptions import ValidateError
+ValidateError(
+    detail='You do not have permission to perform this action.', 
+    code='permission_denied',
+)
+```
+
+🥭 [Build-in custom exceptions](rework/core/exceptions.py)
+
+
 # _App docs_
 
 | App      | Description | docs in English | 简体中文文档  |
diff --git a/requirements.txt b/requirements.txt
index f48105f..2f5b655 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,15 +1,15 @@
 Django>=3.2,<5.0
-django-cors-headers==3.13.0
-django-environ==0.9.0
-django-filter==22.1
+django-cors-headers>=3.13.0,<4.0
+django-environ>=0.9.0,<1.0
+django-filter==22.1,<23
 django-mysql~=4.7
 django-ninja~=0.20
 djangorestframework>=3.13,<4.0
 djangorestframework-simplejwt~=5.2
-fabric~=2.7
+fabric>=2.7.1,<3.0
 Markdown~=3.4
 mysqlclient~=2.1
-patchwork==1.0.1
+patchwork~=1.0.1
 Pillow~=9.0
 drf-nested-routers~=0.93
 drf-yasg~=1.21
diff --git a/rework/core/__init__.py b/rework/core/__init__.py
index 93b60a1..45869b6 100644
--- a/rework/core/__init__.py
+++ b/rework/core/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '0.5.1'
+__version__ = '0.5.2'
diff --git a/rework/core/exceptions.py b/rework/core/exceptions.py
new file mode 100644
index 0000000..140352c
--- /dev/null
+++ b/rework/core/exceptions.py
@@ -0,0 +1,18 @@
+from rest_framework.exceptions import APIException
+
+
+class ServiceUnavailable(APIException):
+    status_code = 503
+    default_detail = 'Service temporarily unavailable, try again later.'
+    default_code = 'service_unavailable'
+
+
+class ValidateError(APIException):
+    status_code = 400
+    default_detail = 'Request validate error.'
+    default_code = 'validate_error'
+
+    def __init__(self, detail=None, code=None, errors=None):
+        self.errors = errors
+        super().__init__(detail, code)
+
diff --git a/rework/core/views.py b/rework/core/views.py
new file mode 100644
index 0000000..207fc37
--- /dev/null
+++ b/rework/core/views.py
@@ -0,0 +1,16 @@
+from rest_framework.views import exception_handler as rest_framework_exception_handler
+
+
+def exception_handler(exc, context):
+    # Call REST framework's default exception handler first,
+    # to get the standard error response.
+    response = rest_framework_exception_handler(exc, context)
+
+    # Now add the HTTP status code to the response.
+    if response is not None:
+        response.data['code'] = exc.get_codes()
+        response.data['detail'] = str(exc.detail)
+        if exc.errors:
+            response.data['errors'] = exc.errors
+
+    return response


Test patch:
diff --git a/tests/test_clis/test_handle_urls.py b/tests/test_clis/test_handle_urls.py
index d83b28d..8a17546 100644
--- a/tests/test_clis/test_handle_urls.py
+++ b/tests/test_clis/test_handle_urls.py
@@ -24,7 +24,7 @@ def test_add_route_to_urlpatterns(self):
 
 urlpatterns = [
     path('admin/', admin.site.urls),
-    re_path(r'^api/tests/', include('rework.contrib.tests.urls', namespace='tests')),
+    re_path(r'^api/tests/', include('rework.contrib.tests.urls')),
 ]
 
 """
diff --git a/tests/test_contrib/__init__.py b/tests/test_contrib/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_core/__init__.py b/tests/test_core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_core/test_custom_exceptions.py b/tests/test_core/test_custom_exceptions.py
new file mode 100644
index 0000000..95ada0d
--- /dev/null
+++ b/tests/test_core/test_custom_exceptions.py
@@ -0,0 +1,38 @@
+import unittest
+
+from rework.core.exceptions import ValidateError
+
+
+class ValidateErrorTestCase(unittest.TestCase):
+    def test_validate_error_with_code(self):
+        detail = 'You do not have permission to perform this action.'
+        code = 'permission_denied'
+        ex = ValidateError(
+            detail=detail,
+            code=code,
+        )
+        self.assertEqual(ex.get_codes(), code)
+        self.assertEqual(str(ex.detail), detail)
+        self.assertIsNone(ex.errors)
+
+    def test_validate_error_with_errors(self):
+        detail = 'You do not have permission to perform this action.'
+        code = 'permission_denied'
+        errors = {
+            "name": {
+                "message": "This field is required.",
+                "code": "required"
+            },
+            "age": {
+                "message": "A valid integer is required.",
+                "code": "invalid"
+            }
+        }
+        ex = ValidateError(
+            detail=detail,
+            code=code,
+            errors=errors,
+        )
+        self.assertEqual(ex.get_codes(), code)
+        self.assertEqual(str(ex.detail), detail)
+        self.assertIsInstance(ex.errors, dict)