Editing the user profile page with additional fields

Hello! Following this discussion: Adding new custom fields to user profile

I took the approach of implementing a plug-in to add custom fields. To simplify this example, currently I am trying to add an additional hardcoded field called “Country”:

class CustomProfileForm(IndicoForm):
    """Form for custom profile fields."""

    country = StringField(
        "Country", [Optional()], description="Your country of residence"
    )

However, I am not sure where the best place to include this in the templates is. I don’t think there exists a template-hook as of right now that I can use ( correct me if I am wrong ). So what would be the best approach here? Is it:

1- I can see in indico/modules/users/client/js/PersonalDataForm.jsx that there is a rendering functions for plugin components:

            {renderPluginComponents('user-personal-data-form-inputs', {
              userValues,
              syncedValues,
              lockedFields,
              lockedFieldMessage,
            })}

I haven’t worked with .js before but I assume I have a way of sending the plugin components through user-personal-data-form-inputs entry point? If so, what’s the best way to do it via Indico API?

2- Create a template hook somewhere, contributing to core? I am not sure where though.

3- Override existing template with an HTML of my own? ( Dirtiest approach I assume )

Many thanks for the help!

For reference ( just in case it helps ): This is the current DB model I am using for this particular plugin:

from indico.core.db import db
from indico.modules.users.models.users import User
from indico.util.string import format_repr


class UserCustomProfile(db.Model):
    """Model for storing custom user profile fields."""

    __tablename__ = "user_custom_profiles"
    __table_args__ = (
        db.UniqueConstraint("user_id"),
        {"schema": "plugin_custom_profile_fields"},
    )

    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(
        db.Integer, db.ForeignKey("users.users.id"), nullable=False, index=True
    )

    # Professional Information
    country = db.Column(db.String, nullable=True)  # Country of residence

    # Relationships
    user = db.relationship(
        "User",
        lazy=True,
        backref=db.backref(
            "custom_profile", lazy=True, uselist=False, cascade="all, delete-orphan"
        ),
    )

    def __repr__(self):
        """String representation."""
        return format_repr(
            self, "id", "user_id", _text=self.user.full_name if self.user else None
        )

    @classmethod
    def get_for_user(cls, user):
        """Get custom profile for a user, creating if necessary."""
        if isinstance(user, int):
            user = User.get(user)

        if not user:
            return None

        profile = cls.query.filter_by(user_id=user.id).first()
        if not profile:
            profile = cls(user=user)
        return profile

Hi!

Option 1 seems reasonable given that we already have the React hook in place. The way it works is that you register a React component in your plugin using registerPluginComponent. Have a look at e.g. the Captcha plugin ( indico-plugins/cloud_captchas at master · indico/indico-plugins · GitHub ) that is using it to display a captcha when registering for events.

Hey thanks for the answer! So from my understanding of it, the way I extend the profile page would be to do:

import {registerPluginComponent} from 'indico/utils/plugins';

// code here to create an additional form additionalFormObject

registerPluginComponent('my-plugin', 'user-personal-data-form-inputs', additionalFormObject);

where my-plugin is the name of the plugin I am implementing. I will give this a shot, thanks!

Yeah, pretty much. additionalFormObject should be a React component.

1 Like

Following up on this, I managed to create the form component and add it to web page but to make it functional, I was looking at hooking with signal, but couldn’t figure out which signal I need to look at. I have the necessary DB model, I just need to figure out how to call fetching/saving methods via the plugin API. Any pointers? None of the signals seemed the way to go for this.

I think you want either before_process or process

The first one triggers before RHPersonalDataUpdate._process has been called and the second right after. The sender is the RH class so you can check that it is RHPersonalDataUpdate and otherwise do nothing. Inside the signal handler you can implement your custom DB logic.

1 Like

Thank you for this, this worked out!

I have one final feature for this, which is prefilling the custom fields with the info from the DB. For this, I assumed the RHPersonalData handler is the one I want to use with process, which correctly triggers when the profile page is visited. However I could not figure out how to pass the information so that it prefills the fields. Where should I look for to pass the info? I probably want to somehow pass it to userValues of PersonalDataForm ( as it is passed to initialValues in indico/modules/users/client/js/PersonalDataForm.jsx ) but not sure how to correctly tackle this from the plug-in side. Any pointers?

I think you can use schema_post_dump for that. Check if it’s UserPersonalDataSchema and then inject your extra fields. The result is then passed into user_values (which becomes userValues in PersonalDataForm.jsx) so in theory everything should just work™️ .

1 Like

Thanks, yes that indeed is working!

I got ( hopefully ) 1 final question that I am stuck at. I would like these fields to also appear in the event registration form and have the data prefilled, similar to the current fields in the profile page. Ideally, I would like to be able to add these fields from the administration side from the registration form configuration ( event/{event_id}/manage/registration/{form_id}/form ) I couldn’t figure out what hook would be useful here, or how to connect the added profile fields to registration form.

