thermondo Developer Handbook
We embrace the Django coding style with the following additions.
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 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.
We declare ModelForm
s just like we do Model
s.
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 (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
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'),
]
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
Don’t use timezone.datetime
& timezone.date
. These are just plain datetime
& date
module
and they don’t return aware datetimes
.
Always try to use django.utils.timezone
or helpers from
common.l10n.localtime
instead of datetime.datetime
and
datetime.date
.
For unit tests there’s one particularly nice helper
test_utils.datetime.parse_datetime
:
>>> 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>)
In general try to work in the current timezone and leave all the
timezone conversions to Django
& Postgres
.
Try to use datetimes
instead of plain dates. You can have datetimes
w/
zeroed out time part which can be used in the same way as a plain dates:
>>> 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())
Prefer models.DateTimeField
instead of models.DateField
. This
can avoid a whole lot of comparison and casting issues. You can leave
the time part out in the UI if you don’t need to show it.
Calls to .date()
and .replace()
will almost always be
wrong. Django stores aware datetimes
in UTC, so calling .date()
on
it will give you the date in UTC, not in Germany. The same goes for
.replace()
- dt.replace(hours=5)
is usually 5 AM in UTC, not in
Germany.
When you need to set a specific time (or travel through time!) in
tests, please use the freezegun
library. It is most useful in it’s
context manager form:
with freeze_time(the_morning_after):
assert datetime.datetime.now() == datetime.datetime(2010, 1, 1)
In the following order, business logic belongs to
QuerySet
/Manager
if it’s related to multiple instancesBusiness 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 us are a more well-defined business-logic-helper-method that can be used all over apps.
services.py
of each Django app (and tested in tests/test_services.py
)kwarg
s and with type annotations)# 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)