thermondo

thermondo Developer Handbook

View My GitHub Profile

Django

We embrace the Django coding style with the following additions.

Apps

Apps should be named according to the system they contain. If there is no system name you may use the plural of the main model’s name.

Models

Models are defined according to the Django coding style with the addition of choices. Choices are declared right above the corresponding field. The field default is set using a choices attribute.

Models, mixins and related QuerySet and Manager classes typically sit in the default models.py in each app. When that one gets too big, you can split out mixins, querysets or managers into separate file, or even convert the module into a package.

Forms

We declare ModelForms just like we do Models.

Templates

Templates are located in the following directory structure: /{app_name}/templates/{app_name}/ Every template should be named {model_name}_{template_name_suffix}, for example:

customer_create.html
customer_update.html
customer_detail.html

Template mixins

Template mixins (used via include) are stored in a separate mixin folder, like so: /{app_name}/templates/{app_name}/mixins/ The mixin file names should always start with the object they render, for example:

mixins/phonenumber.html
mixins/address.html

URL patterns

Eg:

from . import views

urlpatterns = [
    url(r'^customer/$',
     views.CustomerListView.as_view(), name='customer-list'),
    url(r'^customer/create',
     views.CustomerCreateView.as_view(), name='customer-create'),
    url(r'^customer/(?P<pk>\d+)/$',
     views.CustomerDetailView.as_view(), name='customer-detail'),
    url(r'^customer/(?P<pk>\d+)/update',
     views.CustomerUpdateView.as_view(), name='customer-update'),
]

Tests

All tests including conftest.py are created per app, in a separate tests module. Tests files are named test_{module}.py, for example:

test_models.py
test_utils.py
test_views.py

Dates & Times

>>> from test_utils.datetime import parse_datetime

>>> parse_datetime('2017-04-01 01:02:03')
datetime.datetime(2017, 4, 1, 1, 2, 3, tzinfo=<DstTzInfo 'Europe/Berlin' CEST+2:00:00 DST>)

>>> parse_datetime('2017-04-01 01:02:03Z')
datetime.datetime(2017, 4, 1, 1, 2, 3, tzinfo=<UTC>)
>>> from common.l10n.localtime import local_start_of_the_day

>>> local_start_of_the_day()
>>> datetime.datetime(2017, 7, 7, 0, 0, tzinfo=<DstTzInfo 'Europe/Berlin' CEST+2:00:00 DST>)

>>> SomeModel.objects.filter(due_date__gte=local_start_of_the_day())
with freeze_time(the_morning_after):
    assert datetime.datetime.now() == datetime.datetime(2010, 1, 1)

Business Logic

In the following order, business logic belongs to

  1. methods on the model if it’s related to one model instance
  2. methods on a custom QuerySet/Manager if it’s related to multiple instances
  3. helper-methods, asynchronous tasks or services otherwise.

Business logic should never be in views, forms or serializers.

Advantage of this is that most of the business logic can then be reused, and also easily be tested in isolation.

Services for business logic

Services for us are a more well-defined business-logic-helper-method that can be used all over apps.

# services.py

# simple private method
def _private1():
    return 123

def my_simple_service(arg1: int, arg2: int, arg3: int) -> int:
    """Multiplies."""
    v = _private1()
    return v * arg1 * arg2 * arg3

# complex private methods
class MyComplexService:
    """Also Multiplies."""
    def _private2(self):
        return 123

    def __call__(self, arg1: int, arg2: int, arg3: int) -> int:
        v = self._private2()
        return v * arg1 * arg2 * arg3

my_complex_service = MyService()

# caller.py
from services import my_simple_service, my_complex_service

result1 = my_simple_service(1, 2, 3)
result2 = my_complex_service(1, 2, 3)