惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
Docker
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
Cisco Blogs
Scott Helme
Scott Helme
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
C
Cyber Attacks, Cyber Crime and Cyber Security
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
S
Schneier on Security
I
Intezer
Spread Privacy
Spread Privacy
AWS News Blog
AWS News Blog
V
Vulnerabilities – Threatpost
Cloudbric
Cloudbric
V2EX - 技术
V2EX - 技术
Google Online Security Blog
Google Online Security Blog
L
Lohrmann on Cybersecurity
Recent Commits to openclaw:main
Recent Commits to openclaw:main
L
LINUX DO - 热门话题
S
Secure Thoughts
T
The Exploit Database - CXSecurity.com
博客园 - 【当耐特】
Recent Announcements
Recent Announcements
Security Archives - TechRepublic
Security Archives - TechRepublic
Stack Overflow Blog
Stack Overflow Blog
罗磊的独立博客
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
K
Kaspersky official blog
阮一峰的网络日志
阮一峰的网络日志
博客园_首页
Latest news
Latest news
B
Blog
F
Full Disclosure
大猫的无限游戏
大猫的无限游戏
博客园 - 叶小钗
L
LangChain Blog
GbyAI
GbyAI
Last Week in AI
Last Week in AI
S
Security Affairs
Apple Machine Learning Research
Apple Machine Learning Research
N
Netflix TechBlog - Medium
Security Latest
Security Latest
Vercel News
Vercel News
Y
Y Combinator Blog
G
GRAHAM CLULEY
S
Securelist
T
Troy Hunt's Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
雷峰网
雷峰网

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Inside a Horilla CRM App: registration.py, menu.py, and What AppLauncher Actually Loads
Horilla · 2026-05-27 · via DEV Community

TL;DR: AppLauncher loads your app at startup. These four files are what actually wire it into Horilla CRM: registration.py (features + permissions), menu.py (sidebar & FAB), signals.py (cross-module events), and dashboard.py (charts). Skip registration.py and your model is invisible to global search, import, export, and workflows.


In last post on AppLauncher, we saw how a Horilla CRM app declares url_prefix, auto_import_modules, and Celery schedules — then integrates without touching the root urls.py.

The next question every developer asks:

“What runs when AppLauncher imports registration, menu, signals, and dashboard?”

This post answers that using the Leads module (horilla_crm/leads) — the reference implementation for every CRM app in Horilla CRM.


The standard app folder

Every production module follows the same layout:

horilla_crm/leads/
├── apps.py              # AppLauncher config
├── registration.py      # Feature + platform hooks  ← critical
├── menu.py              # Sidebar / FAB / settings menus
├── signals.py           # Cross-module event handlers
├── dashboard.py         # Dashboard chart generators
├── models/              # HorillaCoreModel subclasses
├── views/               # horilla_generics CBVs
├── forms.py             # HorillaModelForm / HorillaMultiStepForm
├── filters.py           # HorillaFilterSet
├── urls.py              # Namespaced routes (app_name = "leads")
├── api/                 # DRF serializers + router
├── celery_schedules.py  # Beat tasks (merged by AppLauncher)
└── templates/           # HTMX partials

Enter fullscreen mode Exit fullscreen mode

You declare integration in apps.py and the convention files above — not in a central project config file.


1. apps.py — the integration contract

from horilla.apps import AppLauncher
from horilla.utils.translation import gettext_lazy as _


class LeadsConfig(AppLauncher):
    default = True
    name = "horilla_crm.leads"
    verbose_name = _("Leads")

    url_prefix = "crm/leads/"
    url_module = "horilla_crm.leads.urls"
    url_namespace = "leads"

    auto_import_modules = [
        "registration",
        "signals",
        "menu",
        "dashboard",
    ]

    celery_schedule_module = "celery_schedules"
    celery_schedule_variable = "HORILLA_CRM_BEAT_SCHEDULE"

    def get_api_paths(self):
        return [{
            "pattern": "crm/leads/",
            "view_or_include": "horilla_crm.leads.api.urls",
            "name": "horilla_crm_leads_api",
            "namespace": "horilla_crm_leads",
        }]

Enter fullscreen mode Exit fullscreen mode

When Django calls LeadsConfig.ready(), AppLauncher:

  1. Appends /crm/leads/ to root URLconf
  2. Imports each module in auto_import_modules
  3. Merges Celery beat entries from celery_schedules.py
  4. Registers API routes from get_api_paths()

