Django and Oscar

Evaluating Django-Oscar, a F/OSS Shopping Cart

Published 2021-02-11. Last modified 2021-04-06.
Time to read: 8 minutes.

This page is part of the django collection.

I discussed why I decided to check out the django-oscar e-commerce framework in an earlier article. In this article I describe my experience setting up and running django v3.1.6 and django-oscar v3.0 on Ubuntu 20.04 with Python 3.8.6.

BTW, there are two online django-oscar user groups: the Slack channels and the Google Group.

Getting Started

The very first page in the django-oscar documentation describes a sample project called Frobshop. This article describes my experience installing Frobshop.

Frob (Noun) Any small device or object (usually hand-sized) which can be manipulated.

Etymology: It was adopted by the community of computer programmers which grew out of the MIT Tech Model Railroad Club in the 1950s, and allegedly among the oldest existing words in hacker jargon.

Definitions.net

I disabled Django Haystack for Frobshop, and I wrote this article to explain why. I also did not install Apache Solr, as explained in this article.

Installing django-oscar

For manageability of Python runtime versions and libraries, a virtual Python environment should be set up. The required virtual environment will house compatible versions of Python, Django, Django-Oscar and all their dependencies. I decided to locate a virtual environment at ~/venv/aw as explained in A Python Virtual Environment For Every Project:

Shell
$ newVenv aw
created virtual environment CPython3.8.6.final.0-64 in 528ms
  creator CPython3Posix(dest=/home/mslinn/venv/aw, clear=False, global=False)
  seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=/home/mslinn/.local/share/virtualenv)
    added seed packages: pip==20.1.1, pkg_resources==0.0.0, setuptools==44.0.0, wheel==0.34.2
  activators BashActivator,CShellActivator,FishActivator,PowerShellActivator,PythonActivator,XonshActivator 
$ use aw
(aw) $

Next, I installed the Python libraries and their dependencies. Django‑oscar installs Django as a dependency. I also specified a few more dependencies that do not automatically get pulled in. Pip‑tools is installed so dependencies can be managed properly. I installed django.db.backends.postgresql because I wanted to use PostgreSQL instead of SQLite.

BTW, django.db.backends.postgresql_psycopg2 was renamed in django 1.9 to django.db.backends.postgresql, and is installed by pip as django‑postgresql.

Shell
(aw) $ pip install django-oscar[sorl-thumbnail] \
pip-tools psycopg2-binary pycountry

BTW, the Frobshop instructions say to install Pillow, but that library is already specified as a django-oscar dependency, so there is no need to install it manually.

Now the virtual environment also contains 2 Django executable images:

ls ~/venv/aw/**
/home/mslinn/venv/aw/pyvenv.cfg

/home/mslinn/venv/aw/bin:
__pycache__
activate
activate.csh
activate.fish
activate.ps1
activate.xsh
activate_this.py
django-admin
django-admin.py
easy_install
easy_install-3.8
easy_install3
faker
pip
pip3
pip3.10
pip3.11
pybabel
python
python3
python3.8
sqlformat
wheel
wheel-3.8
wheel3

/home/mslinn/venv/aw/lib:
python3.10
python3.11
python3.8
python3.9

Let’s see information about the versions of django and django-oscar that were just installed into the oscar virtual environment:

Shell
(aw) $ pip show django django-oscar
Name: Django
Version: 3.1.6
Summary: A high-level Python Web framework that encourages rapid development and clean, pragmatic design.
Home-page: https://www.djangoproject.com/
Author: Django Software Foundation
Author-email: foundation@djangoproject.com
License: BSD-3-Clause
Location: /home/mslinn/oscar/lib/python3.8/site-packages
Requires: asgiref, sqlparse, pytz
Required-by: django-treebeard, django-tables2, django-phonenumber-field, django-oscar, django-haystack, django-extra-views
---
Name: django-oscar
Version: 3.0
Summary: A domain-driven e-commerce framework for Django
Home-page: https://github.com/django-oscar/django-oscar
Author: David Winterbottom
Author-email: david.winterbottom@gmail.com
License: BSD
Location: /home/mslinn/oscar/lib/python3.8/site-packages
Requires: django-haystack, phonenumbers, django-widget-tweaks, django-treebeard, django-extra-views, Babel, purl, factory-boy, django-tables2, django-phonenumber-field, pillow, django
Required-by: 

Generating Frobshop

Now it was time to generate the frobshop project from the template:

Shell
(aw) $ mkdir $work/django && cd $work/django

(aw) $ django-admin startproject frobshop

(aw) $ cd frobshop

I now had these files and directories:

Shell
(aw) $ tree
├── db.sqlite3
├── frobshop
│   ├── __init__.py
│   ├── __pycache__
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py %}

For convenience, I made frobshop’s copy of the manage.py Django command-line utility for administrative tasks executable:

Shell
(aw) $ chmod a+x manage.py

Creating a Git Repository

I checked in the frobshop project into a private git repository of the same name:

Shell
(aw) $ git init

(aw) $ hub create -p frobshop

(aw) $ cat > .gitignore <<EOF
**/__pycache__/
.idea/
.vscode/
EOF

