mirror of
https://github.com/HackSoftware/Django-Styleguide.git
synced 2024-11-29 04:53:46 +03:00
Apply prettier to README
This commit is contained in:
parent
0e6f5f1430
commit
2720e607c6
426
README.md
426
README.md
|
@ -9,7 +9,6 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
**Table of contents:**
|
**Table of contents:**
|
||||||
|
|
||||||
<!-- toc -->
|
<!-- toc -->
|
||||||
|
@ -20,58 +19,58 @@
|
||||||
- [Why not?](#why-not)
|
- [Why not?](#why-not)
|
||||||
- [Cookie Cutter](#cookie-cutter)
|
- [Cookie Cutter](#cookie-cutter)
|
||||||
- [Models](#models)
|
- [Models](#models)
|
||||||
* [Base model](#base-model)
|
- [Base model](#base-model)
|
||||||
* [Validation - `clean` and `full_clean`](#validation---clean-and-full_clean)
|
- [Validation - `clean` and `full_clean`](#validation---clean-and-full_clean)
|
||||||
* [Validation - constraints](#validation---constraints)
|
- [Validation - constraints](#validation---constraints)
|
||||||
* [Properties](#properties)
|
- [Properties](#properties)
|
||||||
* [Methods](#methods)
|
- [Methods](#methods)
|
||||||
* [Testing](#testing)
|
- [Testing](#testing)
|
||||||
- [Services](#services)
|
- [Services](#services)
|
||||||
* [Example - function-based service](#example---function-based-service)
|
- [Example - function-based service](#example---function-based-service)
|
||||||
* [Example - class-based service](#example---class-based-service)
|
- [Example - class-based service](#example---class-based-service)
|
||||||
* [Naming convention](#naming-convention)
|
- [Naming convention](#naming-convention)
|
||||||
* [Modules](#modules)
|
- [Modules](#modules)
|
||||||
* [Selectors](#selectors)
|
- [Selectors](#selectors)
|
||||||
* [Testing](#testing-1)
|
- [Testing](#testing-1)
|
||||||
- [APIs & Serializers](#apis--serializers)
|
- [APIs & Serializers](#apis--serializers)
|
||||||
* [Naming convention](#naming-convention-1)
|
- [Naming convention](#naming-convention-1)
|
||||||
* [Class-based vs. Function-based](#class-based-vs-function-based)
|
- [Class-based vs. Function-based](#class-based-vs-function-based)
|
||||||
* [List APIs](#list-apis)
|
- [List APIs](#list-apis)
|
||||||
+ [Plain](#plain)
|
- [Plain](#plain)
|
||||||
+ [Filters + Pagination](#filters--pagination)
|
- [Filters + Pagination](#filters--pagination)
|
||||||
* [Detail API](#detail-api)
|
- [Detail API](#detail-api)
|
||||||
* [Create API](#create-api)
|
- [Create API](#create-api)
|
||||||
* [Update API](#update-api)
|
- [Update API](#update-api)
|
||||||
* [Fetching objects](#fetching-objects)
|
- [Fetching objects](#fetching-objects)
|
||||||
* [Nested serializers](#nested-serializers)
|
- [Nested serializers](#nested-serializers)
|
||||||
* [Advanced serialization](#advanced-serialization)
|
- [Advanced serialization](#advanced-serialization)
|
||||||
- [Urls](#urls)
|
- [Urls](#urls)
|
||||||
- [Settings](#settings)
|
- [Settings](#settings)
|
||||||
* [Prefixing environment variables with `DJANGO_`](#prefixing-environment-variables-with-django_)
|
- [Prefixing environment variables with `DJANGO_`](#prefixing-environment-variables-with-django_)
|
||||||
* [Integrations](#integrations)
|
- [Integrations](#integrations)
|
||||||
* [Reading from `.env`](#reading-from-env)
|
- [Reading from `.env`](#reading-from-env)
|
||||||
- [Errors & Exception Handling](#errors--exception-handling)
|
- [Errors & Exception Handling](#errors--exception-handling)
|
||||||
* [How exception handling works (in the context of DRF)](#how-exception-handling-works-in-the-context-of-drf)
|
- [How exception handling works (in the context of DRF)](#how-exception-handling-works-in-the-context-of-drf)
|
||||||
+ [DRF's `ValidationError`](#drfs-validationerror)
|
- [DRF's `ValidationError`](#drfs-validationerror)
|
||||||
+ [Django's `ValidationError`](#djangos-validationerror)
|
- [Django's `ValidationError`](#djangos-validationerror)
|
||||||
* [Describe how your API errors are going to look like.](#describe-how-your-api-errors-are-going-to-look-like)
|
- [Describe how your API errors are going to look like.](#describe-how-your-api-errors-are-going-to-look-like)
|
||||||
* [Know how to change the default exception handling behavior.](#know-how-to-change-the-default-exception-handling-behavior)
|
- [Know how to change the default exception handling behavior.](#know-how-to-change-the-default-exception-handling-behavior)
|
||||||
* [Approach 1 - Use DRF's default exceptions, with very little modifications.](#approach-1---use-drfs-default-exceptions-with-very-little-modifications)
|
- [Approach 1 - Use DRF's default exceptions, with very little modifications.](#approach-1---use-drfs-default-exceptions-with-very-little-modifications)
|
||||||
* [Approach 2 - HackSoft's proposed way](#approach-2---hacksofts-proposed-way)
|
- [Approach 2 - HackSoft's proposed way](#approach-2---hacksofts-proposed-way)
|
||||||
* [More ideas](#more-ideas)
|
- [More ideas](#more-ideas)
|
||||||
- [Testing](#testing-2)
|
- [Testing](#testing-2)
|
||||||
* [Naming conventions](#naming-conventions)
|
- [Naming conventions](#naming-conventions)
|
||||||
- [Celery](#celery)
|
- [Celery](#celery)
|
||||||
* [The basics](#the-basics)
|
- [The basics](#the-basics)
|
||||||
* [Error handling](#error-handling)
|
- [Error handling](#error-handling)
|
||||||
* [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
* [Structure](#structure)
|
- [Structure](#structure)
|
||||||
* [Periodic Tasks](#periodic-tasks)
|
- [Periodic Tasks](#periodic-tasks)
|
||||||
* [Beyond](#beyond)
|
- [Beyond](#beyond)
|
||||||
- [Cookbook](#cookbook)
|
- [Cookbook](#cookbook)
|
||||||
* [Handling updates with a service](#handling-updates-with-a-service)
|
- [Handling updates with a service](#handling-updates-with-a-service)
|
||||||
- [DX (Developer Experience)](#dx-developer-experience)
|
- [DX (Developer Experience)](#dx-developer-experience)
|
||||||
* [`mypy` / type annotations](#mypy--type-annotations)
|
- [`mypy` / type annotations](#mypy--type-annotations)
|
||||||
- [Django Styleguide in the Wild](#django-styleguide-in-the-wild)
|
- [Django Styleguide in the Wild](#django-styleguide-in-the-wild)
|
||||||
- [Additional resources](#additional-resources)
|
- [Additional resources](#additional-resources)
|
||||||
- [Inspiration](#inspiration)
|
- [Inspiration](#inspiration)
|
||||||
|
@ -116,24 +115,24 @@ The core of the Django Styleguide can be summarized as follows:
|
||||||
|
|
||||||
**In Django, business logic should live in:**
|
**In Django, business logic should live in:**
|
||||||
|
|
||||||
* Services - functions, that mostly take care of writing things to the database.
|
- Services - functions, that mostly take care of writing things to the database.
|
||||||
* Selectors - functions, that mostly take care of fetching things from the database.
|
- Selectors - functions, that mostly take care of fetching things from the database.
|
||||||
* Model properties (with some exceptions).
|
- Model properties (with some exceptions).
|
||||||
* Model `clean` method for additional validations (with some exceptions).
|
- Model `clean` method for additional validations (with some exceptions).
|
||||||
|
|
||||||
**In Django, business logic should not live in:**
|
**In Django, business logic should not live in:**
|
||||||
|
|
||||||
* APIs and Views.
|
- APIs and Views.
|
||||||
* Serializers and Forms.
|
- Serializers and Forms.
|
||||||
* Form tags.
|
- Form tags.
|
||||||
* Model `save` method.
|
- Model `save` method.
|
||||||
* Custom managers or querysets.
|
- Custom managers or querysets.
|
||||||
* Signals.
|
- Signals.
|
||||||
|
|
||||||
**Model properties vs selectors:**
|
**Model properties vs selectors:**
|
||||||
|
|
||||||
* If the property spans multiple relations, it should better be a selector.
|
- If the property spans multiple relations, it should better be a selector.
|
||||||
* If the property is non-trivial & can easily cause `N + 1` queries problem, when serialized, it should better be a selector.
|
- If the property is non-trivial & can easily cause `N + 1` queries problem, when serialized, it should better be a selector.
|
||||||
|
|
||||||
The general idea is to "separate concerns" so those concerns can be maintainable / testable.
|
The general idea is to "separate concerns" so those concerns can be maintainable / testable.
|
||||||
|
|
||||||
|
@ -167,7 +166,7 @@ But trying to place all of your business logic in a custom manager is not a grea
|
||||||
|
|
||||||
1. Business logic has its own domain, which is not always directly mapped to your data model (models)
|
1. Business logic has its own domain, which is not always directly mapped to your data model (models)
|
||||||
1. Business logic most often spans across multiple models, so it's really hard to choose where to place something.
|
1. Business logic most often spans across multiple models, so it's really hard to choose where to place something.
|
||||||
- Let's say you have a custom piece of logic that touches models `A`, `B`, `C`, and `D`. Where do you put it?
|
- Let's say you have a custom piece of logic that touches models `A`, `B`, `C`, and `D`. Where do you put it?
|
||||||
1. There can be additional calls to 3rd party systems. You don't want those in your custom manager methods.
|
1. There can be additional calls to 3rd party systems. You don't want those in your custom manager methods.
|
||||||
|
|
||||||
**The idea is to let your domain live separately from your data model & API layer.**
|
**The idea is to let your domain live separately from your data model & API layer.**
|
||||||
|
@ -196,9 +195,9 @@ We recommend starting every new project with some kind of cookiecutter. Having t
|
||||||
|
|
||||||
Few examples:
|
Few examples:
|
||||||
|
|
||||||
* You can use the [`Styleguide-Example`](https://github.com/HackSoftware/Styleguide-Example) project as a starting point.
|
- You can use the [`Styleguide-Example`](https://github.com/HackSoftware/Styleguide-Example) project as a starting point.
|
||||||
* You can also use [`cookiecutter-django`](https://github.com/pydanny/cookiecutter-django) since it has a ton of good stuff inside.
|
- You can also use [`cookiecutter-django`](https://github.com/pydanny/cookiecutter-django) since it has a ton of good stuff inside.
|
||||||
* Or you can create something that works for your case & turn it into a [cookiecutter](https://cookiecutter.readthedocs.io/en/latest/) project.
|
- Or you can create something that works for your case & turn it into a [cookiecutter](https://cookiecutter.readthedocs.io/en/latest/) project.
|
||||||
|
|
||||||
## Models
|
## Models
|
||||||
|
|
||||||
|
@ -723,8 +722,8 @@ If we take the example above, our service is named `user_create`. The pattern is
|
||||||
|
|
||||||
This is what we prefer in HackSoft's projects. This seems odd at first, but it has few nice features:
|
This is what we prefer in HackSoft's projects. This seems odd at first, but it has few nice features:
|
||||||
|
|
||||||
* **Namespacing.** It's easy to spot all services starting with `user_` and it's a good idea to put them in a `users.py` module.
|
- **Namespacing.** It's easy to spot all services starting with `user_` and it's a good idea to put them in a `users.py` module.
|
||||||
* **Greppability.** Or in other words, if you want to see all actions for a specific entity, just grep for `user_`.
|
- **Greppability.** Or in other words, if you want to see all actions for a specific entity, just grep for `user_`.
|
||||||
|
|
||||||
### Modules
|
### Modules
|
||||||
|
|
||||||
|
@ -788,12 +787,12 @@ If you decide to cover the service layer with tests, we have few general rules o
|
||||||
|
|
||||||
When creating the required state for a given test, one can use a combination of:
|
When creating the required state for a given test, one can use a combination of:
|
||||||
|
|
||||||
* Fakes (We recommend using [`faker`](https://github.com/joke2k/faker))
|
- Fakes (We recommend using [`faker`](https://github.com/joke2k/faker))
|
||||||
* Other services, to create the required objects.
|
- Other services, to create the required objects.
|
||||||
* Special test utility & helper methods.
|
- Special test utility & helper methods.
|
||||||
* Factories (We recommend using [`factory_boy`](https://factoryboy.readthedocs.io/en/latest/orms.html))
|
- Factories (We recommend using [`factory_boy`](https://factoryboy.readthedocs.io/en/latest/orms.html))
|
||||||
* Plain `Model.objects.create()` calls, if factories are not yet introduced in the project.
|
- Plain `Model.objects.create()` calls, if factories are not yet introduced in the project.
|
||||||
* Usually, whatever suits you better.
|
- Usually, whatever suits you better.
|
||||||
|
|
||||||
**Let's take a look at our service from the example:**
|
**Let's take a look at our service from the example:**
|
||||||
|
|
||||||
|
@ -827,9 +826,9 @@ def item_buy(
|
||||||
|
|
||||||
The service:
|
The service:
|
||||||
|
|
||||||
* Calls a selector for validation.
|
- Calls a selector for validation.
|
||||||
* Creates an object.
|
- Creates an object.
|
||||||
* Delays a task.
|
- Delays a task.
|
||||||
|
|
||||||
**Those are our tests:**
|
**Those are our tests:**
|
||||||
|
|
||||||
|
@ -887,31 +886,31 @@ When using services & selectors, all of your APIs should look simple & identical
|
||||||
|
|
||||||
**When we are creating new APIs, we follow those general rules:**
|
**When we are creating new APIs, we follow those general rules:**
|
||||||
|
|
||||||
* Have 1 API per operation. This means, for CRUD on a model, having 4 APIs.
|
- Have 1 API per operation. This means, for CRUD on a model, having 4 APIs.
|
||||||
* Inherit from the most simple `APIView` or `GenericAPIView`.
|
- Inherit from the most simple `APIView` or `GenericAPIView`.
|
||||||
* Avoid the more abstract classes, since they tend to manage things via serializers & we want to do that via services & selectors.
|
- Avoid the more abstract classes, since they tend to manage things via serializers & we want to do that via services & selectors.
|
||||||
* **Don't do business logic in your API.**
|
- **Don't do business logic in your API.**
|
||||||
* You can do **object fetching / data manipulation in your APIs** (potentially, you can extract that to somewhere else).
|
- You can do **object fetching / data manipulation in your APIs** (potentially, you can extract that to somewhere else).
|
||||||
* If you are calling `some_service` in your API, you can extract object fetching / data manipulation to `some_service_parse`.
|
- If you are calling `some_service` in your API, you can extract object fetching / data manipulation to `some_service_parse`.
|
||||||
* Basically, keep the APIs are simple as possible. They are an interface towards your core business logic.
|
- Basically, keep the APIs are simple as possible. They are an interface towards your core business logic.
|
||||||
|
|
||||||
When we are talking about APIs, we need a way to deal with data serialization - both incoming & outgoing data.
|
When we are talking about APIs, we need a way to deal with data serialization - both incoming & outgoing data.
|
||||||
|
|
||||||
**Here are our rules for API serialization:**
|
**Here are our rules for API serialization:**
|
||||||
|
|
||||||
* There should be a dedicated **input serializer** & a dedicated **output serializer**.
|
- There should be a dedicated **input serializer** & a dedicated **output serializer**.
|
||||||
* **Input serializer** takes care of the data coming in.
|
- **Input serializer** takes care of the data coming in.
|
||||||
* **Output serializer** takes care of the data coming out.
|
- **Output serializer** takes care of the data coming out.
|
||||||
* In terms of serialization, Use whatever abstraction works for you.
|
- In terms of serialization, Use whatever abstraction works for you.
|
||||||
|
|
||||||
**In case you are using DRF's serializers, here are our rules:**
|
**In case you are using DRF's serializers, here are our rules:**
|
||||||
|
|
||||||
* Serializer should be **nested in the API** and be named either `InputSerializer` or `OutputSerializer`.
|
- Serializer should be **nested in the API** and be named either `InputSerializer` or `OutputSerializer`.
|
||||||
* Our preference is for both serializers to inherit from the simpler `Serializer` and avoid using `ModelSerializer`
|
- Our preference is for both serializers to inherit from the simpler `Serializer` and avoid using `ModelSerializer`
|
||||||
* This is a matter of preference and choice. If `ModelSerializer` is working fine for you, use it.
|
- This is a matter of preference and choice. If `ModelSerializer` is working fine for you, use it.
|
||||||
* If you need a nested serializer, use the `inline_serializer` util.
|
- If you need a nested serializer, use the `inline_serializer` util.
|
||||||
* Reuse serializers as little as possible.
|
- Reuse serializers as little as possible.
|
||||||
* Reusing serializers may expose you to unexpected behavior, when something changes in the base serializers.
|
- Reusing serializers may expose you to unexpected behavior, when something changes in the base serializers.
|
||||||
|
|
||||||
### Naming convention
|
### Naming convention
|
||||||
|
|
||||||
|
@ -931,10 +930,10 @@ We have the following preferences:
|
||||||
For us, the added benefits of using classes for APIs / views are the following:
|
For us, the added benefits of using classes for APIs / views are the following:
|
||||||
|
|
||||||
1. You can inherit a `BaseApi` or add mixins.
|
1. You can inherit a `BaseApi` or add mixins.
|
||||||
- If you are using function-based APIs / views, you'll need to do the same, but with decorators.
|
- If you are using function-based APIs / views, you'll need to do the same, but with decorators.
|
||||||
2. The class creates a namespace where you can nest things (attributes, methods, etc.).
|
2. The class creates a namespace where you can nest things (attributes, methods, etc.).
|
||||||
- Additional API configuration can be done via class attributes.
|
- Additional API configuration can be done via class attributes.
|
||||||
- In the case of function-based APIs / views, you need to stack decorators.
|
- In the case of function-based APIs / views, you need to stack decorators.
|
||||||
|
|
||||||
Here's an example with a class, inheriting a `BaseApi`:
|
Here's an example with a class, inheriting a `BaseApi`:
|
||||||
|
|
||||||
|
@ -983,7 +982,7 @@ class UserListApi(APIView):
|
||||||
return Response(data)
|
return Response(data)
|
||||||
```
|
```
|
||||||
|
|
||||||
*Keep in mind this API is public by default. Authentication is up to you.*
|
_Keep in mind this API is public by default. Authentication is up to you._
|
||||||
|
|
||||||
#### Filters + Pagination
|
#### Filters + Pagination
|
||||||
|
|
||||||
|
@ -1418,8 +1417,8 @@ When it comes to Django settings, we tend to follow the folder structure from [`
|
||||||
|
|
||||||
- We separate Django specific settings from other settings.
|
- We separate Django specific settings from other settings.
|
||||||
- Everything should be included in `base.py`.
|
- Everything should be included in `base.py`.
|
||||||
- There should be nothing that's only included in `production.py`.
|
- There should be nothing that's only included in `production.py`.
|
||||||
- Things that need to only work in production are controlled via environment variables.
|
- Things that need to only work in production are controlled via environment variables.
|
||||||
|
|
||||||
Here's the folder structure of our [`Styleguide-Example`](https://github.com/HackSoftware/Styleguide-Example) project:
|
Here's the folder structure of our [`Styleguide-Example`](https://github.com/HackSoftware/Styleguide-Example) project:
|
||||||
|
|
||||||
|
@ -1449,9 +1448,9 @@ In `config/django`, we put everything that's Django related:
|
||||||
- `base.py` contains most of the settings & imports everything else from `config/settings`
|
- `base.py` contains most of the settings & imports everything else from `config/settings`
|
||||||
- `production.py` imports from `base.py` and then overwrites some specific settings for production.
|
- `production.py` imports from `base.py` and then overwrites some specific settings for production.
|
||||||
- `test.py` imports from `base.py` and then overwrites some specific settings for running tests.
|
- `test.py` imports from `base.py` and then overwrites some specific settings for running tests.
|
||||||
- This should be used as the settings module in `pytest.ini`.
|
- This should be used as the settings module in `pytest.ini`.
|
||||||
- `local.py` imports from `base.py` and can overwrite some specific settings for local development.
|
- `local.py` imports from `base.py` and can overwrite some specific settings for local development.
|
||||||
- If you want to use that, point to `local` in `manage.py`. Otherwise stick with `base.py`
|
- If you want to use that, point to `local` in `manage.py`. Otherwise stick with `base.py`
|
||||||
|
|
||||||
In `config/settings`, we put everything else:
|
In `config/settings`, we put everything else:
|
||||||
|
|
||||||
|
@ -1588,9 +1587,7 @@ def some_service():
|
||||||
The response payload is going to look like this:
|
The response payload is going to look like this:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[
|
["Some message"]
|
||||||
"Some message"
|
|
||||||
]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This looks strange, because if we do it like this:
|
This looks strange, because if we do it like this:
|
||||||
|
@ -1607,7 +1604,7 @@ The response payload is going to look like this:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"error": "Some message"
|
"error": "Some message"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1627,7 +1624,7 @@ The response payload is going to look like this:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": "Not found."
|
"detail": "Not found."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1719,7 +1716,7 @@ As an example, we might decide that our errors are going to look like this:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"message": "Some error message here"
|
"message": "Some error message here"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1750,7 +1747,7 @@ We want to end up with errors, always looking like that:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": "Some error"
|
"detail": "Some error"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1758,7 +1755,7 @@ or
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": ["Some error", "Another error"]
|
"detail": ["Some error", "Another error"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1766,7 +1763,7 @@ or
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": { "key": "... some arbitrary nested structure ..." }
|
"detail": { "key": "... some arbitrary nested structure ..." }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1824,11 +1821,9 @@ Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": {
|
"detail": {
|
||||||
"non_field_errors": [
|
"non_field_errors": ["Some error message"]
|
||||||
"Some error message"
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1847,7 +1842,7 @@ Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": "You do not have permission to perform this action."
|
"detail": "You do not have permission to perform this action."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1866,7 +1861,7 @@ Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": "Not found."
|
"detail": "Not found."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1883,9 +1878,7 @@ Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": [
|
"detail": ["Some error message"]
|
||||||
"Some error message"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1902,9 +1895,9 @@ Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": {
|
"detail": {
|
||||||
"error": "Some error message"
|
"error": "Some error message"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1937,20 +1930,16 @@ Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": {
|
"detail": {
|
||||||
"foo": [
|
"foo": ["This field is required."],
|
||||||
"This field is required."
|
"email": [
|
||||||
],
|
"Ensure this field has at least 200 characters.",
|
||||||
"email": [
|
"Enter a valid email address."
|
||||||
"Ensure this field has at least 200 characters.",
|
],
|
||||||
"Enter a valid email address."
|
"nested": {
|
||||||
],
|
"bar": ["This field is required."]
|
||||||
"nested": {
|
|
||||||
"bar": [
|
|
||||||
"This field is required."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1970,7 +1959,7 @@ Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": "Request was throttled."
|
"detail": "Request was throttled."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1988,14 +1977,10 @@ Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": {
|
"detail": {
|
||||||
"password": [
|
"password": ["This field cannot be blank."],
|
||||||
"This field cannot be blank."
|
"email": ["This field cannot be blank."]
|
||||||
],
|
}
|
||||||
"email": [
|
|
||||||
"This field cannot be blank."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -2007,10 +1992,10 @@ We are going to propose an approach, that can be easily extended into something
|
||||||
|
|
||||||
1. **Your application will have its own hierarchy of exceptions**, that are going to be thrown by the business logic.
|
1. **Your application will have its own hierarchy of exceptions**, that are going to be thrown by the business logic.
|
||||||
1. Lets say, for simplicity, that we are going to have only 1 error - `ApplicationError`.
|
1. Lets say, for simplicity, that we are going to have only 1 error - `ApplicationError`.
|
||||||
- This is going to be defined in a special `core` app, within `exceptions` module. Basically, having `project.core.exceptions.ApplicationError`.
|
- This is going to be defined in a special `core` app, within `exceptions` module. Basically, having `project.core.exceptions.ApplicationError`.
|
||||||
1. We want to let DRF handle everything else, by default.
|
1. We want to let DRF handle everything else, by default.
|
||||||
1. `ValidationError` is now special and it's going to be handled differently.
|
1. `ValidationError` is now special and it's going to be handled differently.
|
||||||
- `ValidationError` should only come from either serializer or a model validation.
|
- `ValidationError` should only come from either serializer or a model validation.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -2018,8 +2003,8 @@ We are going to propose an approach, that can be easily extended into something
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"message": "The error message here",
|
"message": "The error message here",
|
||||||
"extra": {}
|
"extra": {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -2029,17 +2014,13 @@ For example, whenerver we have a `ValidationError` (usually coming from a Serial
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"message": "Validation error.",
|
"message": "Validation error.",
|
||||||
"extra": {
|
"extra": {
|
||||||
"fields": {
|
"fields": {
|
||||||
"password": [
|
"password": ["This field cannot be blank."],
|
||||||
"This field cannot be blank."
|
"email": ["This field cannot be blank."]
|
||||||
],
|
}
|
||||||
"email": [
|
}
|
||||||
"This field cannot be blank."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -2125,12 +2106,13 @@ Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"message": "Something is not correct",
|
"message": "Something is not correct",
|
||||||
"extra": {
|
"extra": {
|
||||||
"type": "RANDOM"
|
"type": "RANDOM"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Code:
|
Code:
|
||||||
|
@ -2144,14 +2126,12 @@ Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"message": "Validation error",
|
"message": "Validation error",
|
||||||
"extra": {
|
"extra": {
|
||||||
"fields": {
|
"fields": {
|
||||||
"non_field_errors": [
|
"non_field_errors": ["Some error message"]
|
||||||
"Some error message"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -2170,8 +2150,8 @@ Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"message": "You do not have permission to perform this action.",
|
"message": "You do not have permission to perform this action.",
|
||||||
"extra": {}
|
"extra": {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -2190,8 +2170,8 @@ Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"message": "Not found.",
|
"message": "Not found.",
|
||||||
"extra": {}
|
"extra": {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -2208,12 +2188,10 @@ Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"message": "Validation error",
|
"message": "Validation error",
|
||||||
"extra": {
|
"extra": {
|
||||||
"fields": [
|
"fields": ["Some error message"]
|
||||||
"Some error message"
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -2230,12 +2208,12 @@ Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"message": "Validation error",
|
"message": "Validation error",
|
||||||
"extra": {
|
"extra": {
|
||||||
"fields": {
|
"fields": {
|
||||||
"error": "Some error message"
|
"error": "Some error message"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -2268,23 +2246,19 @@ Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"message": "Validation error",
|
"message": "Validation error",
|
||||||
"extra": {
|
"extra": {
|
||||||
"fields": {
|
"fields": {
|
||||||
"foo": [
|
"foo": ["This field is required."],
|
||||||
"This field is required."
|
"email": [
|
||||||
],
|
"Ensure this field has at least 200 characters.",
|
||||||
"email": [
|
"Enter a valid email address."
|
||||||
"Ensure this field has at least 200 characters.",
|
],
|
||||||
"Enter a valid email address."
|
"nested": {
|
||||||
],
|
"bar": ["This field is required."]
|
||||||
"nested": {
|
}
|
||||||
"bar": [
|
|
||||||
"This field is required."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -2304,8 +2278,8 @@ Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"message": "Request was throttled.",
|
"message": "Request was throttled.",
|
||||||
"extra": {}
|
"extra": {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -2323,19 +2297,16 @@ Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"message": "Validation error",
|
"message": "Validation error",
|
||||||
"extra": {
|
"extra": {
|
||||||
"fields": {
|
"fields": {
|
||||||
"password": [
|
"password": ["This field cannot be blank."],
|
||||||
"This field cannot be blank."
|
"email": ["This field cannot be blank."]
|
||||||
],
|
|
||||||
"email": [
|
|
||||||
"This field cannot be blank."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Now, this can be extended & made to better suit your needs:
|
Now, this can be extended & made to better suit your needs:
|
||||||
|
@ -2383,8 +2354,8 @@ project_name
|
||||||
|
|
||||||
We follow 2 general naming conventions:
|
We follow 2 general naming conventions:
|
||||||
|
|
||||||
* The test file names should be `test_the_name_of_the_thing_that_is_tested.py`
|
- The test file names should be `test_the_name_of_the_thing_that_is_tested.py`
|
||||||
* The test case should be `class TheNameOfTheThingThatIsTestedTests(TestCase):`
|
- The test case should be `class TheNameOfTheThingThatIsTestedTests(TestCase):`
|
||||||
|
|
||||||
For example, if we have:
|
For example, if we have:
|
||||||
|
|
||||||
|
@ -2412,8 +2383,8 @@ For example, if we have `project_name/common/utils.py`, then we are going to hav
|
||||||
|
|
||||||
If we are to split the `utils.py` module into submodules, the same will happen for the tests:
|
If we are to split the `utils.py` module into submodules, the same will happen for the tests:
|
||||||
|
|
||||||
* `project_name/common/utils/files.py`
|
- `project_name/common/utils/files.py`
|
||||||
* `project_name/common/tests/utils/test_files.py`
|
- `project_name/common/tests/utils/test_files.py`
|
||||||
|
|
||||||
We try to match the structure of our modules with the structure of their respective tests.
|
We try to match the structure of our modules with the structure of their respective tests.
|
||||||
|
|
||||||
|
@ -2421,9 +2392,9 @@ We try to match the structure of our modules with the structure of their respect
|
||||||
|
|
||||||
We use [Celery](http://www.celeryproject.org/) for the following general cases:
|
We use [Celery](http://www.celeryproject.org/) for the following general cases:
|
||||||
|
|
||||||
* Communicating with 3rd party services (sending emails, notifications, etc.)
|
- Communicating with 3rd party services (sending emails, notifications, etc.)
|
||||||
* Offloading heavier computational tasks outside the HTTP cycle.
|
- Offloading heavier computational tasks outside the HTTP cycle.
|
||||||
* Periodic tasks (using Celery beat)
|
- Periodic tasks (using Celery beat)
|
||||||
|
|
||||||
### The basics
|
### The basics
|
||||||
|
|
||||||
|
@ -2665,10 +2636,10 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
Few key things:
|
Few key things:
|
||||||
|
|
||||||
* We use this task as part of a deploy procedure.
|
- We use this task as part of a deploy procedure.
|
||||||
* We always put a link to [`crontab.guru`](https://crontab.guru) to explain the cron. Otherwise it's unreadable.
|
- We always put a link to [`crontab.guru`](https://crontab.guru) to explain the cron. Otherwise it's unreadable.
|
||||||
* Everything is in one place.
|
- Everything is in one place.
|
||||||
* ⚠️ We use, almost exclusively, a cron schedule. **If you plan on using the other schedule objects, provided by Celery, please read thru their documentation** & the important notes - <https://django-celery-beat.readthedocs.io/en/latest/#example-creating-interval-based-periodic-task> - about pointing to the same schedule object. ⚠️
|
- ⚠️ We use, almost exclusively, a cron schedule. **If you plan on using the other schedule objects, provided by Celery, please read thru their documentation** & the important notes - <https://django-celery-beat.readthedocs.io/en/latest/#example-creating-interval-based-periodic-task> - about pointing to the same schedule object. ⚠️
|
||||||
|
|
||||||
### Beyond
|
### Beyond
|
||||||
|
|
||||||
|
@ -2705,8 +2676,8 @@ def user_update(*, user: User, data) -> User:
|
||||||
return user
|
return user
|
||||||
```
|
```
|
||||||
|
|
||||||
* We're calling the generic `model_update` service for the fields that have no side-effects related to them (meaning that they're just set to the value that we provide).
|
- We're calling the generic `model_update` service for the fields that have no side-effects related to them (meaning that they're just set to the value that we provide).
|
||||||
* This pattern allows us to extract the repetitive field setting in a generic service and perform only the specific tasks inside of the update service (side-effects).
|
- This pattern allows us to extract the repetitive field setting in a generic service and perform only the specific tasks inside of the update service (side-effects).
|
||||||
|
|
||||||
The generic `model_update` implementation looks like this:
|
The generic `model_update` implementation looks like this:
|
||||||
|
|
||||||
|
@ -2735,8 +2706,9 @@ def model_update(
|
||||||
```
|
```
|
||||||
|
|
||||||
The full implementations of these services can be found in our example project:
|
The full implementations of these services can be found in our example project:
|
||||||
* [`model_update`](https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/common/services.py)
|
|
||||||
* [`user_update`](https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/users/services.py)
|
- [`model_update`](https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/common/services.py)
|
||||||
|
- [`user_update`](https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/users/services.py)
|
||||||
|
|
||||||
## DX (Developer Experience)
|
## DX (Developer Experience)
|
||||||
|
|
||||||
|
@ -2783,6 +2755,6 @@ Additional resources that we found useful and that can add value to the stylegui
|
||||||
|
|
||||||
The way we do Django is inspired by the following things:
|
The way we do Django is inspired by the following things:
|
||||||
|
|
||||||
* The general idea for **separation of concerns**
|
- The general idea for **separation of concerns**
|
||||||
* [Boundaries by Gary Bernhardt](https://www.youtube.com/watch?v=yTkzNHF6rMs)
|
- [Boundaries by Gary Bernhardt](https://www.youtube.com/watch?v=yTkzNHF6rMs)
|
||||||
* Rails service objects
|
- Rails service objects
|
||||||
|
|
Loading…
Reference in New Issue
Block a user