I meant to ask only one question at the beginning of the topic and spiraled into asking a lot, sorry for the bother! I believe this is the final requirement for the plug-in though!

Oops apparently I have another question as well…

For other fields I am able to import and use the following successfully:

import {FinalInput, FinalRadio} from 'indico/react/forms';
import {
  SyncedFinalAffiliationDropdown,
  SyncedFinalInput,
  SyncedFinalTextArea,
  SyncedFinalDropdown,
} from 'indico/react/components/syncedInputs';

But, for the “Country” field, I wanted to try and replicate the “Country” field in the registration forms. I could see that it uses “CountryInput” in /modules/events/registration/client/js/form/fields/CountryInput.jsx, although I could not figure out how to properly import this without doing relative path to the core repo in my local from the plugin repo.

This and the previous questions are the only blockers right now for me and hope that they are the final questions I have! Many thanks for all the help so far, really appreciate it!

You should be able to import it as import CountryInput from indico/modules/events/registration/form/fields/CountryInput

For the first question I don’t know off the top of my head but if you can share your (ideally runnable) plugin I can have a look

If you’re just looking to populate the default country field in a registration form, you could intercept the get_user_data function. Something like this should work:

class YourPlugin(IndicoPlugin):
    def __init__(self):
        # Your init code here

        self.connect(signals.plugin.interceptable_function, self._get_user_data, sender=interceptable_sender(get_user_data))

    def _get_user_data(self, sender, func, args, **kwargs):
        user_data = func(*args.args, **args.kwargs)
        # Populate the country field with the user's data
        user_data['country'] = ...
        return user_data

This import CountryInput from indico/modules/events/registration/form/fields/CountryInput returns error from the webpack builder:

RROR in ./customFieldForms.jsx 8:0-87
Module not found: Error: Can't resolve 'indico/modules/events/registration/form/fields/CountryInput' in '/home/dogaykamar/Desktop/indicodev/indico-plugin-custom-profile-fields/indico_custom_profile_fields/client'
resolve 'indico/modules/events/registration/form/fields/CountryInput' in '/home/dogaykamar/Desktop/indicodev/indico-plugin-custom-profile-fields/indico_custom_profile_fields/client'
  Parsed request is a module
  No description file found in /home/dogaykamar/Desktop/indicodev/indico-plugin-custom-profile-fields/indico_custom_profile_fields/client or above
  aliased with mapping 'indico/modules/events': '/home/dogaykamar/Desktop/indicodev/src/indico/modules/events/client/js' to '/home/dogaykamar/Desktop/indicodev/src/indico/modules/events/client/js/registration/form/fields/CountryInput'
    No description file found in /home/dogaykamar/Desktop/indicodev/indico-plugin-custom-profile-fields/indico_custom_profile_fields/client or above
    root path /home/dogaykamar/Desktop/indicodev/indico-plugin-custom-profile-fields/indico_custom_profile_fields/client
      No description file found in /home/dogaykamar/Desktop/indicodev/indico-plugin-custom-profile-fields/indico_custom_profile_fields/client/home/dogaykamar/Desktop/indicodev/src/indico/modules/events/client/js/registration/form/fields or above
      no extension
        /home/dogaykamar/Desktop/indicodev/indico-plugin-custom-profile-fields/indico_custom_profile_fields/client/home/dogaykamar/Desktop/indicodev/src/indico/modules/events/client/js/registration/form/fields/CountryInput doesn't exist
      .js
        /home/dogaykamar/Desktop/indicodev/indico-plugin-custom-profile-fields/indico_custom_profile_fields/client/home/dogaykamar/Desktop/indicodev/src/indico/modules/events/client/js/registration/form/fields/CountryInput.js doesn't exist
      .jsx
        /home/dogaykamar/Desktop/indicodev/indico-plugin-custom-profile-fields/indico_custom_profile_fields/client/home/dogaykamar/Desktop/indicodev/src/indico/modules/events/client/js/registration/form/fields/CountryInput.jsx doesn't exist
      .ts
        /home/dogaykamar/Desktop/indicodev/indico-plugin-custom-profile-fields/indico_custom_profile_fields/client/home/dogaykamar/Desktop/indicodev/src/indico/modules/events/client/js/registration/form/fields/CountryInput.ts doesn't exist
      .tsx
        /home/dogaykamar/Desktop/indicodev/indico-plugin-custom-profile-fields/indico_custom_profile_fields/client/home/dogaykamar/Desktop/indicodev/src/indico/modules/events/client/js/registration/form/fields/CountryInput.tsx doesn't exist
      .json
        /home/dogaykamar/Desktop/indicodev/indico-plugin-custom-profile-fields/indico_custom_profile_fields/client/home/dogaykamar/Desktop/indicodev/src/indico/modules/events/client/js/registration/form/fields/CountryInput.json doesn't exist
      as directory
        /home/dogaykamar/Desktop/indicodev/indico-plugin-custom-profile-fields/indico_custom_profile_fields/client/home/dogaykamar/Desktop/indicodev/src/indico/modules/events/client/js/registration/form/fields/CountryInput doesn't exist
    using description file: /home/dogaykamar/Desktop/indicodev/src/package.json (relative path: ./indico/modules/events/client/js/registration/form/fields/CountryInput)
      no extension
        Field 'browser' doesn't contain a valid alias configuration
        /home/dogaykamar/Desktop/indicodev/src/indico/modules/events/client/js/registration/form/fields/CountryInput doesn't exist
      .js
        Field 'browser' doesn't contain a valid alias configuration
        /home/dogaykamar/Desktop/indicodev/src/indico/modules/events/client/js/registration/form/fields/CountryInput.js doesn't exist
      .jsx
        Field 'browser' doesn't contain a valid alias configuration
        /home/dogaykamar/Desktop/indicodev/src/indico/modules/events/client/js/registration/form/fields/CountryInput.jsx doesn't exist
      .ts
        Field 'browser' doesn't contain a valid alias configuration
        /home/dogaykamar/Desktop/indicodev/src/indico/modules/events/client/js/registration/form/fields/CountryInput.ts doesn't exist
      .tsx
        Field 'browser' doesn't contain a valid alias configuration
        /home/dogaykamar/Desktop/indicodev/src/indico/modules/events/client/js/registration/form/fields/CountryInput.tsx doesn't exist
      .json
        Field 'browser' doesn't contain a valid alias configuration
        /home/dogaykamar/Desktop/indicodev/src/indico/modules/events/client/js/registration/form/fields/CountryInput.json doesn't exist
      as directory
        /home/dogaykamar/Desktop/indicodev/src/indico/modules/events/client/js/registration/form/fields/CountryInput doesn't exist
 @ ./index.js 2:0-46 5:0-12

