Skip to main content

Python library (premium)

All notable changes to this project are documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[7.2.0] — 2026-10-19​

Changed​

  • BREAKING: Requires the free library habarico>=7.2.0, installed from pypi.org. The version now follows the free libraries' shared 7.2 line. (HAB-1034)

[6.0.0] — 2026-10-10​

Added​

  • Initial habarico-enterprise package, built on the extension points of the free habarico library. (HAB-811)

  • LICENSE — the package shipped with a proprietary classifier but no licence text at all, so a built wheel carried no terms. It now declares [project.license] and ships the file through MANIFEST.in: commercial and proprietary, usable only under a licence agreement with Eluvia.land s.r.o. The free habarico library it extends stays MIT. (HAB-811)

  • habarico_enterprise.extensions.CONTRIBUTIONS — core imports this module by name while resolving its settings, so installing the package wires up ModelAdmin, AdminModelViewSet, AdminSite, FieldSchemaGenerator and the file field mixin on its own. A project needs no ADMIN_REST_FRAMEWORK block, and still overrides any single key by setting it explicitly. (HAB-811)

  • habarico_enterprise.schema.GlobalResourceSchema / AdminSchemaGenerator and habarico_enterprise.validation.AdminConfigurationValidator — the composed classes habarico_enterprise.admin.ModelAdmin now points schema_generator_class / configuration_validator_class at. The resource-schema keys global_fulltext_search_enabled, aggregate_enabled and dashboard_enabled, the aggregate_max_buckets / aggregate_fields registration-time checks and the list endpoint's meta.highlights sidecar are all contributed from here, so the free library carries no trace of them. (HAB-811)

  • CZ localflavor, moved from the free habarico library: habarico_enterprise.models.localflavor.cz and habarico_enterprise.validators.localflavor.cz. (HAB-845)

  • AI integration, moved from the free habarico library's contrib.ai: habarico_enterprise.ai — AIModelAdmin, PredictiveCharField/PredictiveTextField, AIAdminModelSerializer, AIAdminModelViewSet, AIAdminFilterSet, the AIClient/ai_client (OpenAI) and the QParser/AISearchFilter natural-language filter. Requires the ai extra (openai, pyparsing). (HAB-845)

  • AIModelAdminMixin payload limits — ai_max_text_length (2000), ai_max_length (500), ai_max_value_length (1000), ai_max_fields (100), ai_assistant_max_messages (30) and ai_assistant_max_message_length (4000). Each bounds one dimension of what a request may forward to the AI provider and can be raised or lowered per resource; a non-positive value is rejected at registration time with ImproperlyConfigured.

    Most of them trim rather than refuse, because what they bound is context the caller never chose to send — data and current_values are whatever the record holds, schema.fields is however many fields the form declares, and the prediction prefix is whatever the user has typed. A resource with a long description would otherwise have the AI switched off for good, with nobody able to shrink it. Over-limit context is therefore dropped or cut (the prediction prefix from the front, so the tail the completion continues survives), and ai_assistant_max_messages is a sliding window over the conversation the frontend re-sends every turn.

    Two limits still refuse with HTTP 422, and both are the caller's own choice rather than the record's: an explicit length above ai_max_length, and a single chat message longer than ai_assistant_max_message_length — cutting that one would corrupt what the user wrote. (HAB-907)

  • AIModelAdminMixin.is_ai_action_allowed() — whether the AI actions may be called on this resource. Override it to narrow the gate further; widening it past a writable resource would undo the AI-action permission gate (see Security). (HAB-907)

  • AIClient.get_assistant_response(messages) — runs one turn of the form assistant conversation. The view used to reach into the client's private _client attribute to do this. (HAB-907)

  • AIModelAdminMixin.ai_search_fields — an optional complete whitelist of field paths the AI search filter may use in generated Q expressions. It fully replaces the default filter_fields + search_fields whitelist, so a project can allow relation paths such as "author__name" beyond the standard filters. (HAB-902)

  • Change history, moved from the free habarico library's contrib.reversion: habarico_enterprise.reversion — VersionAdmin, RevisionAdmin, VersionListBox, VersionSerializer, VersionFilterSet, built on top of django-reversion. Requires the reversion extra (django-reversion, dmp). (HAB-845)

  • The change history is switched on by listing habarico_enterprise.reversion in INSTALLED_APPS, the way habarico_enterprise.uploads already is, replacing the bare import habarico_enterprise.reversion # noqa: F401 the feature used to need in the project's own code. habarico's autodiscovery then registers the version and revision resources, and the admin mixin joins ModelAdmin only for projects that asked for the feature — so nothing of it is loaded, and no attribute of it appears on an admin, in a project that did not. (HAB-903)

  • can_read_history on ModelAdmin — declares who may read a tracked resource's change history: a bool, or a method taking the request for per-user rules. Left undeclared, the history follows the admin's can_read. It is declared by ReversionAdminMixin alongside history_fields, and has_read_history_permission(request) is their always-callable form. (HAB-903)

  • Chunked/S3/tus uploads, moved from the free habarico library's contrib.uploads: habarico_enterprise.uploads. Contributes to core's extension points via habarico_enterprise.site.AdminSite (upload routes) and habarico_enterprise.schema.FieldSchemaGenerator (the field schema upload block), wired in by CONTRIBUTIONS together with ChunkedUploadFieldMixin as DEFAULT_FILE_FIELD_MIXIN_CLASS. Requires the uploads-s3 and/or uploads-tus extra (boto3, django-storages). (HAB-845)

  • ChunkedUploadFieldMixin.is_pending_upload(value) — tells core whether a validated value is a not yet finalized upload, which replaced core's DEFAULT_UPLOAD_REFERENCE_CLASSES setting. The package therefore contributes one upload key instead of two — DEFAULT_FILE_FIELD_MIXIN_CLASS, core's renamed single-class setting. (HAB-811)

  • BREAKING: habarico_enterprise.uploads.fields._UploadReference is renamed to UploadReference. It is the value ChunkedUploadFieldMixin.is_pending_upload() reports to core, so a project can be asked to name it — a leading underscore promised the opposite. Its sibling ChunkedUploadFieldMixin was public already. (HAB-845)

  • options.max_size / options.accept on file and image fields - the client-side half of the upload rules, derived from the same serializer-field validators initiate reads (MaxFileSizeValidator and the content-type allowlist) and checked on a picked file before the upload starts, so a file that would be refused anyway never leaves the browser. MAX_UPLOAD_SIZE joins max_size only for a field a chunked provider actually serves, since initiate is the only place that enforces it. The hard content-type blocklist is deliberately not reflected in accept - it has no allowlist form - so an absent or permissive accept never means the server will accept the file. An explicitly passed option wins over the derived one. Needs the matching FieldOptions keys in contract/openapi.yaml. (HAB-942)

  • habarico_enterprise.uploads.validators.MaxFileSizeValidator - a declarative per-field upload size limit (Django and DRF ship none), attached to a file/image serializer field like any other validator: AdminFileSerializer(validators=[MaxFileSizeValidator(5 * 1024 * 1024)]). It runs on save where every other field validator does, covering the base64 and tus paths, and the S3 initiate endpoint reads the same declaration up front so an oversized file is refused before it is uploaded. max_size_for_serializer_field() is the reader, alongside the content-type counterpart in uploads.content_types. Nothing here is upload-specific, so both now live in base habarico (habarico.validators) and are re-exported under this module unchanged; effective_max_size(), which folds in the HABARICO_UPLOADS["MAX_UPLOAD_SIZE"] project ceiling that only means anything for a chunked-upload field, stays here. (HAB-942, HAB-965)

  • Aggregations and dashboard, moved from the free habarico library's schema.aggregations and serializers.aggregate: habarico_enterprise.aggregations — FieldAggregation (re-exported from habarico_enterprise), AggregateFunctionType, GroupByType, AggregateSchemaGenerator, AggregateQueryParamsSerializer. Contributes to core's extension points via habarico_enterprise.admin.ModelAdmin (AggregateAdminMixin — aggregate_enabled, aggregate_fields, aggregate_max_buckets, dashboard_enabled) and habarico_enterprise.views.AdminModelViewSet (AggregateViewSetMixin — the /aggregate endpoint and its schema), wired in by CONTRIBUTIONS. (HAB-845)

  • ModelAdmin.can_read_aggregations — declares who may reach a resource's aggregate endpoint and its schema: a bool, or a method taking the request for per-user rules, the same contract as the change history's can_read_history. Left as None, the aggregations follow the admin's can_read. has_read_aggregations_permission(request) is its always-callable form — the name core derives from the flag, and the shape Django's admin uses for permissions — and is what the endpoints ask. (HAB-914)

  • Global spotlight search and match highlighting, moved from the free habarico library's search.py, AdminSite.get_search_result()/get_search_view() and the ModelAdmin global_fulltext_search_enabled/get_search_result() family: habarico_enterprise.search — SearchHighlighter, SearchAdminSchemaGenerator, AdminSearchView. Contributes to core's extension points via habarico_enterprise.admin.ModelAdmin (SearchAdminMixin — global_fulltext_search_enabled, search_highlighter_class, get_search_result(), get_search_object_data()) and habarico_enterprise.site.AdminSite (SearchAdminSiteMixin — the /admin/search/ route and its fan-out across every registered admin), wired in by CONTRIBUTIONS. SearchAdminMixin sets search_highlighter_class unconditionally, so installing habarico-enterprise also switches on meta.highlights for every resource's own per-resource list search (search_fields), independently of the global_fulltext_search_enabled opt-in. (HAB-845)

  • BREAKING: habarico_enterprise is now a Django app and belongs in INSTALLED_APPS alongside habarico_enterprise.uploads. It registers no models, no signals and no checks, and turns no feature on — Django loads a translation catalog only from a directory named in INSTALLED_APPS, and without the entry this package's locale/ is never read. Every paid string used to be translated in the free habarico catalog for exactly this reason; those 49 entries have moved here, so a project that upgrades both packages without adding the entry loses the translations it had (nothing else changes, the admin falls back to English). (HAB-907)

  • This package's own translations — habarico_enterprise/locale, twelve languages (cs, de, es, fr, it, ja, nl, pl, pt, ro, sk, uk), covering every string it raises, including the new AI payload-limit messages. Shared wording such as Yes/No stays in the free catalog rather than being duplicated. (HAB-907)

