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 marks
message = "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.

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 Self as 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.Path to represent file paths (rather than string representation).

Project requirements

Use requirements.txt to list packages required by the project. Exact package version should be specified. To pin down version of an indirect dependency, use constraints.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 a super() 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.

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.g PermissionRequiredMixin, 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 template
Correct 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 LocaleWrapper and TimezoneWrapper context 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(), Subquery as 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 JSONField is to avoid migration, documenting config fields in model’s help text should be avoided, as updating help_text in django requires a migration. Instead, help text can be added to the field on form level.

  • The TypedDict should list all fields, their expected type, and have total=True.

  • The model’s JSONField should be type-annotated

  • The field should use TypedDictValidator to enforce that user-entered data conforms to the expected structure

  • Any validation that cannot be expressed using TypedDict should be implemented separately (e.g. on model’s clean class)

  • Nested TypedDict should 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 dict
class 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 .jshintrc file 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.js has tests in js_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

BaseModel provides a published field used to mark objects as deleted. Use the provided published() 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.py to 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 BaseModel as the base model. If implementing a custom queryset class, the queryset should inherit from BaseQuerySet

Non-abstract models must implement a Meta class with a translatable verbose_name/plural.

All models should implement __str__(). The implementation should not query related models if possible (e.g. by only using the model’s name or title field 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 BaseAdmin as 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:
        ...
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

This snippet shows cases a well documented and annotated function with clean implementation and usage examples.
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)
An example Django model showcasing correct use of string localization, type annotations, and use of base model / queryset.
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')