u/admin.display(description='Status')
def toggleswitch(self, obj):
if obj.iscore:
return formathtml(
'<label class="module-toggle module-toggle--core">'
'<input type="checkbox" checked disabled>'
'<span class="module-togglelabel">Core</span>'
'</label>',
)
checked = 'checked' if obj.isenabled else ''
return formathtml(
'<label class="module-toggle" data-pk="{}">'
'<input type="checkbox" {}>'
'<span class="module-togglelabel">{}</span>'
'</label>',
obj.pk, checked,
'Enabled' if obj.isenabled else 'Disabled',
)
Custom URL endpoint handles the POST, toggles is_enabled, clear the cache. Core modules get a disabled checkbox with a tooltip explaining why. I also disabled add/delete permissions on the Module admin — modules are created automatically, not manually.
# Auto-sync with postmigrate
New module appears in the database automatically. When you run migrate, a post\migrate signal fires and syncs the in-memory registry with the DB:
def syncmodulesaftermigrate(sender, kwargs):
registry = getregistry()
if not registry:
return
for name, info in registry.items():
module, created = Module.objects.getorcreate(
name=name,
defaults={
'label': info['label'],
'isenabled': True,
'iscore': info.get('iscore', False),
'urlprefixes': info.get('urlprefixes', ),
'dependencies': info.get('dependencies', ),
},
)
if not created:
# Update metadata but don't touch isenabled
module.urlprefixes = info.get('urlprefixes', [])
module.dependencies = info.get('dependencies', [])
module.save()
The important line is don't touch is\enabled. If the owner disabled ALPR last week, running migrate shouldn't silently re-enable it. Metadata gets updated, toggle state is preserved.
# Per-client feature access
Global module toggles solve "we don't want this feature at all." But what about "client X should have this, client Y shouldn't"?
Each client get a ClientFeature record with boolean fields:
class ClientFeature(models.Model):
client = models.OneToOneField(
Client, ondelete=models.CASCADE,
relatedname='features',
)
cabinet = models.BooleanField(default=True)
bot = models.BooleanField(default=False)
invoices = models.BooleanField(default=False)
appointments = models.BooleanField(default=False)
notificationstelegram = models.BooleanField(default=False)
notificationswhatsapp = models.BooleanField(default=False)
New client get only cabinet=True by default. Everything else is opt-in. The views check both layers — global module must be enabled AND client must have the feature. If either is off, access is denied.
I know what you are thinking — "why not use django-flags or wafle or some other feature flag library." This system has maybe 10 modules and 50 clients. A full-blown feature flag framework would be like using a chainsaw to cut bread. The boolean fields are stupid simple, easy to understand in the admin, and they work.
# What I learned
The two-layer approach (global + per-client) covers 95% of cases. Global toggle for "this feature does not exists yet." Per-client toggles for "this client pay for premium features." Haven't needed anything more complex so far.
post_migrate sync was worth the effort. Before this I had to manually create Module records when add a new app. Forgot once, the middleware let everything through because the module wasn't in the DB. Now it is automatic and I can not forget.
Cache invalidation is the actual hard part. 60-second TTL is a lazy solution. If the owner disables a module and a request comes in at
4 ·