Changed​

  • BREAKING: the package moves to version 6, the major of the habarico it extends, and requires habarico>=6.0.0. The constraint is now declared in the package metadata too, so installing the wheel pulls in a matching core instead of accepting any habarico at all. The paid permissions — can_read_aggregations, can_read_history — are resolved through core's ModelAdmin._resolve_permission and the has_*_permission methods added there, so the rule for a declaration given as a method lives in one place for the free and the paid flags alike. An older core has none of it and the install now says so instead of failing at request time. (HAB-914, HAB-1019)
  • max_size / accept on AdminFileSerializer / AdminImageFileSerializer are now derived by base habarico itself (MaxFileSizeValidator, FileExtensionValidator / validate_image_file_extension) rather than only here - a free-only install gets both options too, not just the S3/tus one. habarico_enterprise.uploads no longer re-derives them from scratch: content_types.content_types_for_serializer_field() is now a re-export of base's function, and get_field_upload_limits() is replaced by correct_max_size_for_chunked_ceiling(), which folds MAX_UPLOAD_SIZE into whatever base already put in max_size instead of recomputing it. No behaviour change for a project using habarico-enterprise - initiate, the signed target token, and the schema options it advertises are unaffected. (HAB-966)
  • The AI search filter now builds the GPT prompt from the field whitelist instead of the full model meta: only whitelisted fields are shown, and related models are included (pruned to the referenced fields) only when the whitelist contains a relation path such as "author__name". The AI can no longer suggest fields the validation would reject, non-exposed columns no longer leak into the OpenAI prompt, and the prompt itself is smaller. (HAB-902)
  • The prediction request now carries max_completion_tokens, derived from the requested length. length was only ever a sentence in the prompt (Max length of the returned content is N characters) with nothing enforcing it — no token ceiling, no truncation anywhere — so the completion, and the bill for it, were unbounded however small a length the caller asked for. (HAB-907)
  • The prediction prompt's examples no longer contradict the length they are sent with. All four answered with 180–204 characters whatever budget the request stated — the strongest signal in a few-shot prompt, drowning the instruction out — so length shaped the answer far less than it looked. Each example now states its own budget and answers within it, and the real request is rendered in the same shape. A test asserts the examples stay consistent. (HAB-907)
  • BREAKING: a prediction request that omits length asks for a 200-character completion — or ai_max_length, where a resource sets it lower — instead of 1. The frontend always sends the field and is unaffected; a caller that relied on the old default now gets a usable completion rather than a single character. (HAB-907)
  • BREAKING: AIClient.get_predicted_text() takes the extra source fields as a single data mapping (get_predicted_text(prefix, length, data=...)) instead of **data keyword arguments. The mapping comes straight from the request, so a key colliding with a parameter name ({"self": 1}) or one that is not a Python identifier used to raise TypeError — a 500 on an unexpected payload. A direct caller of the client passing keyword arguments has to be updated. (HAB-907)
  • The prediction action accepts POST only. A GET carries no body, so it could only ever produce a degenerate provider call while being the easiest shape to trigger from a browser. (HAB-907)
  • The ai-assistant request body is validated by a serializer, so a malformed one is refused with HTTP 422 and the standard error envelope instead of the hand-built HTTP 400 body — the same way every other endpoint of the library reports a bad request. A missing label, type, help_text or choices on a field no longer raises KeyError, and a completion that is valid JSON but not an object no longer raises AttributeError; both used to be a 500. (HAB-907)

