Event Reminder template not working

Good afternoon,

I’ve rsync’ed the /opt/indico/custom/templates/core/events/reminders/emails/event_reminder.txt from the old server (v2.1.4) to the new one (v3.3.6), I’ve restarted the indico-uwsgi service, but the reminder email is the default one and not my template.

The reminder email I’m getting is:

"[Indico] [Event reminder] Test IT (7/23/25, 4:00 PM Europe/Madrid)

to me

Please note that the event “Test IT” will start on Jul 23, 2025, 4:00 PM (Europe/Madrid).
It will take place at Zoom Rooms (Zoom/Online-Room - Seminar room).
Address:"

While the reminder from the old server, with the same template is like:

"[Indico] REMINDER - Theory Seminar: Wednesday, July 2, 2025 at 11:45

to

Theory Seminar:
“Seminar title”

by

“Person doing the Seminar”

Wednesday, July 2, 2025 at 11:45"

Asking ChatGPT I’ve modified permissions so now it’s “-rw-r–r-- 1 indico www-data”. Also, it told me that for Indico v3.3.6 the file should be named event_reminder_default.txt and also that it should be located at /opt/indico/custom/templates/events/reminders/emails, instead of /opt/indico/custom/templates/core/events/reminders/emails. This came from using the following commands with the indico user:

“indico shell
from flask import render_template
render_template(‘events/reminders/emails/event_reminder.txt’, event=…, note=‘REMINDER’)”

And getting errors related to the …/core… directory. Do you need the content from the template?

Thanks,

J

ChatGPT hallucinated nonsense. It’s certainly not a permission error (that would cause an error, not silently ignore the file), and the template filename did not change.

I recommend you to check the output of git diff v2.1.4..v3.3.6 -- indico/modules/events/reminders/templates/emails/event_reminder.txt (assuming you have a local clone of the Indico source code) to see what changed between the two versions in that template.

And then depending on what you have in your custom template (yes, please post it!) either adapt it, or recreate it from scratch.

I tried using the git diff command with the users manager, root and indico in the v3.3.6 server, and with all of them I got “error: Could not access ‘v2.1.4..v3.3.6’”. Maybe I’m doing something wrong?

Also, here’s the template:

(indico) indico@indico:~/custom/templates/events/reminders/emails$ cat event_reminder.txt 
{% extends "~events/reminders/emails/event_reminder.txt" %}

{% set mycategories = ["Pizza Seminars","Experimental Seminars","Thesis","TH Seminars","Colloquia"] %}
{% set mycategories_id = [15,28,37,16,17] %}
{% set mycategories_singular = {15:"Pizza Seminar",28:"Experimental Seminar",37:"Thesis",16:"Theory Seminar",17:"Colloquium"} %}

{% set title = event.title %}
{% set category_title = event.category.title %}
{% set category_id = event.category.id %}
{% set category_singular = mycategories_singular[category_id] %}
{% set type = event.type %}
{% set url = "https://indico.ifae.es/event/%s"|format(event.id) %}
{% set url2 = "https://indico.ifae.es/%s"|format(event.url) %}
{% set url_shortcut = event.url_shortcut %}
{% set address = event.address %}
{% set location = event.venue_name %}
{% set room = event.room_name %}
{% set description = event.description | html_to_plaintext | trim %}
{% set description2 = event.description | striptags | wordwrap %}
{% set date_time =  event.start_dt | format_datetime('EEEE, MMMM d,  yyyy HH:mm', timezone=event.tzinfo) %}
{% set date =  event.start_dt | format_datetime('EEEE, MMMM d,  yyyy', timezone=event.tzinfo) %}
{% set time =  event.start_dt | format_datetime('HH:mm', timezone=event.tzinfo) %}
{% set organizer_info = event.organizer_info %}

{% set reminder = False %}
{% if (note|upper) == "REMINDER" %}
{% set reminder = True %}
{% endif %}

{% set correction = False %}
{% if (note|upper) == "CORRECTION" %}
{% set correction = True %}
{% endif %}

{% set mysubject = "IFAE %s: %s at %s"|format(category_singular, date, time) %}
{% if reminder %}
{% set mysubject = "REMINDER - IFAE %s: %s at %s"|format(category_singular, date, time) %}
{% endif %}
{% if correction %}
{% set mysubject = "CORRECTION - IFAE %s: %s at %s"|format(category_singular, date, time) %}
{% endif %}


{%- block subject -%}
{%- if category_id in mycategories_id -%}
{{ mysubject }}
{%- else -%}
{{super()}}
{%- endif -%}
{%- endblock %}