(aw) $ git add .

(aw) $ git commit -m -

(aw) $ git push origin master

As I worked through the tutorial I used git commits, branches and tags to insulate me from messing things up while experimenting. Git made it easy to restore the project to previous states, and to see what had changed between commits. My commit script saved a lot of typing.

commit
#!/bin/bash
# Originally written May 9/05 by Mike Slinn

function help {
   echo "Runs git commit without prompting for a message."
   echo "Usage: commit [options] [file...]"
   echo "   Where options are:"
   echo "      -a \"tag message\""
   echo "      -d # enables debug mode"
   echo "      -m \"commit message\""
   echo "Examples:"
   echo "  commit  # The default commit message is just a single dash (-)"
   echo "  commit -m \"This is a commit message\""
   echo "  commit -a 0.1.2"
   exit 1
}

function isGitProject {
  cd "$( git rev-parse --git-dir )/.."
  [ -d .git ]
}


BRANCH="$(git rev-parse --abbrev-ref HEAD)"
MSG=""
while getopts "a:dhm:?" opt; do
   case $opt in
       a ) TAG="$OPTARG"
           git tag -a "$TAG" -m "v$TAG"
           git push origin --tags
           exit
           ;;
       d ) set -xv ;;
       m ) MSG="$OPTARG" ;;
       h ) help ;;
       \?) help ;;
   esac
done
shift "$((OPTIND-1))"


for o in "$@"; do
   if [ "$o" == "-m" ]; then unset MSG; fi
done

if isGitProject; then
  if [ "$@" ]; then
    git add -A "$@"
  else
    git add -A .
  fi
  shift
  if [ "$MSG" == "" ]; then MSG="-"; fi
  git commit -m "$MSG" "$@" 3>&1 1>&2 2>&3 | sed -e '/^X11/d'  | sed -e '/^Warning:/d'
  git push origin "$BRANCH" --tags 3>&1 1>&2 2>&3 | sed -e '/^X11/d'  | sed -e '/^Warning:/d'
else
  echo "Error: '$( pwd )' is not a git project"
fi

if [ -f 0 ]; then rm -f 0; fi
Shell
(aw) $ commit -h
Runs git commit without prompting for a message.
Usage: commit [options] [file...]
   Where options are:
      -a "tag message"
      -d # enables debug mode
      -m "commit message"
Examples:
  commit  # The default commit message is just a single dash (-)
  commit -m "This is a commit message"
  commit -a 0.1.2 

manage.py Help

The manage.py script can do many things. Here are its sub-commands:

Shell
(aw) $ ./manage.py
Type 'manage.py help <subcommand>' for help on a specific subcommand.
  
  Available subcommands:
  
  [auth]
      changepassword
      createsuperuser
  
  [contenttypes]
      remove_stale_contenttypes
  
  [django]
      check
      compilemessages
      createcachetable
      dbshell
      diffsettings
      dumpdata
      flush
      inspectdb
      loaddata
      makemessages
      makemigrations
      migrate
      sendtestemail
      shell
      showmigrations
      sqlflush
      sqlmigrate
      sqlsequencereset
      squashmigrations
      startapp
      startproject
      test
      testserver
  
  [haystack]
      build_solr_schema
      clear_index
      haystack_info
      rebuild_index
      update_index
  
  [oscar]
      oscar_calculate_scores
      oscar_cleanup_alerts
      oscar_find_duplicate_emails
      oscar_fork_app
      oscar_fork_statics
      oscar_generate_email_content
      oscar_import_catalogue
      oscar_import_catalogue_images
      oscar_populate_countries
      oscar_send_alerts
      oscar_update_product_ratings
  
  [sessions]
      clearsessions
  
  [staticfiles]
      collectstatic
      findstatic
      runserver
  
  [thumbnail]
      thumbnail 

requirements.txt

I found it strange that Frobshop did not provide requirements.txt to install managed dependencies. I created the file within the frobshop directory so I could replicate this installation on other machines:

Shell
(aw) $ pip freeze > requirements.txt

The file looked like this:

requirements.txt
appdirs==1.4.3
asgiref==3.3.1
Babel==2.9.0
CacheControl==0.12.6
certifi==2019.11.28
chardet==3.0.4
colorama==0.4.3
contextlib2==0.6.0
distlib==0.3.0
distro==1.4.0
Django==3.1.6
django-extra-views==0.13.0
django-haystack==3.0
django-oscar==3.0
django-phonenumber-field==3.0.1
django-tables2==2.3.4
django-treebeard==4.4
django-widget-tweaks==1.4.8
factory-boy==2.12.0
Faker==6.1.1
html5lib==1.0.1
idna==2.8
ipaddr==2.2.0
lockfile==0.12.2
msgpack==0.6.2
packaging==20.3
pep517==0.8.2
phonenumbers==8.12.18
Pillow==8.1.0
progress==1.5
purl==1.5
pycountry==20.7.3
pyparsing==2.4.6
python-dateutil==2.8.1
pytoml==0.1.21
pytz==2021.1
requests==2.22.0
retrying==1.3.3
six==1.14.0
sorl-thumbnail==12.6.3
sqlparse==0.4.1
text-unidecode==1.3
urllib3==1.25.8
webencodings==0.5.1

I think that requirements.txt should have been provided with Frobshop. If it had been provided, all one would have to do to install the proper version of all the Python modules would be to type:

Shell
(aw) $ pip install -r requirements.txt

This was a good time to commit my work to the git repository:

Shell
(aw) $ commit -m "First cut at dependencies"

Configuring Frobshop

Here is a comprehensive list of all django-oscar settings. This is a list of all django-oscar default values. Maximizing Django Oscar Settings Variables discusses some poorly documented settings.

The Frobshop documentation says to make massive changes to configuration files, without explaining why. No references are given, but I linked to all the documentation I could find. It seems some core apps, essential to proper operation, are undocumented.

Caution: the contents of these files change quite a bit between recent Django versions, so be warned that the config files are tightly bound to the Django version that it was generated for. This means that if you see a solution on StackOverflow to a problem that you currently have, blindly applying that solution may cause more problems instead of fixing them.

Here are the configured files that were altered, including a few changes I found necessary to make things work.

frobshop/settings.py
"""
Django settings for frobshop project.

Generated by 'django-admin startproject' using Django 3.1.6.

For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""

from pathlib import Path
from oscar.defaults import *

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'irs@7(qzjki2)gv7%b)zj5$t-6!l^kod0n4+!&hbgcku&b=$iv'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['*']


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'django.contrib.sites',
    'django.contrib.flatpages',

    'oscar.config.Shop',
    'oscar.apps.analytics.apps.AnalyticsConfig',
    'oscar.apps.checkout.apps.CheckoutConfig',
    'oscar.apps.address.apps.AddressConfig',
    'oscar.apps.shipping.apps.ShippingConfig',
    'oscar.apps.catalogue.apps.CatalogueConfig',
    'oscar.apps.catalogue.reviews.apps.CatalogueReviewsConfig',
    'oscar.apps.communication.apps.CommunicationConfig',
    'oscar.apps.partner.apps.PartnerConfig',
    'oscar.apps.basket.apps.BasketConfig',
    'oscar.apps.payment.apps.PaymentConfig',
    'oscar.apps.offer.apps.OfferConfig',
    'oscar.apps.order.apps.OrderConfig',
    'oscar.apps.customer.apps.CustomerConfig',
    'oscar.apps.search.apps.SearchConfig',
    'oscar.apps.voucher.apps.VoucherConfig',
    'oscar.apps.wishlists.apps.WishlistsConfig',
    'oscar.apps.dashboard.apps.DashboardConfig',
    'oscar.apps.dashboard.reports.apps.ReportsDashboardConfig',
    'oscar.apps.dashboard.users.apps.UsersDashboardConfig',
    'oscar.apps.dashboard.orders.apps.OrdersDashboardConfig',
    'oscar.apps.dashboard.catalogue.apps.CatalogueDashboardConfig',
    'oscar.apps.dashboard.offers.apps.OffersDashboardConfig',
    'oscar.apps.dashboard.partners.apps.PartnersDashboardConfig',
    'oscar.apps.dashboard.pages.apps.PagesDashboardConfig',
    'oscar.apps.dashboard.ranges.apps.RangesDashboardConfig',
    'oscar.apps.dashboard.reviews.apps.ReviewsDashboardConfig',
    'oscar.apps.dashboard.vouchers.apps.VouchersDashboardConfig',
    'oscar.apps.dashboard.communications.apps.CommunicationsDashboardConfig',
    'oscar.apps.dashboard.shipping.apps.ShippingDashboardConfig',

    # 3rd-party apps that oscar depends on
    'widget_tweaks',
    'haystack',
    'treebeard',
    'sorl.thumbnail',   # Default thumbnail backend, can be replaced
    'django_tables2',
]

SITE_ID = 1

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',

    'oscar.apps.basket.middleware.BasketMiddleware',
    'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
]

ROOT_URLCONF = 'frobshop.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'oscar.apps.search.context_processors.search_form',
                'oscar.apps.checkout.context_processors.checkout',
                'oscar.apps.communication.notifications.context_processors.notifications',
                'oscar.core.context_processors.metadata',
            ],
        },
    },
]

WSGI_APPLICATION = 'frobshop.wsgi.application'


# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases

DATABASES = {
    'default': {
        #'ENGINE': 'django.db.backends.sqlite3',
        #'NAME': BASE_DIR / 'db.sqlite3',

        'ENGINE': 'django.db.backends.postgresql',
        'HOST': 'localhost',
        'NAME': 'frobshop',
        'PASSWORD': 'secret',
        'PORT': '5432',
        'USER': 'postgres',

        'ATOMIC_REQUESTS': True,
    }
}


# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

AUTHENTICATION_BACKENDS = (
    'oscar.apps.customer.auth_backends.EmailBackend',
    'django.contrib.auth.backends.ModelBackend',
)
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

HAYSTACK_CONNECTIONS = {
    'default': {
         'ENGINE': 'haystack.backends.simple_backend.SimpleEngine',
        #'ENGINE': 'haystack.backends.solr_backend.SolrEngine',
        #'URL': 'http://127.0.0.1:8983/solr/tester',                 # Assuming you created a core named 'tester' as described in installing search engines.
        #'ADMIN_URL': 'http://127.0.0.1:8983/solr/admin/cores'
        # ...or for multicore...
        # 'URL': 'http://127.0.0.1:8983/solr/mysite',
    },
}


# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'America/New_York'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/

STATIC_URL = '/static/'

Here is documentation that helps explain what the above application-related settings above pertain to:

Django’s routing table is defined using urls.py:

frobshop/urls.py
"""
frobshop URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/3.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    # path('i18n/', include('django.conf.urls.i18n')),
    path('admin/', admin.site.urls),
    path('', include(apps.get_app_config('oscar').urls[0])),
]

