Published 2021-03-19.
Last modified 2021-03-24.
Time to read: 5 minutes.
django
collection.
I continue to share my journey learning Django and django-oscar
.
The big lesson I got from researching this article 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:
{ // 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:
- CTRL+Shift+P.
- Select Python: Select interpreter.
- Select the bottom choice, Entire Workspace.
- 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:
Wherever feasible, Oscar uses
get_classes
instead of a regular import
statement.
– “Dynamic 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.
-
manage.py main()
callsdjango.core.management.execute_from_command_line
-
Which called
ManagementUtility.execute()
, also inmanage.py
. This method:- Selected the appropriate
manage.py
subcommand parser. - Marshalled the arguments.
- Supplied default option values.
- Loaded
settings.INSTALLED_APPS
. - Started the the auto-reloading dev server.
- Computed bash tab completion, unclear why.
- Checked to see if a help message should be generated.
- Calls
fetch_command()
, which:- Loads the desired subcommand (
runserver
) with arguments. -
Calls
BaseCommand.run_from_argv()
inbase.py
.BaseCommand
is really well documented.
- Loads the desired subcommand (
- Selected the appropriate
-
This dynamically loads
django-oscar
src/oscar/config.py
, which defines the attributes of Django apps (name
,label
,models
, etc.). - This dynamically loads each of my installed apps
Config
. - This dynamically loads my
settings/dev.py
-
Calls
OscarConfigMixin.__init__()
indjango-oscar/
, which provides basic functionality forsrc/ oscar/ core/ application.py django-oscar
apps beyond that provided by Django. -
This dynamically loads
src/
, which defines classloader-related functions such asoscar/ core/ loading.py get_class()
-
Calls the
ready()
method in each of thedjango-oscar
apps, in the order listed insettings
, including each of the dashboard sub-apps. Here are two of them:-
CustomerConfig.ready()
, which loads all theCustomerConfig
views. -
SearchConfig.ready()
, which loads all theSearchConfig
views.
-
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.
-
Call
BasketMiddleware.__call__()
insrc/oscar/apps/basket/middleware.py
. This sets up cookies and caches for the shopping basket. In so doing, it:- Loads
src/oscar/apps/customer/views.py
-
Defines
EmailAuthenticationForm
, which uses'customer.forms'
for the value ofmodule_label
.EmailAuthenticationForm
extends the standard DjangoAuthenticationForm
. - Sets
login_form_class = EmailAuthenticationForm
- Calls
AccountAuthView.get_login_form()
-
Calls
BasketMiddleware.process_template_response()
- A bunch of context processors run (to fill in template values).
- Loads
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/
to
$frobshop/
.
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:
{% block navigation %}{% endblock %}
I also replaced the definition of the breadcrumbs
block in the same file with the empty string, like this:
{% 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:
{% include "oscar/partials/nav_accounts.html" %}
Looking at django-oscar/
, lines 30 and lines 62-69 are:
{% 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:
- Copied
nav_accounts.html
from$django/django-oscar
to$frobshop/
.templates/ oscar/ partials/ nav_accounts.html - Changed the
{% else %}
line to{% elif request.path != "/accounts/login/" %}
.
Lines 62-69 of the partial (copied to $frobshop/
) now look like:
{% 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.