mirror of
https://github.com/HackSoftware/Django-Styleguide.git
synced 2025-03-12 23:05:48 +03:00
Merge pull request #5 from HackSoftware/add-models-section
Add section about models
This commit is contained in:
commit
2366ff729a
193
README.md
193
README.md
|
@ -9,6 +9,11 @@ Expect often updates as we discuss & decide upon different things.
|
|||
<!-- toc -->
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Models](#models)
|
||||
* [Custom validation](#custom-validation)
|
||||
* [Properties](#properties)
|
||||
* [Methods](#methods)
|
||||
* [Testing](#testing)
|
||||
- [Services](#services)
|
||||
- [Selectors](#selectors)
|
||||
- [APIs & Serializers](#apis--serializers)
|
||||
|
@ -42,6 +47,194 @@ Expect often updates as we discuss & decide upon different things.
|
|||
* 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.
|
||||
|
||||
## Models
|
||||
|
||||
Lets take a look at an example model:
|
||||
|
||||
```python
|
||||
class Course(models.Model):
|
||||
name = models.CharField(unique=True, max_length=255)
|
||||
|
||||
start_date = models.DateField()
|
||||
end_date = models.DateField()
|
||||
|
||||
attendable = models.BooleanField(default=True)
|
||||
|
||||
students = models.ManyToManyField(
|
||||
Student,
|
||||
through='CourseAssignment',
|
||||
through_fields=('course', 'student')
|
||||
)
|
||||
|
||||
teachers = models.ManyToManyField(
|
||||
Teacher,
|
||||
through='CourseAssignment',
|
||||
through_fields=('course', 'teacher')
|
||||
)
|
||||
|
||||
slug_url = models.SlugField(unique=True)
|
||||
|
||||
repository = models.URLField(blank=True)
|
||||
video_channel = models.URLField(blank=True, null=True)
|
||||
facebook_group = models.URLField(blank=True, null=True)
|
||||
|
||||
logo = models.ImageField(blank=True, null=True)
|
||||
|
||||
public = models.BooleanField(default=True)
|
||||
|
||||
generate_certificates_delta = models.DurationField(default=timedelta(days=15))
|
||||
|
||||
objects = CourseManager()
|
||||
|
||||
def clean(self):
|
||||
if self.start_date > self.end_date:
|
||||
raise ValidationError("End date cannot be before start date!")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.full_clean()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def visible_teachers(self):
|
||||
return self.teachers.filter(course_assignments__hidden=False).select_related('profile')
|
||||
|
||||
@property
|
||||
def duration_in_weeks(self):
|
||||
weeks = rrule.rrule(
|
||||
rrule.WEEKLY,
|
||||
dtstart=self.start_date,
|
||||
until=self.end_date
|
||||
)
|
||||
return weeks.count()
|
||||
|
||||
@property
|
||||
def has_started(self):
|
||||
now = get_now()
|
||||
|
||||
return self.start_date <= now.date()
|
||||
|
||||
@property
|
||||
def has_finished(self):
|
||||
now = get_now()
|
||||
|
||||
return self.end_date <= now.date()
|
||||
|
||||
@property
|
||||
def can_generate_certificates(self):
|
||||
now = get_now()
|
||||
|
||||
return now.date() <= self.end_date + self.generate_certificates_delta
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
```
|
||||
|
||||
Few things to spot here.
|
||||
|
||||
**Custom validation:**
|
||||
|
||||
* 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.
|
||||
|
||||
**Properties:**
|
||||
|
||||
* All properties, expect `visible_teachers` work directly on model fields.
|
||||
* `visible_teachers` is a great candidate for a **selector**.
|
||||
|
||||
We have few general rules for custom validations & model properties / methods:
|
||||
|
||||
### 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 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`.
|
||||
|
||||
|
||||
### 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.
|
||||
|
||||
|
||||
### 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.
|
||||
* Every model method should be wrapped in a service. There should be no model method calling outside a service.
|
||||
|
||||
### Testing
|
||||
|
||||
Models need to be tested only if there's something additional to them - like custom validation or properties.
|
||||
|
||||
If we are strict & don't do custom validation / properties, then we can test the models without actually writing anything to the database => we are going to get quicker tests.
|
||||
|
||||
Foe example, if we want to test the custom validation, here's how a test could look like:
|
||||
|
||||
```python
|
||||
from datetime import timedelta
|
||||
|
||||
from django.test import TestCase
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from odin.common.utils import get_now
|
||||
|
||||
from odin.education.factories import CourseFactory
|
||||
from odin.education.models import Course
|
||||
|
||||
|
||||
class CourseTests(TestCase):
|
||||
def test_course_end_date_cannot_be_before_start_date(self):
|
||||
start_date = get_now()
|
||||
end_date = get_now() - timedelta(days=1)
|
||||
|
||||
course_data = CourseFactory.build()
|
||||
course_data['start_date'] = start_date
|
||||
course_data['end_date'] = end_date
|
||||
|
||||
course = Course(**course_data)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
course.full_clean()
|
||||
|
||||
```
|
||||
|
||||
There's a lot going on in this test:
|
||||
|
||||
* `get_now()` returns a timezone aware datetime.
|
||||
* `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 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.
|
||||
|
||||
Here's how `CourseFactory` looks like:
|
||||
|
||||
```python
|
||||
class CourseFactory(factory.DjangoModelFactory):
|
||||
name = factory.Sequence(lambda n: f'{n}{faker.word()}')
|
||||
start_date = factory.LazyAttribute(
|
||||
lambda _: get_now()
|
||||
)
|
||||
end_date = factory.LazyAttribute(
|
||||
lambda _: get_now() + timedelta(days=30)
|
||||
)
|
||||
|
||||
slug_url = factory.Sequence(lambda n: f'{n}{faker.slug()}')
|
||||
|
||||
repository = factory.LazyAttribute(lambda _: faker.url())
|
||||
video_channel = factory.LazyAttribute(lambda _: faker.url())
|
||||
facebook_group = factory.LazyAttribute(lambda _: faker.url())
|
||||
|
||||
class Meta:
|
||||
model = Course
|
||||
|
||||
@classmethod
|
||||
def _build(cls, model_class, *args, **kwargs):
|
||||
return kwargs
|
||||
|
||||
@classmethod
|
||||
def _create(cls, model_class, *args, **kwargs):
|
||||
return create_course(**kwargs)
|
||||
```
|
||||
|
||||
## Services
|
||||
|
||||
A service is a simple function that:
|
||||
|
|
Loading…
Reference in New Issue
Block a user