Skip to content

OpenCraft Coding Standards and Best Practices

Here are the guidelines that OpenCraft follows with all of our software development. Some of these are not strict rules, but all of them are the standards that we aim for.

Open Source

We are an open source company which means that all software we create should be open source by default (unless there is some compelling or legal reason not to).

New software that we create should generally use the AGPL or GPL license.

Documentation or other non-software works that we create should be licensed under a Creative Commons license, usually CC BY-SA 3.0.

In addition, any time we make a change (feature, bugfix, etc.) to an external software project (like Open edX, Django, etc.), we contribute that change back to the project. (Sometimes they won't accept it as a contribution, but we always try.)

Coding Standards and Best Practices

1. Do your best to ensure you've written clean, maintainable code.

  • Remember "Don't make me think!": use descriptive variable/function/class names in your code and comments to make your code as easy to read as a book.
  • Short, simple code is usually better than long, complex code, but don't fall for the temptation of writing code that's visually dense, which is hard to read and understand. Add docstrings, blank lines, and comments to space things out and make your code more readable.

Here is an example, showing the same python function written in a "bad" and "good" way:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
########## Bad Example:

def get_anon_id(user):
    if not user or not user.is_anonymous:
        raise TypeError("user is not anonymous")
    if hasattr(user, '_anon_id'):
        return user._anon_id
    cr = crum.get_current_request()
    if cr and cr.session:
        if "_anon_id" not in cr.session:
            new_id = "anon{}".format(uuid4().hex[:20])
            cr.session["_anon_id"] = new_id
        return cr.session["_anon_id"]
    else:
        raise RuntimeError("No current request!")

# ^^ This code looks simple but is full of subtle actions that aren't well explained.
# It will take someone quite a while to figure out what the code is actually doing
# in various cases, and even longer to refactor.
# Let's fix it:

########## Good Example:

def get_xblock_id_for_anonymous_user(user):
    """
    Get a unique string that identifies the current anonymous (not logged in)
    user. (This is different than the "anonymous user ID", which is an
    anonymized identifier for a logged in user.)
    Note that this ID is a string, not an int. It is guaranteed to be in a
    unique namespace that won't collide with "normal" user IDs, even when
    they are converted to a string.
    """
    if not user or not user.is_anonymous:
        raise TypeError("get_xblock_id_for_anonymous_user() is only for anonymous (not logged in) users.")

    if hasattr(user, 'xblock_id_for_anonymous_user'):
        # If code elsewhere (like the xblock_handler API endpoint) has stored
        # the ID on the AnonymousUser object, just return that - it supersedes
        # everything else. (See the xblock_handler API endpoint for details.)
        return user.xblock_id_for_anonymous_user

    # We use the session to track (and create if needed) a unique ID for this anonymous user:
    current_request = crum.get_current_request()
    if current_request and current_request.session:
        # If we already have a random ID for this anonymous user cached in the session, return it:
        cache_key = "xblock_id_for_anonymous_user"
        if cache_key not in current_request.session:
            # If not, generate a new random ID and cache it in the session:
            new_id = "anon{}".format(uuid4().hex[:20])
            current_request.session[cache_key] = new_id
        return current_request.session[cache_key]
    else:
        raise RuntimeError("Cannot get a user ID for an anonymous user outside of an HTTP request context.")

2. Document your decisions

  • When you're building something new, you'll make a lot of decisions about how it works. Write those decisions down so that other developers can understand more easily.
  • It's tempting (and helpful) to put these details into a pull request description, but pull requests are temporary and hard to find once merged. It's much better to put the details of your decisions into the codebase itself as a docstring (for small components), a README, or an Architecture Decision Record (ADR) (example).

3. Write tests for your code

  • Testing is critical for designing robust, high quality software components and avoiding regressions.
  • "Unit Tests: Best and Worst Practices" provides good tips about the role of tests and how to maximize their value. ("It's overwhelmingly easy to write bad unit tests that add very little value to a project while inflating the cost of code changes astronomically.")

4. Backend guidelines:

  • Choosing a language: we default to using Python for most new projects for simplicity and consistency.
  • Follow the edX Python Style Guide (even for non-edX projects, unless the project has its own style guide).
  • Use pycodestyle and pylint to enforce coding standards automatically

5. Frontend guidelines:

  • Follow the edX JavaScript Style Guide for JavaScript in edX projects.
  • Use TypeScript for non-edX projects.
  • Ensure that any UI you build follows best practices for accessibility and complies with the WCAG 2.0 Level AA standards:
    • Every UI component and content item has a text version (alt text, screenreader text), so that non-visual users can still understand and use it.
    • The UI can be used with a keyboard only (tab order, keyboard controls).
    • Text colors have sufficient contrast.
    • See the complete checklist/cheatsheet (levels A and AA only).

More in-depth guidelines and discussion of particular technologies can be found in our technical documentation handbook.

Infrastructure as Code

As much as possible, infrastructure, server provisioning, and devstack setup should be defined by code and version controlled using git.

In particular, this means:

  • AWS and OpenStack/OVH infrastructure should be configured using Terraform.
  • Set up for the environment your application requires (installing apt packages, etc.) should be scripted using either Ansible or a Dockerfile.
  • Setting up client-specific devstacks should be automated using bash scripts or whatever else is convenient.

Contributing Features to edx-platform

For feature contributions to the Open edX core platform in particular:

Discuss with the team and/or edX to determine if the feature you're building should be part of the core platform or should be a plugin (like an XBlock or a django app plugin). Generally, if it provides a platform feature that most of the community will use or provides a foundation for building other functionality, it would be a good candidate for the core platform, and if it's a feature that only a minority of Open edX instances are likely to use, it would be better as a plugin.

If you are writing the feature as a plugin, ensure that it only uses stable public APIs such as the XBlock API, the Django App Plugin API, and/or any inter-app API (api.py files within each Django app in the core platform). If there is no public API defined for the integration you need, modify the core platform to provide the API that you need, e.g. in an api.py file, and contribute the API to the core platform.


Last update: 2023-10-16