Django and Oscar

Django Apps, AppConfig, OscarConfig and Sub-Apps

Published 2021-02-24.
Time to read: 3 minutes.

This page is part of the django collection.

Let’s start off today’s exploration of Django and django-oscar by learning about Django apps.

Django contains a registry of installed applications that stores configuration and provides introspection. This registry is called apps, and it is available in django.apps:

Python REPL
>>> from django.apps import apps
>>> apps.get_app_config('admin').verbose_name
'Administration' 

Replicating the Session

To replicate the interactive session shown in the Django documentation, we can launch a Python interactive shell by using the shell subcommand of the Frobshop manage.py. Regular readers of this blog will recognize Frobshop as the django-oscar tutorial e-commerce site that I’ve been working through.

Notice that django.apps has type <class 'django.apps.registry.Apps'> and resides at memory address 0x7ffb21210340.

While we are here, we might as well look around. Tab completion is a wonderful thing; it is a good way to discover properties and methods of Python classes and objects.

Python REPL
>>> admin.tab
admin.apps            admin.import_models(  admin.name
admin.create(         admin.label           admin.path
admin.default_site    admin.models          admin.ready(
admin.get_model(      admin.models_module   admin.verbose_name
admin.get_models(     admin.module
>>>
admin.apps <django.apps.registry.Apps object at 0x7f8da050f9a0>
>>>
admin.apps.tab admin.apps.all_models admin.apps.app_configs admin.apps.apps_ready admin.apps.check_apps_ready( admin.apps.check_models_ready( admin.apps.clear_cache( admin.apps.do_pending_operations( admin.apps.get_app_config( admin.apps.get_app_configs( admin.apps.get_containing_app_config( admin.apps.get_model( admin.apps.get_models( admin.apps.get_registered_model( admin.apps.get_swappable_settings_name( admin.apps.is_installed( admin.apps.lazy_model_operation( admin.apps.loading admin.apps.models_ready admin.apps.populate( admin.apps.ready admin.apps.ready_event admin.apps.register_model( admin.apps.set_available_apps( admin.apps.set_installed_apps( admin.apps.stored_app_configs admin.apps.unset_available_apps( admin.apps.unset_installed_apps(
😁

But wait, there is more!

Python REPL
>>> admin.default_site
'django.contrib.admin.sites.AdminSite' 
>>> admin.label 'admin'
>>>
admin.models {'logentry': <class 'django.contrib.admin.models.LogEntry'>}
>>>
admin.models_module <module 'django.contrib.admin.models' from '/var/work/django/oscar/lib/python3.8/site-packages/django/contrib/admin/models.py'>
>>>
admin.module <module 'django.contrib.admin' from '/var/work/django/oscar/lib/python3.8/site-packages/django/contrib/admin/__init__.py'>
>>>
admin.name 'django.contrib.admin'
>>>
admin.path >>> '/var/work/django/oscar/lib/python3.8/site-packages/django/contrib/admin'
😁

django-oscar AppConfigs

We can display a list of all the default django-oscar AppConfig subclasses:

Python REPL
>>> configs = apps.get_app_configs()
>>> configs dict_values([<AdminConfig: admin>, <AuthConfig: auth>, <ContentTypesConfig: contenttypes>, <SessionsConfig: sessions>, <MessagesConfig: messages>, <StaticFilesConfig: staticfiles>, <SitesConfig: sites>, <FlatPagesConfig: flatpages>, <Shop: oscar>, <AnalyticsConfig: analytics>, <CheckoutConfig: checkout>, <AddressConfig: address>, <ShippingConfig: shipping>, <CatalogueConfig: catalogue>, <CatalogueReviewsConfig: reviews>, <CommunicationConfig: communication>, <PartnerConfig: partner>, <BasketConfig: basket>, <PaymentConfig: payment>, <OfferConfig: offer>, <OrderConfig: order>, <CustomerConfig: customer>, <SearchConfig: search>, <VoucherConfig: voucher>, <WishlistsConfig: wishlists>, <DashboardConfig: dashboard>, <ReportsDashboardConfig: reports_dashboard>, <UsersDashboardConfig: users_dashboard>, <OrdersDashboardConfig: orders_dashboard>, <CatalogueDashboardConfig: catalogue_dashboard>, <OffersDashboardConfig: offers_dashboard>, <PartnersDashboardConfig: partners_dashboard>, <PagesDashboardConfig: pages_dashboard>, <RangesDashboardConfig: ranges_dashboard>, <ReviewsDashboardConfig: reviews_dashboard>, <VouchersDashboardConfig: vouchers_dashboard>, <CommunicationsDashboardConfig: communications_dashboard>, <ShippingDashboardConfig: shipping_dashboard>, <AppConfig: widget_tweaks>, <HaystackConfig: haystack>, <AppConfig: treebeard>, <AppConfig: thumbnail>, <AppConfig: django_tables2>, <DebugToolbarConfig: debug_toolbar>])

Let’s dissect configs, which is of type dict_values. It would be useful to obtain a list of all 44 django-oscar AppConfig labels.

Python REPL
>>> labels = [c.label for c in configs]
>>> print(", ".join(labels)) admin, auth, contenttypes, sessions, messages, staticfiles, sites, flatpages, oscar, analytics, checkout, address, shipping, catalogue, reviews, communication, partner, basket, payment, offer, order, customer, search, voucher, wishlists, dashboard, reports_dashboard, users_dashboard, orders_dashboard, catalogue_dashboard, offers_dashboard, partners_dashboard, pages_dashboard, ranges_dashboard, reviews_dashboard, vouchers_dashboard, communications_dashboard, shipping_dashboard, widget_tweaks, haystack, treebeard, thumbnail, django_tables2, debug_toolbar
>>>
len(labels) 44

It would also be useful to see a list of all 13 django-oscar AppConfig labels that contain the word dashboard.

Python REPL
>>> dashboard_labels = sorted([label for label in labels if "dashboard" in label])
>>> print(", ".join(dashboard_labels)) catalogue_dashboard, communications_dashboard, dashboard, offers_dashboard, orders_dashboard, pages_dashboard, partners_dashboard, ranges_dashboard, reports_dashboard, reviews_dashboard, shipping_dashboard, users_dashboard, vouchers_dashboard
>>>
len(dashboard_labels) 13

Finally, it would be useful to see a list of all django-oscar AppConfig labels and the corresponding names. I’ve made the labels bold to make reading easier. The dashboard app names are of particular interest, so I highlighted them.

Python REPL
>>> configs = list(apps.get_app_configs())
>>> label_name_tuples = sorted([(c.label, c.name) for c in configs], key=lambda x: x[0])
>>> [print(f'<b>{label}</b>: {name}') for (label, name) in label_name_tuples] address: oscar.apps.address admin: django.contrib.admin analytics: oscar.apps.analytics auth: django.contrib.auth basket: oscar.apps.basket catalogue: oscar.apps.catalogue catalogue_dashboard: oscar.apps.dashboard.catalogue checkout: oscar.apps.checkout communication: oscar.apps.communication communications_dashboard: oscar.apps.dashboard.communications contenttypes: django.contrib.contenttypes customer: oscar.apps.customer dashboard: oscar.apps.dashboard debug_toolbar: debug_toolbar django_tables2: django_tables2 flatpages: django.contrib.flatpages haystack: haystack messages: django.contrib.messages offer: oscar.apps.offer offers_dashboard: oscar.apps.dashboard.offers order: oscar.apps.order orders_dashboard: oscar.apps.dashboard.orders oscar: oscar pages_dashboard: oscar.apps.dashboard.pages partner: oscar.apps.partner partners_dashboard: oscar.apps.dashboard.partners payment: oscar.apps.payment ranges_dashboard: oscar.apps.dashboard.ranges reports_dashboard: oscar.apps.dashboard.reports reviews: oscar.apps.catalogue.reviews reviews_dashboard: oscar.apps.dashboard.reviews search: oscar.apps.search sessions: django.contrib.sessions shipping: oscar.apps.shipping shipping_dashboard: oscar.apps.dashboard.shipping sites: django.contrib.sites staticfiles: django.contrib.staticfiles thumbnail: sorl.thumbnail treebeard: treebeard users_dashboard: oscar.apps.dashboard.users voucher: oscar.apps.voucher vouchers_dashboard: oscar.apps.dashboard.vouchers widget_tweaks: widget_tweaks wishlists: oscar.apps.wishlists [None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None]

Let’s take a closer look at the first AppConfig subclass, AdminConfig. I’ve made its property names bold to make reading easier.

Python REPL
>>> config = list(apps.get_app_configs())[0]
>>> type(config) <class 'django.contrib.admin.apps.AdminConfig'>
>>>
dict = config.__dict__
>>> [print(f"{key}={dict[key]}") for key in dict] name=django.contrib.admin module=<module 'django.contrib.admin' from '/var/work/django/oscar/lib/python3.8/site-packages/django/contrib/admin/__init__.py'> apps=<django.apps.registry.Apps object at 0x7f8da050f9a0> label=admin path=/var/work/django/oscar/lib/python3.8/site-packages/django/contrib/admin models_module=<module 'django.contrib.admin.models' from '/var/work/django/oscar/lib/python3.8/site-packages/django/contrib/admin/models.py'> models={'logentry': <class 'django.contrib.admin.models.LogEntry'>} [None, None, None, None, None, None, None]

The CatalogConfig app

The source code for CatalogConfig is located within the django-oscar GitHub project at django-oscar/src/oscar/apps/catalogue/apps.py, and just consists of a Python class definition with a docstring:

class CatalogueConfig(CatalogueOnlyConfig, CatalogueReviewsOnlyConfig):
    """
    Composite class combining Products with Reviews
    """

The remainder of the file (apps.py) consists of the source for the CatalogueOnlyConfig and CatalogueReviewsOnlyConfig classes. Both of those classes subclass OscarConfig, and only have 2 methods: get_urls() and ready(), which is a Django AppConfig method.

OscarConfig

OscarConfig subclasses AppConfig and mixes in OscarConfigMixin:

class OscarConfig(OscarConfigMixin, AppConfig):
    """
    Base Oscar app configuration.
    This is subclassed by each app to provide a customisable container for its
    configuration, URL configurations, and permissions.
    """

The django-oscar docs describe OscarConfig this way:

Oscar’s apps organise their URLs and associated views using a “OscarConfig” class instance. This works in a similar way to Django’s admin app, and allows Oscar projects to subclass and customised URLs and views.

The above is a terrible explanation. OscarConfig should be used instead of django.apps.AppConfig as the parent of the autogenerated Config classes that result from manage.py startapp. The source code has a comment that says OscarConfig extends AppConfig to also provide URL configurations and permissions; this is explained here.

For example, this was generated for one of my apps:

myWebApp/myApp/apps.py
from django.apps import AppConfig

class CoreConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'core'

This is the same code, but subclasses OscarConfig

myWebApp/myApp/apps.py
from oscar.core.application import OscarConfig
class CoreConfig(OscarConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'core'

ready()

The Django docs describe the ready() method like this:

Subclasses can override this method to perform initialization tasks such as registering signals. It is called as soon as the registry is fully populated.

Although you can’t import models at the module-level where AppConfig classes are defined, you can import them in ready(), using either an import statement or get_model().

If you’re registering model signals, you can refer to the sender by its string label instead of using the model class itself.

However, django-oscar AppConfig subclasses usually set view URLs in ready().

Psst: I talk about ready() in this article, where I trace through the django-oscar startup sequence.

Sub-Apps

The Django documentation does not use the term sub-app, and it is rather vague on the subject, however the data structures are unambiguous. Let’s look at oscar.apps.catalogue.apps – it is different from the django.apps we looked at in detail above because it contains CatalogConfig’s sub-apps (it is an empty container because CatalogConfig has no sub-apps).

Shell
(aw) $ ./manage.py shell
Python 3.8.6 (default, Sep 25 2020, 09:36:53)
[GCC 10.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> oscar.apps.catalogue.apps as apps
>>> apps <module 'oscar.apps.catalogue.apps' from '/var/work/django/oscar/lib/python3.8/site-packages/oscar/apps/catalogue/apps.py'>
>>>
type(apps) <class 'module'>
>>>
apps.tab apps.CatalogueConfig( apps.get_class( apps.CatalogueOnlyConfig( apps.include( apps.CatalogueReviewsOnlyConfig( apps.path( apps.OscarConfig( apps.re_path( apps.apps

Notice that oscar.apps.catalogue.apps.apps is a back-reference to django.apps which we saw at the beginning of the article, with the same memory address (0x7ffb21210340) and the same type (<class 'django.apps.registry.Apps'>).

Python REPL
>>> apps.apps
<django.apps.registry.Apps object at 0x7ffb21210340>
>>>
type(apps.apps) <class 'django.apps.registry.Apps'>

Dashboard Sub-Apps

Exploring with the REPL

As we have seen, the django-oscar Dashboard app has 12 sub-apps. Messing with the import statement allows us to walk through the sub-apps easily.

Python REPL
>>> import oscar.apps.dashboard as d
>>> d.tab
d.apps                d.orders              d.shipping
d.catalogue           d.pages               d.tables
d.communications      d.partners            d.users
d.default_app_config  d.ranges              d.views
d.models              d.reports             d.vouchers
d.offers              d.reviews             d.widgets
>>>
d.apps >>> <module 'oscar.apps.dashboard.apps' from '/var/work/django/oscar/lib/python3.8/site-packages/oscar/apps/dashboard/apps.py'>
>>>
type(d.apps) <class 'module'>

Let’s look at the dashboard.catalogue sub-app:

Python REPL
>>> d.catalogue
<module 'oscar.apps.dashboard.catalogue' from '/var/work/django/oscar/lib/python3.8/site-packages/oscar/apps/dashboard/catalogue/__init__.py'>
>>>
d.catalogue.tab d.catalogue.apps d.catalogue.models d.catalogue.default_app_config d.catalogue.tables d.catalogue.forms d.catalogue.views d.catalogue.formsets d.catalogue.widgets d.catalogue.mixins

Examining the Source Code

The source code for DashboardConfig shows the dashboard sub-apps clearly:

def ready(self):
    self.index_view = get_class('dashboard.views', 'IndexView')
    self.login_view = get_class('dashboard.views', 'LoginView')
self.catalogue_app = apps.get_app_config('catalogue_dashboard') self.reports_app = apps.get_app_config('reports_dashboard') self.orders_app = apps.get_app_config('orders_dashboard') self.users_app = apps.get_app_config('users_dashboard') self.pages_app = apps.get_app_config('pages_dashboard') self.partners_app = apps.get_app_config('partners_dashboard') self.offers_app = apps.get_app_config('offers_dashboard') self.ranges_app = apps.get_app_config('ranges_dashboard') self.reviews_app = apps.get_app_config('reviews_dashboard') self.vouchers_app = apps.get_app_config('vouchers_dashboard') self.comms_app = apps.get_app_config('communications_dashboard') self.shipping_app = apps.get_app_config('shipping_dashboard')

The source code for all the dashboard apps has 12 subdirectories, one for each sub-app, as expected. The catalogue sub-app contains all the files one would expect in a Django app:

Shell
$ cd src/oscar/apps/dashboard/catalogue/
$ ls __init__.py apps.py forms.py formsets.py mixins.py models.py tables.py views.py widgets.py

Django App Loading and Initialization

Django uses dynamic discovery and loading. Read about app initialization.

Making A Sub-App

According to the Django documentation, the syntax for creating a sub-app is:

Shell
(aw) $ ./manage.py startapp name [directory]

Here is how to create a sub-app within my_webapp/my_app

Shell
(aw) $ mkdir -p ~/Code/my_webapp/my_app/my_sub_app
(aw) $ ./manage.py startapp my_sub_app ~/Code/my_webapp/my_app/my_sub_app
* 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.