Overview
Kolibri maintains high code quality standards to ensure a maintainable, reliable, and scalable codebase. These principles guide how we write and review code across the full stack—Python, JavaScript, Vue, and infrastructure.Linting and Auto-Formatting
Tools Used
Kolibri uses automated tools to enforce code quality and consistency:- Prettier: JavaScript, Vue, SCSS, and CSS formatting
- Black: Python code formatting
- Flake8: Python linting
- ESLint: JavaScript and Vue linting
Manual Linting and Formatting
You can manually run the auto-formatters: Frontend:pre-commit (see below).
Pre-commit Hooks
The pre-commit hooks are identical to the automated build checks run by CI in Pull Requests, so using them locally will help you catch issues early.Installing Pre-commit Hooks
pre-commit is used to apply a full set of checks and formatting automatically each time thatgit commit runs. If there are errors, the Git commit is aborted and you are asked to fix the error and run git commit again.
Pre-commit is already installed as a development dependency, but you also need to enable it:
Always run this command after cloning the repository to enable pre-commit hooks for your local development environment.
Running Pre-commit Manually
To run all pre-commit checks in the same way that they will be run on GitHub CI servers:Recommended Workflow with Pre-commit
- Make code changes
- Stage your changes:
git add <files> - Attempt commit:
git commit -m "Your message" - If pre-commit finds issues:
- Review the errors and warnings
- Many formatting issues will be auto-fixed - just review and re-stage the changes
- Fix any remaining issues manually
- Stage the fixes:
git add <files> - Retry the commit:
git commit -m "Your message"
- Once all checks pass, your commit will succeed
Pre-commit Configuration
Kolibri’s pre-commit configuration includes:- trailing-whitespace: Removes trailing whitespace
- check-yaml: Validates YAML files
- check-added-large-files: Prevents committing large files
- debug-statements: Prevents Python debug statements
- end-of-file-fixer: Ensures files end with a newline
- flake8: Python linting with flake8-print
- reorder-python-imports: Sorts Python imports
- lint-frontend: Linting of JS, Vue, SCSS, and CSS files
- core-js-api: Rebuilds kolibri package on changes to core JS API
- no-auto-migrations: Prevents auto-named migrations (migrations must have descriptive names)
- no-swappable-auth-migrations: Prevents migrations with swappable auth models
- no-kolibri-common-imports: Prevents invalid package imports
- check-lfs-pointers: Ensures LFS files are pointers not binary data
- black: Python code formatting (always runs last)
Code Quality Principles
These principles apply across the full stack and reflect patterns that have proven valuable in practice.Every Concern Lives at Exactly One Layer
If a behavior like validation, retry, error handling, or permission checking is implemented at one layer, do not reimplement it at another. Choose the layer that owns the concern and trust it. In Kolibri:- Permission checks belong in
KolibriAuthPermissionsandRoleBasedPermissionson models—not duplicated in frontend route guards and API views - Validation logic belongs in serializers or model
clean()methods—not scattered across both the viewset and the serializer - Do not introduce global or shared mutable state to coordinate between modules
Prefer the Simplest Implementation
Do not introduce abstraction layers until a concrete second use case demands it. Flat is better than nested. Anif/else is better than a strategy pattern when there are two cases.
Preserve Existing Comments
Only remove a comment if it describes code that has been deleted or is provably incorrect. When modifying code that has comments, update the comments to reflect the changes. Do not strip comments to “clean up” a file.Interfaces Stay Small
Every public method needs justification. If something can be private, it must be. Push implementation details behind the interface boundary. In Kolibri:- Keep the
valuestuple inValuesViewsetminimal—only fetch fields that are actually needed - Composables should return a focused public API—not expose every internal ref
- A growing public surface area is a design smell; refactor to reduce it before continuing
Tests Assert Behavior, Not Implementation
Test inputs and outputs. Mock only at hard boundaries: network, filesystem, external services. Do not mock internal modules or classes to isolate units—test them through the real call chain. In Kolibri:- Frontend tests use Vue Testing Library which encourages testing from the user’s perspective—query by text, role, and label rather than component internals
- Backend tests call API endpoints through Django’s test client and assert on response data, not on internal method calls
Do Not Store What Can Be Computed
Do not add fields to data structures that are derivable from other fields in the same structure. Expose derived values as methods or computed properties. In Kolibri:- Use
computed()in Vue composables for derived state rather than maintaining separate refs that must be kept in sync - Use
annotate_querysetinValuesViewsetfor computed database fields rather than post-processing in Python - If caching is needed for performance, keep the cached value private—never expose redundant state in the public interface
Let Errors Propagate
Do not wrap every call in try/catch blocks that just log and rethrow, or that catch broad exception types only to obscure the original failure. Let exceptions propagate to the layer that can actually handle them meaningfully.If something that “cannot happen” happens, crash—a dead program does less damage than a crippled one continuing to run on corrupted state.
Tell, Don’t Ask
Do not reach into an object, inspect its internal state, make a decision based on that state, and then update the object. Instead, tell the object what you want done and let it manage its own state. In Kolibri:- Use
RoleBasedPermissionsdeclaratively on models rather than checking roles manually in views - Composables should expose actions (
fetchChannels()) rather than forcing callers to manipulate internal refs directly - Avoid method chains that traverse multiple levels of abstraction to reach into nested structures
Prefer Composition Over Inheritance
Do not use class inheritance to share behavior between types. Inheritance couples the child to the parent’s implementation—when the parent changes, the child breaks silently. In Kolibri:- Vue composables are preferred over mixins for sharing component logic—composables use explicit composition rather than implicit merging
- Use interfaces or protocols to define shared behavior contracts, delegation to reuse implementation
- Reserve inheritance only for true “is-a” relationships where substitutability is required and the hierarchy is shallow
Follow Existing Naming Conventions
Match the naming style of the language (snake_case in Python, camelCase in JavaScript) and the conventions already established in the codebase. Use the same terms the project already uses for domain concepts—do not introduce synonyms.
In Kolibri:
- A group of learners is a
Collection, not a “group” or “class” (Classroomis a specific kind ofCollection) - Content items are
ContentNodeobjects, not “resources” or “materials” - Use
Facility,FacilityUser,Classroom,LearnerGroup—these are the established model names
Do Not Weaken Existing Tests
Do not modify or delete existing tests unless the behavior they test has been intentionally changed. If new code breaks existing tests, fix the code, not the tests. Never loosen assertions, add workarounds, or reduce coverage to make a failing test pass.Editor Configuration
We have a project-level.editorconfig file to help you configure your text editor or IDE to use our internal conventions.
Check your editor to see if it supports EditorConfig out-of-the-box, or if a plugin is available.