Skip to main content

Indico Group Registration

Participant-run group registration with plan-based discounts for Indico 3.3.

A participant picks a group plan while registering, gets a code and a join link, and shares them however they like. Everyone who joins pays that plan's rate straight away. The group confirms itself the moment the plan's seat count is filled — there is no button to press. If it never fills, the plugin reprices it at the reconciliation deadline and reports what each member still owes.

Organizers set the plans and the disclaimer. They never have to create a group.

How it works

Plans

An organizer defines plans per registration form, one row per plan, in a table on the settings page:

Plan Seats Discount Off
Group of 3 3 % off 10
Group of 10 10 % off 15
Group of 25 25 fixed amount off 50

Leave the discount columns empty for a plan that carries none. Each plan is given a hidden, permanent id when it is added, so renaming, repricing or reordering a plan never detaches the groups already formed under it.

The seat count is both the target and the cap: filling it confirms the group, and the group is then full.

When another plugin discounts the same registration

The plan picker quotes a price per member, and it has to be the price that member will really be charged — it is the number somebody decides to register on. So the picker is given two fees rather than one, in the ext__group_plan field's data:

basePrice the form's standard registration fee
payerBasePrice what this person pays before a group plan is applied

They are the same number until another plugin takes something off the registration first — the STSA plugin's member discount is the case this exists for — and such a plugin overwrites payerBasePrice only. Which fee a plan's own rate is worked out from is the Discount applies to setting, read in the browser exactly as pricing.py reads it on the server: against the registration fee it is basePrice, so two discounts do not compound; against the whole price it is what the other discount left behind. Leaving basePrice alone is what keeps those two answers the same.

Lifecycle

State What it means
forming Seats filling. Members pay the chosen plan's rate, whenever they like.
confirmed The seat count was reached. Automatic. The rate is final and never gets worse.
short The deadline passed with seats empty. Repriced to whatever the group does qualify for; balances may be due.
dissolved A manager took it apart. Everyone is back on the standard rate.

The only transition a human triggers is dissolution.

Reconciliation, and the one thing to know before enabling this

Members may pay before their group fills. If the group then falls short, the plugin reprices everyone — including people who have already paid.

Indico has no concept of a partial payment or a balance. A transaction is one amount against one registration, and its checkout would charge the whole new price again rather than the difference. Indico therefore reads an already-paid member whose price rose as settled, because their transaction is still successful.

The plugin corrects that where it counts. For as long as a member owes a top-up, their registration reads awaiting payment — in the registrant list, on their own page, and in the data the check-in app is given — and the online checkout is closed to them, with an explanation, so nobody pays the whole price a second time. It returns to complete on its own once the balance is settled.

The plugin also gives organizers a Balances due list (paid amount, new price, delta) and e-mails every affected member, but a person collects the money — at the desk or by transfer, recorded as a manual payment. That page's Refresh payment states button puts every member's state back in step with what they owe; it is only needed for groups repriced before this version. Two settings soften the whole problem:

  • set the reconciliation deadline well before the event, so balances surface while there is still time to chase them;
  • turn off allow early payment if you would rather not chase anyone.

Sharing a group

The group panel shows the group code and the join link, each with a copy button. The plugin sends no invitations. There is no invite box and no way for a participant to make your server e-mail a stranger. Leaders share the link themselves.

The plugin does send e-mails that nobody can trigger on demand: group confirmed, group short with the new rate, and group dissolved.

There is one more, and an organizer sends it. Remind forming groups, on the Groups page, opens Indico's own e-mail dialog with the recipients already found -- every member of every group that has not filled yet -- and a draft you can read, rewrite and preview before anything is sent. Groups that are already confirmed, short or dissolved are not written to, and a group that fills while the dialog is open is dropped when you press Send.

The draft is only a starting point. The figures in it are placeholders Indico fills in per recipient, from that person's own group, so you can move them, drop them or write around them and they still quote the right numbers:

Placeholder What it becomes
{group_name} the name of the recipient's group
{group_code} the code others type to join it
{group_link} the join link
{group_plan} the rate the group is forming under
{group_fallback_plan} the rate it qualifies for at its current size
{group_members} / {group_target} / {group_seats_left} how full it is
{group_deadline} when it gets repriced if it has not filled
{group_price} what the recipient pays at the group rate
{group_new_price} what they would pay at the group's current size
{group_difference} how much more that is
{group_balance} what they would then still owe, against what they have paid

