Mike Slinn
Mike Slinn

Django-Oscar Startup & User Registration

Published 2021-03-19. Last modified 2021-03-24.
Time to read: about 5 minutes.

This article is categorized under AWS, Django, VSCode.

I continue to share my journey learning Django and django-oscar.

The big lesson I got from researching this blog post was that django-oscar’s implementation leans heavily on dynamic class loaders. The Spring web framework, written in Java, also uses class loaders to a similar extent, but not for the same reason.

Microsoft Visual Studio Code

I used Microsoft Visual Studio Code to set breakpoints in my Frobshop webapp, and in the django-oscar source code, and in the Django source code.

I walked up and down the call stack and examined classes and variables. Whenever I found an interesting file I opened the corresponding GitHub page in the default web browser by pressing Ctrl+L+G.

Here is the launch configuration I used ($frobshop/.vscode/launch.json):

launch.json
{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "justMyCode": false,
      "name": "Frobshop / Django-Oscar",
      "type": "python",
      "request": "launch",
      "program": "${workspaceFolder}/manage.py",
      "python": "${env:oscar}/bin/python",
      "args": [
        "runserver",
        "--noreload",
        "0.0.0.0:8001",
      ],
      "django": true
    }
  ]
}

I found that a breakpoint in some Python code could not be set, while breakpoints in other code could be set. Adding "justMyCode": false, to the Visual Studio Code launch configuration solved that problem. It is unclear how Visual Studio Code determines what is "my code" and what is presumably library code.

Launch Bug

Then I hit a Visual Studio Code bug: the virtual environment's Python interpreter, specified in the launch configuration above, was not being used. Apparently this bug was introduced recently and I found several arcane solutions. The solution I found only requires that the top-level directory of the desired Python virtual environment be added to the Visual Studio Code workspace:

  1. Ctrl+Shift+P.
  2. Select Python: Select interpreter.
  3. Select the bottom choice, Entire Workspace.
  4. Select the option containing the desired folder, which for me was /var/work/django/oscar/bin/python.

Django-Oscar Class-Load-A-Rama

The unofficial Gentoo Linux motto is If it moves, compile it!

Django-oscar has a similar emphasis, but on classloaders. In fact, django-oscar has collections of layered classloaders. As I read through the source code, I realized that if you don’t understand how django-oscar’s classloaders work, you won’t be able to customize it very much. Later I discovered that the documentation is explicit about this:

Dynamic class loading is the foundation for making Oscar extensively customisable. It is hence worth understanding how it works, because most customisation depends on it …

Wherever feasible, Oscar uses get_classes instead of a regular import statement.

  — “Dynamic class loading explained”, from the django-oscar documentation.

This is why the django-oscar classloaders are important:

Django-oscar classloaders allow individual classes to be overlaid (replaced), and portions of templates replaced. This works very well for iterating towards an end goal, while the system continues to run locally. Each change you make to the project causes an immediate recompilation and project reload. This is like doing brain surgery on an awake patient.

The downside of django-oscar’s class-load-a-rama is that the execution path through the source code is non-linear. As classes are dynamically loaded, they are evaluated on-the-fly, which might mean that a new stack context is initiated.

Forking Django-Oscar Apps

The django-oscar documentation then goes on to discuss how to fork a django-oscar app, without defining what the term fork means. Instead, the documentation merely instructs the reader to use oscar_fork_app. Unfortunately, oscar_fork_app is broken at present, and I have not found a detailed description of what it does. This has caused me to waste appreciable time.

Startup Sequence

In Visual Studio Code, pressing the green right triangle in the Run and Debug panel initiates debugging of the selected launch configuration.

  1. manage.py main() calls django.core.management.execute_from_command_line
  2. Which called ManagementUtility.execute(), also in manage.py. This method:
    1. Selected the appropriate manage.py subcommand parser.
    2. Marshalled the arguments.
    3. Supplied default option values.
    4. Loaded settings.INSTALLED_APPS.
    5. Started the the auto-reloading dev server.
    6. Computed bash tab completion, unclear why.
    7. Checked to see if a help message should be generated.
    8. Calls fetch_command(), which:
      1. Loads the desired subcommand (runserver) with arguments.
      2. Calls BaseCommand.run_from_argv() in base.py. BaseCommand is really well documented.
  3. This dynamically loads django-oscar src/oscar/config.py, which defines the attributes of Django apps (name, label, models, etc.).
  4. This dynamically loads each of my installed appsConfig.
  5. This dynamically loads my settings/dev.py
  6. Calls OscarConfigMixin.__init__() in django-oscar/src/oscar/core/application.py, which provides basic functionality for django-oscar apps beyond that provided by Django.
  7. This dynamically loads src/oscar/core/loading.py, which defines classloader-related functions such as get_class()
  8. Calls the ready() method in each of the django-oscar apps, in the order listed in settings, including each of the dashboard sub-apps. Here are two of them:
YASXL (Yet Another SpaceX Launch)
YASXL (Yet Another SpaceX Launch)

User Registration Sequence

Psst: django-oscar uses the word “basket” instead of “shopping cart”.

After a user clicks on Login or register, django-oscar does a little dance, just in case the user (who has been anonymous until now) had items in their basket, which need to be transferred to their personal basket once logged in.

  1. Call BasketMiddleware.__call__() in src/oscar/apps/basket/middleware.py. This sets up cookies and caches for the shopping basket. In so doing, it:
    1. Loads src/oscar/apps/customer/views.py
    2. Defines EmailAuthenticationForm, which uses 'customer.forms' for the value of module_label. EmailAuthenticationForm extends the standard Django AuthenticationForm.
    3. Sets login_form_class = EmailAuthenticationForm
    4. Calls AccountAuthView.get_login_form()
    5. Calls BasketMiddleware.process_template_response()
    6. A bunch of context processors run (to fill in template values).
  2. src/oscar/templates/oscar/customer/login_registration.html is now displayed.

Modifying the User Log In / Registration Page

Armed with the knowledge gained from the foregoing, I decided to modify the user login / registration page. I saw no reason that this page should be cluttered with a search bar. I also need to apply a skin of some sort to this page, and in order to do so I would examine these same files and make similar modifications. I am not quite ready to do that, however.

After examining login_registration.html, I realized that setting the navigation block to the empty string would make the desired change. I copied $django-oscar/src/oscar/templates/oscar/customer/login_registration.html to $frobshop/templates/oscar/customer/login_registration.html.

I then added the following to the end of the file to set the navigation block to the null string, thereby suppressing some of the template detail provided as part of the django-oscar framework:

$frobshop/templates/oscar/customer/login_registration.html
{% block navigation %}{% endblock %}

I also replaced the definition of the breadcrumbs block in the same file with the empty string, like this:

$frobshop/templates/oscar/customer/login_registration.html
{% block breadcrumbs %}{% endblock %}

I also noticed that the registration / login page had a link to itself, which is pointless. This comes from block layout in src/oscar/templates/oscar/layout.html. That block contains many things, most of which are useful, however the first item in that block is the culprit:

django-oscar/src/oscar/templates/oscar/layout.html
{% include "oscar/partials/nav_accounts.html" %}

Looking at django-oscar/src/oscar/templates/oscar/partials/nav_accounts.html, lines 30 and lines 62-69 are:

django-oscar/src/oscar/templates/oscar/partials/nav_accounts.html
{% if user.is_authenticated %}
  ... 32 lines ignored ...
{% else %}
  <li class="nav-item mr-2">
    <a id="login_link" href="{% url 'customer:login' %}" class="nav-link">
      <i class="fas fa-sign-in-alt mr-2"></i>
      {% trans "Login or register" %}
    </a>
  </li>
{% endif %}

The problem is that the else clause highlighted above always displays the login link for every page based on layout.html, even the registration / login page. A conditional test is required to suppress this link for the registration / login page. To do this, I:

  1. Copied nav_accounts.html from $django/django-oscar to $frobshop/templates/oscar/partials/nav_accounts.html.
  2. Changed the {% else %} line to {% elif request.path != "/accounts/login/" %}.

Lines 62-69 of the partial (copied to $frobshop/templates/oscar/partials/nav_accounts.html) now look like:

$frobshop/templates/oscar/partials/nav_accounts.html
{% if user.is_authenticated %}
  ... several lines ignored ...
{% elif request.path != "/accounts/login/" %}
  <li class="nav-item mr-2">
    <a id="login_link" href="{% url 'customer:login' %}" class="nav-link">
      <i class="fas fa-sign-in-alt mr-2">
      {% trans "Login or register" %}
    </a>
  </li>
{% endif %}

No more pointless link!

That worked nicely. 😁