webpack 5.94.0 compiled with 1 error in 53 ms

I might need to setup JS project somehow in the repo maybe? It looks like it is looking for a description file of some sort.


For the Country field, that could possibly work, however the idea with this plugin was also to introduce new, custom fields. This is what I have ( Haven’t set up a proper repo yet, sorry! ) :

indico_custom_profile_fields.zip (4.8 MB)
This adds a Test Field to the profile page, and properly updates the DB and the profile page, and is working in my Indico instance as a plug-in right now. I would like to add this exact same form in the event registration form so that the admins can just select to add this field, similarly how Address or Title fields are by default disabled, but are seen in the page and can be added with a click, i.e., “Test Field” in this plug-in would also appear in the following page, under Title field:


For this minimal example plug-in, if I could also make the “Test Field” in the profile page appear in the event registration configuration field, I would be happy. Hope that makes sense!

Just realized I forgot to reply to you in my previous reply so you might not have got a notification, sorry if that wasn’t case and I am spamming you!

I believe this is an issue on our side. I put up a fix for it: Webpack: Sort module aliases by specificity by tomasr8 · Pull Request #7043 · indico/indico · GitHub. You can try applying the diff and see if it works for you.

Adding new fields, as opposed to just prefilling existing fields, is going to be much more involved. Perhaps you could play with PersonalDataType or create_personal_data_fields? Extending personal data with your new fields seems like the best way since personal data fields are present in every registration form by default.

I’d suggest first modifying indico directly to get something working. Once you’ve identified the parts you need to modify, we can see if it makes sense on our side to make the functions interceptable so that it can be done cleanly from a plugin.

Thank you, I can confirm that the diff is working!

Ah I see. Alright I will play with the core code and see what I can do, and report back to you if it works. Many thanks!

Hello again!

Playing with PersonalDataType, I could add a field named “address2” to the event registration form by adding the following at the end of the FIELD_DATA class method:

            (
                cls.address2,
                {
                    "title": cls.address2.get_title(),
                    "input_type": "textarea",
                    "is_enabled": False,
                    "position": 1006,
                },
            ),

I also needed to update the following constraint to include 11:

def upgrade():
    op.drop_constraint('ck_form_items_valid_enum_personal_data_type', 'form_items', schema='event_registration')
    op.create_check_constraint('valid_enum_personal_data_type', 'form_items',
                               '(personal_data_type = ANY (ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))',
                               schema='event_registration')

How should we proceed from here, any suggestions?

I strongly recommend to avoid creating new “personal data” field types. Changing the core database will be a huge pain during upgrades.

I only made changes just to test to see if I can edit the registration form the way I require, as suggested by @troun ( if I understood correctly). I do not intend to push these changes to prod instance, the only interaction will be through the plugin.

Edit: oh did you mean adding to personal data types is a pain in general? Do you have any suggestions on how else I could tackle this @ThiefMaster

The other suggestion I mentioned was hooking into create_personal_data_fields and manually creating the field you need there (without actually modifying PersonalDataType). This function is convenient because it’s called every time a registration form is created (there might be other ways, but for testing this seems fine).