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-enterprisepackage, built on the extension points of the freehabaricolibrary. (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 throughMANIFEST.in: commercial and proprietary, usable only under a licence agreement with Eluvia.land s.r.o. The freehabaricolibrary 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 upModelAdmin,AdminModelViewSet,AdminSite,FieldSchemaGeneratorand the file field mixin on its own. A project needs noADMIN_REST_FRAMEWORKblock, and still overrides any single key by setting it explicitly. (HAB-811) -
habarico_enterprise.schema.GlobalResourceSchema/AdminSchemaGeneratorandhabarico_enterprise.validation.AdminConfigurationValidator— the composed classeshabarico_enterprise.admin.ModelAdminnow pointsschema_generator_class/configuration_validator_classat. The resource-schema keysglobal_fulltext_search_enabled,aggregate_enabledanddashboard_enabled, theaggregate_max_buckets/aggregate_fieldsregistration-time checks and the list endpoint'smeta.highlightssidecar are all contributed from here, so the free library carries no trace of them. (HAB-811) -
CZ localflavor, moved from the free
habaricolibrary:habarico_enterprise.models.localflavor.czandhabarico_enterprise.validators.localflavor.cz. (HAB-845) -
AI integration, moved from the free
habaricolibrary'scontrib.ai:habarico_enterprise.ai—AIModelAdmin,PredictiveCharField/PredictiveTextField,AIAdminModelSerializer,AIAdminModelViewSet,AIAdminFilterSet, theAIClient/ai_client(OpenAI) and theQParser/AISearchFilternatural-language filter. Requires theaiextra (openai,pyparsing). (HAB-845) -
AIModelAdminMixinpayload limits —ai_max_text_length(2000),ai_max_length(500),ai_max_value_length(1000),ai_max_fields(100),ai_assistant_max_messages(30) andai_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 withImproperlyConfigured.Most of them trim rather than refuse, because what they bound is context the caller never chose to send —
dataandcurrent_valuesare whatever the record holds,schema.fieldsis 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), andai_assistant_max_messagesis 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
lengthaboveai_max_length, and a single chat message longer thanai_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_clientattribute to do this. (HAB-907) -
AIModelAdminMixin.ai_search_fields— an optional complete whitelist of field paths the AI search filter may use in generatedQexpressions. It fully replaces the defaultfilter_fields+search_fieldswhitelist, so a project can allow relation paths such as"author__name"beyond the standard filters. (HAB-902) -
Change history, moved from the free
habaricolibrary'scontrib.reversion:habarico_enterprise.reversion—VersionAdmin,RevisionAdmin,VersionListBox,VersionSerializer,VersionFilterSet, built on top ofdjango-reversion. Requires thereversionextra (django-reversion,dmp). (HAB-845) -
The change history is switched on by listing
habarico_enterprise.reversioninINSTALLED_APPS, the wayhabarico_enterprise.uploadsalready is, replacing the bareimport habarico_enterprise.reversion # noqa: F401the feature used to need in the project's own code. habarico's autodiscovery then registers theversionandrevisionresources, and the admin mixin joinsModelAdminonly 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_historyonModelAdmin— 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'scan_read. It is declared byReversionAdminMixinalongsidehistory_fields, andhas_read_history_permission(request)is their always-callable form. (HAB-903) -
Chunked/S3/tus uploads, moved from the free
habaricolibrary'scontrib.uploads:habarico_enterprise.uploads. Contributes to core's extension points viahabarico_enterprise.site.AdminSite(upload routes) andhabarico_enterprise.schema.FieldSchemaGenerator(the field schemauploadblock), wired in byCONTRIBUTIONStogether withChunkedUploadFieldMixinasDEFAULT_FILE_FIELD_MIXIN_CLASS. Requires theuploads-s3and/oruploads-tusextra (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'sDEFAULT_UPLOAD_REFERENCE_CLASSESsetting. 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._UploadReferenceis renamed toUploadReference. It is the valueChunkedUploadFieldMixin.is_pending_upload()reports to core, so a project can be asked to name it — a leading underscore promised the opposite. Its siblingChunkedUploadFieldMixinwas public already. (HAB-845) -
options.max_size/options.accepton file and image fields - the client-side half of the upload rules, derived from the same serializer-field validatorsinitiatereads (MaxFileSizeValidatorand 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_SIZEjoinsmax_sizeonly for a field a chunked provider actually serves, sinceinitiateis the only place that enforces it. The hard content-type blocklist is deliberately not reflected inaccept- it has no allowlist form - so an absent or permissiveacceptnever means the server will accept the file. An explicitly passed option wins over the derived one. Needs the matchingFieldOptionskeys incontract/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 S3initiateendpoint 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 inuploads.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 theHABARICO_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
habaricolibrary'sschema.aggregationsandserializers.aggregate:habarico_enterprise.aggregations—FieldAggregation(re-exported fromhabarico_enterprise),AggregateFunctionType,GroupByType,AggregateSchemaGenerator,AggregateQueryParamsSerializer. Contributes to core's extension points viahabarico_enterprise.admin.ModelAdmin(AggregateAdminMixin—aggregate_enabled,aggregate_fields,aggregate_max_buckets,dashboard_enabled) andhabarico_enterprise.views.AdminModelViewSet(AggregateViewSetMixin— the/aggregateendpoint and its schema), wired in byCONTRIBUTIONS. (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'scan_read_history. Left asNone, the aggregations follow the admin'scan_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
habaricolibrary'ssearch.py,AdminSite.get_search_result()/get_search_view()and theModelAdminglobal_fulltext_search_enabled/get_search_result()family:habarico_enterprise.search—SearchHighlighter,SearchAdminSchemaGenerator,AdminSearchView. Contributes to core's extension points viahabarico_enterprise.admin.ModelAdmin(SearchAdminMixin—global_fulltext_search_enabled,search_highlighter_class,get_search_result(),get_search_object_data()) andhabarico_enterprise.site.AdminSite(SearchAdminSiteMixin— the/admin/search/route and its fan-out across every registered admin), wired in byCONTRIBUTIONS.SearchAdminMixinsetssearch_highlighter_classunconditionally, so installing habarico-enterprise also switches onmeta.highlightsfor every resource's own per-resource list search (search_fields), independently of theglobal_fulltext_search_enabledopt-in. (HAB-845) -
BREAKING:
habarico_enterpriseis now a Django app and belongs inINSTALLED_APPSalongsidehabarico_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 inINSTALLED_APPS, and without the entry this package'slocale/is never read. Every paid string used to be translated in the freehabaricocatalog 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 asYes/Nostays in the free catalog rather than being duplicated. (HAB-907)
Changed
- BREAKING: the package moves to version 6, the major of the
habaricoit extends, and requireshabarico>=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 anyhabaricoat all. The paid permissions —can_read_aggregations,can_read_history— are resolved through core'sModelAdmin._resolve_permissionand thehas_*_permissionmethods 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/acceptonAdminFileSerializer/AdminImageFileSerializerare 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.uploadsno longer re-derives them from scratch:content_types.content_types_for_serializer_field()is now a re-export of base's function, andget_field_upload_limits()is replaced bycorrect_max_size_for_chunked_ceiling(), which foldsMAX_UPLOAD_SIZEinto whatever base already put inmax_sizeinstead of recomputing it. No behaviour change for a project usinghabarico-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 requestedlength.lengthwas 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 alengththe caller asked for. (HAB-907) - The prediction prompt's examples no longer contradict the
lengththey 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 — solengthshaped 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
predictionrequest that omitslengthasks for a 200-character completion — orai_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 singledatamapping (get_predicted_text(prefix, length, data=...)) instead of**datakeyword 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 raiseTypeError— a 500 on an unexpected payload. A direct caller of the client passing keyword arguments has to be updated. (HAB-907) - The
predictionaction acceptsPOSTonly. AGETcarries 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-assistantrequest 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 missinglabel,type,help_textorchoiceson a field no longer raisesKeyError, and a completion that is valid JSON but not an object no longer raisesAttributeError; both used to be a 500. (HAB-907)
Fixed
AIAdminModelViewSetextended core's barehabarico.views.AdminModelViewSetinstead of the composedhabarico_enterprise.views.AdminModelViewSet, so any admin built onAIModelAdminlostAggregateViewSetMixin/SearchViewSetMixin—aggregate_enabled/dashboard_enabledsilently had no/aggregateroute 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 serializersource=makes the schema's field name and the model's differ. A storage is now always named by itsSTORAGESalias instead, a name the project chose itself;sign_upload_target()no longer acceptsfield=, andprovider.get_field_schema()no longer takesmodel_label/field_name. A chunked-upload field whose storage matches no configured alias now raisesImproperlyConfiguredwhen the schema is built - configure the storage underSTORAGESand 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 unsetFileField()resolves todefaultthrough Django's lazy proxy. (HAB-942) - BREAKING: A plaintext
?target=on the chunked-upload endpoints no longer accepts aSTORAGESalias. Any alias the project configures —staticfilesincluded — could be named by the client, and the endpoint presigned an upload into that bucket's tmp prefix. Plaintext now resolves only names registered throughregister_upload_target; any other storage requires a token fromsign_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=defaultgoes with it — omit the parameter for the default storage. (HAB-910) - The S3
initiateendpoint trusted the client-suppliedcontent_typeoutright and used it unchanged in the presigned URL's signature andcreate_multipart_upload;finalize'scopy_objectthen carried it forward into the final, served object with no re-check - so a client could make the browser later rendertext/html/image/svg+xmlinline (stored XSS).initiatenow rejectstext/html,application/xhtml+xmlandimage/svg+xmlby default, and additionally enforces an allowlist derived from the target serializer field's own declared validators when one exists (validate_image_file_extension→image/*; aFileExtensionValidator→ 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'sacceptoption 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+xmlblock 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
initiateendpoint applied no per-field size limit, only the project-wideHABARICO_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.initiatenow also enforces the newMaxFileSizeValidatordeclared on the target serializer field and refuses the upload before anything is presigned; the stricter of the two ceilings wins, soMAX_UPLOAD_SIZEstays 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
initiateendpoint built the temp upload key from the client-suppliedfilenamewith no sanitization. The name is now passed through the target storage's ownget_valid_namebefore being used in the key — the same sanitizationfinalizealready gets for free viaFileField.generate_filename— so a name that resolves to nothing valid (e.g..or..) is rejected instead of reaching S3. (HAB-917) - Unbounded
sizein the S3 multipartinitiate— the provider presigned one URL per part with no ceiling, so a claimedsizeof 10**16 asked it for ~1.9 billion presigned URLs and took the worker down with it.initiatenow raisesUploadTooLarge, before anything is created in S3, for any upload needing more thanproviders.s3.MAX_PARTS(10 000, S3's own multipart limit) parts. The newHABARICO_UPLOADS["MAX_UPLOAD_SIZE"](defaultNone) lets a host cap uploads further; left unset, the effective ceiling isMAX_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
AdminPermissionresolvesPERMISSION_FLAG_PER_ACTION, which maps only the CRUD actions, so neitherGET /admin/<resource>/aggregate(view.action == "aggregate") nor its schema route (dispatched toschema_list) was ever checked: a user barred from listing a resource could still read individual column values throughmin/max, their distribution throughgroup_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 newcan_read_aggregations, falling back to the admin'scan_readwhen it is not declared: the data endpoint refuses with405, the wayAdminPermissionrefuses a disallowed action, and the schema endpoint with404, the way it already answers for an admin without the opt-in.aggregate_enabledin the resource schema is emitted asfalsefor 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 withoutcan_readmust declarecan_read_aggregations = Trueon those admins. (HAB-914) - The AI actions (
prediction,ai-assistant) had no permission gate. Core'sAdminPermissionmaps only the CRUD actions and returnsTruefor anything else, so every caller that could reach the resource could drive paid requests to the AI provider — anonymously under the defaultAllowAny, and with no write access at all on a read-only resource. Both actions now require an authenticated user (deliberately independent ofDEFAULT_PERMISSION_CLASSES) and a resource somebody may write to (can_createorcan_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 topermission_classes. (HAB-914, HAB-907) - The AI actions (
prediction,ai-assistant) readcan_create/can_updateas plain values, so an admin declaring either as a method taking the request — the per-user form core accepts fromhabarico@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'shas_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.fieldsandcurrent_valuesall 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 (seeAIModelAdminMixinunder Added). (HAB-907) rolein anai-assistantconversation is restricted touserandassistant. The system prompt is built server-side, so a caller that could sendrole: "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 (includingF()references) is now validated against the admin's whitelist —ai_search_fieldswhen defined, otherwisefilter_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_contenthad neither a timeout nor a size limit —requests.get(url).contentwaited indefinitely on a stalled host and buffered whatever that host chose to send. The fetch now streams under a connect/read timeout (IMAGE_CONNECT_TIMEOUT3.05 s,IMAGE_READ_TIMEOUT30 s) and counts the bytes as they arrive, raising the newhabarico_enterprise.ai.exceptions.GeneratedFileTooLargeand closing the connection mid-transfer as soon asMAX_GENERATED_FILE_SIZE(10 MiB) is passed. AContent-Lengthover 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 aContentFilebefore and can now raise, so a project calling it directly has to handle that. (HAB-918, HAB-904) - The
versionresource narrows every query to the content types whose tracked admin allows reading history — versions of models withcan_read = False(without acan_read_historyoverride) or without a registered admin are no longer readable, and fetching such a version by id returns 404. (HAB-903) - The
versionresource narrowed its queryset withbool(admin.can_read)for a tracked admin built on core's bareModelAdmin(a project overridingDEFAULT_MODEL_ADMIN_CLASSor opting out of the contributed settings), so acan_readdeclared as a method left every version of that model readable, for the same reason. The fallback now asks core'shas_read_permission(request); an admin carryingReversionAdminMixinwas already resolved throughcan_read_historyand 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 waySearchHighlighteralready does. (HAB-897) - BREAKING: Unbounded
limitin the global spotlight search -GET /admin/search/accepted any number of digits and applied it per admin, so one request sliced up tolimitobjects out of every admin withglobal_fulltext_search_enabledand ran the highlighter over each of them.limitis now validated by the newsearch.utils.resolve_search_limit(): it must be an integer between 1 andHABARICO_SEARCH["MAX_LIMIT"](default 100), and anything else - non-numeric, zero, negative, or over the ceiling - is refused with422. Only an absent (or empty)limitfalls back toHABARICO_SEARCH["DEFAULT_LIMIT"](default 5), which replaces the hardcoded value. Previously such a request silently returned 5 results, so a client sending a malformedlimitnow gets an error instead of data.AdminSearchViewresolves the param before callingAdminSite.get_search_result(), which now takes the resolved integer. (HAB-913) SearchAdminMixin.get_search_result()built its queryset fromviewset_instance.get_queryset()and the viewset'sfilter_backends, but never ranAdminPermission/the admin's ownpermission_classes— so the global spotlight search returned matches from resources acan_read = Falseadmin (or any custompermission_classesdenial) should have hidden entirely. It now runscheck_permissions()for thelistaction first and skips the resource the same way it does when there are simply no matches. (HAB-898)- Updated
djangoto==5.2.17anddjangorestframeworkto==3.17.2, addressing known CVEs (django: CVE-2026-15830 and 11 further CVEs;djangorestframework: CVE-2026-73228, CVE-2026-73229). (HAB-896)