auto_import_modules is the bridge between AppLauncher and the convention files below .


2. registration.py — the most important file

Without this file, your model exists in PostgreSQL but Horilla’s platform does not know about it: no global search, no import/export, no duplicate merge, no approvals, no scoring.

Feature registry

horilla/registry/feature.py maintains:

  • FEATURE_CONFIG — maps feature names to registry keys ("global_search""global_search_models")
  • FEATURE_REGISTRY — maps keys to lists of model classes

Registration happens at import time:

from horilla.registry.feature import register_model_for_feature
from horilla.contrib.cadences.registration import register_cadence_tab

register_model_for_feature(
    app_label="leads",
    model_name="LeadStatus",
    features=["import_data", "export_data", "global_search"],
)

register_model_for_feature(
    app_label="leads",
    model_name="Lead",
    all=True,
    features=[
        "duplicate_models",
        "approval_models",
        "reviews_models",
        "workflow_models",
        "scoring",
    ],
)

register_cadence_tab(
    app_label="leads",
    model_name="Lead",
    url_prefix="lead-cadences-tab/<int:pk>/",
    url_name="lead_cadences_tab",
)

Enter fullscreen mode Exit fullscreen mode

features=[...] vs all=True

Style When to use
features=["import_data", "global_search"] Lookup tables, config models
all=True Primary business entities (Lead, Opportunity, Account)
register_feature(...) Define a new platform capability other apps can opt into

Rule of thumb: If users work with it daily, use all=True (with exclude= if needed). If it’s a status or config row, list features explicitly.

Why this matters for permissions

Feature registration is tied to permission generation and UI discovery. Skipping registration.py is the #1 mistake when adding a new module — the app “works” in isolation but vanishes from search, exports, and workflows.


3. menu.py — navigation without editing base templates

Horilla collects menus via decorators in horilla/menu/. Your app only ships menu.py; the layout renders entries at runtime.

Floating action button (quick create)

from horilla.urls import reverse_lazy
from horilla.menu import floating_menu
from .models import Lead

@floating_menu.register
class LeadFloating:
    title = Lead()._meta.verbose_name
    url = reverse_lazy("leads:leads_create")
    icon = "/assets/icons/leads.svg"
    items = {
        "hx-target": "#modalBox",
        "hx-swap": "innerHTML",
        "onclick": "openModal()",
        "perm": ["leads.add_lead"],
    }

Enter fullscreen mode Exit fullscreen mode

Sidebar: main section + sub-section

from horilla.menu import (
    main_section_menu,
    sub_section_menu,
    MAIN_CONTENT_HX_ATTRS,
)
from horilla.utils.translation import gettext_lazy as _

@main_section_menu.register
class SalesSection:
    section = "sales"
    name = _("Sales")
    icon = "/assets/icons/sales.svg"
    position = 1

@sub_section_menu.register
class LeadSubSection:
    section = "sales"
    app_label = "leads"
    position = 1
    verbose_name = _("Leads")
    icon = "/assets/icons/leads.svg"
    url = reverse_lazy("leads:leads_view")
    attrs = MAIN_CONTENT_HX_ATTRS
    perm = ["leads.view_lead", "leads.view_own_lead"]

Enter fullscreen mode Exit fullscreen mode

Menu registry types

Registry Purpose
main_section_menu Top-level sidebar (Sales, Marketing, …)
sub_section_menu Links under a section
floating_menu Quick-create FAB
settings_menu Module settings area
my_settings_menu Per-user preferences

HTMX-aware navigation

MAIN_CONTENT_HX_ATTRS = {
    "hx-boost": "true",
    "hx-target": "#mainContent",
    "hx-select": "#mainContent",
    "hx-swap": "outerHTML",
}

Enter fullscreen mode Exit fullscreen mode

Sub-section links swap #mainContent instead of reloading the full page — fast, server-rendered navigation without React.

Menu items support "perm" so entries hide for unauthorized users.


4. signals.py — platform events, not only post_save

Leads uses Django receivers and Horilla CRM-wide signals:

Signal Purpose
company_created Seed default lead stages for new tenant
company_currency_changed Bulk-update MoneyField on leads
lead_stage_created Opportunity pipeline setup