Core's own placeholders -- {first_name}, {event_title}, {link} and the rest -- work alongside them. They are offered on any registration form that has group registration switched on, so they are available in Indico's ordinary E-mail action there too; on a form without group registration they do not appear at all.

The Groups page itself lists every group on the form and sorts by any column heading.

Changing plan

While a group is still forming and nobody in it has paid, the leader can move it onto another plan from the group panel. Only plans that seat everybody already in the group are offered, and the switch reprices every member at once. Once anybody has paid, the control disappears: one person's change must not quietly rebill somebody who has already handed money over.

Installation

Do not let pip upgrade Indico by accident. This plugin declares indico>=3.3.5,<3.4, and pip will happily upgrade an installed Indico to satisfy that. An Indico whose code is newer than its database fails on every page that touches a migrated table — the whole site, not just registration. Install into the existing Indico virtualenv and check what pip says it is about to do:

/opt/indico/.venv/bin/pip install indico-plugin-group-registration

If Indico appears in the "Installing collected packages" line, run the core migrations before restarting (see below). To keep pip away from Indico entirely:

/opt/indico/.venv/bin/pip install --no-deps indico-plugin-group-registration

Add it to indico.conf:

PLUGINS = {'group_registration'}

Then migrate. --all-plugins runs core migrations too, which is what you want — if pip moved Indico forward, this is the step that moves the database with it:

indico db --all-plugins upgrade

Sanity-check that nothing is pending:

indico db alembic current   # should match
indico db alembic heads

Restart Indico and its Celery workers — the reconciliation task runs every fifteen minutes from the Celery beat schedule.

That is the whole install. The wheel ships the compiled webpack bundle, so there is no asset build to run on the server — which is just as well, since building one needs node and an Indico source checkout, and a pip-installed Indico has neither.

If registration pages throw Assets for plugin group_registration have not been built

Indico could not find static/dist/manifest.json next to the installed plugin, so it refuses to render any page the plugin puts a script on — which is every registration page in the instance, management side included.

Check what got installed:

ls /opt/indico/.venv/lib/python3.12/site-packages/indico_group_registration/static/dist/

If that directory is missing or empty, the plugin was installed from a source checkout, or from a wheel built before the assets were packaged. Reinstall from PyPI:

/opt/indico/.venv/bin/pip install --no-deps --force-reinstall indico-plugin-group-registration

and restart Indico. If you are deliberately running from a git checkout, build the bundle yourself — see Building the assets.

If the registration form editor goes blank

Symptom: Registration → Registration form, click the gear on any field, and the page empties. The browser console has TypeError: undefined is not an object (evaluating '…showIfOptions') from form/fields/ShowIfInput.jsx.

Core builds the "show this field if" dropdown by looking every item on the form up in its React field registry, without checking the type is there — so one plugin field the browser does not know about throws and unmounts the whole editor. That was this plugin's ext__group_discount before 0.2.2, and it is worth knowing the shape of it: any plugin that provisions a field type it never registers client-side breaks the editor for every field on the form, not just its own.

Upgrade the plugin and hard-reload the page:

/opt/indico/.venv/bin/pip install --no-deps -U indico-plugin-group-registration

If it still happens, the culprit is another plugin's field. The console error does not name the input type; the form editor prints Unknown input type: <name> in place of the offending field, so look for that before opening the dialog.

If the site is throwing UndefinedColumn errors

Indico's code is ahead of its database. Nothing to do with this plugin's tables; run the migrations:

indico db --all-plugins upgrade

Or put Indico back where it was and migrate later:

/opt/indico/.venv/bin/pip install 'indico==<your previous version>'

Configuration

Event management → Group registration, then pick a registration form. Nothing changes on a form until group registration is enabled for it.

Setting Default Notes
Plans A row per plan; validated on save
Discount applies to Registration fee Or the whole price, including paid options
Reconciliation deadline Registration end Set it well before the event
Allow paying before the group fills on
Groups per person 1 0 for no limit
Count registrations awaiting approval on Withdrawn and rejected never count
Reprice a confirmed group that loses a member off Nobody should be rebilled over somebody else's moderation
Disclaimer a sensible default Versioned; the version and time of acceptance are stored per membership

