Mike Slinn
Mike Slinn

Evaluating Django-Oscar, a F/OSS Shopping Cart

Published 2021-02-11. Last modified 2021-02-14.
Time to read: about 7 minutes.

This article is categorized under Django, Django-Oscar, Open Source, PostgreSQL, Python, e-commerce

In a previous blog post I discussed why I decided to check out the django-oscar e-commerce framework in depth. The next step in my evaluation was to set up and run django-oscar to experience it in action. This post shows the details of how I installed django v3.1.6 and django-oscar v3.0 on Ubuntu 20.04 with Python 3.8.6.

The very first page in the django-oscar documentation describes a sample project called Frobshop. This blog post describes my Frobshop installation. A follow-on post discusses why I disabled Django Haystack for Frobshop and also did not install Apache Solr.

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 the virtual environment at ~/oscar, like this:

Shell
$ sudo apt install python3-virtualenv

$ cd

$ virtualenv oscar
created virtual environment CPython3.8.6.final.0-64 in 528ms
  creator CPython3Posix(dest=/home/mslinn/oscar, 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 

$ source ~/oscar/bin/activate

(oscar) $ 

Notice that the last command above changed the shell prompt, in that (oscar) was prepended to the normal prompt. To cause all future shells to use this virtual environment by default, add the last command above to ~/.bashrc, like this:

Shell
$ echo "source ~/oscar/bin/activate" >> ~/.bashrc

At this point the virtual environment just contained executable images for Python.

Shell
$ ls ~/oscar/**
oscar/pyvenv.cfg

oscar/bin:
activate       activate.ps1      chardetect      distro      easy_install      pip      pip3.8   python3.8  wheel3
activate.csh   activate.xsh      chardetect-3.8  distro-3.8  easy_install-3.8  pip-3.8  python   wheel
activate.fish  activate_this.py  chardetect3     distro3     easy_install3     pip3     python3  wheel-3.8

oscar/lib:
python3.8 

Next, I installed the Python libraries and their dependencies. django-oscar installs Django as a dependency, plus I specified a few other dependencies that do not automatically get pulled in. I also 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
(oscar) $ pip install django-oscar[sorl-thumbnail] \
  django-postgresql Pillow pycountry

Now the virtual environment also contains 3 Django executable images:

Shell
(oscar) $ ls ~/oscar/**
/home/mslinn/oscar/pyvenv.cfg

/home/mslinn/oscar/bin:
__pycache__    activate.ps1      chardetect-3.8  distro3          easy_install-3.8  pip-3.8  python     wheel
activate       activate.xsh      chardetect3     django-admin     easy_install3     pip3     python3    wheel-3.8
activate.csh   activate_this.py  distro          django-admin.py  faker             pip3.8   python3.8  wheel3
activate.fish  chardetect        distro-3.8      easy_install     pip               pybabel  sqlformat

/home/mslinn/oscar/lib:
python3.8 

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

Shell
(oscar) $ 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
(oscar) $ mkdir $work/django && cd $work/django

(oscar) $ django-admin startproject frobshop

(oscar) $ cd frobshop

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

Shell
(oscar) $ 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
(oscar) $ git init

(oscar) $ hub create -p frobshop

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

(oscar) $ git add .

(oscar) $ git commit -m -

(oscar) $ 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`/..
  if [ -d .git ]; then true; else false; fi
}


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

CWD=`pwd`
if [ isGitProject ]; then HAVE_GIT=true; fi

if [ "$HAVE_GIT" ]; then 
  if [ "$@" ]; then
    git add -A $@
  else
    git add -A .
  fi
  shift
  if [ "$MSG" == "" ]; then MSG="-"; fi
  git commit -m "$MSG" $@
  git push origin "$BRANCH"
fi

if [ -f 0 ]; then rm -f 0; fi
Shell
$ 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 subcommands:

Shell
$ ./manage.py
Type 'manage.py help ' 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
(oscar) $ 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 in order to install the proper version of all the Python modules would be to type:

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

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

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

Configuring Frobshop

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
(oscar) $ commit -m "Initial settings."

Installing PostgreSQL

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

(oscar) $ 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'

(oscar) $ yes | sudo apt install postgresql pgadmin4

(oscar) $ sudo service postgresql start

(oscar) $ 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
$ /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
$ sudo apt install pgloader

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

$ PGPASSWORD=secret pgloader -U postgres -h localhost \
  db.sqlite3 postgresql:///frobshop
$ 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';

I view SQLite as a toy, which does not lend itself to proper experimentation. 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
$ ./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. 

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
    $ ./manage.py oscar_populate_countries --no-shipping
    Successfully added 249 countries. 
  2. Use django-admin (for me that was http://localhost:8001/admin/).
  3. Navigate to the country editor.
  4. Click on Canada.
  5. Enable Is shipping country and press Save.
  6. Click on Countries at the top of the left-hand menu.
  7. 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
(oscar) $ ./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
$ ./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

This warning appeared every time manage.py ran:

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. Here is the incantation I used, which relies on an environment variable called oscar to point to the Python virtual environment.

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

Creating Django Superuser

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

Shell
(oscar) $ ./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
(oscar) $ ./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:

Shell
(oscar) $ ./manage.py runserver
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. Apache Solr is mandated for Haystack:
    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), the official position is that they should not be used in production.

I wrote up a blog post on this issue, and made further suggestions in a GitHub issue that I created for this topic.

Workaround

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

After I installed and enabled django-haystack, and configured frobshop/settings.py to use SimpleEngine django-oscar 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
(oscar) $ commit -m "Switched haystack.backends from SolrEngine to SimpleEngine."

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
(oscar) $ commit -m "Defined order pipeline without understanding what I did."

More Clues

The django-oscar docs are sorely lacking. 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, repsonsive and helpful. However, django-oscar, like many open-source products, suffers from poor documentation and has many problems. Nothing is perfect. However, django-oscar might be good enough.