I am setting up Indico for a conference with rather complicated hotel booking and pricing requirements. Upon registration users have to select hotel categories and the price for accompanying persons also depends on that hotel category.
My solution is to write a plugin that encapsulates all options and the resulting price as a custom registration field - and while searching for all required APIs and setting up the (to me) unfamiliar toolchains and frameworks took me ages, I am deeply impressed by the plugin system as it allowed me to do pretty much all I needed. And all as a plugin in a way that feels future proof in the sense that I do not worry that updating Indico will break my plugin.
There is only one issue that I cannot seem to fix in an elegant way:
In the registration overview as well as in the receipt emails the complex data from my field is presented as a JSON object. I need to rewrite it in a user-friendly way. So, I looked into how the built-in accommodation field in Indico does it to find that its formatting is pretty much a hardcoded special case in the respective templates.
So, tldr:
Is there a way to get custom formatting for my field into these templates without overwriting them entirely? (I am sure that overwriting them will require me to patch all those templates each time I update Indico)
Using the for_humans parameter of get_friendly_data to produce a well-formatted string would be fine, too, but for_humans is not used for these parts. Would it make sense to suggest using it as a default for those templates in the next release of Indico? To me this seems to be the intended use of this parameter and most fields do not have special requirements here…
As a last resort: Are there any drawbacks to returning a human-readable string instead of JSON for all calls to get_friendly_data regardless of for_humans? Is this used somewhere except for field-specific templates that do not apply to my field anyway?
Try adding this patch to the core and then implement those template hooks:
diff --git a/indico/modules/events/registration/templates/display/_registration_summary_blocks.html b/indico/modules/events/registration/templates/display/_registration_summary_blocks.html
index 4c547fdca1..c2637eaf7f 100644
--- a/indico/modules/events/registration/templates/display/_registration_summary_blocks.html
+++ b/indico/modules/events/registration/templates/display/_registration_summary_blocks.html
@@ -107,7 +107,8 @@
url_for('.registration_picture', data[field.id].locator.registrant_file) %}
<img class="picture-preview" src="{{ picture_url }}" alt="{{ friendly_data }}">
{% elif friendly_data is not none %}
- {{- friendly_data -}}
+ {{- template_hook('registration-summary-blocks-render-data', type=field.input_type, friendly_data=friendly_data)
+ or friendly_data -}}
{% endif %}
{% endmacro %}
diff --git a/indico/modules/events/registration/templates/emails/base_registration_details.html b/indico/modules/events/registration/templates/emails/base_registration_details.html
index cdcedb169f..27528b7b15 100644
--- a/indico/modules/events/registration/templates/emails/base_registration_details.html
+++ b/indico/modules/events/registration/templates/emails/base_registration_details.html
@@ -99,7 +99,8 @@
{%- elif type == 'picture' and friendly_data -%}
{{- render_picture(friendly_data, raw_data) -}}
{%- else -%}
- {{- friendly_data -}}
+ {{- template_hook('registration-email-render-data', type=type, friendly_data=friendly_data, raw_data=raw_data)
+ or friendly_data -}}
{%- endif -%}
{% endmacro %}
diff --git a/indico/modules/events/registration/templates/management/_reglist.html b/indico/modules/events/registration/templates/management/_reglist.html
index 9736790daf..df104f5a21 100644
--- a/indico/modules/events/registration/templates/management/_reglist.html
+++ b/indico/modules/events/registration/templates/management/_reglist.html
@@ -162,11 +162,20 @@
{%- endif %}
</td>
{% else %}
- <td class="i-table" data-text="{{ search_value }}">
- {%- if item.id in data and data[item.id].friendly_data %}
- {{- data[item.id].friendly_data }}
- {%- endif %}
- </td>
+ {% set custom_column = (
+ template_hook('registration-list-render-data', type=data[item.id].field_data.field.input_type, friendly_data=data[item.id].friendly_data)
+ if item.id in data
+ else none
+ ) %}
+ {% if custom_column %}
+ {{ custom_column }}
+ {% else %}
+ <td class="i-table" data-text="{{ search_value }}">
+ {%- if item.id in data and data[item.id].friendly_data %}
+ {{- data[item.id].friendly_data }}
+ {%- endif %}
+ </td>
+ {% endif %}
{% endif %}
{% endfor %}
</tr>
Happy to add them upstream if they do the job. They are untested, so maybe there’s a type somewhere.
Would it make sense to suggest using it as a default for those templates in the next release of Indico? To me this seems to be the intended use of this parameter and most fields do not have special requirements here…
This code is a bit messy so I’d rather not touch it w/o cleaning it up a bit altogether.
Would you mind sending a PR containing both mine and your changes? Easier to take care of it that way, also because I’m currently on something else. But yes, I don’t see why this would not make it into the next release.
Looks fine to me. At the moment I have three plugins using that feature: One handles custom hotel assignments with complicated for accompanying persons pricing and renders a compact overview for planning and assignment. The next one allows entering the exact sum someone has paid to highlight and keep track of discrepencies. The third plugin adds a column to show the state of submitted abstracts next to the registration entry of the submitter. Each of them accesses a bunch of data outside the scope of RegistrationData, so from my point of view the initial solution of putting everything into a big friendly_data JSON package and render the required items via Jinja templates was just fine. But since I can just render the same templates from the new render methods, migration sould be easy. (Except for the snapshot / diff ones - have to look into these.)
The bigger problem for me is time. We are currently in a test run with several groups from different Universities trying to deal with a bunch of feature requests four days from my summer vacation. The server is on an official v3.3.6 release and I am developing on the v3.3.6 release commit with my patches applied because I am working with a clone of our test server database and do not want to migrate it to the development version during the test run. (Yes, my colleagues are now learning what JSON looks like :D)
So, I cannot really do a quick test of the new implementation and the most important question for me is if you have a rough ETA for the release date, so I can see if we can incorporate it into the test run?
Since we are in an extremely “dynamic” testing phase, the plugins are quite bloated at the moment. I think the abstract state plugin is the easiest to test at the moment:
Don’t be surprised, it does a few more things: It automatically assigns tags to the registrants according to their abstract submission state (we also have locks in place to enforce only a single abstract). At the top of the plugin there are tag names defined that the plugin expects to be available for this (it should should be ok if they are not, though). We use these tags on badges (long story) and for filtering, so it is ok that they only update whenever the registration list is shown.
While on the subject:
I think some of the plugins might be usefull to others (like making tags and roles available in the badge designer or adding mass modification buttons to the registration list) and some might be a helpful example to learn how to implement custom fields (especially if I remember how long it took me to figure out some things).
Once I found some time to clean up and document what they do, I would like to share them. Are you interested in PRs to the official plugin repo for the useful ones or should I just dump all into my own repo? (I will probably also ask you to have a look at some few details for which I think there might be a more exemplary solution to what I did.)
…from code that’s used to render a menu! You might want to consider using the rh.process or rh.before_process signals instead (bind to the sender of the RH class used on that page, which you can easily spot at the end of the page source in an HTML comment)
Your own repo. You can use indico-plugins or indico-plugins-cern as an example for multi-plugin repos, or indico-plugin-jacow as an example for a single-plugin repo.
So honestly, I think the best solution would be to stop using a fake field for this purpose! It feels incredibly hacky, and I think there are better options that avoid polluting the registration data w/ a dummy value…
For example there’s already a registration-status-flag template hook which can be used to insert custom columns into the table w/o being linked to a field. However, the data is inserted right before the registration ID column since it’s meant to show icons (we use it to show whether a visitor badge to access CERN has been created for that registration):
But I’d be happy to add another template hook after the rendering the static columns (ie right before {% for item in dynamic_columns %}). Just let me know if that will be useful and I’ll include it in my PR.
These are only used to show data in the registration emails. For a fake field with no meaningful value, returning an empty string may be the best option here.
Just as a heads-up, 3.3.7 will contain a security fix so updating shortly after it’s released is recommended. But if needed it shouldn’t be too awful to manually add the template hooks again until you have time to switch to a cleaner solution.
PS: I think the hotel field, even though you did not share its source code so far, may be the easiest one to adapt since AFAICT this is an actual field, and thus something where the new methods work fine.
Also, one obvious bug: Your code looks at all abstracts the user ever submitted, not only within the same event…
Thanks for all the tips. These solutions are hard to find without reading all the code of Indico.
Of course it is hacky, but the colleagues don’t seem to mind a “this will break in the future” (yet). Using a hook might be an alternative, but a problem is that we have several custom columns that are used by different team members responsible for different tasks. This one could not be hidden like the others. I will think about it when I find the time. - In the meantime this plugin has gained another hacky feature to reflect the abstract’s accepted contribution type in an invited speaker tag…
Yes, but they will matter for the hotel plugin, which renders a complete breakdown of its price calculation including accompanying children, discounts etc.
And thanks for finding that bug - that would have been a nice surprise for future me as this conference will recur yearly.
Well, I was motivated to play around w/ the registration list generator, and it wasn’t too hard to add proper support for custom columns and filters. I think that’s better than using fake fields for this purpose
Here are two examples on how this could look like:
class CustomReglistItemByManager(CustomRegistrationListItem):
name = 'by_manager'
title = 'By manager'
filter_choices = {
'0': _('No'),
'1': _('Yes')
}
filter_only = True
def get_filter_criterion(self, values):
if len(values) == 1:
return Registration.created_by_manager == bool(int(values[0]))
class CustomReglistItemAbstracts(CustomRegistrationListItem):
name = 'abstracts'
title = 'Abstracts submitted'
filter_choices = {
'none': 'None',
'one': 'One',
'many': 'Multiple',
}
filter_only = False
def filter_list(self, registrations, values):
from collections import Counter
counter = Counter(
abstract.submitter_id
for abstract in db.m.Abstract.query.with_parent(self.event).filter(~db.m.Abstract.is_deleted)
)
return [reg for reg in registrations if (
('none' in values and not counter[reg.user_id]) or
('one' in values and counter[reg.user_id] == 1) or
('many' in values and counter[reg.user_id] > 1)
)]
def load_data(self, registrations):
from collections import Counter
counter = Counter(
abstract.submitter_id
for abstract in db.m.Abstract.query.with_parent(self.event).filter(~db.m.Abstract.is_deleted)
)
return {
reg: RegistrationListColumn(str(counter[reg.user_id]), str(counter[reg.user_id]))
for reg in registrations
}
That looks so much better for a lot of things I’ve been hacking together. Also that filter function would have been so much better a few weeks ago.
Also thanks for the new release.
I have set aside a few hours tomorrow to update our system, get everything running again and maybe clean up a few things if time permits. However, since I am not doing this full time and there are other urgent things I cannot promise that I can give feedback before mid August.
I managed to do the update and adapt my plugins just before my vacation. Worked perfectly! Thanks a lot!
Maybe you have an idea on one more thing: I mentioned that we limit the number of abstract to only one. The way I do it at the moment is by automatically assigning a role “No Abstract” to users that have not submitted an abstract and keeping the Call for Abstracts closed with an exception for users with this role. The problem we just noticed is, that revoking the right to submit abstracts after the first submission also prevents users from editing that first submission. I am afraid that this cannot be avoided without a change in Indico itself..?