Skip to content

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.

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.

Diagram
Channel modelFields
PIM Channelidx (varchar, unique), name (varchar), default_language (FK → Language), default_currency (FK → Currency), languages (M2M), currencies (M2M), is_default (bool), inheritance_enabled (bool)
Checkout Channelidx (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 Channelidx (varchar), label (varchar), from_name (varchar), from_email (varchar), bcc_email (varchar)
Matrix Channelidx (varchar), label (varchar), default_language (FK → Language), default_currency (FK → Currency), default_order (varchar)
ContentDB ContentChannelidx (varchar, unique), name (varchar), default_language (FK → ContentDB Language), is_default (bool)
DeliveryPointChannelidx (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.

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 Channel

Most new modules land on Pattern 2.

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.

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.

A scoped record has a channels many-to-many field to the module’s Channel model:

channelsMeaning
emptyglobal: every channel sees the record
one or more channelsonly 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.

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 count

The 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:

ModuleManagement commandAdmin endpointDjango admin action
DeliveryPointssync_dp_channelsPOST /api/deliverypoints/v2/admin/channels/sync/Sync channels from PIM
Agreementssync_agreement_channelsPOST /api/agreements/v2/admin/channels/sync/Sync channels from PIM
FAQsync_faq_channelsPOST /api/faq/v2/admin/channels/sync/Sync channels from PIM
Pricefightersync_pricefighter_channelsPOST /api/pricefighter/v2/admin/channels/sync/Sync channels from PIM
ContentDBsync_contentdb_channels, sync_contentdb_languagesnone, the admin channel routes are read-onlySync channels from PIM, Sync languages from PIM
Price managerPOST /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.

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.

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:

Moduledefault_languagelanguages
PIM, Checkout, Matrixyesyes
DeliveryPoints, Agreements, FAQ, Pricefighteryesyes
ContentDByes, to its own Language modelno
Accountsa single language foreign keyno
Emailno; per-language settings sit in LangChannelConfig, keyed by an ISO2 codeno
Price manager, QMSnono
  1. idx is the shared key. PIM is the source of truth for which channels exist. Never reference a PIM channel by primary key.
  2. Own the Channel model. No foreign key and no many-to-many field to django_pim.Channel.
  3. Language fields on a new Channel. default_language and languages, as above.
  4. A CMS panel calls its own module’s API. The FAQ panel reads /api/faq/v2/admin/channels/, not PIM’s channel list.
  5. 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.