{% block body %}
{%- if category_id in mycategories_id -%}
{{"IFAE %s:"|format(category_singular)}}
"{{title}}"
{% if event.person_links %}
by
{% for link in event.person_links|sort(attribute='full_name') %}
{{ render_user(link) }}
{% endfor %}
{%- endif -%}

{{ "%s at %s"|format(date, time) }}

{{ "%s [%s]"|format(location, room) }}
{% if address -%}
{{"Streaming: %s"|format(address)}}
{%- endif %}

{% if category_id != 37 -%}
{{ 'Abstract:'  | underline }}
{% endif %}
{{description}}

You can access the full event here:
{%- if url_shortcut -%}
{{"https://indico.ifae.es/event/%s"|format(url_shortcut) }}
{% else -%}
{{url}}
{% endif -%}

{% if organizer_info %}
Organized by:
{{organizer_info}}
{% endif -%}
{%- else -%}
{{super()}}
{%- endif -%}

{% endblock %}

{% macro render_user(user_data)  -%}
{{ user_data.get_full_name(last_name_upper=False, last_name_first=False, abbrev_first_name=False, show_title=True)  }} {% if user_data.affiliation -%} [{{ user_data.affiliation }}]{%- endif %}
{% endmacro %}

There isn’t a git tree in a production setup. You need do do that on a local git clone (or full local dev setup of Indico).

Anyway, here’s the diff:

diff --git a/indico/modules/events/reminders/templates/emails/event_reminder.txt b/indico/modules/events/reminders/templates/emails/event_reminder.txt
index e984ff473f..28d1cbb4d1 100644
--- a/indico/modules/events/reminders/templates/emails/event_reminder.txt
+++ b/indico/modules/events/reminders/templates/emails/event_reminder.txt
@@ -1,31 +1,38 @@
-{% extends 'emails/base.txt' %}
+{% extends 'emails/base_i18n.txt' %}
 {% from 'events/reminders/emails/_agenda.txt' import render_agenda %}

 {% set address = event.address %}
 {% set location = event.venue_name %}
 {% set room = event.room_name %}
+{% set label = ' {%s}'|format(event.label.title) if event.label else '' %}


 {% block subject -%}
-    [Event reminder] {{ event.title }} ({{ event.start_dt | format_datetime('short', timezone=event.tzinfo) }} {{ event.timezone }})
+    [{% trans %}Event reminder{% endtrans %}] {{ event.title }}{{ label }} ({{ event.start_dt | format_datetime('short', timezone=event.tzinfo) }} {{ event.timezone }})
 {%- endblock %}
 {% block header %}{% endblock %}
 {% block footer_url %}{{ url }}{% endblock %}


 {% block body -%}
-Please note that the event "{{ event.title }}" will start on {{ event.start_dt | format_datetime(timezone=event.tzinfo) }} ({{ event.timezone }}).
+{%- trans title=event.title, start=event.start_dt|format_datetime(timezone=event.tzinfo), tz=event.timezone -%}
+    Please note that the event "{{ title }}"{{ label }} will start on {{ start }} ({{ tz }}).
+{%- endtrans %}

-{%- if location or room or address %}
-It will take place at {{ render_location() }}
+{%- if location or room %}
+{% trans loc=render_location() %}It will take place at {{ loc }}{% endtrans %}
+{%- elif address %}
+{% trans %}It will take place at the following address:{% endtrans %}
+
+    {{ address | trim | indent }}
 {%- endif %}

-You can access the full event here:
+{% trans %}You can access the full event here:{% endtrans %}
 {{ url }}

 {%- if with_description and event.description %}

-{% filter underline %}Description{% endfilter %}
+{% filter underline %}{% trans %}Description{% endtrans %}{% endfilter %}

 {{ event.description | html_to_plaintext | trim }}

@@ -35,7 +42,7 @@ You can access the full event here:
 {%- if note %}


-{% filter underline %}Note{% endfilter %}
+{% filter underline %}{% trans %}Note{% endtrans %}{% endfilter %}
 {{ note }}
 {%- endif -%}

@@ -69,18 +76,14 @@ You can access the full event here:
         {%- if location -%}
             ({{ room | trim }})
         {%- else -%}
-            room {{ room | trim }}
+            {% trans r=room|trim %}room {{ r }}{% endtrans %}
         {%- endif -%}
     {%- endif -%}
 {%- endmacro -%}

 {%- macro render_address() -%}
-    {%- if address -%}
-        {%- if location or room %}
-Address:
-        {%- else -%}
-the following address:
-        {%- endif %}
+    {%- if address %}
+{% trans %}Address:{% endtrans %}

     {{ address | trim | indent }}
     {%- endif -%}

