Apply prettier to README

This commit is contained in:
Radoslav Georgiev 2022-11-02 16:30:17 +02:00
parent 0e6f5f1430
commit 2720e607c6
No known key found for this signature in database
GPG Key ID: 0B7753A4DFCE646D

438
README.md
View File

@ -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
@ -318,9 +317,9 @@ This can actually be a downside (_this is not the case, starting from Django 4.1
The Django's documentation on constraints is quite lean, so you can check the following articles by Adam Johnson, for examples of how to use them: The Django's documentation on constraints is quite lean, so you can check the following articles by Adam Johnson, for examples of how to use them:
1. [Using Django Check Constraints to Ensure Only One Field Is Set](https://adamj.eu/tech/2020/03/25/django-check-constraints-one-field-set/) 1. [Using Django Check Constraints to Ensure Only One Field Is Set](https://adamj.eu/tech/2020/03/25/django-check-constraints-one-field-set/)
1. [Djangos Field Choices Dont Constrain Your Data](https://adamj.eu/tech/2020/01/22/djangos-field-choices-dont-constrain-your-data/) 1. [Djangos Field Choices Dont Constrain Your Data](https://adamj.eu/tech/2020/01/22/djangos-field-choices-dont-constrain-your-data/)
1. [Using Django Check Constraints to Prevent Self-Following](https://adamj.eu/tech/2021/02/26/django-check-constraints-prevent-self-following/) 1. [Using Django Check Constraints to Prevent Self-Following](https://adamj.eu/tech/2021/02/26/django-check-constraints-prevent-self-following/)
### Properties ### Properties
@ -632,7 +631,7 @@ And
@admin.register(File) @admin.register(File)
class FileAdmin(admin.ModelAdmin): class FileAdmin(admin.ModelAdmin):
# ... other code here ... # ... other code here ...
# https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/files/admin.py # https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/files/admin.py
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
try: try:
@ -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:
@ -1540,7 +1539,7 @@ Now you can have a `.env` (but it's not required) file in your project root & pl
There are 2 things worth mentioning here: There are 2 things worth mentioning here:
1. Don't put `.env` in your source control, since this will leak credentials. 1. Don't put `.env` in your source control, since this will leak credentials.
2. Rather put an `.env.example` with empty values for everything, so new developers can figure out what's being used. 2. Rather put an `.env.example` with empty values for everything, so new developers can figure out what's being used.
## Errors & Exception Handling ## Errors & Exception Handling
@ -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
@ -2522,7 +2493,7 @@ def user_complete_onboarding(user: User) -> User:
**So, in general, the way we use Celery can be described as:** **So, in general, the way we use Celery can be described as:**
1. Tasks call services. 1. Tasks call services.
2. We import the service in the function body of the task. 2. We import the service in the function body of the task.
3. When we want to trigger a task, we import the task, at module level, giving the `_task` suffix. 3. When we want to trigger a task, we import the task, at module level, giving the `_task` suffix.
4. We execute tasks, as a side effect, whenever our transaction commits. 4. We execute tasks, as a side effect, whenever our transaction commits.
@ -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