Fixed​

  • AIAdminModelViewSet extended core's bare habarico.views.AdminModelViewSet instead of the composed habarico_enterprise.views.AdminModelViewSet, so any admin built on AIModelAdmin lost AggregateViewSetMixin/SearchViewSetMixin — aggregate_enabled/dashboard_enabled silently had no /aggregate route to serve them. (HAB-813)

Security​

  • BREAKING: A chunked-upload field's ?target= token carried the model field backing it (app_label.model.field) as the way to resolve that field's storage. The token is signed, not encrypted, so anyone holding the URL could decode it - and it revealed more than the admin schema does, since a serializer source= makes the schema's field name and the model's differ. A storage is now always named by its STORAGES alias instead, a name the project chose itself; sign_upload_target() no longer accepts field=, and provider.get_field_schema() no longer takes model_label / field_name. A chunked-upload field whose storage matches no configured alias now raises ImproperlyConfigured when the schema is built - configure the storage under STORAGES and point the field at it. Matching is by instance first, then by the storage's deconstructed configuration, so building the same storage yourself still resolves to its alias, and an unset FileField() resolves to default through Django's lazy proxy. (HAB-942)
  • BREAKING: A plaintext ?target= on the chunked-upload endpoints no longer accepts a STORAGES alias. Any alias the project configures — staticfiles included — could be named by the client, and the endpoint presigned an upload into that bucket's tmp prefix. Plaintext now resolves only names registered through register_upload_target; any other storage requires a token from sign_upload_target, the way the field schemas already issue one. A project that relied on the plaintext alias registers it as a target (register_upload_target("media", storage=storages["media"])) or hands the frontend a signed token. ?target=default goes with it — omit the parameter for the default storage. (HAB-910)
  • The S3 initiate endpoint trusted the client-supplied content_type outright and used it unchanged in the presigned URL's signature and create_multipart_upload; finalize's copy_object then carried it forward into the final, served object with no re-check - so a client could make the browser later render text/html/image/svg+xml inline (stored XSS). initiate now rejects text/html, application/xhtml+xml and image/svg+xml by default, and additionally enforces an allowlist derived from the target serializer field's own declared validators when one exists (validate_image_file_extension → image/*; a FileExtensionValidator → the concrete types guessed from its allowed extensions) - the model field is never consulted for this, only used to resolve storage, since a custom serializer field's validators are independent of it and are what actually runs on save. The allowlist is derived once, when the field schema is built, and travels inside the signed ?target= token - the signature is what makes that safe, and the same values are advertised to the frontend as the field's accept option in the same response, so the two cannot disagree. The token is now always signed (previously only for a non-default storage), since it is what carries them. (HAB-912)
  • The text/html/application/xhtml+xml/image/svg+xml block above is a default, not an unconditional wall: a field whose allowlist names one of them exactly (e.g. FileExtensionValidator(allowed_extensions=["svg"])) now opts that field back in - a deliberate, field-scoped declaration, unlike a wildcard match (image/*), which still does not override the block, since "accepts images" is not a considered decision about SVG specifically. Serving an opted-in file safely (forced download, a sandboxed origin, CSP, sanitisation) stays the host's own responsibility. The default itself is now also a project-wide setting - HABARICO_UPLOADS["DANGEROUS_CONTENT_TYPES"] - rather than a hardcoded constant, so a host can replace or empty it entirely; emptying it removes the floor for every field with no restriction of its own, not just one naming a type exactly, so it carries the same serving-side responsibility at a larger scale. (HAB-966)
  • The S3 initiate endpoint applied no per-field size limit, only the project-wide HABARICO_UPLOADS["MAX_UPLOAD_SIZE"], so a field with a tighter ceiling of its own had it enforced for the first time on save - after the whole file had already been uploaded to the bucket. initiate now also enforces the new MaxFileSizeValidator declared on the target serializer field and refuses the upload before anything is presigned; the stricter of the two ceilings wins, so MAX_UPLOAD_SIZE stays the project-wide limit no field can raise. Carried in the target token exactly like the content-type allowlist, from the same single derivation. (HAB-942)
  • The S3 initiate endpoint built the temp upload key from the client-supplied filename with no sanitization. The name is now passed through the target storage's own get_valid_name before being used in the key — the same sanitization finalize already gets for free via FileField.generate_filename — so a name that resolves to nothing valid (e.g. . or ..) is rejected instead of reaching S3. (HAB-917)
  • Unbounded size in the S3 multipart initiate — the provider presigned one URL per part with no ceiling, so a claimed size of 10**16 asked it for ~1.9 billion presigned URLs and took the worker down with it. initiate now raises UploadTooLarge, before anything is created in S3, for any upload needing more than providers.s3.MAX_PARTS (10 000, S3's own multipart limit) parts. The new HABARICO_UPLOADS["MAX_UPLOAD_SIZE"] (default None) lets a host cap uploads further; left unset, the effective ceiling is MAX_PARTS * CHUNK_SIZE — ~48.8 GiB at the default chunk size, which S3 could not have completed anyway. The refusals are translated, from this package's own catalogs (see Added). (HAB-904)
  • BREAKING: The aggregate endpoints enforce no permission at all. Core's AdminPermission resolves PERMISSION_FLAG_PER_ACTION, which maps only the CRUD actions, so neither GET /admin/<resource>/aggregate (view.action == "aggregate") nor its schema route (dispatched to schema_list) was ever checked: a user barred from listing a resource could still read individual column values through min / max, their distribution through group_by, and the whole field whitelist describing them from the schema endpoint. Row-level scoping held — filter_queryset(get_queryset()) still applied — but the resource-level permission did not. Both routes now resolve the new can_read_aggregations, falling back to the admin's can_read when it is not declared: the data endpoint refuses with 405, the way AdminPermission refuses a disallowed action, and the schema endpoint with 404, the way it already answers for an admin without the opt-in. aggregate_enabled in the resource schema is emitted as false for a user who may not aggregate, so the frontend does not offer a widget it cannot fill. A project relying on the aggregate endpoints being reachable without can_read must declare can_read_aggregations = True on those admins. (HAB-914)
  • The AI actions (prediction, ai-assistant) had no permission gate. Core's AdminPermission maps only the CRUD actions and returns True for anything else, so every caller that could reach the resource could drive paid requests to the AI provider — anonymously under the default AllowAny, and with no write access at all on a read-only resource. Both actions now require an authenticated user (deliberately independent of DEFAULT_PERMISSION_CLASSES) and a resource somebody may write to (can_create or can_update), and the assistant action is left out of the resource schema where it would be refused. Per-user authorization is unchanged and still belongs to permission_classes. (HAB-914, HAB-907)
  • The AI actions (prediction, ai-assistant) read can_create / can_update as plain values, so an admin declaring either as a method taking the request — the per-user form core accepts from habarico@5.11.0b30 — passed the resource gate for everyone: a bound method is always truthy and was never called. Both are now asked through core's has_create_permission / has_update_permission, so the declaration is evaluated for the request being served. A project that declared one as a method will find the AI actions refused where it meant them to be. (HAB-914, HAB-907)
  • Neither AI action bounded the payload it forwarded to the provider — text, data, length, messages, schema.fields and current_values all went into the prompt at whatever size the request carried, so one call could drive arbitrary token spend. Every one of them is now bounded by a per-resource limit (see AIModelAdminMixin under Added). (HAB-907)
  • role in an ai-assistant conversation is restricted to user and assistant. The system prompt is built server-side, so a caller that could send role: "system" was able to overwrite the instructions the assistant runs under. (HAB-907)
  • The AI search filter applied the GPT-generated Q(...) expression without validating it, so a crafted search could filter on any column or relation of the model (e.g. author__user__password__startswith=...) and use the result count as a boolean oracle — a prompt-injection channel into the ORM. Every lookup path in the parsed Q tree (including F() references) is now validated against the admin's whitelist — ai_search_fields when defined, otherwise filter_fields + search_fields — and a rejected expression falls back to the plain fulltext search. (HAB-902)
  • The image download in ai_client.get_generated_file_content had neither a timeout nor a size limit — requests.get(url).content waited indefinitely on a stalled host and buffered whatever that host chose to send. The fetch now streams under a connect/read timeout (IMAGE_CONNECT_TIMEOUT 3.05 s, IMAGE_READ_TIMEOUT 30 s) and counts the bytes as they arrive, raising the new habarico_enterprise.ai.exceptions.GeneratedFileTooLarge and closing the connection mid-transfer as soon as MAX_GENERATED_FILE_SIZE (10 MiB) is passed. A Content-Length over the cap is refused before any of the body is read, but it is only ever an early exit — the running total is what decides, so a missing or dishonest header buys nothing. The response status is now checked as well: the provider's URL expires about an hour after it is issued, and the 403 body was previously stored as the image. Note that the method could only return a ContentFile before and can now raise, so a project calling it directly has to handle that. (HAB-918, HAB-904)
  • The version resource narrows every query to the content types whose tracked admin allows reading history — versions of models with can_read = False (without a can_read_history override) or without a registered admin are no longer readable, and fetching such a version by id returns 404. (HAB-903)
  • The version resource narrowed its queryset with bool(admin.can_read) for a tracked admin built on core's bare ModelAdmin (a project overriding DEFAULT_MODEL_ADMIN_CLASS or opting out of the contributed settings), so a can_read declared as a method left every version of that model readable, for the same reason. The fallback now asks core's has_read_permission(request); an admin carrying ReversionAdminMixin was already resolved through can_read_history and is unaffected. (HAB-914)
  • Stored XSS in the change history diff — field values were inserted into <ins>/<del> tags without HTML escaping, and the frontend renders the diff as rich text. A user with write access to a tracked field could store a payload that executes when an admin views the record's history. Values are now escaped the same way SearchHighlighter already does. (HAB-897)
  • BREAKING: Unbounded limit in the global spotlight search - GET /admin/search/ accepted any number of digits and applied it per admin, so one request sliced up to limit objects out of every admin with global_fulltext_search_enabled and ran the highlighter over each of them. limit is now validated by the new search.utils.resolve_search_limit(): it must be an integer between 1 and HABARICO_SEARCH["MAX_LIMIT"] (default 100), and anything else - non-numeric, zero, negative, or over the ceiling - is refused with 422. Only an absent (or empty) limit falls back to HABARICO_SEARCH["DEFAULT_LIMIT"] (default 5), which replaces the hardcoded value. Previously such a request silently returned 5 results, so a client sending a malformed limit now gets an error instead of data. AdminSearchView resolves the param before calling AdminSite.get_search_result(), which now takes the resolved integer. (HAB-913)
  • SearchAdminMixin.get_search_result() built its queryset from viewset_instance.get_queryset() and the viewset's filter_backends, but never ran AdminPermission/the admin's own permission_classes — so the global spotlight search returned matches from resources a can_read = False admin (or any custom permission_classes denial) should have hidden entirely. It now runs check_permissions() for the list action first and skips the resource the same way it does when there are simply no matches. (HAB-898)
  • Updated django to ==5.2.17 and djangorestframework to ==3.17.2, addressing known CVEs (django: CVE-2026-15830 and 11 further CVEs; djangorestframework: CVE-2026-73228, CVE-2026-73229). (HAB-896)