horilla.contrib.core owns the CSV/XLSX import wizard and ExportSchedule. List views use HorillaBulkExportMixin; Celery runs process_scheduled_exports.
This post is Part 15 of 28 in the Horilla CRM Technical Blog series.
What is import and export in Horilla CRM?
Import and export in Horilla CRM enable you to:
- import_data and export_data feature flags
- Column mapping and validation on import
- Bulk export from HorillaListView
- Scheduled export jobs
Import and export ownership
The import/export foundation lives in horilla.contrib.core. Its history and scheduling models are shared infrastructure rather than features owned by each CRM module. ImportHistory records an import operation and its outcome, while ExportSchedule stores the configuration necessary for recurring export delivery. Module code supplies a registered model and permitted queryset; it should not create a parallel CSV importer or scheduler per app.
The feature registry controls availability. FEATURE_REGISTRY exposes import_models and export_models, so a model participates only when it explicitly registers the appropriate capability. This prevents administrative tables or sensitive models from appearing in generic data tools merely because they have fields. It also provides a central place to align import/export availability with model and role permissions.
| Element | Scope | Key responsibility |
|---|---|---|
| ImportHistory | Core | Audit source, progress, errors, and created records |
| Four-step import wizard | Core UI | Guide source, mapping, validation, and execution |
| HorillaBulkExportMixin | Generics | Add constrained exports to list-style views |
| ExportSchedule | Core | Persist recurring export criteria and delivery timing |
| process_scheduled_exports | Celery | Execute due schedules outside the request cycle |
The four-step import workflow
The core importer uses a four-step wizard. Although presentation details may differ by deployment, the technical workflow should remain distinct: select the target and source file, map incoming columns to allow model fields, validate the proposed rows, then execute the import. Keeping validation separate from execution is essential. A preview can report row-level errors and required-field issues without partially persisting valid rows while the user is still deciding how to handle failures.
state = {
"model": "leads.Lead",
"uploaded_file": upload_reference,
"field_mapping": {"Email": "email", "Company": "company_name"},
"validation_token": validation_token,
}
Never trust the client to submit model names or writable field names unchanged between steps. Resolve the target from import_models, check model and field permissions, reject identity, ownership, company, and audit fields unless the importer explicitly supports them, and validate all relational values within the current user’s allowed company scope. The final step should revalidate the authoritative upload and mapping before writing records because browser state can be tampered with or become stale.
For large imports, produce actionable per-row diagnostics and store them with ImportHistory. Use database transactions appropriately: atomic all-or-nothing imports are useful for tightly coupled data, while chunked imports need a clear partial-success policy and repeat-safe row identity. Do not claim an import is complete until post-processing and history status updates have succeeded.
Exporting through generic views
HorillaBulkExportMixin in the generics layer provides the standard integration point for export-capable views. A list view should export the same authorized, filtered queryset that the user can see in the interface. Do not construct a fresh unrestricted queryset just because exporting happens in a separate method; that is a common multi-tenant data exposure.
class LeadListView(HorillaBulkExportMixin, HorillaListView):
model = Lead
def get_queryset(self):
return super().get_queryset().filter(is_active=True)
The mixin should work alongside normal filtering, searching, selected columns, and row-level permissions. Establish explicit limits and asynchronous behavior for large exports. Export field choices should exclude passwords, tokens, internal audit values, and inaccessible related fields. CSV formula injection is another practical concern: values beginning with spreadsheet formula characters need safe handling before the file is opened by an analyst.
Scheduled export execution
ExportSchedule captures a report or model export’s timing, recipient configuration, filters, and lifecycle state. The Celery task process_scheduled_exports finds due schedules and performs delivery without holding a web request open. The worker must reconstruct the schedule’s permission-safe query context; it must not run the export as an implicit superuser simply because it is asynchronous.
for schedule in due_schedules:
export = build_export_from_schedule(schedule)
deliver_export(export, schedule)
mark_schedule_processed(schedule)
Make schedule processing idempotent. Lock or claim a due schedule before generating it, record the last successful execution only after delivery succeeds, and include a bounded retry policy. Time zones need explicit treatment: persist the schedule time zone, calculate the next occurrence consistently across daylight-saving transitions, and avoid duplicate sends when workers overlap.
Operational controls and testing
Imports and exports are high-value data paths. Log initiating user, company context, target model, field mapping or filter definition, timestamps, status, and a bounded error summary. Do not log full uploaded rows or export contents when they may contain personal data. Restrict download URLs, expire generated files according to retention policy, and check authorization again at download time.
Test imports with invalid headers, duplicate natural keys, malformed encodings, unauthorized foreign keys, and a mixture of valid and invalid rows. Test exports with a user limited to own records, a filtered list, no matching rows, and a scheduled job whose creator later loses access. These checks validate that FEATURE_REGISTRY, the core wizard, HorillaBulkExportMixin, and process_scheduled_exports compose into one secure data boundary.
Benefits of Import Export in Horilla CRM
- Migrate data from legacy CRMs
- Ad-hoc reporting via CSV
- No custom scripts for standard entities
Register import_data and export_data in registration.py — list views gain import/export actions automatically.
Continue the series
Previous: Part 14 — Everything You Need to Know About Global Search in Horilla CRM
Next: Part 16 — How to Detect and Merge Duplicate Records in Horilla CRM
More posts are on the Horilla Blogs; share feedback on GitHub.