Time once again to commit my work:

Shell
(aw) $ commit -m "Initial settings."

Installing PostgreSQL

Shell
(aw) $ sudo curl https://www.pgadmin.org/static/packages_pgadmin_org.pub | sudo apt-key add

(aw) $ sudo sh -c 'echo "deb https://ftp.postgresql.org/pub/pgadmin/pgadmin4/apt/$(lsb_release -cs) pgadmin4 main" > /etc/apt/sources.list.d/pgadmin4.list && apt update'

(aw) $ yes | sudo apt install postgresql pgadmin4

(aw) $ sudo service postgresql start

(aw) $ sudo -u postgres /usr/bin/psql -h localhost -c "ALTER USER postgres PASSWORD 'whatever';"

Creating the Database

I had previously installed PostgreSQL. I just needed to create an empty database called frobshop. One option for doing that would be to run psql and pass it parameters that indicate the network node and user to connect, then create the database:

Shell
(aw) $ /usr/bin/psql -U postgres \
  -h localhost \
  -c 'create database frobshop'

BTW, I had originally followed the Frobshop tutorial's suggestion to use SQLite. I then tried to convert the frobshop SQLite database to PostgreSQL like this, but the conversion failed:

Shell
(aw) $ sudo apt install pgloader

(aw) $ PGPASSWORD=secret /bin/createdb -U postgres -h localhost frobshop

(aw) $ PGPASSWORD=secret pgloader -U postgres -h localhost \
  db.sqlite3 postgresql:///frobshop
(aw) $ PGPASSWORD=secret pgloader -U postgres \
  ./sqlite.to.postgres
sqlite.to.postgres
load database
  from db.sqlite3
  into postgresql:///frobshop
  with include drop, create tables, create indexes, reset sequences
  set work_mem to '16MB', maintenance_work_mem to '512 MB';

It would make sense to just set up PostgreSQL straight away if you have any desire to understand how django-oscar works.

Since this step did not change any Frobshop files, there was nothing to commit to the git repository.

oscar_populate_countries Help

A django-oscar website will not work until country information is defined. The oscar_populate_countries subcommand of manage.py defines countries. Here is the help information for that subcommand:

Shell
(aw) $ ./manage.py oscar_populate_countries -h
usage: manage.py oscar_populate_countries [-h] [--no-shipping] [--initial-only] [--version] [-v {0,1,2,3}]
                                          [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color]
                                          [--force-color] [--skip-checks]

Populates the list of countries with data from pycountry.

optional arguments:
  -h, --help            show this help message and exit
  --no-shipping         Don<t mark countries for shipping
  --initial-only        Exit quietly without doing anything if countries were already populated.
  --version             show program<s version number and exit
  -v {0,1,2,3}, --verbosity {0,1,2,3}
                        Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output
  --settings SETTINGS   The Python path to a settings module, e.g. "myproject.settings.main". If this isn<t provided,
                        the DJANGO_SETTINGS_MODULE environment variable will be used.
  --pythonpath PYTHONPATH
                        A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".
  --traceback           Raise on CommandError exceptions
  --no-color            Don<t colorize the command output.
  --force-color         Force colorization of the command output.
  --skip-checks         Skip system checks. 

Alternative Invocation Style

It is also possible to invoke the manage program like this:

Shell
(aw) $ python -m manage oscar_populate_countries -h

Defining Countries

I just want to be able to ship to Canada and the USA. Several steps are required to do that:

  1. Invoke oscar_populate_countries with --no-shipping
    Shell
    (aw) $ ./manage.py oscar_populate_countries --no-shipping
    Successfully added 249 countries. 
  2. You can enable shipping to selected countries two ways:
    1. Command-line instructions:
      Shell
      (aw) $ psql -c "update address_country set is_shipping_country = 't' where iso_3166_1_a2 in ('CA', 'US');"
    2. Web Admin instructions
      1. Use django-admin (for me that was http://localhost:8001/admin/).
      2. Navigate to the country editor.
      3. Click on Canada.
      4. Enable Is shipping country and press Save.
      5. Click on Countries at the top of the left-hand menu.
      6. Type United States into the text search field, click on United States, enable Is shipping country and press Save.

Since this step did not change any Frobshop files, there was nothing to commit to the git repository.

manage.py migrate Options

The Frobshop tutorial does not define data migrations very well. This is a gentle introduction to Django migrations. This is the official documentation, and this is information about writing database migrations.

The help for manage.py migrate is:

Shell
(aw) $ ./manage.py migrate -h
usage: manage.py migrate [-h] [--noinput] [--database DATABASE] [--fake] [--fake-initial] [--plan] [--run-syncdb] [--check]
                         [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback]
                         [--no-color] [--force-color] [--skip-checks]
                         [app_label] [migration_name]
Updates database schema. Manages both apps with migrations and those without.
positional arguments: app_label App label of an application to synchronize the state. migration_name Database state will be brought to the state after that migration. Use the name "zero" to unapply all migrations.
optional arguments: -h, --help show this help message and exit --noinput, --no-input Tells Django to NOT prompt the user for input of any kind. --database DATABASE Nominates a database to synchronize. Defaults to the "default" database. --fake Mark migrations as run without actually running them. --fake-initial Detect if tables already exist and fake-apply initial migrations if so. Make sure that the current database schema matches your initial migration before using this flag. Django will only check for an existing table name. --plan Shows a list of the migration actions that will be performed. --run-syncdb Creates tables for apps without migrations. --check Exits with a non-zero status if unapplied migrations exist. --version show program’s version number and exit -v {0,1,2,3}, --verbosity {0,1,2,3} Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output --settings SETTINGS The Python path to a settings module, e.g. "myproject.settings.main". If this isn’t provided, the DJANGO_SETTINGS_MODULE environment variable will be used. --pythonpath PYTHONPATH A directory to add to the Python path, e.g. "/home/djangoprojects/myproject". --traceback Raise on CommandError exceptions --no-color Don’t colorize the command output. --force-color Force colorization of the command output. --skip-checks Skip system checks.

Creating the Database from Migrations

Shell
(aw) $ ./manage.py migrate
System check identified some issues:
WARNINGS: catalogue.ProductAttributeValue.value_boolean: (fields.W903) NullBooleanField is deprecated. Support for it (except in historical migrations) will be removed in Django 4.0. HINT: Use BooleanField(null=True) instead. Operations to perform: Apply all migrations: address, admin, analytics, auth, basket, catalogue, communication, contenttypes, customer, flatpages, offer, order, partner, payment, reviews, sessions, shipping, sites, thumbnail, voucher, wishlists Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying address.0001_initial... OK Applying address.0002_auto_20150927_1547... OK Applying address.0003_auto_20150927_1551... OK Applying address.0004_auto_20170226_1122... OK Applying address.0005_regenerate_user_address_hashes... OK Applying address.0006_auto_20181115_1953... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying catalogue.0001_initial... OK Applying analytics.0001_initial... OK Applying analytics.0002_auto_20140827_1705... OK Applying analytics.0003_auto_20200801_0817... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying auth.0012_alter_user_first_name_max_length... OK Applying sites.0001_initial... OK Applying partner.0001_initial... OK Applying customer.0001_initial... OK Applying basket.0001_initial... OK Applying basket.0002_auto_20140827_1705... OK Applying order.0001_initial... OK Applying offer.0001_initial... OK Applying voucher.0001_initial... OK Applying basket.0003_basket_vouchers... OK Applying basket.0004_auto_20141007_2032... OK Applying basket.0005_auto_20150604_1450... OK Applying basket.0006_auto_20160111_1108... OK Applying basket.0007_slugfield_noop... OK Applying basket.0008_auto_20181115_1953... OK Applying basket.0009_line_date_updated... OK Applying catalogue.0002_auto_20150217_1221... OK Applying catalogue.0003_data_migration_slugs... OK Applying catalogue.0004_auto_20150217_1710... OK Applying catalogue.0005_auto_20150604_1450... OK Applying catalogue.0006_auto_20150807_1725... OK Applying catalogue.0007_auto_20151207_1440... OK Applying catalogue.0008_auto_20160304_1652... OK Applying catalogue.0009_slugfield_noop... OK Applying catalogue.0010_auto_20170420_0439... OK Applying catalogue.0011_auto_20170422_1355... OK Applying catalogue.0012_auto_20170609_1902... OK Applying catalogue.0013_auto_20170821_1548... OK Applying catalogue.0014_auto_20181115_1953... OK Applying catalogue.0015_product_is_public... OK Applying catalogue.0016_auto_20190327_0757... OK Applying catalogue.0017_auto_20190816_0938... OK Applying catalogue.0018_auto_20191220_0920... OK Applying catalogue.0019_option_required... OK Applying catalogue.0020_auto_20200801_0817... OK Applying catalogue.0021_auto_20201005_0844... OK Applying order.0002_auto_20141007_2032... OK Applying order.0003_auto_20150113_1629... OK Applying order.0004_auto_20160111_1108... OK Applying order.0005_update_email_length... OK Applying order.0006_orderstatuschange... OK Applying order.0007_auto_20181115_1953... OK Applying customer.0002_auto_20150807_1725... OK Applying customer.0003_update_email_length... OK Applying customer.0004_email_save... OK Applying customer.0005_auto_20181115_1953... OK Applying communication.0001_initial... OK Applying order.0008_auto_20190301_1035... OK Applying communication.0002_reset_table_names... OK Applying communication.0003_remove_notification_category_make_code_uppercase... OK Applying communication.0004_auto_20200801_0817... OK Applying customer.0006_auto_20190430_1736... OK Applying customer.0007_auto_20200801_0817... OK Applying flatpages.0001_initial... OK Applying offer.0002_auto_20151210_1053... OK Applying offer.0003_auto_20161120_1707... OK Applying offer.0004_auto_20170415_1518... OK Applying offer.0005_auto_20170423_1217... OK Applying offer.0006_auto_20170504_0616... OK Applying offer.0007_conditionaloffer_exclusive... OK Applying offer.0008_auto_20181115_1953... OK Applying offer.0009_auto_20200801_0817... OK Applying offer.0010_conditionaloffer_combinations... OK Applying order.0009_surcharge... OK Applying order.0010_auto_20200724_0909... OK Applying order.0011_auto_20200801_0817... OK Applying partner.0002_auto_20141007_2032... OK Applying partner.0003_auto_20150604_1450... OK Applying partner.0004_auto_20160107_1755... OK Applying partner.0005_auto_20181115_1953... OK Applying partner.0006_auto_20200724_0909... OK Applying payment.0001_initial... OK Applying payment.0002_auto_20141007_2032... OK Applying payment.0003_auto_20160323_1520... OK Applying payment.0004_auto_20181115_1953... OK Applying payment.0005_auto_20200801_0817... OK Applying reviews.0001_initial... OK Applying reviews.0002_update_email_length... OK Applying reviews.0003_auto_20160802_1358... OK Applying reviews.0004_auto_20170429_0941... OK Applying sessions.0001_initial... OK Applying shipping.0001_initial... OK Applying shipping.0002_auto_20150604_1450... OK Applying shipping.0003_auto_20181115_1953... OK Applying sites.0002_alter_domain_unique... OK Applying thumbnail.0001_initial... OK Applying voucher.0002_auto_20170418_2132... OK Applying voucher.0003_auto_20171212_0411... OK Applying voucher.0004_auto_20180228_0940... OK Applying voucher.0005_auto_20180402_1425... OK Applying voucher.0006_auto_20180413_0911... OK Applying voucher.0007_auto_20181115_1953... OK Applying voucher.0008_auto_20200801_0817... OK Applying wishlists.0001_initial... OK Applying wishlists.0002_auto_20160111_1108... OK Applying wishlists.0003_auto_20181115_1953... OK

Since this step did not change any Frobshop files, there was nothing to commit to the git repository.

Mysterious Warning from manage.py

The following warning appeared every time manage.py ran:

Mysterious warning
WARNINGS:
catalogue.ProductAttributeValue.value_boolean: (fields.W903) NullBooleanField is deprecated.
Support for it (except in historical migrations) will be removed in Django 4.0.
HINT: Use BooleanField(null=True) instead.

Thanks to StackOverflow, I was able to make this django-oscar warning disappear. This is what the offending line looked like (line folded for legibility):

Isolated problem
value_boolean = models.NullBooleanField(_('Boolean'),
  blank=True, db_index=True)

This is what I understood the suggested change to be (line folded for legibility):

Suggested change
value_boolean = models.BooleanField(_('Boolean'),
  blank=True, db_index=True)

Here is the incantation I used, which relies on an environment variable called oscar to point to the Python virtual environment. Line 1043 of abstract_models.py was modified, within the definition of a class called AbstractProductAttributeValue.

Shell
(aw) $ sed -i 's/models.NullBooleanField/models.BooleanField/' \
  ~/venv/aw/lib/python3.8/site-packages/oscar/apps/catalogue/abstract_models.py

Startapp Subcommand

The startapp subcommand of manage.py message is:

Shell
(aw) $ ./manage.py startapp --help
usage: manage.py startapp [-h] [--template TEMPLATE] [--extension EXTENSIONS] [--name FILES] [--version]
                          [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color]
                          [--force-color]
                          name [directory]

Creates a Django app directory structure for the given app name in the current directory or optionally in the given
directory.

positional arguments:
  name                  Name of the application or project.
  directory             Optional destination directory

optional arguments:
  -h, --help            show this help message and exit
  --template TEMPLATE   The path or URL to load the template from.
  --extension EXTENSIONS, -e EXTENSIONS
                        The file extension(s) to render (default: "py"). Separate multiple extensions with commas, or
                        use -e multiple times.
  --name FILES, -n FILES
                        The file name(s) to render. Separate multiple file names with commas, or use -n multiple
                        times.
  --version             show program's version number and exit
  -v {0,1,2,3}, --verbosity {0,1,2,3}
                        Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output
  --settings SETTINGS   The Python path to a settings module, e.g. "myproject.settings.main". If this isn't provided,
                        the DJANGO_SETTINGS_MODULE environment variable will be used.
  --pythonpath PYTHONPATH
                        A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".
  --traceback           Raise on CommandError exceptions
  --no-color            Don't colorize the command output.
  --force-color         Force colorization of the command output.

Adding a New Django App

I added a new Django app called pricing by using the startapp subcommand of manage.py:

Shell
(aw) $ ./manage.py startapp pricing

That created the following directories:

Shell
(aw) $ tree pricing
pricing
  ├── __init__.py
  ├── admin.py
  ├── apps.py
  ├── migrations
  │   └── __init__.py
  ├── models.py
  ├── tests.py
  └── views.py 

Nullable Field Problem

After adding the new Django app called pricing to frobshop, I encountered this error:

Shell
(aw) $ ./manage.py makemigrations pricing
You are trying to change the nullable field 'value_boolean' on productattributevalue to non-nullable without a default; we can’t do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Ignore for now, and let me handle existing rows with NULL myself (e.g. because you added a RunPython or RunSQL operation to handle NULL values in a previous data migration)
 3) Quit, and let me add a default in models.py
Select an option: 

Update: now I would just provide a one-off default, or blow away with a delete SQL query, since the database do not yet contain important information. Instead, I continued to stumble forward:

I cancelled the makemigrations subcommand and changed the definition of the field to allow nulls, so it looked like what was in the django-oscar master branch (line folded for legibility):

abstract_models.py
value_boolean = models.BooleanField(_('Boolean'), blank=True,
  null=True, db_index=True)

The error went away the next time I ran makemigrations. The sed command to make this change is:

Shell
(aw) $ sed -i \
  -e 's/value_boolean = /models.NullBooleanField/models.BooleanField/' \
  -e 's/value_boolean = /)/, null=True)/' \
  ~/venv/aw/lib/python3.8/site-packages/oscar/apps/catalogue/abstract_models.py

Creating the Django Superuser

The Frobshop documentation did not mention this topic. It is easy to do.

Shell
(aw) $ ./manage.py createsuperuser
Username (leave blank to use 'mslinn'):
Email address: mslinn@ancientwarmth.com
Password:
Password (again):
Superuser created successfully.

Since this step did not change any Frobshop files, there was nothing to commit to the git repository.

manage.py runserver Options

The help for manage.py runserver is:

Shell
(aw) $ ./manage.py runserver -h
usage: manage.py runserver [-h] [--ipv6] [--nothreading] [--noreload] [--nostatic] [--insecure] [--version] [-v {0,1,2,3}]
                           [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
                           [addrport]

Starts a lightweight Web server for development and also serves static files.

positional arguments:
  addrport              Optional port number, or ipaddr:port

optional arguments:
  -h, --help            show this help message and exit
  --ipv6, -6            Tells Django to use an IPv6 address.
  --nothreading         Tells Django to NOT use threading.
  --noreload            Tells Django to NOT use the auto-reloader.
  --nostatic            Tells Django to NOT automatically serve static files at STATIC_URL.
  --insecure            Allows serving static files even if DEBUG is False.
  --version             show program's version number and exit
  -v {0,1,2,3}, --verbosity {0,1,2,3}
                        Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output
  --settings SETTINGS   The Python path to a settings module, e.g. "myproject.settings.main". If this isn't provided, the
                        DJANGO_SETTINGS_MODULE environment variable will be used.
  --pythonpath PYTHONPATH
                        A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".
  --traceback           Raise on CommandError exceptions
  --no-color            Don't colorize the command output.
  --force-color         Force colorization of the command output. 

Launching Oscar

Finally it was time to launch oscar on port 8000: BTW, Django forks into 2 processes when it launches, and each process loads the settings separately. Use the --noreload option to force loading the settings only once.

Shell
(aw) $ ./manage.py runserver --noreload
Watching for file changes with StatReloader
February 11, 2021 - 15:45:01
Django version 3.1.6, using settings 'frobshop.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C. 

Launch Failure

Frobshop failed to launch. This was due to a serious django-oscar implementation deficiency:

  1. django-oscar forms is hard-coded to require django-haystack:
    Excerpt from oscar/apps/search/forms.html
    from haystack.forms import FacetedSearchForm
  2. Oscar uses Haystack to abstract away from different search backends. Unfortunately, writing backend-agnostic code is nonetheless hard and Apache Solr is currently the only supported production-grade backend.
    While the django-oscar documentation describes various other fulltext search backends (ElasticSearch, Whoosh, and Xapian), apparently the official position is that they should not be used in production. ... or so the docs say. No-one seems to believe that statement. The documentation contains some strongly expressed views that are not widely held.

I wrote up an article on this issue, and posted to new GitHub discussion that I created for this topic.

Workaround

There is a django-haystack backend called SimpleEngine that uses ordinary SQL queries instead of fulltext search.

I installed and enabled django-haystack, and configured frobshop/settings.py to use SimpleEngine. The next time I tried to run django-oscar, it presented itself in my browser at http://localhost:8000.

I logged in as the superuser and saw:

Time once again to commit my work:

Shell
(aw) $ commit -m "Switched haystack.backends from SolrEngine to SimpleEngine."

Dashboard Navigation

Browsing throught the django-oscar, I stumbled across where the menu structure of the dashboard navigation is defined, in JSON.

Order Pipeline

The Frobshop tutorial does not offer any information about how the order status of line items affects the status of orders. I found How to set up order processing, which addressed the issue. The tutorial should link to that page.

Reading “How to set up order processing”, it seems there is a checkout app and an order app. A link to these from the article “How to set up order processing” would have been helpful. This page would be a good place to provide a discussion of how apps work together to enable django-oscar’s functionality, but I could only find a generic explanation of Django apps in the Django documentation.

Unfortunately, “How to set up order processing” does not provide a sufficiently detailed explanation to understand the suggested settings in the Frobshop tutorial, which are:

OSCAR_INITIAL_ORDER_STATUS = 'Pending'
OSCAR_INITIAL_LINE_STATUS = 'Pending'
OSCAR_ORDER_STATUS_PIPELINE = {
    'Pending': ('Being processed', 'Cancelled',),
    'Being processed': ('Processed', 'Cancelled',),
    'Cancelled': (),
}

Reading the Frobshop tutorial, I expected that if I copied the above settings into frobshop/settings.py that “With these three settings defined in your project you’ll be able to see the different statuses in the order management dashboard.” However, the information presented in “How to set up order processing” made me think that some custom programming would be required. This Frobshop tutorial is horrible!

I pasted the above into frobshop/settings.py, and django-oscar started without errors or warnings. I am unsure how to ‘order’ a product from this tutorial, however.

Time once again to commit my work:

Shell
(aw) $ commit -m "Defined order pipeline without understanding what I did."

More Clues

The django-oscar docs are sorely lacking for this topic. I found this buried in the source code for django-oscar: “The pipeline defines the statuses that an order or line item can have and what transitions are allowed in any given status. The pipeline is defined as a dictionary where the keys are the available statuses. Allowed transitions are defined as iterable values for the corresponding status.”

Additional settings in the same internal document include:

OSCAR_ORDER_STATUS_CASCADE

This defines a mapping of status changes for order lines which cascade from an order status change. For example:

OSCAR_ORDER_STATUS_CASCADE = {
    'Being processed': 'In progress'
}

With this mapping, when an order has its status set to Being processed, all lines within it have their status set to In progress. In a sense, the status change cascades down to the related objects. Note that this cascade ignores restrictions from the OSCAR_LINE_STATUS_PIPELINE.

OSCAR_LINE_STATUS_PIPELINE

Same as OSCAR_ORDER_STATUS_PIPELINE but for lines. Default: {}

Conclusion

Django-oscar seems well-designed, and seems to have all the features that I am looking for. The community is active, responsive and helpful. However, django-oscar, like many open-source products, suffers from poor documentation and has many problems. Nothing is perfect. However, django-oscar seems to be good enough for my needs.



* indicates a required field.

Please select the following to receive Mike Slinn’s newsletter:

You can unsubscribe at any time by clicking the link in the footer of emails.

Mike Slinn uses Mailchimp as his marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp’s privacy practices.