TBH I don’t see any changes that would be likely to have (significantly) changed how your custom template behaves…

Did you, by any chance, simply forget to set the CUSTOMIZATION_DIR again in your indico.conf file after updating and likely changing servers? (compare the old indico.conf with the new one)

No, I already wrote the Customization dir and it is the same as the old one, only that I put it at the end of the General Settings part instead of at the end of the file. Here’s the indico.conf from the old server:

# General settings
SQLALCHEMY_DATABASE_URI = 'postgresql:///indico'
SECRET_KEY = 'whatever'
BASE_URL = 'https://indico.ifae.es'
CELERY_BROKER = 'redis://127.0.0.1:6379/0'
REDIS_CACHE_URL = 'redis://127.0.0.1:6379/1'
CACHE_BACKEND = 'redis'
DEFAULT_TIMEZONE = 'Europe/Madrid'
DEFAULT_LOCALE = 'en_GB'
ENABLE_ROOMBOOKING = True
CACHE_DIR = '/opt/indico/cache'
TEMP_DIR = '/opt/indico/tmp'
LOG_DIR = '/opt/indico/log'
ASSETS_DIR = '/opt/indico/assets'
STORAGE_BACKENDS = {'default': 'fs:/opt/indico/archive', 'legacy': 'fs-readonly:/opt/indico/legacy-archive', 'fs-legacy-symlinks': '/opt/indico/archive/legacy-symlinks'}
ATTACHMENT_STORAGE = 'default'
ROUTE_OLD_URLS = True
# Email settings
SMTP_SERVER = ('localhost', 25)
SMTP_USE_TLS = False
SMTP_LOGIN = ''
SMTP_PASSWORD = ''
SUPPORT_EMAIL = 'whatever'
PUBLIC_SUPPORT_EMAIL = 'whatever'
NO_REPLY_EMAIL = 'whatever'
STATIC_FILE_METHOD = 'xsendfile'
XELATEX_PATH = '/opt/texlive/bin/x86_64-linux/xelatex' 
#PLUGINS = {'vc_vidyo'}
#PLUGINS = {'roombooking','vc_vidyo'} 
CUSTOMIZATION_DIR = '/opt/indico/custom'
CUSTOMIZATION_DEBUG = True

And this is the indico.conf from the new server:

# General settings
SQLALCHEMY_DATABASE_URI = 'postgresql:///indico'
SECRET_KEY = whatever
BASE_URL = 'https://indico.ifae.es'
BASE_URL_ALLOW_LIST = ['https://indico.ifae.es']
CELERY_BROKER = 'redis://127.0.0.1:6379/0'
REDIS_CACHE_URL = 'redis://127.0.0.1:6379/1'
DEFAULT_TIMEZONE = 'Europe/Madrid'
DEFAULT_LOCALE = 'en_US'
ENABLE_ROOMBOOKING = True
CACHE_DIR = '/opt/indico/cache'
TEMP_DIR = '/opt/indico/tmp'
LOG_DIR = '/opt/indico/log'
CUSTOMIZATION_DEBUG = True
CUSTOMIZATION_DIR = '/opt/indico/custom'
#STORAGE_BACKENDS = {'default': 'fs:/opt/indico/archive'}
STORAGE_BACKENDS = {'default': 'fs:/opt/indico/archive', 'legacy': 'fs-readonly:/opt/indico/legacy-archive', 'fs-legacy-symlinks': '/opt/indico/archive/legacy-symlinks'}
ATTACHMENT_STORAGE = 'default'
ROUTE_OLD_URLS = True
#Plugin
PLUGINS_FOLDER = '/opt/indico/indico_plugins'
# Email settings
SMTP_SERVER = ('whatever', 25)
SMTP_USE_TLS = False
SMTP_LOGIN = ''
SMTP_PASSWORD = ''
SUPPORT_EMAIL = 'whatever'
PUBLIC_SUPPORT_EMAIL = 'whatever'
NO_REPLY_EMAIL = 'whatever'

STATIC_FILE_METHOD = 'xsendfile'

ok that seems fine. Anything useful in indico.log (since you enabled CUSTOMIZATION_DEBUG) when the reminder is sent?

Also, you need to restart indico-celery.service as well so background tasks use your new config and template file…

Ok, I found something useful. Yesterday I tried to search for “event_reminder.txt” and “Customizable” but I found nothing. Now I searched again and I got that, if I’m not wrong, the template that it’s actually using is located in /opt/indico/.venv/lib/python3.12/site-packages/indico/modules/events/reminders/templates/emails, and its content is:

"(indico) indico@indico:~/.venv/lib/python3.12/site-packages/indico/modules/events/reminders/templates/emails$ cat event_reminder.txt
{% extends ‘emails/base_i18n.txt’ %}
{% from ‘events/reminders/emails/_agenda.txt’ import render_agenda %}

{% set address = event.address %}
{% set location = event.venue_name %}
{% set room = event.room_name %}
{% set label = ’ {%s}'|format(event.label.title) if event.label else ‘’ %}

{% block subject -%}
[{% trans %}Event reminder{% endtrans %}] {{ event.title }}{{ label }} ({{ event.start_dt | format_datetime(‘short’, timezone=event.tzinfo) }} {{ event.timezone }})
{%- endblock %}
{% block header %}{% endblock %}
{% block footer_url %}{{ url }}{% endblock %}

{% block body -%}
{%- trans title=event.title, start=event.start_dt|format_datetime(timezone=event.tzinfo), tz=event.timezone -%}
Please note that the event “{{ title }}”{{ label }} will start on {{ start }} ({{ tz }}).
{%- endtrans %}

{%- if location or room %}
{% trans loc=render_location() %}It will take place at {{ loc }}{% endtrans %}
{%- elif address %}
{% trans %}It will take place at the following address:{% endtrans %}

{{ address | trim | indent }}

{%- endif %}

{% trans %}You can access the full event here:{% endtrans %}
{{ url }}

{%- if with_description and event.description %}

{% filter underline %}{% trans %}Description{% endtrans %}{% endfilter %}

{{ event.description | html_to_plaintext | trim }}

{%- endif -%}

{#- Blank lines are intended #}
{%- if note %}

{% filter underline %}{% trans %}Note{% endtrans %}{% endfilter %}
{{ note }}
{%- endif -%}

{# Blank lines are intended #}
{%- if with_agenda %}

{{ render_agenda(event, agenda) }}
{%- endif -%}
{%- endblock %}

{%- macro render_location() -%}
{%- if location -%}
{%- if room -%}
{{ location | trim }} {{ render_room() }}.
{%- else -%}
{{ location | trim }}.
{%- endif -%}
{{- render_address() }}
{%- elif room -%}
{{ render_room() }}.
{{- render_address() }}
{%- else -%}
{{ render_address() }}
{%- endif -%}
{%- endmacro -%}

{%- macro render_room() -%}
{%- if room -%}
{%- if location -%}
({{ room | trim }})
{%- else -%}
{% trans r=room|trim %}room {{ r }}{% endtrans %}
{%- endif -%}
{%- endif -%}
{%- endmacro -%}

{%- macro render_address() -%}
{%- if address %}
{% trans %}Address:{% endtrans %}

{{ address | trim | indent }}
{%- endif -%}

{%- endmacro -%}"

So I guess I have to substitute this one with the template I wanted to use?

Ok, I finally got it. I could “just” substitute the one from /opt/indico/.venv/lib/python3.12/site-packages/indico/modules/events/reminders/templates/emails with my template, but with the following change: at the beginning I can’t use

{% extends "~events/reminders/emails/event_reminder.txt" %}

since it continuously sent me an “Unexpected Exception occurred at indico.ifae.es: maximum recursion depth exceeded” email error when trying to send a reminder, so I had to substitute it with

{% extends ‘emails/base_i18n.txt’ %}
{% from ‘events/reminders/emails/_agenda.txt’ import render_agenda %}

Basically, the first two lines from the template that was already in that directory. Now my template is in use. Thanks for the help!

Are you 100% sure you have the ~ at the beginning? This is what ensures that it ignores customized templates and imports the original one instead.

The route to my template was either ~/custom/templates/events/reminders/emails or ~/custom/templates/core/events/reminders/emails, being “~” “/opt/indico”. Does it mean that if I substitute

{% extends “~events/reminders/emails/event_reminder.txt” %}

for

{% extends “~/custom/templates/events/reminders/emails/event_reminder.txt” %}

it should work?

The ~ there has absolutely nothing to do with the Linux “homedir” meaning of ~.

See Settings — Indico 3.3.7 documentation - but especially the output logged when CUSTOMIZATION_DEBUG is enabled is what’s important for you.

For event_reminder.txt the correct path to extend the original one should be ~events/reminders/emails/event_reminder.txt. So what you posted here earlier is correct…

Oh I just realized that you apparently replaced the template in /opt/indico/.venv/lib/python3.12/site-packages/indico/modules/events/reminders/templates/emails? If yes, this is NOT a good idea, as that file will be overwritten during the next update.

Then, sorry, to recapitulate: I should leave the /opt/indico/.venv/lib/python3.12/site-packages/indico/modules/events/reminders/templates/emails/event_reminder.txt as it was, and then do what with the /opt/indico/custom/templates/(core/)events/reminders/emails/event_reminder.txt?

Your custom template should be in /opt/indico/custom/templates/core/events/reminders/emails/event_reminder.txt, and the one in site-packages should be left alone. Then using ~events/reminders/emails/event_reminder.txt in your custom template to inherit from the original one should work.

1 Like

Fine, everything is already where it should and how it should be, and it’s working after the indico-celery restart. Thanks a lot!

Good morning again,

Since last Friday afternoon I’m getting this email error message every 5 minutes:

"Unexpected Exception occurred at indico.ifae.es: Task event_reminders[3e3f0eb9-aef5-4244-8a09-09fe621ef4fc] raised unexpected: UndefinedError(‘dict object has no element 59’)

logger
Fri, Jul 25, 5:00 PM (3 days ago)

2025-07-25 17:00:02,496  0000000000000000  -       celery.app.trace - ERROR trace.py:267 -- Task event_reminders[3e3f0eb9-aef5-4244-8a09-09fe621ef4fc] raised unexpected: UndefinedError('dict object has no element 59')

Traceback (most recent call last):
  File "/opt/indico/.venv/lib/python3.12/site-packages/celery/app/trace.py", line 453, in trace_task
    R = retval = fun(*args, **kwargs)
                 ^^^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/celery.py", line 325, in _inner
    reraise(*exc_info)
  File "/opt/indico/.venv/lib/python3.12/site-packages/sentry_sdk/_compat.py", line 127, in reraise
    raise value
  File "/opt/indico/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/celery.py", line 320, in _inner
    return f(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/indico/core/celery/core.py", line 139, in __call__
    rv = super().__call__(*args, **kwargs)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/celery/app/trace.py", line 736, in __protected_call__
    return self.run(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/indico/core/celery/util.py", line 31, in wrapper
    return f(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/indico/modules/events/reminders/tasks.py", line 30, in send_event_reminders
    reminder.send()
  File "/opt/indico/.venv/lib/python3.12/site-packages/indico/modules/events/reminders/models/reminders.py", line 228, in send
    email_tpl = make_reminder_email(self.event, self.include_summary, self.include_description, self.message)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/indico/modules/events/reminders/util.py", line 22, in make_reminder_email
    return get_template_module('events/reminders/emails/event_reminder.txt', event=event,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/indico/web/flask/templating.py", line 137, in get_template_module
    return tpl.make_module(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/flask_pluginengine/templating.py", line 87, in make_module
    module = super().make_module(vars, shared, locals)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/jinja2/environment.py", line 1405, in make_module
    return TemplateModule(self, ctx)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/jinja2/environment.py", line 1537, in __init__
    body_stream = list(template.root_render_func(context))
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/flask_pluginengine/util.py", line 131, in generator
    yield from gen
  File "/opt/indico/custom/templates/core/events/reminders/emails/event_reminder.txt", line 142, in root
  File "/opt/indico/.venv/lib/python3.12/site-packages/jinja2/filters.py", line 1043, in do_format
    return soft_str(value) % (kwargs or args)
           ~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~
  File "/opt/indico/.venv/lib/python3.12/site-packages/jinja2/runtime.py", line 859, in _fail_with_undefined_error
    raise self._undefined_exception(self._undefined_message)
jinja2.exceptions.UndefinedError: dict object has no element 59"

At the end it says I must check line 142 of my event_reminder.txt file, but mine has only 95 lines. Also, there’s no “59” reference anywhere in the event reminder template. I tried restarting the celery service, but it did nothing. What’s happening?

Maybe related to this?

{% set category_singular = mycategories_singular[category_id] %}

You could use .get() instead of brackets.

I finally solved it, somehow. I think the essential was just restarting indico celery, indico uwsgi and apache2 at the same time, since for the part of the template I didn’t change a thing. But what you say maybe solves it for hypothetical future repetitions of the same error. So how exactly do I change the line? Maybe like this?:

{% set category_singular = mycategories_singular.get() %}

I’m not very skilled at this :sweat_smile: Thanks for your help!

.get(category_id)

But again it’s just a guess of the most likely case where that UndefinedError is coming from.

Good morning. Just an hour ago Indico started sending the same error again, I then tried the {% set category_singular = mycategories_singular.get(category_id) %} line, then restarted indico-celery, indico-uwsgi and apache2 (I guess not all these services need to be restarted, but I wanted to make sure, since yesterday it worked after that), and it seems to have been solved. Let’s see if it doesn’t happen again. Thanks!