Code Style
Code style in this project is based off general style guide defined in SOP-008
General Python style guidelines
- Quote style
As a general rule, double quotes (
") should be preferred for user-facing strings and messages such as display strings, log entries, error messages, etc., while single quotes (') should be used for other strings where possible.Example use of quotation marksmessage = "You're using an outdated browser. Please upgrade." code = 'browser-version' get_logger().debug("User logged in")
- Docstrings
Classes, functions and methods should be documented using docstrings where practical, in particular utility functions and classes. Docstring should document the purpose, accepted arguments, return value and any relevant exceptions that may be raised by the documented code.
A docstring should go beyond what can already be inferred by function name, signature or type annotation. A complex function’s docstring can be broken into multiple paragraphs.
See also
- Type annotations
This project uses type annotations since migration to Python 3 in Task #22300. All newly implemented methods, functions, properties, etc should be properly type-annotated where possible.
Methods returning instance of its class should use
Selfas the return type. For example:def for_user(self, user: User) -> Self:Generic types should be specified where known (e.g.
list[str],dict[str, tuple[int]])- File paths
Use
pathlib.Pathto represent file paths (rather than string representation).See also
- Project requirements
Use
requirements.txtto list packages required by the project. Exact package version should be specified. To pin down version of an indirect dependency, useconstraints.txt.Requirements only used in development, or CI pipelines should be added to a separate
requirements-dev.txt.Static analysis tools and flake8 extensions can be added to a separate
requirements-lint.txt.- Binary operations
Use binary operators instead of class methods when operating on Python data structures for better readability.
a: set[int] = {1, 2, 3, 4, 5} b: set[int] = {4, 5, 6, 7, 8} union = a | b intersection = a & b diff = a - b # When combining operators, the improved readability becomes more obvious union_intersection_diff = (a | b) - (a & b)
- Calling
super()implementation All calls to
super()should be done without any arguments to ensure the full class hierarchy is called even if superclass structure changes. This call should be the first one in the method body.Example of asuper()call to__init__().def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs)
Django-specific style rules
- Django API and version
This project should use the latest available Long Term Support (LTS) version of django, with latest security patches.
See also
- Class Based Views
The project uses Django’s Class Based Views (CBV). CBVs should be preferred over the legacy function views.
There’s a number of built-in base view classes provided by django (e.g.
ListView,FormView,UpdateView, etc) as well as mix-ins (e.gPermissionRequiredMixin,LoginRequiredMixin) that reduce the boilerplate code for handling forms, lists, etc.This project also provides a number of useful mix-ins for common use cases:
AuditorInstitutionMixin,AuditFormMixin,FilterFormMixin.
- Internationalization & Localization
This product is used internationally and supports multiple languages and a wide range of timezones.
See also
More about translation and internationalization in Translations
Translation strings should contain placeholders for any variables used within the sentence. This gives the translators the necessary context information, and control over how the value is positioned within the sentence.
Example of correct variable interpolation within a django templateCorrect translation: {% blocktrans %}Hello {{ name }},{% endblocktrans %} Incorrect: {% trans "Hello" %} {{ name }},
Dates and times are stored in the database in UTC timezone. When displaying to the user, ensure that correct timezone is used.
While locale is automatically set for authenticated users using middleware, celery jobs and other code executed outside of a request stack should set appropriate locale and timezone (e.g. by using
LocaleWrapperandTimezoneWrappercontext managers).Example how to correctly set locales within a celery task@app.task(**CELERY_TASK_LOW_PRIORITY) def celery_job(institution_id: int): institution: Institution = Institution.objects.get(pk=institution_id) with LocaleWrapper(institution.language), TimezoneWrapper(institution.timezone_name): ... # This code will run correct in institution's timezone and language
- Performance
Use available tools such as
select_related(),prefetch_related(),annotate(),Subqueryas applicable to prefetch any data that will be needed and avoid additional round trips to the database.Any long running code should be delegated to Celery tasks (e.g. batch processing, generating exports, parsing import data).
Use appropriate data structures for their intended purpose (i.e.
list,tuple,set,dict).- Config JSON fields
JSON fields are commonly used in the project as a store for arbitrary configuration options to avoid creating multiple dedicated model fields.
To enforce validation of the input data and type safety, a TypedDict should be created and maintained that will explicitly describe the correct schema for a the config field.
As one of the benefits of using
JSONFieldis to avoid migration, documenting config fields in model’s help text should be avoided, as updatinghelp_textin django requires a migration. Instead, help text can be added to the field on form level.The
TypedDictshould list all fields, their expected type, and havetotal=True.The model’s
JSONFieldshould be type-annotatedThe field should use
TypedDictValidatorto enforce that user-entered data conforms to the expected structureAny validation that cannot be expressed using
TypedDictshould be implemented separately (e.g. on model’s clean class)Nested
TypedDictshould be used when config needs to be broken into multiple sections of related options - this will also help enforce that all options within that section are set when the section is present in teh given config dict.
Example setup of a json config field and its schema defined as a typed dictclass PayloadDict(TypedDict, total=True): name: str address: list[str] class RequestConfig(TypedDict, total=True): url: str headers: dict[str, str] method: Literal['GET', 'POST', 'PUT'] verify: bool timeout: NotRequired[float | int] payload: PayloadDict certificates: list[str] class Request(BaseModel): config: RequestConfig = models.JSONField(verbose_name=_('configuration'), validators=[TypedDictValidator(RequestConfig)])
JavaScript rules
- Supported version
JS can leverage features declared on the
.jshintrcfile in the project (e.g. ES9)- Implementation
JavaScript logic should be implemented in individual js files, and served from
/static/folder in the relevant app. HTML files should contain minimal amount of js code in the<script>tags; e.g. single line invoking a parametrized function residing in a js file.- Tests
Each javascript file should have a test file in
js_tests/tests/. E.g./static/js/script.jshas tests injs_tests/tests/script.test.js.- Custom JS for django widgets and forms
Form and widget-specific js code should be referenced in the form’s or widget’s
Media. It should never be manually included in the html file.- Internationalization
User-facing strings in JavaScript code should be translated using
gettext()See also
Internationalization: in JavaScript code in Django documentation
Examples
Example how to pass simple variables from django into a js script<script src="{% static 'js/popups.js' %}"></script> <script>showPopup("{{ message|escapejs }}", "{% url 'login' %}")</script>More complex data structures can be securely passed to a js function by rendering them into a data tag first{{ message|json_script:"popup-data" }} <script src="{% static 'js/popups.js' %}"></script> <script> const data = JSON.parse(document.getElementById('popup-data').textContent); showPopup(data.message, data.url); </script>
Project-specific rules
- Unpublished objects
BaseModelprovides apublishedfield used to mark objects as deleted. Use the providedpublished()queryset method to exclude those objects from querysets.- Ringfencing
Before displaying data to the user, ensure that the underlying querysets are properly ringfenced by using
for_user(),for_institutions()and other queryset methods as appropriate.- Project settings
Use django’s
settings.pyto keep project configuration.In addition, most settings will also benefit from being configurable by environment variable. This allows the value to be changed and tweaked without re-building and re-deploying the project. Use
env()to read the environment variable value.Settings example# Setting with a default value SAMPLE_RATE: float = env('SAMPLE_RATE', 1.0)
- Models
New models should inherit
BaseModelas the base model. If implementing a custom queryset class, the queryset should inherit fromBaseQuerySetNon-abstract models must implement a
Metaclass with a translatableverbose_name/plural.All models should implement
__str__(). The implementation should not query related models if possible (e.g. by only using the model’snameortitlefield if one exist).- Django admin
Django admin pages should:
use decorators to register the admin class, and to annotate any any actions or display names
Extend
BaseAdminas the base class
Example of a well-formatted django admin class, using decorators@admin.register(ExampleModel) class ExampleAdmin(BaseAdmin): readonly_fields = 'example_computed_field', actions = [ 'example_action', ] @admin.action(description=_('Example action'), permissions=('change',)) def example_action(self, request: HttpRequest, qs: QuerySet[ExampleModel]): ... @admin.display(description=_('Label for the computed field')) def example_computed_field(self, obj: ExampleModel) -> str: ...
See also
- Capitalization
User-facing strings in templates, field labels, etc, should use “Sentence case”, or “lower case”. If other style is required by Style guide, it should be applied using css.
Examples
These code snippets are considered good practice and show example of well written code according to the above guidelines
def calculate_average(*values: float | int) -> float | None:
"""
Calculate the average of a given set of numerical values.
If no values are provided, the function returns `None`.
:param values: A variable number of numeric values (integers or floats) to be averaged.
:return: The average of the given values, or ``None`` if no values are provided.
:raises TypeError: If any value in ``values`` is not an integer or float.
Example usage::
result = calculate_average(10, 20, 30)
print(result) # Output: 20.0
result = calculate_average()
print(result) # Output: None
"""
if not values:
return None
if not all(isinstance(v, (int, float)) for v in values):
raise TypeError("All values must be integers or floats.")
return sum(values) / len(values)
from megforms.base_model import BaseQuerySet, BaseModel
from django.utils.translation import gettext_lazy as _, gettext
from typing import Self
class ExampleQueryset(BaseQuerySet):
def for_auditor(auditor: Auditor) -> Self:
return self.filter(institution__in=auditor.institutions)
class ExampleModel(BaseModel):
institution = models.ForeignKey(verbose_name=_('institution'), to=Institution, on_delete=CASCADE)
name = models.CharField(verbose_name=_('name'), max_length=70)
objects = ExampleQueryset.as_manager()
def __str__(self) -> str:
return gettext("Example: {name}").format(
name=self.name,
)
def get_absolute_url(self) -> str:
return reverse('example-page', kwargs={'example_id': self.pk})
class Meta:
verbose_name = _('example')
verbose_name_plural = _('examples')