Channels
Every module that needs channels defines its own Channel model, and the models agree on one string, idx. What a
channel is and how idx links records across modules: Concepts.
Why Every Module Owns Its Channel
Section titled “Why Every Module Owns Its Channel”Each module needs different settings per channel. Checkout keeps order numbering and order splitting rules. Email keeps the sender and the colours of the mail layout. Matrix keeps the default sort order and the lists of filterable and sortable features. One shared model would collect the fields and the migrations of every module.
A foreign key to PIM’s Channel would also make PIM a requirement of every module that scopes anything by channel.
So no Channel model points at another module’s Channel. PIM says which channels exist; a module that has a channel
with the same idx holds its own settings for it.
| Channel model | Fields |
|---|---|
| PIM Channel | idx (varchar, unique), name (varchar), default_language (FK → Language), default_currency (FK → Currency), languages (M2M), currencies (M2M), is_default (bool), inheritance_enabled (bool) |
| Checkout Channel | idx (varchar, unique), label (varchar), default_language (FK → Language), default_currency (FK → Currency), default_country (FK → Country), order_pretty_id_prefix (varchar), split_order_mechanism (varchar), min_order_price (int) |
| Email Channel | idx (varchar), label (varchar), from_name (varchar), from_email (varchar), bcc_email (varchar) |
| Matrix Channel | idx (varchar), label (varchar), default_language (FK → Language), default_currency (FK → Currency), default_order (varchar) |
| ContentDB ContentChannel | idx (varchar, unique), name (varchar), default_language (FK → ContentDB Language), is_default (bool) |
| DeliveryPointChannel | idx (varchar, unique), name (varchar), default_language (FK → Language), languages (M2M) |
The table shows a selection of fields; the Checkout and Matrix models have more. idx is unique in PIM, Checkout,
Accounts, the price manager, QMS and the scoping channels below. The Email and Matrix models do not declare it
unique.
The independence holds for the Channel models, not for whole modules. Matrix is a read model of PIM: its
FilterableFeatures and SortableFeatures models have foreign keys to django_pim.Feature.
Decision Guide
Section titled “Decision Guide”Does the module keep settings per channel? yes → Pattern 1: Domain Channel no, it only decides which records a channel sees → Pattern 2: Scoping Channel no, its data is the same for every channel → Pattern 3: No ChannelMost new modules land on Pattern 2.
Pattern 1: Domain Channel
Section titled “Pattern 1: Domain Channel”The module’s Channel carries idx and the module’s own settings.
class Channel(BaseModel): # django_matrix idx = models.CharField(max_length=64) label = models.CharField(max_length=64) default_language = models.ForeignKey( "django_regional.Language", related_name="matrix_channel", on_delete=models.SET_NULL, blank=True, null=True ) default_order = models.CharField(...) sku_order_numeric_sort = models.BooleanField(default=False)
class FilterableFeatures(models.Model): channel = models.ForeignKey("Channel", on_delete=models.CASCADE, related_name="filterable_features") feature = models.ForeignKey("django_pim.Feature", on_delete=models.CASCADE) is_or_filter = models.BooleanField(default=False) position = models.IntegerField(default=0)Each module sets these channels up its own way: the Django admin, fixtures, an import command or the module’s admin
API. Their settings are decisions, and no sync can guess them. The price manager also has a sync endpoint that copies
idx and name from PIM.
Used by: PIM, Checkout, Matrix, Email, Accounts, the price manager, QMS.
Pattern 2: Scoping Channel
Section titled “Pattern 2: Scoping Channel”The channel exists only to decide visibility: a record is global or assigned to channels.
class DeliveryPointChannel(BaseModel): idx = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=128, default="", blank=True) default_language = models.ForeignKey( "django_regional.Language", on_delete=models.SET_NULL, null=True, blank=True, related_name="dp_default_channels" ) languages = models.ManyToManyField("django_regional.Language", blank=True, related_name="dp_channels")Used by: DeliveryPoints, Agreements, FAQ, ContentDB. Pricefighter has a Channel of the same shape with the same sync.
The Scoping Convention
Section titled “The Scoping Convention”A scoped record has a channels many-to-many field to the module’s Channel model:
channels | Meaning |
|---|---|
| empty | global: every channel sees the record |
| one or more channels | only those channels see it |
The filter means the same in every module: the records of this channel plus the global ones. In Agreements:
Q(channels__idx=channel_idx) | Q(channels__isnull=True)Delivery points, agreement definitions and ContentDB drafts carry the field themselves. FAQ items take it from their group, and an item without a group is global.
Channel Sync
Section titled “Channel Sync”Pattern 2 channels are copied from PIM by a service function, sync_channels_from_pim(). The one in
django_deliverypoints/services/channel_service.py:
def sync_channels_from_pim() -> int: # RuntimeError: an app that is installed but not in INSTALLED_APPS raises it on model import, not ImportError. try: from django_pim.models import Channel except (ImportError, RuntimeError): logger.info("django_pim not installed, skipping channel sync") return 0
from django_regional.models import Language
count = 0 for pim_channel in Channel.objects.prefetch_related("languages").all(): local_lang = None if pim_channel.default_language: local_lang = Language.objects.filter(iso2__iexact=pim_channel.default_language.iso2).first()
dp_channel, _ = DeliveryPointChannel.objects.update_or_create( idx=pim_channel.idx, defaults={"name": pim_channel.name, "default_language": local_lang} )
pim_lang_iso2s = list(pim_channel.languages.values_list("iso2", flat=True)) local_langs = Language.objects.filter(iso2__in=pim_lang_iso2s) dp_channel.languages.set(local_langs)
count += 1
return countThe other modules follow the same idea with differences. ContentDB writes with bulk_create and bulk_update,
copies is_default, and resolves the default language against its own Language model
(Languages). The price manager copies only
idx and name.
The guard. The import of django_pim sits in a try, so the module works without PIM. A missing package raises
ImportError. A package that is installed but left out of INSTALLED_APPS raises RuntimeError, and that is the
normal case in a service that enables a subset of its modules. DeliveryPoints, FAQ and the price manager catch
both. Agreements, Pricefighter and ContentDB catch ImportError only.
Sync runs when someone calls it:
| Module | Management command | Admin endpoint | Django admin action |
|---|---|---|---|
| DeliveryPoints | sync_dp_channels | POST /api/deliverypoints/v2/admin/channels/sync/ | Sync channels from PIM |
| Agreements | sync_agreement_channels | POST /api/agreements/v2/admin/channels/sync/ | Sync channels from PIM |
| FAQ | sync_faq_channels | POST /api/faq/v2/admin/channels/sync/ | Sync channels from PIM |
| Pricefighter | sync_pricefighter_channels | POST /api/pricefighter/v2/admin/channels/sync/ | Sync channels from PIM |
| ContentDB | sync_contentdb_channels, sync_contentdb_languages | none, the admin channel routes are read-only | Sync channels from PIM, Sync languages from PIM |
| Price manager | POST /api/pricemanager/v2/admin/channels/sync/ |
Each module with a sync endpoint lists its channels at GET on the same path without sync/. The endpoint answers
{"synced": <count>}; the price manager adds created and updated. A second run creates nothing new.
Starting a local stack does not run the sync; the demo seed does. Which commands it runs and when: Data Import.
Pattern 3: No Channel
Section titled “Pattern 3: No Channel”The module has no Channel model. django_regional (languages, currencies, countries) and munin (the module
registry) work this way: their data is the same for every channel.
Language Fields on Channels
Section titled “Language Fields on Channels”A new Channel model carries default_language (a foreign key) and languages (a many-to-many field), both to
django_regional.Language. The existing models:
| Module | default_language | languages |
|---|---|---|
| PIM, Checkout, Matrix | yes | yes |
| DeliveryPoints, Agreements, FAQ, Pricefighter | yes | yes |
| ContentDB | yes, to its own Language model | no |
| Accounts | a single language foreign key | no |
no; per-language settings sit in LangChannelConfig, keyed by an ISO2 code | no | |
| Price manager, QMS | no | no |
idxis the shared key. PIM is the source of truth for which channels exist. Never reference a PIM channel by primary key.- Own the Channel model. No foreign key and no many-to-many field to
django_pim.Channel. - Language fields on a new Channel.
default_languageandlanguages, as above. - A CMS panel calls its own module’s API. The FAQ panel reads
/api/faq/v2/admin/channels/, not PIM’s channel list. - Guard the PIM import. A sync service catches
(ImportError, RuntimeError)and returns zero.
How modules store translated content and which language a request gets: Languages.