Problem/Motivation

Every booking row carries a tenant, the realm it belongs to, and the tenant is a hard access boundary: YoyakuAccessControlHandler reaches a row only when its tenant is the one currently being served. The field is not form-displayable, and its default is the shipped default tenant, so nothing about the realm an operator is working in reaches a row created from an add form.

Where a row is added from the thing it belongs to, the controller prefills it: a resource's Allotments tab creates the record with the resource's tenant, and every other parent tab does the same. But each of those types also has an add form of its own, reached from its collection, where the parent is picked in a select instead. That form writes the default tenant next to a parent belonging to another one, and the boundary then refuses the operator the row they just created.

These add forms take neither the parent's tenant nor the tenant being served.

  • The rows that belong to no other row, which have nothing to inherit and want the tenant being served: yoyaku_resource, yoyaku_tariff_preset, yoyaku_tariff_class, yoyaku_allotment, and yoyaku_venue.
  • The rows that belong to another one, which want its tenant: yoyaku_slot, yoyaku_resource_tariff, yoyaku_slot_tariff, yoyaku_tariff, yoyaku_resource_allotment, yoyaku_slot_allotment, and yoyaku_preset_allotment.

Two more paths write the default tenant. A booking channel's form does ask which tenant the front belongs to, and it is the one field that should, since a front cannot be moved afterwards, but the select opens on the default tenant rather than on the one being served. And the venue importer stamps a tenant only when the command was given one, so the grades, sections and places it writes take the option rather than the venue's own tenant, which can split one venue across two realms.

The allotment select has the same hole from the other side. The allotment reference names no selection handler, so the generic one offers every allotment on the site: a production in one realm is offered the names another realm holds units under. A neighbouring field already does this correctly, BookingChannelSelection narrowing a front to the tenant being served, and an allotment needs the same.

Scoping it that way raises the question it hides today, which is whether an allotment is always one realm's. An authority running several halls as separate tenants holds units for the same schools and the same press in all of them, and typing that vocabulary once per realm loses the one thing the name exists for, which is that a season reads the same everywhere.

A single-tenant site is unaffected throughout: there is one tenant, every row already carries it, and the value written today is the value these forms would write.

Proposed resolution

A row takes its tenant from the row it belongs to, wherever it is created. A type that belongs to something says so by extending OwnedContentEntityBase, whose preSave() follows the chain the entity type already declares as parent_fields, and the per-controller prefills are left with nothing to do. Nothing is dispatched for the types this does not concern: a booking, a node and everything else on the site pay nothing at all, where a global entity_presave would cost each of them a call to be told it had nothing to do.

The parent is taken from its storage rather than off this row's own reference field. Both reach the same row, and the storage's static cache means the caller that created this row from the parent has already paid for it, so the tenant costs no query at all. The reference field would also hang the loaded parent on this row's field item, where it stays for the rest of the request: a parent configured after its children were written would then be read, through them, as it was before.

The booking needs no flag saying it settles its own realm. It does not extend the base, which is where a reader looks. A hold agrees every ask on one tenant before anything is written and refuses the ones that span two.

The grant cleanup in yoyaku_manager is named for the two types it concerns, yoyaku_resource_predelete and user_predelete, rather than implemented as a global entity_predelete that matched two type ids and returned for every other delete on the site.

A row that belongs to nothing but its tenant takes the one being served: TenantField reads its default from the tenant context instead of naming the default tenant outright. The booking channel form keeps its select and opens on the same answer. The importer stamps its children from the venue it just wrote, so the option decides one thing rather than four.

An allotment select then offers the tenant being served, plus the allotments shared with every tenant, which is the shape a resource type already has: an empty tenant means shared, a named one means private to it, and one query condition covers both. Only the name and the color are shared. How many units are held lives on the resource and the slot, which are rows of one realm like any other, so counting, release and reporting stay inside a tenant whatever the name is shared with.

A shared allotment is not one tenant's to rename or delete, because both would change what another realm's tickets and reports say, so editing one needs a permission of its own rather than the administer permission every tenant operator already holds. The refusal to delete an allotment that bookings still name needs nothing added: it already counts across every realm, which is what a shared name needs, so an operator can be told a name is still in use while their own realm shows nothing using it.

The tenant field stays required for every type that must never be without one. The types that may be shared take a definition of their own, so nothing about the boundary the other twenty types rely on is loosened to make room for this.

User interface changes

The allotment form gains the tenant scope its resource type counterpart already has, shared or one named tenant, and the allotment list says which a name is. The tenant stays off every form it is not already on.

API changes

None.

Data model changes

An allotment's tenant may be empty, meaning shared with every tenant. The column, its type and its length are unchanged; what changes is that emptiness now means something. A new permission covers editing a shared allotment.

AI-Generated: Yes (Claude Code was used to audit the creation paths and to draft this issue summary. I reviewed it, and the affected forms were read out of the code rather than inferred.)

Issue fork yoyaku-3620544

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

mably created an issue. See original summary.

mably’s picture

Status: Active » Needs review
mably’s picture

Issue summary: View changes
mably’s picture

Issue summary: View changes

  • mably committed e184206d on 1.x
    task: #3620544 Give a new row the tenant it belongs to instead of the...
mably’s picture

Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.