Enabling group registration provisions two fields on the form:

  • ext__group_plan — the plan picker the participant fills in. Put it wherever you like in the form.
  • ext__group_discount — a manager-only field carrying the discount. It is locked, written only by the plugin, and appears as a named line on the invoice. You will not see it in the form editor: it is registered with core's React field registry so that it can render nothing at all, and its section is hidden. It is not offered as a column in the registration list's Customize list dialog either — the Group column is the one that belongs to you. Nothing about it is an organizer's to set.

Development

pip install -e '.[dev]'
pytest
ruff check .

The tests here are unit tests over the money arithmetic and the code handling; they do not need a database or Indico's pytest plugin.

Building the assets

The plan picker is a React component compiled by Indico's own webpack setup, which means the build needs an Indico source checkout at the version the plugin will run against — bin/maintenance/, webpack/ and node_modules/ are not in the Indico wheel.

git clone --branch v3.3.13 https://github.com/indico/indico ~/dev/indico
cd ~/dev/indico && npm ci && cd -

/opt/indico/.venv/bin/python build-assets.py --indico-source ~/dev/indico

Run it with the Python that Indico and this plugin are installed into: the build imports the plugin to resolve its own URL rules. The result lands in indico_group_registration/static/dist/, which is git-ignored and shipped as a packaging artifact. --dev and --watch are passed straight through to Indico's build script.

Releases do all of this in CI — see .github/workflows/build.yml, which builds the bundle, packs it into the wheel and refuses to publish a wheel that is missing its assets, templates or migrations.

How it hooks into Indico

Nothing in core is patched. See docs/EXTENSION-POINTS.md for the full map with file-and-line references, and docs/PLAN.md for the design. The short version:

  • registration field types via signals.core.get_fields
  • the registration lifecycle via registration_created, registration_deleted and registration_state_updated
  • is_field_data_locked to keep core out of the plugin's own field
  • registrant_list_items for the Group column
  • Flask's before_render_template for the one thing Indico has no hook for: keeping the internal discount field out of the Customize list dialog, which otherwise offers every field on the form as a column
  • before-render-registration-info for the group panel
  • the React field registry via regformCustomFieldsboth field types, the internal one included; core's form editor crashes on an input type that is not in the registry
  • get_template_customization_paths to name the discount on the checkout page, using {% extends '~...' %} so it inherits the core template rather than forking it

Licence

MIT.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

indico_plugin_group_registration-0.4.1.tar.gz (2.0 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

File details

Details for the file indico_plugin_group_registration-0.4.1.tar.gz.

File metadata

File hashes

Hashes for indico_plugin_group_registration-0.4.1.tar.gz
Algorithm Hash digest
SHA256 964c3730a25eb6977a9363fc3c51400df28260bd82521fdf3c757811c4021505
MD5 9c17db06e5e4fb41a94adf3c627876b5
BLAKE2b-256 04722ffc20ee7ddf488b5d445f70ed57e35f14c00c3160abeca6d57d0e0b06d6

See more details on using hashes here.

Provenance

The following attestation bundles were made for indico_plugin_group_registration-0.4.1.tar.gz:

Publisher: publish.yml on RobotHanzo/IndicoGroupRegistration

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file indico_plugin_group_registration-0.4.1-py3-none-any.whl.

File metadata

File hashes

Hashes for indico_plugin_group_registration-0.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9eda9f4decded1b7c8e8681ee78454bcfb470cd0aa849c3aad0914cb08d242bd
MD5 e0c4f9f1ced540a0c272ee3893cf5ee6
BLAKE2b-256 a9da5611809c1b4f6f6b9e209deccb27a49830360022c9556a894a733e42f9e2

See more details on using hashes here.

Provenance

The following attestation bundles were made for indico_plugin_group_registration-0.4.1-py3-none-any.whl:

Publisher: publish.yml on RobotHanzo/IndicoGroupRegistration

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.4.1 This release

2 files

0.4.0

2 files

0.3.0

2 files

0.2.4

2 files

0.2.3

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page