Prettify README.md

This commit is contained in:
Ivaylo Bachvarof 2018-11-18 12:11:36 +02:00
parent 26d9d1853a
commit 995596784b

124
README.md
View File

@ -12,18 +12,18 @@ Expect often updates as we discuss & decide upon different things.
- [Overview](#overview) - [Overview](#overview)
- [Cookie Cutter](#cookie-cutter) - [Cookie Cutter](#cookie-cutter)
- [Models](#models) - [Models](#models)
* [Custom validation](#custom-validation) - [Custom validation](#custom-validation)
* [Properties](#properties) - [Properties](#properties)
* [Methods](#methods) - [Methods](#methods)
* [Testing](#testing) - [Testing](#testing)
- [Services](#services) - [Services](#services)
- [Selectors](#selectors) - [Selectors](#selectors)
- [APIs & Serializers](#apis--serializers) - [APIs & Serializers](#apis--serializers)
* [An example list API](#an-example-list-api) - [An example list API](#an-example-list-api)
* [An example detail API](#an-example-detail-api) - [An example detail API](#an-example-detail-api)
* [An example create API](#an-example-create-api) - [An example create API](#an-example-create-api)
* [An example update API](#an-example-update-api) - [An example update API](#an-example-update-api)
* [Nested serializers](#nested-serializers) - [Nested serializers](#nested-serializers)
- [Inspiration](#inspiration) - [Inspiration](#inspiration)
<!-- tocstop --> <!-- tocstop -->
@ -36,22 +36,22 @@ Most of the examples are taken from HackSoft's Learning Management System - Odin
**In Django, business logic should live in:** **In Django, business logic should live in:**
* 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).
* Services - functions, that take care of code written to the database. - Services - functions, that take care of code written to the database.
* Selectors - functions, that take care of code taken from the database. - Selectors - functions, that take care of code taken from the database.
**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.
**Model properties vs selectors:** **Model properties vs selectors:**
* If the model property spans multiple relations, it should better be a selector. - If the model property spans multiple relations, it should better be a selector.
* If a model property, added to some list API, will cause `N + 1` problem that cannot be easily solved with `select_related`, it should better be a selector. - If a model property, added to some list API, will cause `N + 1` problem that cannot be easily solved with `select_related`, it should better be a selector.
## Cookie Cutter ## Cookie Cutter
@ -61,9 +61,9 @@ Once this is done, depending on the context, remove everything that's not needed
The usual list is: The usual list is:
* `allauth` - `allauth`
* templates - templates
* Settings for things that are not yet required (always add settings when necessary) - Settings for things that are not yet required (always add settings when necessary)
## Models ## Models
@ -151,33 +151,31 @@ Few things to spot here.
**Custom validation:** **Custom validation:**
* There's a custom model validation, defined in `clean`. This validation uses only model fields and no relations. - There's a custom model validation, defined in `clean`. This validation uses only model fields and no relations.
* This requires someone to call `full_clean()` on the model instance. The best place to do that is in the `save()` method of the model. Otherwise people can forget to call `full_clean()` in the respective service. - This requires someone to call `full_clean()` on the model instance. The best place to do that is in the `save()` method of the model. Otherwise people can forget to call `full_clean()` in the respective service.
**Properties:** **Properties:**
* All properties, expect `visible_teachers` work directly on model fields. - All properties, expect `visible_teachers` work directly on model fields.
* `visible_teachers` is a great candidate for a **selector**. - `visible_teachers` is a great candidate for a **selector**.
We have few general rules for custom validations & model properties / methods: We have few general rules for custom validations & model properties / methods:
### Custom validation ### Custom validation
* If the custom validation depends only on the **non-relational model fields**, define it in `clean` and call `full_clean` in `save`. - If the custom validation depends only on the **non-relational model fields**, define it in `clean` and call `full_clean` in `save`.
* If the custom validation is more complex & **spans relationships**, do it in the service that creates the model. - If the custom validation is more complex & **spans relationships**, do it in the service that creates the model.
* It's OK to combine both `clean` and additional validation in the `service`. - It's OK to combine both `clean` and additional validation in the `service`.
### Properties ### Properties
* If your model properties use only **non-relational model fields**, they are OK to stay as properties. - If your model properties use only **non-relational model fields**, they are OK to stay as properties.
* If a property, such as `visible_teachers` starts **spanning relationships**, it's better to define a selector for that. - If a property, such as `visible_teachers` starts **spanning relationships**, it's better to define a selector for that.
### Methods ### Methods
* If you need a method that updates several fields at once (for example - `created_at` and `created_by` when something happens), you can create a model method that does the job. - If you need a method that updates several fields at once (for example - `created_at` and `created_by` when something happens), you can create a model method that does the job.
* Every model method should be wrapped in a service. There should be no model method calling outside a service. - Every model method should be wrapped in a service. There should be no model method calling outside a service.
### Testing ### Testing
@ -217,11 +215,11 @@ class CourseTests(TestCase):
There's a lot going on in this test: There's a lot going on in this test:
* `get_now()` returns a timezone aware datetime. - `get_now()` returns a timezone aware datetime.
* `CourseFactory.build()` will return a dictionary with all required fields for a course to exist. - `CourseFactory.build()` will return a dictionary with all required fields for a course to exist.
* We replace the values for `start_date` and `end_date`. - We replace the values for `start_date` and `end_date`.
* We assert that a validation error is going to be raised if we call `full_clean`. - We assert that a validation error is going to be raised if we call `full_clean`.
* We are not hitting the database at all, since there's no need for that. - We are not hitting the database at all, since there's no need for that.
Here's how `CourseFactory` looks like: Here's how `CourseFactory` looks like:
@ -257,11 +255,11 @@ class CourseFactory(factory.DjangoModelFactory):
A service is a simple function that: A service is a simple function that:
* Lives in `your_app/services.py` module - Lives in `your_app/services.py` module
* Takes keyword-only arguments - Takes keyword-only arguments
* Is type-annotated (even if you are not using `mypy` at the moment) - Is type-annotated (even if you are not using `mypy` at the moment)
* Works mostly with models & other services and selectors - Works mostly with models & other services and selectors
* Does business logic - from simple model creation to complex cross-cutting concerns, to calling external services & tasks. - Does business logic - from simple model creation to complex cross-cutting concerns, to calling external services & tasks.
An example service that creates an user: An example service that creates an user:
@ -283,16 +281,15 @@ def create_user(
As you can see, this service calls 2 other services - `create_profile` and `send_confirmation_email` As you can see, this service calls 2 other services - `create_profile` and `send_confirmation_email`
## Selectors ## Selectors
A selector is a simple function that: A selector is a simple function that:
* Lives in `your_app/selectors.py` module - Lives in `your_app/selectors.py` module
* Takes keyword-only arguments - Takes keyword-only arguments
* Is type-annotated (even if you are not using `mypy` at the moment) - Is type-annotated (even if you are not using `mypy` at the moment)
* Works mostly with models & other services and selectors - Works mostly with models & other services and selectors
* Does business logic around fetching data from your database - Does business logic around fetching data from your database
An example selector that list users from the database: An example selector that list users from the database:
@ -313,15 +310,15 @@ When using services & selectors, all of your APIs should look simple & the same.
General rules for an API is: General rules for an API is:
* Do 1 API per operation. For CRUD on a model, this means 4 APIs. - Do 1 API per operation. For CRUD on a model, this means 4 APIs.
* Use the most simple `APIView` or `GenericAPIView` - Use the most simple `APIView` or `GenericAPIView`
* Use services / selectors & don't do business logic in your API. - Use services / selectors & don't do business logic in your API.
* Use serializers for fetching objects from params - passed either via `GET` or `POST` - Use serializers for fetching objects from params - passed either via `GET` or `POST`
* 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`
* `OutputSerializer` can subclass `ModelSerializer`, if needed. - `OutputSerializer` can subclass `ModelSerializer`, if needed.
* `InputSerializer` should always be a plain `Serializer` - `InputSerializer` should always be a plain `Serializer`
* Reuse serializers as little as possible - Reuse serializers as little as possible
* If you need a nested serializer, use the `inline_serializer` util - If you need a nested serializer, use the `inline_serializer` util
### An example list API ### An example list API
@ -407,11 +404,10 @@ class Serializer(serializers.Serializer):
The implementation of `inline_serializer` can be found in `utils.py` in this repo. The implementation of `inline_serializer` can be found in `utils.py` in this repo.
## Inspiration ## Inspiration
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