Skip to content

Instantly share code, notes, and snippets.

@rednafi
Last active November 4, 2021 01:18
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rednafi/9076ce59d6241711641938a68dbe68fa to your computer and use it in GitHub Desktop.
Save rednafi/9076ce59d6241711641938a68dbe68fa to your computer and use it in GitHub Desktop.
Django Init Model for Bro

Directory Structure

.
├── app             # I know this app name sucks.
│   ├── migrations
│   │   ├── 0001_initial.py
│   │   └── __init__.py
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── selectors.py # Keep your code that fetches data from db here.
│   ├── services.py  # Keep your code that creates db objects here.
│   ├── tests.py
│   └── views.py
├── project
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── db.sqlite3
└── manage.py
import uuid as _uuid # Avoids namespace clashing.
from functools import lru_cache
from django.contrib.auth.models import AbstractUser
from django.db import models
# Create your models here.
class AuditLogModel(models.Model):
"""We need to add 'uuid' and 'timestamps' to every model object.
However, we don't want to repeat that, DRY. The auditlog model does that.
The name of the class is fairly standardized across the industry.
Also, we don't want to create an actual model in this case.
So let's create an abstract model."""
# Timestamp should always be in UTC. The client code will need to convert
# them as necessary.
uuid = models.UUIDField(default=_uuid.uuid4)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
is_active = models.BooleanField(default=True)
class Meta:
# It's an abstract model, so a table isn't created.
abstract = True
# You usually search via uuids in APIs. Let's add an index here.
indexes = [models.Index(fields=["uuid"])]
class User(AbstractUser, AuditLogModel): # NOTE: Mixin order is significant!
"""A generic user object. You want this because you don't want to hardcode
your user to any particular object like patients or customers."""
# Usually second unique identifier is always good. Email is a good one.
email = models.EmailField(unique=True, null=True, blank=True)
# Max length `2*n -1` where `n % 2 == 0`.
first_name = models.CharField(max_length=255, null=True, blank=True)
middle_name = models.CharField(max_length=255, null=True, blank=True)
last_name = models.CharField(max_length=255, null=True, blank=True)
# The order of the decorators is important!
@property
@lru_cache(maxsize=16)
def full_name(self):
"""Saving full name in the db would be redundant. Also, people rarely
change their names. So let's cache them up to 16 hits."""
return f"{self.first_name} {self.last_name}"
class Meta:
# Let's add some indexes. Primary fields and foreign keys are indexed by
# default. So you don't want to go supersonic with indexing. Indexing make
# reads go faster but the writes take a hit. Tradeoffs buddy! We live in a twilight world.
# Also, Django allows you to add indexes to the fields themselves. But let's
# keep the infrastructure concerns separate from the domain code.
# You're probably going to search by 'first_name' and 'last_name' the most.
# These are good candidates for indexing. The default B-tree index is a good
# starting point.
indexes = [
models.Index(fields=["first_name"]),
models.Index(fields=["last_name"]),
]
class Patient(AuditLogModel):
"""
This model inherits 'uuid', 'full_name', 'created_at', 'updated_at' field form the
'AuditLogModel'.
"""
class Genders(models.TextChoices):
"""This is an 'enum.Enum' class. New in Django 3+.
Using this enum means you won't have to define the choices multiple times.
This is a pain to do in earlier Django versions. Let's utilize that.
"""
MALE = "M", "MALE"
FEMALE = "F", "FEMALE"
OTHER = "O", "OTHER"
# Every patient is also a user. Cascade delete of sensitive data is bad. So
# we use 'on_delete=models.SET_NULL'. Also, adding the model as a string allows
# us to take the advantage of postponed evaluation.
user = models.OneToOneField(
"User", on_delete=models.SET_NULL, null=True, blank=True, related_name="patient"
)
sex = models.CharField(max_length=1, choices=Genders.choices, default=Genders.MALE)
dob = models.DateField(verbose_name="Date of Birth", null=True, blank=True)
"""You don't want to keep your domain code in the 'models.py' file.
'Services' is a common name for a module that creates data in the db."""
import os
import django
# This is necessary so that django doesn't raise
# django.core.exceptions.ImproperlyConfigured in orphan scripts.
# It needs to be set before the model import occurs.
os.environ["DJANGO_SETTINGS_MODULE"] = "project.settings"
django.setup()
import logging
import string
from datetime import date
from . import models as app_models # appName_models is a convention!
# Services files should have a logger.
logging.basicConfig(level=logging.INFO)
def create_user(
*,
first_name: str,
middle_name: str,
last_name: str,
email: str,
) -> None:
"""A good practice is to keep the services function keyword only.
This is a python 3.8+ feature."""
logging.info(f"Creating user {first_name} {last_name}")
app_models.User.objects.create(
first_name=first_name,
middle_name=middle_name,
last_name=last_name,
email=email,
username=email,
)
logging.info(f"Success!")
def create_patient(*, user: app_models.User, sex: str, dob: date) -> None:
logging.info(f"Creating patient with user {user.first_name} {user.last_name}")
patient = app_models.Patient.objects.create(sex=sex, dob=dob)
patient.user = user
patient.save()
logging.info(f"Success!")
def create_users() -> None:
"""Bulk create users."""
for first_name, middle_name, last_name in zip(
string.ascii_lowercase,
string.ascii_lowercase,
string.ascii_lowercase,
):
email = f"{first_name}.{last_name}@gmail.com"
print(email)
create_user(
first_name=first_name,
middle_name=middle_name,
last_name=last_name,
email=email,
)
def create_patients() -> None:
"""Bulk create patients."""
users = app_models.User.objects.filter(
first_name__in=string.ascii_lowercase,
last_name__in=string.ascii_lowercase,
)
for user in users:
create_patient(user=user, sex="M", dob=date.today())
if __name__ == "__main__":
create_users()
create_patients()
from datetime import date
from django.test import TestCase
from . import models as app_models
# Create your tests here.
class UserTestCase(TestCase):
def setUp(self):
# Setting up data.
self.first_name = "test_user_first_name"
self.middle_name = "test_user_middle_name"
self.last_name = "test_user_last_name"
self.email = f"{self.first_name}.{self.last_name}@gmail.com"
self.username = self.email
# Create a user.
app_models.User.objects.create(
first_name=self.first_name,
middle_name=self.middle_name,
last_name=self.last_name,
email=self.email,
username=self.email,
)
def test_crud(self):
user = app_models.User.objects.get(username=self.username)
self.assertEqual(user.first_name, self.first_name)
self.assertEqual(user.middle_name, self.middle_name)
self.assertEqual(user.last_name, self.last_name)
self.assertEqual(user.email, self.email)
self.assertEqual(user.email, self.username)
self.assertEqual(user.full_name, f"{self.first_name} {self.last_name}")
class PatientTestCase(TestCase):
def setUp(self):
# Setting up data.
# User data.
self.first_name = "test_user_first_name"
self.middle_name = "test_user_middle_name"
self.last_name = "test_user_last_name"
self.email = f"{self.first_name}.{self.last_name}@gmail.com"
self.username = self.email
# Patient data.
self.sex = "M"
self.dob = date.today()
# Create a user.
self.user = app_models.User.objects.create(
first_name=self.first_name,
middle_name=self.middle_name,
last_name=self.last_name,
email=self.email,
username=self.email,
)
# Create a patient.
app_models.Patient.objects.create(
user=self.user,
sex=self.sex,
dob=self.dob,
)
def test_crud(self):
user = app_models.User.objects.get(username=self.username)
patient = app_models.Patient.objects.get(user=user)
self.assertEqual(patient.user.first_name, self.first_name)
self.assertEqual(patient.user.middle_name, self.middle_name)
self.assertEqual(patient.user.last_name, self.last_name)
self.assertEqual(patient.user.email, self.email)
self.assertEqual(patient.user.email, self.username)
self.assertEqual(patient.sex, self.sex)
self.assertEqual(patient.dob, self.dob)
@rednafi
Copy link
Author

rednafi commented Nov 3, 2021

User table looks like this.

Screenshot from 2021-11-04 04-22-00

Patient Table looks like this after running the second script.

Screenshot from 2021-11-04 04-21-37

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment