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