Published 2021-02-24.
Time to read: 3 minutes.
django
collection.
Let’s start off today’s exploration of Django and django-oscar
by learning about
Django apps
.
apps
,
and it is available in
django.apps
:
>>> 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.
>>> 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!
>>> 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:
>>> 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.
>>> 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
.
>>> 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.
>>> 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.
>>> 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:
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:
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
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:
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).
(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'>
).
>>> 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.
>>> 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:
>>> 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:
$ 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:
(aw) $ ./manage.py startapp name [directory]
Here is how to create a sub-app within my_webapp/my_app
(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