Real handler when a new company is created:

from django.dispatch import receiver
from horilla.contrib.core.signals import company_created, company_currency_changed
from horilla.urls import reverse_lazy
from horilla.shortcuts import render

@receiver(company_created)
def handle_company_created(sender, instance, request, view, is_new, **kwargs):
    if is_new:
        url = reverse_lazy("leads:load_lead_stages", kwargs={"company_id": instance.id})
        return render(
            request,
            "lead_status/reload_and_load_url_script.html",
            {"load_url": str(url)},
        )
    return None

Enter fullscreen mode Exit fullscreen mode

Currency change updates all leads for that company:

@receiver(company_currency_changed)
def update_crm_on_currency_change(sender, **kwargs):
    company = kwargs.get("company")
    conversion_rate = kwargs.get("conversion_rate")
    leads_to_update = []
    for lead in Lead.objects.filter(company=company).only("id", "annual_revenue"):
        if lead.annual_revenue is not None:
            # apply conversion_rate, collect for bulk_update
            ...

Enter fullscreen mode Exit fullscreen mode

Convention: Keep signals.py thin — heavy work goes to methods.py or Celery tasks.


5. dashboard.py — charts and KPIs

Dashboards are not hard-coded in core. Apps register generators on DefaultDashboardGenerator:

from horilla.db.models import Count
from horilla.contrib.dashboard.utils import DefaultDashboardGenerator
from .models import Lead

def create_lead_source_charts(self, queryset, model_info):
    data = queryset.values("lead_source").annotate(count=Count("id")).order_by("-count")
    if not data.exists():
        return None
    labels = [item["lead_source"] or "Unknown" for item in data]
    values = [item["count"] for item in data]
    return {
        "title": "Leads by Source",
        "type": "funnel",
        "data": {"labels": labels, "data": values, "urls": urls},
    }

DefaultDashboardGenerator.extra_models.append({
    "model": Lead,
    "name": "Leads",
    "kpi_func": lead_kpi_cards,
    "chart_func": [create_lead_source_charts, create_lead_charts_by_stage],
    "table_func": [lead_convert_table_func, lead_open_pipeline_table_func],
})

Enter fullscreen mode Exit fullscreen mode

When a user builds a dashboard around Leads, Horilla calls these functions and renders ECharts JSON — clickable segments can deep-link to filtered list views.


6. urls.py — namespaced, HTMX-friendly routes

from horilla.urls import path
from horilla_crm.leads import views

app_name = "leads"

urlpatterns = [
    path("leads-view/", views.LeadView.as_view(), name="leads_view"),
    path("leads-list/", views.LeadListView.as_view(), name="leads_list"),
    path("leads-kanban/", views.LeadKanbanView.as_view(), name="leads_kanban"),
    path("leads-detail/<int:pk>/", views.LeadDetailView.as_view(), name="leads_detail"),
]

Enter fullscreen mode Exit fullscreen mode

Always reverse with the namespace: reverse("leads:leads_list").

List, kanban, and detail are separate endpoints — the shell swaps HTMX targets between them (covered on Day 5 and Day 7).


Startup flow (end to end)

flowchart TD
    A[Django loads INSTALLED_APPS] --> B[LeadsConfig.ready]
    B --> C[Register /crm/leads/ URLs]
    B --> D[import registration.py]
    B --> E[import menu.py]
    B --> F[import signals.py]
    B --> G[import dashboard.py]
    D --> H[FEATURE_REGISTRY populated]
    E --> I[Menu decorators collected]
    F --> J[Signal handlers connected]
    G --> K[Dashboard generators registered]

Enter fullscreen mode Exit fullscreen mode


Checklist: new Horilla CRM app

  • [ ] apps.pyAppLauncher, auto_import_modules, url_prefix
  • [ ] registration.py — every user-facing model registered
  • [ ] menu.py — at least one sidebar or FAB entry
  • [ ] signals.py — if you handle company/currency/stage events
  • [ ] dashboard.py — if the module appears on dashboards
  • [ ] urls.pyapp_name set

Scaffold the skeleton:

python manage.py start_horilla_app my_module

Enter fullscreen mode Exit fullscreen mode


Explore Horilla CRM


Have you built plugin-style Django apps before? What would you add to auto_import_modules beyond these four files? Drop a comment — I read every one.