From 8e52937316b752072e5551cc325e09f602c88d37 Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Mon, 13 Aug 2018 15:27:32 +0300 Subject: [PATCH] Add section about models --- README.md | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/README.md b/README.md index 9468a9f..6835f2b 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,10 @@ Expect often updates as we discuss & decide upon different things. - [Overview](#overview) +- [Models](#models) + * [Custom validation](#custom-validation) + * [Properties](#properties) + * [Methods](#methods) - [Services](#services) - [Selectors](#selectors) - [APIs & Serializers](#apis--serializers) @@ -42,6 +46,120 @@ 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. + ## Services A service is a simple function that: